|
|
@@ -0,0 +1,100 @@
|
|
|
+/**
|
|
|
+ * OAuth 社交登录提供者 - mock / google / facebook 适配器
|
|
|
+ * 真实渠道凭据经环境变量注入;未配置返回错误提示
|
|
|
+ */
|
|
|
+
|
|
|
+export interface OAuthUserInfo {
|
|
|
+ provider: 'google' | 'facebook' | 'mock';
|
|
|
+ providerId: string;
|
|
|
+ email: string;
|
|
|
+ name: string;
|
|
|
+}
|
|
|
+
|
|
|
+export interface OAuthProvider {
|
|
|
+ name: string;
|
|
|
+ /** 用授权码换取用户信息(真实渠道走 token+userinfo;mock 直接返回演示用户) */
|
|
|
+ exchangeCode(code: string): Promise<OAuthUserInfo>;
|
|
|
+}
|
|
|
+
|
|
|
+/** 演示用 mock 提供者(本地验证完整 OAuth 闭环) */
|
|
|
+class MockProvider implements OAuthProvider {
|
|
|
+ name = 'mock';
|
|
|
+ async exchangeCode(_code: string): Promise<OAuthUserInfo> {
|
|
|
+ return {
|
|
|
+ provider: 'mock',
|
|
|
+ providerId: 'mock-social-001',
|
|
|
+ email: 'social.demo@dotouch.ai',
|
|
|
+ name: 'Social Demo',
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Google OAuth 提供者(真实实现,需 GOOGLE_CLIENT_ID/SECRET) */
|
|
|
+class GoogleProvider implements OAuthProvider {
|
|
|
+ name = 'google';
|
|
|
+ async exchangeCode(code: string): Promise<OAuthUserInfo> {
|
|
|
+ const clientId = process.env['GOOGLE_CLIENT_ID'];
|
|
|
+ const secret = process.env['GOOGLE_CLIENT_SECRET'];
|
|
|
+ const redirectUri = process.env['GOOGLE_REDIRECT_URI'];
|
|
|
+ if (!clientId || !secret) {
|
|
|
+ throw new Error('GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET 未配置');
|
|
|
+ }
|
|
|
+ const tokenResp = await fetch('https://oauth2.googleapis.com/token', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
+ body: new URLSearchParams({
|
|
|
+ code,
|
|
|
+ client_id: clientId,
|
|
|
+ client_secret: secret,
|
|
|
+ redirect_uri: redirectUri ?? `${process.env['NEXT_PUBLIC_SITE_URL'] ?? 'http://localhost:3000'}/vi/oauth-callback`,
|
|
|
+ grant_type: 'authorization_code',
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ if (!tokenResp.ok) throw new Error('Google token 交换失败');
|
|
|
+ const token = (await tokenResp.json()) as { access_token: string };
|
|
|
+ const userResp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
|
+ headers: { Authorization: `Bearer ${token.access_token}` },
|
|
|
+ });
|
|
|
+ if (!userResp.ok) throw new Error('Google userinfo 获取失败');
|
|
|
+ const user = (await userResp.json()) as { id: string; email: string; name: string };
|
|
|
+ return { provider: 'google', providerId: user.id, email: user.email, name: user.name };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Facebook OAuth 提供者(真实实现,需 FACEBOOK_APP_ID/SECRET) */
|
|
|
+class FacebookProvider implements OAuthProvider {
|
|
|
+ name = 'facebook';
|
|
|
+ async exchangeCode(code: string): Promise<OAuthUserInfo> {
|
|
|
+ const appId = process.env['FACEBOOK_APP_ID'];
|
|
|
+ const secret = process.env['FACEBOOK_APP_SECRET'];
|
|
|
+ const redirectUri = process.env['FACEBOOK_REDIRECT_URI'];
|
|
|
+ if (!appId || !secret) {
|
|
|
+ throw new Error('FACEBOOK_APP_ID/FACEBOOK_APP_SECRET 未配置');
|
|
|
+ }
|
|
|
+ const tokenResp = await fetch(
|
|
|
+ `https://graph.facebook.com/v19.0/oauth/access_token?client_id=${appId}&client_secret=${secret}&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(redirectUri ?? '')}`,
|
|
|
+ );
|
|
|
+ if (!tokenResp.ok) throw new Error('Facebook token 交换失败');
|
|
|
+ const token = (await tokenResp.json()) as { access_token: string };
|
|
|
+ const userResp = await fetch(
|
|
|
+ `https://graph.facebook.com/me?fields=id,email,name&access_token=${token.access_token}`,
|
|
|
+ );
|
|
|
+ if (!userResp.ok) throw new Error('Facebook userinfo 获取失败');
|
|
|
+ const user = (await userResp.json()) as { id: string; email: string; name: string };
|
|
|
+ return { provider: 'facebook', providerId: user.id, email: user.email, name: user.name };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 按名称获取提供者(whitelist 校验) */
|
|
|
+export function getOAuthProvider(name: string): OAuthProvider {
|
|
|
+ switch (name) {
|
|
|
+ case 'mock':
|
|
|
+ return new MockProvider();
|
|
|
+ case 'google':
|
|
|
+ return new GoogleProvider();
|
|
|
+ case 'facebook':
|
|
|
+ return new FacebookProvider();
|
|
|
+ default:
|
|
|
+ throw new Error(`不支持的 OAuth 渠道: ${name}`);
|
|
|
+ }
|
|
|
+}
|