mirror of
https://github.com/beilunyang/moemail.git
synced 2025-12-24 11:30:51 +08:00
feat(auth): add Google OAuth support
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
AUTH_GITHUB_ID = ""
|
||||
AUTH_GITHUB_SECRET = ""
|
||||
AUTH_GOOGLE_ID = ""
|
||||
AUTH_GOOGLE_SECRET = ""
|
||||
AUTH_SECRET = ""
|
||||
|
||||
CLOUDFLARE_API_TOKEN = ""
|
||||
|
||||
28
README.md
28
README.md
@@ -22,6 +22,7 @@
|
||||
<a href="#OpenAPI">OpenAPI</a> •
|
||||
<a href="#环境变量">环境变量</a> •
|
||||
<a href="#Github OAuth App 配置">Github OAuth App 配置</a> •
|
||||
<a href="#Google OAuth App 配置">Google OAuth App 配置</a> •
|
||||
<a href="#贡献">贡献</a> •
|
||||
<a href="#许可证">许可证</a> •
|
||||
<a href="#交流群">交流群</a> •
|
||||
@@ -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` 为 `<your-app-name>`
|
||||
- 设置 `Homepage URL` 为 `https://<your-domain>`
|
||||
- 设置 `Authorization callback URL` 为 `https://<your-domain>/api/auth/callback/github`
|
||||
1. 登录 [Github Developer](https://github.com/settings/developers) 创建一个新的 OAuth App
|
||||
2. 生成一个新的 `Client ID` 和 `Client Secret`
|
||||
3. 配置参数:
|
||||
- `Application name`: `<your-app-name>`
|
||||
- `Homepage URL`: `https://<your-domain>`
|
||||
- `Authorization callback URL`: `https://<your-domain>/api/auth/callback/github`
|
||||
|
||||
## Google OAuth App 配置
|
||||
|
||||
1. 访问 [Google Cloud Console](https://console.cloud.google.com/) 创建项目
|
||||
2. 配置 OAuth 同意屏幕
|
||||
3. 创建 OAuth 客户端 ID
|
||||
- 应用类型:Web 应用
|
||||
- 已获授权的 Javascript 来源:`https://<your-domain>`
|
||||
- 已获授权的重定向 URI:`https://<your-domain>/api/auth/callback/google`
|
||||
4. 获取 `Client ID` 和 `Client Secret`
|
||||
5. 配置环境变量 `AUTH_GOOGLE_ID` 和 `AUTH_GOOGLE_SECRET`
|
||||
|
||||
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Pull Request 或者 Issue来帮助改进这个项目
|
||||
欢迎提交 Pull Request 或者 Issue 来帮助改进这个项目
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -200,6 +200,10 @@ export function LoginForm({ turnstile }: LoginFormProps) {
|
||||
signIn("github", { callbackUrl: "/" })
|
||||
}
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
signIn("google", { callbackUrl: "/" })
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-[95%] max-w-lg border-2 border-primary/20">
|
||||
<CardHeader className="space-y-2">
|
||||
@@ -297,6 +301,32 @@ export function LoginForm({ turnstile }: LoginFormProps) {
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
{t("actions.githubLogin")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={handleGoogleLogin}
|
||||
>
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
{t("actions.googleLogin")}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="register" className="space-y-4 mt-0">
|
||||
|
||||
@@ -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) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
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) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-bold truncate">{user.name}</h2>
|
||||
{
|
||||
user.email && (
|
||||
// 先简单实现,后续再完善
|
||||
<div className="flex items-center gap-1 text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full flex-shrink-0">
|
||||
<Github className="w-3 h-3" />
|
||||
{tAuth("linked")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{!!user?.providers?.length && (
|
||||
<div className="flex gap-2">
|
||||
{user.providers.map((provider) => {
|
||||
const config = providerConfigs[provider as keyof typeof providerConfigs]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
return (
|
||||
<div
|
||||
key={provider}
|
||||
className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded-full flex-shrink-0 ${config.className}`}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
{config.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate mt-1">
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"login": "ログイン",
|
||||
"register": "登録",
|
||||
"or": "または",
|
||||
"githubLogin": "GitHub アカウントでログイン"
|
||||
"githubLogin": "GitHub アカウントでログイン",
|
||||
"googleLogin": "Google アカウントでログイン"
|
||||
},
|
||||
"errors": {
|
||||
"usernameRequired": "ユーザー名を入力してください",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"login": "로그인",
|
||||
"register": "회원가입",
|
||||
"or": "또는",
|
||||
"githubLogin": "GitHub로 로그인"
|
||||
"githubLogin": "GitHub로 로그인",
|
||||
"googleLogin": "Google로 로그인"
|
||||
},
|
||||
"errors": {
|
||||
"usernameRequired": "사용자 이름을 입력해주세요",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"or": "或者",
|
||||
"githubLogin": "使用 GitHub 账号登录"
|
||||
"githubLogin": "使用 GitHub 账号登录",
|
||||
"googleLogin": "使用 Google 账号登录"
|
||||
},
|
||||
"errors": {
|
||||
"usernameRequired": "请输入用户名",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"or": "或者",
|
||||
"githubLogin": "使用 GitHub 帳號登入"
|
||||
"githubLogin": "使用 GitHub 帳號登入",
|
||||
"googleLogin": "使用 Google 帳號登入"
|
||||
},
|
||||
"errors": {
|
||||
"usernameRequired": "請輸入使用者名稱",
|
||||
|
||||
@@ -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"
|
||||
@@ -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("输入格式不正确")
|
||||
}
|
||||
@@ -212,6 +219,12 @@ export const {
|
||||
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
|
||||
|
||||
@@ -19,6 +19,10 @@ const nextConfig = {
|
||||
protocol: 'https',
|
||||
hostname: 'avatars.githubusercontent.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.googleusercontent.com',
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
1
types.d.ts
vendored
1
types.d.ts
vendored
@@ -22,6 +22,7 @@ declare module "next-auth" {
|
||||
interface User {
|
||||
roles?: { name: string }[]
|
||||
username?: string | null
|
||||
providers?: string[]
|
||||
}
|
||||
|
||||
interface Session {
|
||||
|
||||
Reference in New Issue
Block a user