Files
seclusion/apps/api/prisma/schema.prisma
charilezhou 90513e8278 feat: 实现完整的 OIDC Provider 功能
- 后端:基于 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>
2026-01-20 17:22:32 +08:00

303 lines
8.8 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")
}