mirror of
https://github.com/beilunyang/moemail.git
synced 2025-12-24 11:30:51 +08:00
260 lines
7.2 KiB
TypeScript
260 lines
7.2 KiB
TypeScript
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"
|
|
import { eq } from "drizzle-orm"
|
|
import { getRequestContext } from "@cloudflare/next-on-pages"
|
|
import { Permission, hasPermission, ROLES, Role } from "./permissions"
|
|
import CredentialsProvider from "next-auth/providers/credentials"
|
|
import { hashPassword, comparePassword } from "@/lib/utils"
|
|
import { authSchema, AuthSchema } from "@/lib/validation"
|
|
import { generateAvatarUrl } from "./avatar"
|
|
import { getUserId } from "./apiKey"
|
|
import { verifyTurnstileToken } from "./turnstile"
|
|
|
|
const ROLE_DESCRIPTIONS: Record<Role, string> = {
|
|
[ROLES.EMPEROR]: "皇帝(网站所有者)",
|
|
[ROLES.DUKE]: "公爵(超级用户)",
|
|
[ROLES.KNIGHT]: "骑士(高级用户)",
|
|
[ROLES.CIVILIAN]: "平民(普通用户)",
|
|
}
|
|
|
|
const getDefaultRole = async (): Promise<Role> => {
|
|
const defaultRole = await getRequestContext().env.SITE_CONFIG.get("DEFAULT_ROLE")
|
|
|
|
if (
|
|
defaultRole === ROLES.DUKE ||
|
|
defaultRole === ROLES.KNIGHT ||
|
|
defaultRole === ROLES.CIVILIAN
|
|
) {
|
|
return defaultRole as Role
|
|
}
|
|
|
|
return ROLES.CIVILIAN
|
|
}
|
|
|
|
async function findOrCreateRole(db: Db, roleName: Role) {
|
|
let role = await db.query.roles.findFirst({
|
|
where: eq(roles.name, roleName),
|
|
})
|
|
|
|
if (!role) {
|
|
const [newRole] = await db.insert(roles)
|
|
.values({
|
|
name: roleName,
|
|
description: ROLE_DESCRIPTIONS[roleName],
|
|
})
|
|
.returning()
|
|
role = newRole
|
|
}
|
|
|
|
return role
|
|
}
|
|
|
|
export async function assignRoleToUser(db: Db, userId: string, roleId: string) {
|
|
await db.delete(userRoles)
|
|
.where(eq(userRoles.userId, userId))
|
|
|
|
await db.insert(userRoles)
|
|
.values({
|
|
userId,
|
|
roleId,
|
|
})
|
|
}
|
|
|
|
export async function getUserRole(userId: string) {
|
|
const db = createDb()
|
|
const userRoleRecords = await db.query.userRoles.findMany({
|
|
where: eq(userRoles.userId, userId),
|
|
with: { role: true },
|
|
})
|
|
return userRoleRecords[0].role.name
|
|
}
|
|
|
|
export async function checkPermission(permission: Permission) {
|
|
const userId = await getUserId()
|
|
|
|
if (!userId) return false
|
|
|
|
const db = createDb()
|
|
const userRoleRecords = await db.query.userRoles.findMany({
|
|
where: eq(userRoles.userId, userId),
|
|
with: { role: true },
|
|
})
|
|
|
|
const userRoleNames = userRoleRecords.map(ur => ur.role.name)
|
|
return hasPermission(userRoleNames as Role[], permission)
|
|
}
|
|
|
|
export const {
|
|
handlers: { GET, POST },
|
|
auth,
|
|
signIn,
|
|
signOut
|
|
} = NextAuth(() => ({
|
|
secret: process.env.AUTH_SECRET,
|
|
adapter: DrizzleAdapter(createDb(), {
|
|
usersTable: users,
|
|
accountsTable: accounts,
|
|
}),
|
|
providers: [
|
|
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",
|
|
credentials: {
|
|
username: { label: "用户名", type: "text", placeholder: "请输入用户名" },
|
|
password: { label: "密码", type: "password", placeholder: "请输入密码" },
|
|
},
|
|
async authorize(credentials) {
|
|
if (!credentials) {
|
|
throw new Error("请输入用户名和密码")
|
|
}
|
|
|
|
const { username, password, turnstileToken } = credentials as Record<string, string | undefined>
|
|
|
|
let parsedCredentials: AuthSchema
|
|
try {
|
|
parsedCredentials = authSchema.parse({ username, password, turnstileToken })
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
} catch (error) {
|
|
throw new Error("输入格式不正确")
|
|
}
|
|
|
|
const verification = await verifyTurnstileToken(parsedCredentials.turnstileToken)
|
|
if (!verification.success) {
|
|
if (verification.reason === "missing-token") {
|
|
throw new Error("请先完成安全验证")
|
|
}
|
|
throw new Error("安全验证未通过")
|
|
}
|
|
|
|
const db = createDb()
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.username, parsedCredentials.username),
|
|
})
|
|
|
|
if (!user) {
|
|
throw new Error("用户名或密码错误")
|
|
}
|
|
|
|
const isValid = await comparePassword(parsedCredentials.password, user.password as string)
|
|
if (!isValid) {
|
|
throw new Error("用户名或密码错误")
|
|
}
|
|
|
|
return {
|
|
...user,
|
|
password: undefined,
|
|
}
|
|
},
|
|
}),
|
|
],
|
|
events: {
|
|
async signIn({ user }) {
|
|
if (!user.id) return
|
|
|
|
try {
|
|
const db = createDb()
|
|
const existingRole = await db.query.userRoles.findFirst({
|
|
where: eq(userRoles.userId, user.id),
|
|
})
|
|
|
|
if (existingRole) return
|
|
|
|
const defaultRole = await getDefaultRole()
|
|
const role = await findOrCreateRole(db, defaultRole)
|
|
await assignRoleToUser(db, user.id, role.id)
|
|
} catch (error) {
|
|
console.error('Error assigning role:', error)
|
|
}
|
|
},
|
|
},
|
|
callbacks: {
|
|
async jwt({ token, user }) {
|
|
if (user) {
|
|
token.id = user.id
|
|
token.name = user.name || user.username
|
|
token.username = user.username
|
|
token.image = user.image || generateAvatarUrl(token.name as string)
|
|
}
|
|
return token
|
|
},
|
|
async session({ session, token }) {
|
|
if (token && session.user) {
|
|
session.user.id = token.id as string
|
|
session.user.name = token.name as string
|
|
session.user.username = token.username as string
|
|
session.user.image = token.image as string
|
|
|
|
const db = createDb()
|
|
let userRoleRecords = await db.query.userRoles.findMany({
|
|
where: eq(userRoles.userId, session.user.id),
|
|
with: { role: true },
|
|
})
|
|
|
|
if (!userRoleRecords.length) {
|
|
const defaultRole = await getDefaultRole()
|
|
const role = await findOrCreateRole(db, defaultRole)
|
|
await assignRoleToUser(db, session.user.id, role.id)
|
|
userRoleRecords = [{
|
|
userId: session.user.id,
|
|
roleId: role.id,
|
|
createdAt: new Date(),
|
|
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
|
|
},
|
|
},
|
|
session: {
|
|
strategy: "jwt",
|
|
},
|
|
}))
|
|
|
|
export async function register(username: string, password: string) {
|
|
const db = createDb()
|
|
|
|
const existing = await db.query.users.findFirst({
|
|
where: eq(users.username, username)
|
|
})
|
|
|
|
if (existing) {
|
|
throw new Error("用户名已存在")
|
|
}
|
|
|
|
const hashedPassword = await hashPassword(password)
|
|
|
|
const [user] = await db.insert(users)
|
|
.values({
|
|
username,
|
|
password: hashedPassword,
|
|
})
|
|
.returning()
|
|
|
|
return user
|
|
}
|