From dd109a464acc7aacc21cf87d300824258723bb23 Mon Sep 17 00:00:00 2001 From: beilunyang <786220806@qq.com> Date: Sun, 7 Dec 2025 17:50:27 +0800 Subject: [PATCH] feat(auth): add Google OAuth support --- .env.example | 2 + README.md | 28 ++++++++--- app/components/auth/login-form.tsx | 30 +++++++++++ app/components/profile/profile-card.tsx | 67 ++++++++++++++++++++----- app/i18n/messages/en/auth.json | 3 +- app/i18n/messages/ja/auth.json | 3 +- app/i18n/messages/ko/auth.json | 3 +- app/i18n/messages/zh-CN/auth.json | 3 +- app/i18n/messages/zh-TW/auth.json | 3 +- app/lib/auth.ts | 25 ++++++--- next.config.ts | 4 ++ tsconfig.json | 24 +++++++-- types.d.ts | 3 +- 13 files changed, 162 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index 47684bd..4ae8726 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ AUTH_GITHUB_ID = "" AUTH_GITHUB_SECRET = "" +AUTH_GOOGLE_ID = "" +AUTH_GOOGLE_SECRET = "" AUTH_SECRET = "" CLOUDFLARE_API_TOKEN = "" diff --git a/README.md b/README.md index 9765d0f..34fd98a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ OpenAPI • 环境变量 • Github OAuth App 配置 • + Google OAuth App 配置 • 贡献 • 许可证 • 交流群 • @@ -782,6 +783,8 @@ console.log('分享链接:', `https://your-domain.com/shared/message/${data.toke ### 认证相关 - `AUTH_GITHUB_ID`: GitHub OAuth App ID - `AUTH_GITHUB_SECRET`: GitHub OAuth App Secret +- `AUTH_GOOGLE_ID`: Google OAuth App ID +- `AUTH_GOOGLE_SECRET`: Google OAuth App Secret - `AUTH_SECRET`: NextAuth Secret,用来加密 session,请设置一个随机字符串 ### Cloudflare 配置 @@ -796,16 +799,29 @@ console.log('分享链接:', `https://your-domain.com/shared/message/${data.toke ## Github OAuth App 配置 -- 登录 [Github Developer](https://github.com/settings/developers) 创建一个新的 OAuth App -- 生成一个新的 `Client ID` 和 `Client Secret` -- 设置 `Application name` 为 `` -- 设置 `Homepage URL` 为 `https://` -- 设置 `Authorization callback URL` 为 `https:///api/auth/callback/github` +1. 登录 [Github Developer](https://github.com/settings/developers) 创建一个新的 OAuth App +2. 生成一个新的 `Client ID` 和 `Client Secret` +3. 配置参数: + - `Application name`: `` + - `Homepage URL`: `https://` + - `Authorization callback URL`: `https:///api/auth/callback/github` + +## Google OAuth App 配置 + +1. 访问 [Google Cloud Console](https://console.cloud.google.com/) 创建项目 +2. 配置 OAuth 同意屏幕 +3. 创建 OAuth 客户端 ID + - 应用类型:Web 应用 + - 已获授权的 Javascript 来源:`https://` + - 已获授权的重定向 URI:`https:///api/auth/callback/google` +4. 获取 `Client ID` 和 `Client Secret` +5. 配置环境变量 `AUTH_GOOGLE_ID` 和 `AUTH_GOOGLE_SECRET` + ## 贡献 -欢迎提交 Pull Request 或者 Issue来帮助改进这个项目 +欢迎提交 Pull Request 或者 Issue 来帮助改进这个项目 ## 许可证 diff --git a/app/components/auth/login-form.tsx b/app/components/auth/login-form.tsx index e2374b9..5fd5de2 100644 --- a/app/components/auth/login-form.tsx +++ b/app/components/auth/login-form.tsx @@ -200,6 +200,10 @@ export function LoginForm({ turnstile }: LoginFormProps) { signIn("github", { callbackUrl: "/" }) } + const handleGoogleLogin = () => { + signIn("google", { callbackUrl: "/" }) + } + return ( @@ -297,6 +301,32 @@ export function LoginForm({ turnstile }: LoginFormProps) { {t("actions.githubLogin")} + + + + + + + + + {t("actions.googleLogin")} + diff --git a/app/components/profile/profile-card.tsx b/app/components/profile/profile-card.tsx index 1798d4c..e4705e6 100644 --- a/app/components/profile/profile-card.tsx +++ b/app/components/profile/profile-card.tsx @@ -26,6 +26,38 @@ const roleConfigs = { civilian: { key: 'CIVILIAN', icon: User2 }, } as const +const providerConfigs = { + google: { + label: "Google", + className: "text-red-500 bg-red-500/10", + icon: (props: any) => ( + + + + + + + ), + }, + github: { + label: "GitHub", + className: "text-primary bg-primary/10", + icon: Github, + }, +} as const + export function ProfileCard({ user }: ProfileCardProps) { const t = useTranslations("profile.card") const tAuth = useTranslations("auth.signButton") @@ -56,15 +88,24 @@ export function ProfileCard({ user }: ProfileCardProps) { {user.name} - { - user.email && ( - // 先简单实现,后续再完善 - - - {tAuth("linked")} - - ) - } + {!!user?.providers?.length && ( + + {user.providers.map((provider) => { + const config = providerConfigs[provider as keyof typeof providerConfigs] + if (!config) return null + const Icon = config.icon + return ( + + + {config.label} + + ) + })} + + )} { @@ -78,7 +119,7 @@ export function ProfileCard({ user }: ProfileCardProps) { const Icon = roleConfig.icon const roleName = t(`roles.${roleConfig.key}` as any) return ( - } - router.push(`/${locale}/moe`)} className="gap-2 flex-1" > {tNav("backToMailbox")} - signOut({ callbackUrl: `/${locale}` })} className="flex-1" > diff --git a/app/i18n/messages/en/auth.json b/app/i18n/messages/en/auth.json index ebc2ed1..d2636db 100644 --- a/app/i18n/messages/en/auth.json +++ b/app/i18n/messages/en/auth.json @@ -21,7 +21,8 @@ "login": "Login", "register": "Sign Up", "or": "OR", - "githubLogin": "Login with GitHub" + "githubLogin": "Login with GitHub", + "googleLogin": "Login with Google" }, "errors": { "usernameRequired": "Please enter username", diff --git a/app/i18n/messages/ja/auth.json b/app/i18n/messages/ja/auth.json index a22cb09..00e7c5d 100644 --- a/app/i18n/messages/ja/auth.json +++ b/app/i18n/messages/ja/auth.json @@ -21,7 +21,8 @@ "login": "ログイン", "register": "登録", "or": "または", - "githubLogin": "GitHub アカウントでログイン" + "githubLogin": "GitHub アカウントでログイン", + "googleLogin": "Google アカウントでログイン" }, "errors": { "usernameRequired": "ユーザー名を入力してください", diff --git a/app/i18n/messages/ko/auth.json b/app/i18n/messages/ko/auth.json index 7525132..1a3feff 100644 --- a/app/i18n/messages/ko/auth.json +++ b/app/i18n/messages/ko/auth.json @@ -21,7 +21,8 @@ "login": "로그인", "register": "회원가입", "or": "또는", - "githubLogin": "GitHub로 로그인" + "githubLogin": "GitHub로 로그인", + "googleLogin": "Google로 로그인" }, "errors": { "usernameRequired": "사용자 이름을 입력해주세요", diff --git a/app/i18n/messages/zh-CN/auth.json b/app/i18n/messages/zh-CN/auth.json index ef9260a..4e3183e 100644 --- a/app/i18n/messages/zh-CN/auth.json +++ b/app/i18n/messages/zh-CN/auth.json @@ -21,7 +21,8 @@ "login": "登录", "register": "注册", "or": "或者", - "githubLogin": "使用 GitHub 账号登录" + "githubLogin": "使用 GitHub 账号登录", + "googleLogin": "使用 Google 账号登录" }, "errors": { "usernameRequired": "请输入用户名", diff --git a/app/i18n/messages/zh-TW/auth.json b/app/i18n/messages/zh-TW/auth.json index 77a694f..105ff78 100644 --- a/app/i18n/messages/zh-TW/auth.json +++ b/app/i18n/messages/zh-TW/auth.json @@ -21,7 +21,8 @@ "login": "登入", "register": "註冊", "or": "或者", - "githubLogin": "使用 GitHub 帳號登入" + "githubLogin": "使用 GitHub 帳號登入", + "googleLogin": "使用 Google 帳號登入" }, "errors": { "usernameRequired": "請輸入使用者名稱", diff --git a/app/lib/auth.ts b/app/lib/auth.ts index e0d330c..916d09f 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -1,5 +1,6 @@ import NextAuth from "next-auth" import GitHub from "next-auth/providers/github" +import Google from "next-auth/providers/google" import { DrizzleAdapter } from "@auth/drizzle-adapter" import { createDb, Db } from "./db" import { accounts, users, roles, userRoles } from "./schema" @@ -30,7 +31,7 @@ const getDefaultRole = async (): Promise => { ) { return defaultRole as Role } - + return ROLES.CIVILIAN } @@ -102,6 +103,12 @@ export const { GitHub({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET, + allowDangerousEmailAccountLinking: true, + }), + Google({ + clientId: process.env.AUTH_GOOGLE_ID, + clientSecret: process.env.AUTH_GOOGLE_SECRET, + allowDangerousEmailAccountLinking: true, }), CredentialsProvider({ name: "Credentials", @@ -119,7 +126,7 @@ export const { let parsedCredentials: AuthSchema try { parsedCredentials = authSchema.parse({ username, password, turnstileToken }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { throw new Error("输入格式不正确") } @@ -196,7 +203,7 @@ export const { where: eq(userRoles.userId, session.user.id), with: { role: true }, }) - + if (!userRoleRecords.length) { const defaultRole = await getDefaultRole() const role = await findOrCreateRole(db, defaultRole) @@ -208,10 +215,16 @@ export const { role: role }] } - + session.user.roles = userRoleRecords.map(ur => ({ name: ur.role.name, })) + + const userAccounts = await db.query.accounts.findMany({ + where: eq(accounts.userId, session.user.id), + }) + + session.user.providers = userAccounts.map(account => account.provider) } return session @@ -224,7 +237,7 @@ export const { export async function register(username: string, password: string) { const db = createDb() - + const existing = await db.query.users.findFirst({ where: eq(users.username, username) }) @@ -234,7 +247,7 @@ export async function register(username: string, password: string) { } const hashedPassword = await hashPassword(password) - + const [user] = await db.insert(users) .values({ username, diff --git a/next.config.ts b/next.config.ts index ba73c25..ade5284 100644 --- a/next.config.ts +++ b/next.config.ts @@ -19,6 +19,10 @@ const nextConfig = { protocol: 'https', hostname: 'avatars.githubusercontent.com', }, + { + protocol: 'https', + hostname: '*.googleusercontent.com', + } ], }, }; diff --git a/tsconfig.json b/tsconfig.json index b52a47b..71f39fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -19,9 +23,19 @@ } ], "paths": { - "@/*": ["./app/*"] + "@/*": [ + "./app/*" + ] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules", + "docs" + ] +} \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 0e53c84..06ba274 100644 --- a/types.d.ts +++ b/types.d.ts @@ -22,8 +22,9 @@ declare module "next-auth" { interface User { roles?: { name: string }[] username?: string | null + providers?: string[] } - + interface Session { user: User }
{ @@ -78,7 +119,7 @@ export function ProfileCard({ user }: ProfileCardProps) { const Icon = roleConfig.icon const roleName = t(`roles.${roleConfig.key}` as any) return ( -