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_ID = ""
|
||||||
AUTH_GITHUB_SECRET = ""
|
AUTH_GITHUB_SECRET = ""
|
||||||
|
AUTH_GOOGLE_ID = ""
|
||||||
|
AUTH_GOOGLE_SECRET = ""
|
||||||
AUTH_SECRET = ""
|
AUTH_SECRET = ""
|
||||||
|
|
||||||
CLOUDFLARE_API_TOKEN = ""
|
CLOUDFLARE_API_TOKEN = ""
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -22,6 +22,7 @@
|
|||||||
<a href="#OpenAPI">OpenAPI</a> •
|
<a href="#OpenAPI">OpenAPI</a> •
|
||||||
<a href="#环境变量">环境变量</a> •
|
<a href="#环境变量">环境变量</a> •
|
||||||
<a href="#Github OAuth App 配置">Github OAuth App 配置</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> •
|
<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_ID`: GitHub OAuth App ID
|
||||||
- `AUTH_GITHUB_SECRET`: GitHub OAuth App Secret
|
- `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,请设置一个随机字符串
|
- `AUTH_SECRET`: NextAuth Secret,用来加密 session,请设置一个随机字符串
|
||||||
|
|
||||||
### Cloudflare 配置
|
### Cloudflare 配置
|
||||||
@@ -796,16 +799,29 @@ console.log('分享链接:', `https://your-domain.com/shared/message/${data.toke
|
|||||||
|
|
||||||
## Github OAuth App 配置
|
## Github OAuth App 配置
|
||||||
|
|
||||||
- 登录 [Github Developer](https://github.com/settings/developers) 创建一个新的 OAuth App
|
1. 登录 [Github Developer](https://github.com/settings/developers) 创建一个新的 OAuth App
|
||||||
- 生成一个新的 `Client ID` 和 `Client Secret`
|
2. 生成一个新的 `Client ID` 和 `Client Secret`
|
||||||
- 设置 `Application name` 为 `<your-app-name>`
|
3. 配置参数:
|
||||||
- 设置 `Homepage URL` 为 `https://<your-domain>`
|
- `Application name`: `<your-app-name>`
|
||||||
- 设置 `Authorization callback URL` 为 `https://<your-domain>/api/auth/callback/github`
|
- `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: "/" })
|
signIn("github", { callbackUrl: "/" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGoogleLogin = () => {
|
||||||
|
signIn("google", { callbackUrl: "/" })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-[95%] max-w-lg border-2 border-primary/20">
|
<Card className="w-[95%] max-w-lg border-2 border-primary/20">
|
||||||
<CardHeader className="space-y-2">
|
<CardHeader className="space-y-2">
|
||||||
@@ -297,6 +301,32 @@ export function LoginForm({ turnstile }: LoginFormProps) {
|
|||||||
<Github className="mr-2 h-4 w-4" />
|
<Github className="mr-2 h-4 w-4" />
|
||||||
{t("actions.githubLogin")}
|
{t("actions.githubLogin")}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="register" className="space-y-4 mt-0">
|
<TabsContent value="register" className="space-y-4 mt-0">
|
||||||
|
|||||||
@@ -26,6 +26,38 @@ const roleConfigs = {
|
|||||||
civilian: { key: 'CIVILIAN', icon: User2 },
|
civilian: { key: 'CIVILIAN', icon: User2 },
|
||||||
} as const
|
} 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) {
|
export function ProfileCard({ user }: ProfileCardProps) {
|
||||||
const t = useTranslations("profile.card")
|
const t = useTranslations("profile.card")
|
||||||
const tAuth = useTranslations("auth.signButton")
|
const tAuth = useTranslations("auth.signButton")
|
||||||
@@ -56,15 +88,24 @@ export function ProfileCard({ user }: ProfileCardProps) {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-xl font-bold truncate">{user.name}</h2>
|
<h2 className="text-xl font-bold truncate">{user.name}</h2>
|
||||||
{
|
{!!user?.providers?.length && (
|
||||||
user.email && (
|
<div className="flex gap-2">
|
||||||
// 先简单实现,后续再完善
|
{user.providers.map((provider) => {
|
||||||
<div className="flex items-center gap-1 text-xs text-primary bg-primary/10 px-2 py-0.5 rounded-full flex-shrink-0">
|
const config = providerConfigs[provider as keyof typeof providerConfigs]
|
||||||
<Github className="w-3 h-3" />
|
if (!config) return null
|
||||||
{tAuth("linked")}
|
const Icon = config.icon
|
||||||
</div>
|
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>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground truncate mt-1">
|
<p className="text-sm text-muted-foreground truncate mt-1">
|
||||||
{
|
{
|
||||||
@@ -78,7 +119,7 @@ export function ProfileCard({ user }: ProfileCardProps) {
|
|||||||
const Icon = roleConfig.icon
|
const Icon = roleConfig.icon
|
||||||
const roleName = t(`roles.${roleConfig.key}` as any)
|
const roleName = t(`roles.${roleConfig.key}` as any)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={name}
|
||||||
className="flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-0.5 rounded"
|
className="flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-0.5 rounded"
|
||||||
title={roleName}
|
title={roleName}
|
||||||
@@ -110,15 +151,15 @@ export function ProfileCard({ user }: ProfileCardProps) {
|
|||||||
{canManageWebhook && <ApiKeyPanel />}
|
{canManageWebhook && <ApiKeyPanel />}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 px-1">
|
<div className="flex flex-col sm:flex-row gap-4 px-1">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push(`/${locale}/moe`)}
|
onClick={() => router.push(`/${locale}/moe`)}
|
||||||
className="gap-2 flex-1"
|
className="gap-2 flex-1"
|
||||||
>
|
>
|
||||||
<Mail className="w-4 h-4" />
|
<Mail className="w-4 h-4" />
|
||||||
{tNav("backToMailbox")}
|
{tNav("backToMailbox")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => signOut({ callbackUrl: `/${locale}` })}
|
onClick={() => signOut({ callbackUrl: `/${locale}` })}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register": "Sign Up",
|
"register": "Sign Up",
|
||||||
"or": "OR",
|
"or": "OR",
|
||||||
"githubLogin": "Login with GitHub"
|
"githubLogin": "Login with GitHub",
|
||||||
|
"googleLogin": "Login with Google"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"usernameRequired": "Please enter username",
|
"usernameRequired": "Please enter username",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"login": "ログイン",
|
"login": "ログイン",
|
||||||
"register": "登録",
|
"register": "登録",
|
||||||
"or": "または",
|
"or": "または",
|
||||||
"githubLogin": "GitHub アカウントでログイン"
|
"githubLogin": "GitHub アカウントでログイン",
|
||||||
|
"googleLogin": "Google アカウントでログイン"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"usernameRequired": "ユーザー名を入力してください",
|
"usernameRequired": "ユーザー名を入力してください",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"login": "로그인",
|
"login": "로그인",
|
||||||
"register": "회원가입",
|
"register": "회원가입",
|
||||||
"or": "또는",
|
"or": "또는",
|
||||||
"githubLogin": "GitHub로 로그인"
|
"githubLogin": "GitHub로 로그인",
|
||||||
|
"googleLogin": "Google로 로그인"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"usernameRequired": "사용자 이름을 입력해주세요",
|
"usernameRequired": "사용자 이름을 입력해주세요",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"login": "登录",
|
"login": "登录",
|
||||||
"register": "注册",
|
"register": "注册",
|
||||||
"or": "或者",
|
"or": "或者",
|
||||||
"githubLogin": "使用 GitHub 账号登录"
|
"githubLogin": "使用 GitHub 账号登录",
|
||||||
|
"googleLogin": "使用 Google 账号登录"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"usernameRequired": "请输入用户名",
|
"usernameRequired": "请输入用户名",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"login": "登入",
|
"login": "登入",
|
||||||
"register": "註冊",
|
"register": "註冊",
|
||||||
"or": "或者",
|
"or": "或者",
|
||||||
"githubLogin": "使用 GitHub 帳號登入"
|
"githubLogin": "使用 GitHub 帳號登入",
|
||||||
|
"googleLogin": "使用 Google 帳號登入"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"usernameRequired": "請輸入使用者名稱",
|
"usernameRequired": "請輸入使用者名稱",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import NextAuth from "next-auth"
|
import NextAuth from "next-auth"
|
||||||
import GitHub from "next-auth/providers/github"
|
import GitHub from "next-auth/providers/github"
|
||||||
|
import Google from "next-auth/providers/google"
|
||||||
import { DrizzleAdapter } from "@auth/drizzle-adapter"
|
import { DrizzleAdapter } from "@auth/drizzle-adapter"
|
||||||
import { createDb, Db } from "./db"
|
import { createDb, Db } from "./db"
|
||||||
import { accounts, users, roles, userRoles } from "./schema"
|
import { accounts, users, roles, userRoles } from "./schema"
|
||||||
@@ -30,7 +31,7 @@ const getDefaultRole = async (): Promise<Role> => {
|
|||||||
) {
|
) {
|
||||||
return defaultRole as Role
|
return defaultRole as Role
|
||||||
}
|
}
|
||||||
|
|
||||||
return ROLES.CIVILIAN
|
return ROLES.CIVILIAN
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +103,12 @@ export const {
|
|||||||
GitHub({
|
GitHub({
|
||||||
clientId: process.env.AUTH_GITHUB_ID,
|
clientId: process.env.AUTH_GITHUB_ID,
|
||||||
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
||||||
|
allowDangerousEmailAccountLinking: true,
|
||||||
|
}),
|
||||||
|
Google({
|
||||||
|
clientId: process.env.AUTH_GOOGLE_ID,
|
||||||
|
clientSecret: process.env.AUTH_GOOGLE_SECRET,
|
||||||
|
allowDangerousEmailAccountLinking: true,
|
||||||
}),
|
}),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
name: "Credentials",
|
name: "Credentials",
|
||||||
@@ -119,7 +126,7 @@ export const {
|
|||||||
let parsedCredentials: AuthSchema
|
let parsedCredentials: AuthSchema
|
||||||
try {
|
try {
|
||||||
parsedCredentials = authSchema.parse({ username, password, turnstileToken })
|
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) {
|
} catch (error) {
|
||||||
throw new Error("输入格式不正确")
|
throw new Error("输入格式不正确")
|
||||||
}
|
}
|
||||||
@@ -196,7 +203,7 @@ export const {
|
|||||||
where: eq(userRoles.userId, session.user.id),
|
where: eq(userRoles.userId, session.user.id),
|
||||||
with: { role: true },
|
with: { role: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!userRoleRecords.length) {
|
if (!userRoleRecords.length) {
|
||||||
const defaultRole = await getDefaultRole()
|
const defaultRole = await getDefaultRole()
|
||||||
const role = await findOrCreateRole(db, defaultRole)
|
const role = await findOrCreateRole(db, defaultRole)
|
||||||
@@ -208,10 +215,16 @@ export const {
|
|||||||
role: role
|
role: role
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
session.user.roles = userRoleRecords.map(ur => ({
|
session.user.roles = userRoleRecords.map(ur => ({
|
||||||
name: ur.role.name,
|
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
|
return session
|
||||||
@@ -224,7 +237,7 @@ export const {
|
|||||||
|
|
||||||
export async function register(username: string, password: string) {
|
export async function register(username: string, password: string) {
|
||||||
const db = createDb()
|
const db = createDb()
|
||||||
|
|
||||||
const existing = await db.query.users.findFirst({
|
const existing = await db.query.users.findFirst({
|
||||||
where: eq(users.username, username)
|
where: eq(users.username, username)
|
||||||
})
|
})
|
||||||
@@ -234,7 +247,7 @@ export async function register(username: string, password: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await hashPassword(password)
|
const hashedPassword = await hashPassword(password)
|
||||||
|
|
||||||
const [user] = await db.insert(users)
|
const [user] = await db.insert(users)
|
||||||
.values({
|
.values({
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ const nextConfig = {
|
|||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'avatars.githubusercontent.com',
|
hostname: 'avatars.githubusercontent.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '*.googleusercontent.com',
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -19,9 +23,19 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./app/*"]
|
"@/*": [
|
||||||
|
"./app/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
}
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"docs"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
types.d.ts
vendored
3
types.d.ts
vendored
@@ -22,8 +22,9 @@ declare module "next-auth" {
|
|||||||
interface User {
|
interface User {
|
||||||
roles?: { name: string }[]
|
roles?: { name: string }[]
|
||||||
username?: string | null
|
username?: string | null
|
||||||
|
providers?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Session {
|
interface Session {
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user