Add WeChat login binding gate and context-aware auth UX.
Stop auto-creating duplicate accounts on OAuth, add bind-or-register gate, PC scan login, mobile open-in-WeChat guide, and fix localhost session cookies. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -222,6 +222,20 @@ export async function rememberProjectContext(
|
||||
export type WechatAuthConfig = {
|
||||
enabled: boolean;
|
||||
inWechat?: boolean;
|
||||
scanEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type WechatPendingProfile = {
|
||||
nickname: string | null;
|
||||
avatarUrl: string | null;
|
||||
returnTo: string;
|
||||
};
|
||||
|
||||
export type WechatBindingStatus = {
|
||||
enabled: boolean;
|
||||
bound: boolean;
|
||||
nickname?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
};
|
||||
|
||||
export async function getWechatAuthConfig(): Promise<WechatAuthConfig> {
|
||||
@@ -1133,6 +1147,141 @@ export async function resetPassword(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWechatPendingProfile(token: string): Promise<WechatPendingProfile> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`/auth/wechat/pending/${encodeURIComponent(token)}`);
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
const body = (await response.json().catch(() => null)) as WechatPendingProfile & {
|
||||
message?: string;
|
||||
};
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, body?.message ?? '绑定会话无效');
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function completeWechatRegister(pendingToken: string): Promise<{
|
||||
user: PortalUser;
|
||||
returnTo: string;
|
||||
}> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch('/auth/wechat/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pendingToken }),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
message?: string;
|
||||
user?: PortalUser;
|
||||
returnTo?: string;
|
||||
};
|
||||
if (!response.ok || !body?.user) {
|
||||
throw new ApiError(response.status, body?.message ?? '微信注册失败');
|
||||
}
|
||||
return { user: body.user, returnTo: body.returnTo ?? '/' };
|
||||
}
|
||||
|
||||
export async function completeWechatBind(
|
||||
pendingToken: string,
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<{ user: PortalUser; returnTo: string }> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch('/auth/wechat/bind', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pendingToken, username, password }),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
message?: string;
|
||||
user?: PortalUser;
|
||||
returnTo?: string;
|
||||
};
|
||||
if (!response.ok || !body?.user) {
|
||||
throw new ApiError(response.status, body?.message ?? '绑定失败');
|
||||
}
|
||||
return { user: body.user, returnTo: body.returnTo ?? '/' };
|
||||
}
|
||||
|
||||
export async function getWechatBindingStatus(): Promise<WechatBindingStatus> {
|
||||
try {
|
||||
const response = await fetch('/auth/wechat/status');
|
||||
if (!response.ok) return { enabled: false, bound: false };
|
||||
return (await response.json()) as WechatBindingStatus;
|
||||
} catch {
|
||||
return { enabled: false, bound: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function startWechatScanLogin(returnTo?: string): Promise<{
|
||||
state: string;
|
||||
qrUrl: string;
|
||||
expiresInMs: number;
|
||||
}> {
|
||||
const params = new URLSearchParams();
|
||||
if (returnTo) params.set('return_to', returnTo);
|
||||
const query = params.toString();
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`/auth/wechat/scan/start${query ? `?${query}` : ''}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
message?: string;
|
||||
state?: string;
|
||||
qrUrl?: string;
|
||||
expiresInMs?: number;
|
||||
};
|
||||
if (!response.ok || !body?.state || !body?.qrUrl) {
|
||||
throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
|
||||
}
|
||||
return {
|
||||
state: body.state,
|
||||
qrUrl: body.qrUrl,
|
||||
expiresInMs: body.expiresInMs ?? 600000,
|
||||
};
|
||||
}
|
||||
|
||||
export async function pollWechatScanLogin(state: string): Promise<{
|
||||
status: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
|
||||
pendingToken?: string;
|
||||
message?: string;
|
||||
}> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`/auth/wechat/scan/poll?state=${encodeURIComponent(state)}`);
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
status?: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
|
||||
pendingToken?: string;
|
||||
message?: string;
|
||||
};
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, body?.message ?? '扫码状态查询失败');
|
||||
}
|
||||
return {
|
||||
status: body?.status ?? 'error',
|
||||
pendingToken: body?.pendingToken,
|
||||
message: body?.message,
|
||||
};
|
||||
}
|
||||
|
||||
export async function register(
|
||||
username: string,
|
||||
password: string,
|
||||
|
||||
Reference in New Issue
Block a user