Files
dukang/packages/weixin-sdk/src/auth.ts
T

150 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { WechatLoginPlatform, WechatLoginResult } from '@dukang/shared-types';
import { getRuntimePlatform, isWechatBrowser } from './env';
import type { WeixinSdkConfig } from './types';
/** 非微信内置浏览器时提示(与 C 端 LoginPage 文案一致) */
export const WECHAT_INAPP_REQUIRED_MSG = '请在微信内打开以使用微信一键授权';
const OAUTH_STATE_KEY = 'dukang_wx_oauth_state';
function randomState() {
return `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}
async function apiRequest<T>(
config: WeixinSdkConfig,
path: string,
options: RequestInit = {},
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-App': config.clientApp,
...(options.headers as Record<string, string>),
};
const token = config.getAccessToken?.();
if (token) headers.Authorization = `Bearer ${token}`;
const apiBase = config.apiBase ?? '/api/v1';
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '请求失败');
return json.data as T;
}
/** 获取公众号 OAuth 授权跳转 URL */
export async function getWechatOAuthUrl(
config: WeixinSdkConfig,
redirectUri: string,
scope: 'snsapi_base' | 'snsapi_userinfo' = 'snsapi_userinfo',
): Promise<string> {
const state = randomState();
sessionStorage.setItem(OAUTH_STATE_KEY, state);
const qs = new URLSearchParams({ redirectUri, scope, state });
const data = await apiRequest<{ url: string }>(config, `/common/wechat/oauth-url?${qs}`);
return data.url;
}
/** 发起微信 OAuth 登录(H5 公众号内跳转授权) */
export async function startWechatOAuthLogin(config: WeixinSdkConfig, redirectUri?: string): Promise<void> {
if (!isWechatBrowser()) {
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
}
const uri = redirectUri ?? window.location.href.split('#')[0];
const url = await getWechatOAuthUrl(config, uri);
window.location.href = url;
}
/** 小程序 wx.login 获取 code */
export function getMiniProgramLoginCode(): Promise<string> {
return new Promise((resolve, reject) => {
if (!window.wx?.login) {
reject(new Error('wx.login 不可用'));
return;
}
window.wx.login({
success: (res) => (res.code ? resolve(res.code) : reject(new Error('未获取到 code'))),
fail: (err) => reject(new Error(err.errMsg || 'wx.login 失败')),
});
});
}
/** 用 code 完成微信登录(自动识别 H5 / 小程序) */
export async function loginWithWechatCode(
config: WeixinSdkConfig,
code: string,
platform?: WechatLoginPlatform,
): Promise<WechatLoginResult> {
const resolvedPlatform = platform ?? (getRuntimePlatform() === 'mini' ? 'mini' : 'h5');
const loginPath = config.wechatLoginPath ?? '/auth/login/wechat';
return apiRequest<WechatLoginResult>(config, loginPath, {
method: 'POST',
body: JSON.stringify({ code, platform: resolvedPlatform }),
});
}
/** 处理 OAuth 回调 URL 中的 code 参数并登录 */
export async function handleWechatOAuthCallback(
config: WeixinSdkConfig,
searchParams?: URLSearchParams,
): Promise<WechatLoginResult | null> {
const params = searchParams ?? new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) return null;
const state = params.get('state');
const saved = sessionStorage.getItem(OAUTH_STATE_KEY);
if (saved && state && saved !== state) {
throw new Error('OAuth state 校验失败');
}
sessionStorage.removeItem(OAUTH_STATE_KEY);
return loginWithWechatCode(config, code, 'h5');
}
/** 微信首登绑定手机号 */
export async function bindWechatPhone(
config: WeixinSdkConfig,
payload: { wxSessionKey: string; phone: string; code: string },
): Promise<WechatLoginResult> {
return apiRequest<WechatLoginResult>(config, '/auth/wechat/bind-phone', {
method: 'POST',
body: JSON.stringify(payload),
});
}
/** 获取微信手机号(小程序 getPhoneNumber 返回的 code,需后端解密) */
export async function getWechatPhoneNumber(config: WeixinSdkConfig): Promise<string> {
const platform = getRuntimePlatform();
if (platform !== 'mini') {
throw new Error('H5 环境请使用短信验证码绑定手机号');
}
const phoneCode = await new Promise<string>((resolve, reject) => {
if (!window.wx?.getPhoneNumber) {
reject(new Error('getPhoneNumber 不可用'));
return;
}
window.wx.getPhoneNumber({
success: (res) => (res.code ? resolve(res.code) : reject(new Error('未获取到手机号 code'))),
fail: (err) => reject(new Error(err.errMsg || '获取手机号失败')),
});
});
const data = await apiRequest<{ phone: string }>(config, '/common/wechat/phone-number', {
method: 'POST',
body: JSON.stringify({ code: phoneCode, platform: 'mini' }),
});
return data.phone;
}
/** 一键微信登录(自动选择 OAuth 或 wx.login */
export async function wechatLogin(config: WeixinSdkConfig): Promise<WechatLoginResult | void> {
const platform = getRuntimePlatform();
if (platform === 'mini') {
const code = await getMiniProgramLoginCode();
return loginWithWechatCode(config, code, 'mini');
}
if (platform === 'wechat-h5') {
await startWechatOAuthLogin(config);
return;
}
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
}