- 后端:基于 node-oidc-provider 实现 OIDC Provider - 支持 authorization_code、refresh_token、client_credentials 授权类型 - Redis adapter 存储会话数据,Prisma adapter 存储持久化数据 - 客户端管理 CRUD API(创建、更新、删除、重新生成密钥) - 交互 API(登录、授权确认、中止) - 第一方应用自动跳过授权确认页面 - 使用 cuid2 生成客户端 ID - 前端:OIDC 客户端管理界面 - 客户端列表表格(支持分页、排序) - 创建/编辑弹窗(支持所有 OIDC 配置字段) - OIDC 交互页面(登录表单、授权确认表单) - 共享类型:添加 OIDC 相关 TypeScript 类型定义 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
303 lines
8.8 KiB
Plaintext
303 lines
8.8 KiB
Plaintext
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(cuid(2))
|
||
email String
|
||
password String
|
||
name String?
|
||
avatarId String? // 头像文件 ID
|
||
isSuperAdmin Boolean @default(false) // 超级管理员标记
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
roles UserRole[]
|
||
uploadFiles File[] @relation("FileUploader")
|
||
oidcGrants OidcGrant[]
|
||
|
||
// 复合唯一约束:未删除用户邮箱唯一,已删除用户邮箱可重复
|
||
@@unique([email, deletedAt])
|
||
@@map("users")
|
||
}
|
||
|
||
// 文件表
|
||
model File {
|
||
id String @id @default(cuid(2))
|
||
filename String // 原始文件名
|
||
objectName String // MinIO 中的对象名
|
||
mimeType String // MIME 类型
|
||
size Int // 文件大小(字节)
|
||
purpose String // 用途: avatar, attachment 等
|
||
uploaderId String // 上传者 ID
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
uploader User @relation("FileUploader", fields: [uploaderId], references: [id])
|
||
|
||
@@index([uploaderId])
|
||
@@index([purpose])
|
||
@@map("files")
|
||
}
|
||
|
||
// 角色表
|
||
model Role {
|
||
id String @id @default(cuid(2))
|
||
code String @unique // 角色编码: admin, user
|
||
name String // 角色名称
|
||
description String?
|
||
isSystem Boolean @default(false) // 系统内置角色不可删
|
||
isEnabled Boolean @default(true)
|
||
sort Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
users UserRole[]
|
||
permissions RolePermission[]
|
||
menus RoleMenu[]
|
||
|
||
@@map("roles")
|
||
}
|
||
|
||
// 权限表
|
||
model Permission {
|
||
id String @id @default(cuid(2))
|
||
code String @unique // 权限编码: user:create
|
||
name String
|
||
description String?
|
||
resource String // 资源: user
|
||
action String // 操作: create
|
||
isEnabled Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
roles RolePermission[]
|
||
|
||
@@index([resource])
|
||
@@map("permissions")
|
||
}
|
||
|
||
// 菜单表
|
||
model Menu {
|
||
id String @id @default(cuid(2))
|
||
parentId String?
|
||
code String @unique
|
||
name String
|
||
type String @default("menu") // dir / menu / button
|
||
path String?
|
||
icon String? // Lucide 图标名
|
||
isExternal Boolean @default(false)
|
||
isHidden Boolean @default(false)
|
||
isEnabled Boolean @default(true)
|
||
isStatic Boolean @default(false) // 静态/动态菜单
|
||
sort Int @default(0)
|
||
meta Json?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
parent Menu? @relation("MenuTree", fields: [parentId], references: [id])
|
||
children Menu[] @relation("MenuTree")
|
||
roles RoleMenu[]
|
||
|
||
@@index([parentId])
|
||
@@map("menus")
|
||
}
|
||
|
||
// 用户-角色关联表
|
||
model UserRole {
|
||
id String @id @default(cuid(2))
|
||
userId String
|
||
roleId String
|
||
createdAt DateTime @default(now())
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([userId, roleId])
|
||
@@map("user_roles")
|
||
}
|
||
|
||
// 角色-权限关联表
|
||
model RolePermission {
|
||
id String @id @default(cuid(2))
|
||
roleId String
|
||
permissionId String
|
||
createdAt DateTime @default(now())
|
||
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([roleId, permissionId])
|
||
@@map("role_permissions")
|
||
}
|
||
|
||
// 角色-菜单关联表
|
||
model RoleMenu {
|
||
id String @id @default(cuid(2))
|
||
roleId String
|
||
menuId String
|
||
createdAt DateTime @default(now())
|
||
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([roleId, menuId])
|
||
@@map("role_menus")
|
||
}
|
||
|
||
// ============ 教学管理模块 ============
|
||
|
||
// 教师表
|
||
model Teacher {
|
||
id String @id @default(cuid(2))
|
||
teacherNo String @unique // 工号
|
||
name String
|
||
gender String? // male / female
|
||
phone String?
|
||
email String?
|
||
subject String? // 任教科目
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
// 关系
|
||
headOfClasses Class[] @relation("HeadTeacher") // 作为班主任的班级(一对一的反向)
|
||
teachClasses ClassTeacher[] // 任课班级(多对多)
|
||
|
||
@@map("teachers")
|
||
}
|
||
|
||
// 班级表
|
||
model Class {
|
||
id String @id @default(cuid(2))
|
||
code String @unique // 班级代码
|
||
name String // 班级名称
|
||
grade String? // 年级
|
||
headTeacherId String? // 班主任 ID(一对一)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
// 关系
|
||
headTeacher Teacher? @relation("HeadTeacher", fields: [headTeacherId], references: [id])
|
||
students Student[] // 班级学生(一对多)
|
||
teachers ClassTeacher[] // 任课教师(多对多)
|
||
|
||
@@index([headTeacherId])
|
||
@@map("classes")
|
||
}
|
||
|
||
// 学生表
|
||
model Student {
|
||
id String @id @default(cuid(2))
|
||
studentNo String @unique // 学号
|
||
name String
|
||
gender String? // male / female
|
||
phone String?
|
||
email String?
|
||
classId String? // 所属班级(多对一)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
// 关系
|
||
class Class? @relation(fields: [classId], references: [id])
|
||
|
||
@@index([classId])
|
||
@@map("students")
|
||
}
|
||
|
||
// 班级-教师关联表(多对多:任课关系)
|
||
model ClassTeacher {
|
||
id String @id @default(cuid(2))
|
||
classId String
|
||
teacherId String
|
||
createdAt DateTime @default(now())
|
||
|
||
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
||
teacher Teacher @relation(fields: [teacherId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([classId, teacherId])
|
||
@@map("class_teachers")
|
||
}
|
||
|
||
// ============ OIDC Provider 模块 ============
|
||
|
||
// OIDC 客户端应用
|
||
model OidcClient {
|
||
id String @id @default(cuid(2))
|
||
clientId String @unique // 客户端 ID
|
||
clientSecret String? // 客户端密钥(公开客户端可为空)
|
||
clientName String // 客户端名称
|
||
clientUri String? // 客户端主页
|
||
logoUri String? // Logo URL
|
||
redirectUris String[] // 回调地址列表
|
||
postLogoutRedirectUris String[] // 登出后回调地址
|
||
grantTypes String[] // 授权类型: authorization_code, refresh_token, client_credentials
|
||
responseTypes String[] // 响应类型: code, token, id_token
|
||
scopes String[] // 允许的 scope: openid, profile, email, etc.
|
||
tokenEndpointAuthMethod String @default("client_secret_basic") // 认证方式
|
||
applicationType String @default("web") // web / native
|
||
isEnabled Boolean @default(true)
|
||
isFirstParty Boolean @default(false) // 是否为第一方应用(跳过授权确认)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
deletedAt DateTime?
|
||
|
||
grants OidcGrant[]
|
||
refreshTokens OidcRefreshToken[]
|
||
|
||
@@map("oidc_clients")
|
||
}
|
||
|
||
// OIDC 刷新令牌(长期,需要持久化)
|
||
model OidcRefreshToken {
|
||
id String @id @default(cuid(2))
|
||
jti String @unique // JWT ID
|
||
grantId String // 关联的授权 ID
|
||
clientId String // 客户端 ID
|
||
userId String // 用户 ID
|
||
scope String // 授权范围
|
||
data Json // 完整令牌数据
|
||
expiresAt DateTime
|
||
consumedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
client OidcClient @relation(fields: [clientId], references: [clientId])
|
||
|
||
@@index([grantId])
|
||
@@index([clientId])
|
||
@@index([userId])
|
||
@@map("oidc_refresh_tokens")
|
||
}
|
||
|
||
// OIDC 授权记录(用户对客户端的授权)
|
||
model OidcGrant {
|
||
id String @id @default(cuid(2))
|
||
grantId String @unique // oidc-provider 生成的 grant ID
|
||
clientId String // 客户端 ID
|
||
userId String // 用户 ID
|
||
scope String // 已授权的 scope
|
||
data Json // 完整授权数据
|
||
expiresAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
client OidcClient @relation(fields: [clientId], references: [clientId])
|
||
user User @relation(fields: [userId], references: [id])
|
||
|
||
@@unique([clientId, userId])
|
||
@@index([userId])
|
||
@@map("oidc_grants")
|
||
}
|