mirror of
https://github.com/beilunyang/moemail.git
synced 2026-06-28 02:42:56 +08:00
feat: Implement role-based access control and enhance permissions system
This commit is contained in:
132
app/lib/auth.ts
132
app/lib/auth.ts
@@ -1,27 +1,127 @@
|
||||
import NextAuth from "next-auth"
|
||||
import GitHub from "next-auth/providers/github"
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter"
|
||||
import { createDb } from "./db"
|
||||
import { accounts, sessions, users } from "./schema"
|
||||
import { createDb, Db } from "./db"
|
||||
import { accounts, sessions, users, roles, userRoles } from "./schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Permission, hasPermission, ROLES, Role } from "./permissions"
|
||||
|
||||
const ROLE_DESCRIPTIONS: Record<Role, string> = {
|
||||
[ROLES.EMPEROR]: "皇帝(网站所有者)",
|
||||
[ROLES.KNIGHT]: "骑士(高级用户)",
|
||||
[ROLES.CIVILIAN]: "平民(普通用户)",
|
||||
}
|
||||
|
||||
const getDefaultRole = (): Role =>
|
||||
process.env.OPEN_REGISTRATION === 'true' ? ROLES.KNIGHT : 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
|
||||
}
|
||||
|
||||
async function assignRoleToUser(db: Db, userId: string, roleId: string) {
|
||||
await db.insert(userRoles)
|
||||
.values({
|
||||
userId,
|
||||
roleId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkPermission(permission: Permission) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return false
|
||||
|
||||
const db = createDb()
|
||||
const userRoleRecords = await db.query.userRoles.findMany({
|
||||
where: eq(userRoles.userId, session.user.id),
|
||||
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(() => {
|
||||
return {
|
||||
secret: process.env.AUTH_SECRET,
|
||||
adapter: DrizzleAdapter(createDb(), {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
}),
|
||||
providers: [
|
||||
GitHub({
|
||||
clientId: process.env.AUTH_GITHUB_ID,
|
||||
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
||||
} = NextAuth(() => ({
|
||||
secret: process.env.AUTH_SECRET,
|
||||
adapter: DrizzleAdapter(createDb(), {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
}),
|
||||
providers: [
|
||||
GitHub({
|
||||
clientId: process.env.AUTH_GITHUB_ID,
|
||||
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
||||
})
|
||||
],
|
||||
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 findOrCreateRole(db, getDefaultRole())
|
||||
await assignRoleToUser(db, user.id, defaultRole.id)
|
||||
} catch (error) {
|
||||
console.error('Error assigning role:', error)
|
||||
}
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/",
|
||||
error: "/",
|
||||
},
|
||||
callbacks: {
|
||||
async session({ session, user }) {
|
||||
if (!session?.user) return session
|
||||
|
||||
const db = createDb()
|
||||
let userRoleRecords = await db.query.userRoles.findMany({
|
||||
where: eq(userRoles.userId, user.id),
|
||||
with: { role: true },
|
||||
})
|
||||
],
|
||||
|
||||
if (!userRoleRecords.length) {
|
||||
const defaultRole = await findOrCreateRole(db, getDefaultRole())
|
||||
await assignRoleToUser(db, user.id, defaultRole.id)
|
||||
userRoleRecords = [{
|
||||
userId: user.id,
|
||||
roleId: defaultRole.id,
|
||||
createdAt: new Date(),
|
||||
role: defaultRole
|
||||
}]
|
||||
}
|
||||
|
||||
session.user.roles = userRoleRecords.map(ur => ({
|
||||
name: ur.role.name,
|
||||
}))
|
||||
|
||||
return session
|
||||
},
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -2,4 +2,6 @@ import { getRequestContext } from "@cloudflare/next-on-pages"
|
||||
import { drizzle } from "drizzle-orm/d1"
|
||||
import * as schema from "./schema"
|
||||
|
||||
export const createDb = () => drizzle(getRequestContext().env.DB, { schema })
|
||||
export const createDb = () => drizzle(getRequestContext().env.DB, { schema })
|
||||
|
||||
export type Db = ReturnType<typeof createDb>
|
||||
|
||||
28
app/lib/permissions.ts
Normal file
28
app/lib/permissions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const ROLES = {
|
||||
EMPEROR: 'emperor',
|
||||
KNIGHT: 'knight',
|
||||
CIVILIAN: 'civilian',
|
||||
} as const;
|
||||
|
||||
export type Role = typeof ROLES[keyof typeof ROLES];
|
||||
|
||||
export const PERMISSIONS = {
|
||||
MANAGE_EMAIL: 'manage_email',
|
||||
MANAGE_WEBHOOK: 'manage_webhook',
|
||||
PROMOTE_USER: 'promote_user',
|
||||
} as const;
|
||||
|
||||
export type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS];
|
||||
|
||||
export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
[ROLES.EMPEROR]: Object.values(PERMISSIONS),
|
||||
[ROLES.KNIGHT]: [
|
||||
PERMISSIONS.MANAGE_EMAIL,
|
||||
PERMISSIONS.MANAGE_WEBHOOK,
|
||||
],
|
||||
[ROLES.CIVILIAN]: [],
|
||||
} as const;
|
||||
|
||||
export function hasPermission(userRoles: Role[], permission: Permission): boolean {
|
||||
return userRoles.some(role => ROLE_PERMISSIONS[role]?.includes(permission));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import type { AdapterAccountType } from "next-auth/adapters"
|
||||
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// https://authjs.dev/getting-started/adapters/drizzle
|
||||
export const users = sqliteTable("user", {
|
||||
id: text("id")
|
||||
@@ -81,4 +82,35 @@ export const webhooks = sqliteTable('webhook', {
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
})
|
||||
})
|
||||
|
||||
export const roles = sqliteTable("role", {
|
||||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const rolesRelations = relations(roles, ({ many }) => ({
|
||||
userRoles: many(userRoles),
|
||||
}));
|
||||
|
||||
export const userRoles = sqliteTable("user_role", {
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
roleId: text("role_id").notNull().references(() => roles.id, { onDelete: "cascade" }),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
}, (table) => ({
|
||||
pk: primaryKey({ columns: [table.userId, table.roleId] }),
|
||||
}));
|
||||
|
||||
export const userRolesRelations = relations(userRoles, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [userRoles.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
role: one(roles, {
|
||||
fields: [userRoles.roleId],
|
||||
references: [roles.id],
|
||||
}),
|
||||
}));
|
||||
Reference in New Issue
Block a user