cabc9e6ebb
Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
5.1 KiB
TypeScript
147 lines
5.1 KiB
TypeScript
import type { WechatLoginPlatform, WechatLoginResult } from '@dukang/shared-types';
|
||
import { getRuntimePlatform, isWechatBrowser } from './env';
|
||
import type { WeixinSdkConfig } from './types';
|
||
|
||
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('请在微信内打开');
|
||
}
|
||
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('请在微信内打开以使用微信登录');
|
||
}
|