refactor(auth): migrate social login to next-auth

This commit is contained in:
2026-06-10 10:26:45 +08:00
parent 109a3e3855
commit a4b902893e
34 changed files with 572 additions and 1421 deletions
-39
View File
@@ -1,39 +0,0 @@
/**
* Facebook SDK 全局类型声明
*/
export {};
declare global {
interface Window {
FB?: {
init(params: {
appId: string;
cookie?: boolean;
xfbml?: boolean;
version: string;
}): void;
login(
callback: (response: {
authResponse?: {
accessToken: string;
userID: string;
expiresIn: number;
};
status?: "connected" | "not_authorized" | "unknown";
}) => void,
options?: { scope: string },
): void;
logout(callback?: () => void): void;
api(
path: string,
callback: (response: {
id?: string;
name?: string;
email?: string;
picture?: { data?: { url?: string } };
}) => void,
): void;
};
fbAsyncInit?: () => void;
}
}
-147
View File
@@ -1,147 +0,0 @@
/**
* Facebook SDK JS 封装
*
* 原始 Dart: `flutter_facebook_auth` 包
* `lib/data/services/auth/auth_platform.dart` + `auth_screen.dart` 中 Facebook 登录入口)。
*
* 浏览器方案:
* 1. 在 `app/layout.tsx` 通过 `<Script src="https://connect.facebook.net/en_US/sdk.js">` 注入。
* 2. 调用 `FB.login` / `FB.getUserData` 完成 OAuth + 用户信息获取。
* 3. TypeScript 类型声明在 `facebook-sdk.d.ts``window.FB`)。
*/
import { Result } from "@/utils/result";
declare global {
interface Window {
FB?: {
init(params: {
appId: string;
cookie?: boolean;
xfbml?: boolean;
version: string;
}): void;
login(
callback: (response: {
authResponse?: {
accessToken: string;
userID: string;
expiresIn: number;
};
status?: "connected" | "not_authorized" | "unknown";
}) => void,
options?: { scope: string },
): void;
logout(callback?: () => void): void;
api(
path: string,
callback: (response: {
id?: string;
name?: string;
email?: string;
picture?: { data?: { url?: string } };
}) => void,
): void;
};
fbAsyncInit?: () => void;
}
}
export interface FacebookUserData {
id: string;
name?: string;
email?: string;
pictureUrl?: string;
}
const FB_APP_ID = process.env.NEXT_PUBLIC_FACEBOOK_APP_ID ?? "";
const FB_VERSION = process.env.NEXT_PUBLIC_FACEBOOK_API_VERSION ?? "v18.0";
/**
* 初始化 FB SDK(在 `window.fbAsyncInit` 触发时调用一次)。
* 由 `app/layout.tsx` 的 inline script 触发。
*/
export function initFacebookSdk(): void {
if (typeof window === "undefined" || !window.FB) return;
window.FB.init({
appId: FB_APP_ID,
cookie: true,
xfbml: false,
version: FB_VERSION,
});
}
/**
* 触发 Facebook OAuth 登录。
* - 成功返回 `{ accessToken, userId }`
* - 取消 / 失败返回 `Result.err`
*/
export async function loginWithFacebook(): Promise<
Result<{ accessToken: string; userId: string }>
> {
if (typeof window === "undefined" || !window.FB) {
return Result.err(new Error("Facebook SDK not loaded"));
}
return new Promise((resolve) => {
window.FB!.login(
(response) => {
if (!response.authResponse) {
resolve(
Result.err(new Error("Facebook login cancelled or failed")),
);
return;
}
resolve(
Result.ok({
accessToken: response.authResponse.accessToken,
userId: response.authResponse.userID,
}),
);
},
{ scope: "email,public_profile" },
);
});
}
/**
* 拉取 Facebook 用户信息(头像、姓名等)。
*/
export async function getFacebookUserData(): Promise<
Result<FacebookUserData>
> {
if (typeof window === "undefined" || !window.FB) {
return Result.err(new Error("Facebook SDK not loaded"));
}
return new Promise((resolve) => {
window.FB!.api("/me", (response) => {
if (!response.id) {
resolve(Result.err(new Error("No Facebook user data")));
return;
}
// 单独拉取头像
window.FB!.api(
"/me/picture?type=large&redirect=false",
(picResp: unknown) => {
const url =
picResp &&
typeof picResp === "object" &&
"data" in picResp &&
picResp.data &&
typeof picResp.data === "object" &&
"url" in picResp.data &&
typeof picResp.data.url === "string"
? picResp.data.url
: undefined;
resolve(
Result.ok({
id: response.id!,
name: response.name!,
email: response.email!,
pictureUrl: url,
}),
);
},
);
});
});
}
-36
View File
@@ -1,36 +0,0 @@
/**
* Google Identity Services 全局类型声明
*/
export {};
declare global {
interface Window {
google?: {
accounts: {
id: {
initialize(params: {
client_id: string;
callback: (response: { credential?: string; select_by?: string }) => void;
auto_select?: boolean;
cancel_on_tap_outside?: boolean;
}): void;
prompt(): void;
renderButton(
parent: HTMLElement,
options: {
type?: "standard" | "icon";
theme?: "outline" | "filled_blue" | "filled_black";
size?: "large" | "medium" | "small";
text?: "signin_with" | "signup_with" | "continue_with" | "signin";
shape?: "rectangular" | "pill" | "circle" | "square";
logo_alignment?: "left" | "center";
width?: number;
locale?: string;
},
): void;
disableAutoSelect(): void;
};
};
};
}
}
-72
View File
@@ -1,72 +0,0 @@
/**
* Google Identity Services (GIS) 封装
*
* 原始 Dart: `google_sign_in_web` 包
* `lib/ui/auth/widgets/google_sign_in_web.dart` + `auth_bloc._onWebGoogleLoginSuccess`)。
*
* 浏览器方案:
* 1. `<Script src="https://accounts.google.com/gsi/client">` 在 layout 注入。
* 2. `renderButton(element, options)` 渲染 GIS 按钮(自动处理 OAuth 流程)。
* 3. 登录成功 callback 回调 `onGoogleSignInSuccess(idToken, email)`。
* 4. 把 idToken 传给 `authRepository.googleLogin`。
*
* 全局类型:`google.accounts.id` 在 `google-identity.d.ts` 中声明。
*/
import { Result } from "@/utils/result";
export interface GoogleSignInSuccess {
idToken: string;
email: string;
}
let _onSuccess: ((s: GoogleSignInSuccess) => void) | null = null;
/** 设置全局成功回调(auth screen 在 mount 时设置,unmount 时清空)。 */
export function setOnGoogleSignInSuccess(
cb: ((s: GoogleSignInSuccess) => void) | null,
): void {
_onSuccess = cb;
}
/** 渲染 GIS 按钮到指定 DOM 元素。 */
export function renderGoogleButton(
element: HTMLElement,
options: { theme?: "outline" | "filled_blue" | "filled_black"; size?: "large" | "medium" | "small"; text?: "signin_with" | "signup_with" | "continue_with" | "signin" } = {},
): void {
if (typeof window === "undefined" || !window.google?.accounts?.id) return;
window.google.accounts.id.renderButton(element, {
type: "standard",
theme: options.theme ?? "outline",
size: options.size ?? "large",
text: options.text ?? "continue_with",
shape: "pill",
width: element.clientWidth || 320,
});
}
/** 触发 GIS One Tap 提示(如果用户已登录过 Google)。 */
export function promptGoogleOneTap(): Result<true> {
if (typeof window === "undefined" || !window.google?.accounts?.id) {
return Result.err(new Error("Google Identity Services not loaded"));
}
window.google.accounts.id.prompt();
return Result.ok(true);
}
/** 处理 GIS 凭据回调(由 GIS 内部调用)。 */
export function handleGoogleCredentialResponse(
response: { credential?: string; select_by?: string },
): Result<GoogleSignInSuccess> {
if (!response.credential) {
return Result.err(new Error("No Google credential returned"));
}
// GIS 凭据是 JWT;本仓库不解析(idToken 直接交给后端校验),
// email 字段需前端手动解码或由后端从 idToken 提取。
// 此处仅返回 idTokenemail 留空(后端会从 idToken 还原)。
const success: GoogleSignInSuccess = {
idToken: response.credential,
email: "",
};
_onSuccess?.(success);
return Result.ok(success);
}
-4
View File
@@ -4,9 +4,5 @@
export * from "./browser-detect";
export * from "./chat-websocket";
export * from "./facebook-sdk";
export * from "./facebook-sdk";
export * from "./google-identity";
export * from "./google-identity";
export * from "./media-recorder";
export * from "./message-queue";