Files
seclusion/apps/api/prisma/seed.ts
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

509 lines
16 KiB
TypeScript
Raw 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.

import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
// 初始权限数据
const permissions = [
// 用户管理权限
{ code: 'user:create', name: '创建用户', resource: 'user', action: 'create' },
{ code: 'user:read', name: '查看用户', resource: 'user', action: 'read' },
{ code: 'user:update', name: '更新用户', resource: 'user', action: 'update' },
{ code: 'user:delete', name: '删除用户', resource: 'user', action: 'delete' },
// 角色管理权限
{ code: 'role:create', name: '创建角色', resource: 'role', action: 'create' },
{ code: 'role:read', name: '查看角色', resource: 'role', action: 'read' },
{ code: 'role:update', name: '更新角色', resource: 'role', action: 'update' },
{ code: 'role:delete', name: '删除角色', resource: 'role', action: 'delete' },
// 权限管理权限
{ code: 'permission:create', name: '创建权限', resource: 'permission', action: 'create' },
{ code: 'permission:read', name: '查看权限', resource: 'permission', action: 'read' },
{ code: 'permission:update', name: '更新权限', resource: 'permission', action: 'update' },
{ code: 'permission:delete', name: '删除权限', resource: 'permission', action: 'delete' },
// 菜单管理权限
{ code: 'menu:create', name: '创建菜单', resource: 'menu', action: 'create' },
{ code: 'menu:read', name: '查看菜单', resource: 'menu', action: 'read' },
{ code: 'menu:update', name: '更新菜单', resource: 'menu', action: 'update' },
{ code: 'menu:delete', name: '删除菜单', resource: 'menu', action: 'delete' },
// 文件管理权限
{ code: 'file:read', name: '查看文件', resource: 'file', action: 'read' },
{ code: 'file:delete', name: '删除文件', resource: 'file', action: 'delete' },
// 教师管理权限
{ code: 'teacher:create', name: '创建教师', resource: 'teacher', action: 'create' },
{ code: 'teacher:read', name: '查看教师', resource: 'teacher', action: 'read' },
{ code: 'teacher:update', name: '更新教师', resource: 'teacher', action: 'update' },
{ code: 'teacher:delete', name: '删除教师', resource: 'teacher', action: 'delete' },
// 班级管理权限
{ code: 'class:create', name: '创建班级', resource: 'class', action: 'create' },
{ code: 'class:read', name: '查看班级', resource: 'class', action: 'read' },
{ code: 'class:update', name: '更新班级', resource: 'class', action: 'update' },
{ code: 'class:delete', name: '删除班级', resource: 'class', action: 'delete' },
// 学生管理权限
{ code: 'student:create', name: '创建学生', resource: 'student', action: 'create' },
{ code: 'student:read', name: '查看学生', resource: 'student', action: 'read' },
{ code: 'student:update', name: '更新学生', resource: 'student', action: 'update' },
{ code: 'student:delete', name: '删除学生', resource: 'student', action: 'delete' },
// OIDC 客户端管理权限
{ code: 'oidc-client:create', name: '创建 OIDC 客户端', resource: 'oidc-client', action: 'create' },
{ code: 'oidc-client:read', name: '查看 OIDC 客户端', resource: 'oidc-client', action: 'read' },
{ code: 'oidc-client:update', name: '更新 OIDC 客户端', resource: 'oidc-client', action: 'update' },
{ code: 'oidc-client:delete', name: '删除 OIDC 客户端', resource: 'oidc-client', action: 'delete' },
];
// 初始角色数据
const roles = [
{
code: 'super_admin',
name: '超级管理员',
description: '拥有系统所有权限',
isSystem: true,
sort: 0,
},
{
code: 'admin',
name: '管理员',
description: '拥有大部分管理权限',
isSystem: true,
sort: 1,
},
{
code: 'user',
name: '普通用户',
description: '基础用户权限',
isSystem: true,
sort: 2,
},
];
// 初始菜单数据
const menus = [
{
code: 'dashboard',
name: '仪表盘',
type: 'menu',
path: '/dashboard',
icon: 'LayoutDashboard',
sort: 0,
isStatic: true,
},
// 教学管理目录
{
code: 'teaching',
name: '教学管理',
type: 'dir',
icon: 'GraduationCap',
sort: 10,
isStatic: false,
},
{
code: 'student-management',
name: '学生管理',
type: 'menu',
path: '/students',
icon: 'Users',
sort: 0,
isStatic: false,
},
{
code: 'teacher-management',
name: '教师管理',
type: 'menu',
path: '/teachers',
icon: 'UserCheck',
sort: 1,
isStatic: false,
},
{
code: 'class-management',
name: '班级管理',
type: 'menu',
path: '/classes',
icon: 'School',
sort: 2,
isStatic: false,
},
// 系统管理目录
{
code: 'system',
name: '系统管理',
type: 'dir',
icon: 'Settings',
sort: 100,
isStatic: true,
},
{
code: 'user-management',
name: '用户管理',
type: 'menu',
path: '/users',
icon: 'Users',
sort: 0,
isStatic: true,
},
{
code: 'file-management',
name: '文件管理',
type: 'menu',
path: '/files',
icon: 'FolderOpen',
sort: 1,
isStatic: true,
},
{
code: 'role-management',
name: '角色管理',
type: 'menu',
path: '/roles',
icon: 'Shield',
sort: 2,
isStatic: true,
},
{
code: 'permission-management',
name: '权限管理',
type: 'menu',
path: '/permissions',
icon: 'Key',
sort: 3,
isStatic: true,
},
{
code: 'menu-management',
name: '菜单管理',
type: 'menu',
path: '/menus',
icon: 'Menu',
sort: 4,
isStatic: true,
},
{
code: 'oidc-client-management',
name: 'OIDC 客户端',
type: 'menu',
path: '/oidc-clients',
icon: 'KeyRound',
sort: 5,
isStatic: true,
},
{
code: 'profile',
name: '个人中心',
type: 'menu',
path: '/profile',
icon: 'User',
sort: 200,
isStatic: true,
},
{
code: 'settings',
name: '系统设置',
type: 'menu',
path: '/settings',
icon: 'Settings',
sort: 201,
isStatic: true,
},
];
// 系统管理子菜单 codes
const systemSubMenuCodes = [
'user-management',
'file-management',
'role-management',
'permission-management',
'menu-management',
'oidc-client-management',
];
// 教学管理子菜单 codes
const teachingSubMenuCodes = [
'student-management',
'teacher-management',
'class-management',
];
async function main() {
console.log('开始初始化种子数据...');
// 1. 创建权限
console.log('创建权限...');
for (const permission of permissions) {
await prisma.permission.upsert({
where: { code: permission.code },
update: permission,
create: permission,
});
}
console.log(`已创建 ${permissions.length} 个权限`);
// 2. 创建角色
console.log('创建角色...');
for (const role of roles) {
await prisma.role.upsert({
where: { code: role.code },
update: role,
create: role,
});
}
console.log(`已创建 ${roles.length} 个角色`);
// 3. 创建菜单
console.log('创建菜单...');
// 所有子菜单 codes
const allSubMenuCodes = [...systemSubMenuCodes, ...teachingSubMenuCodes];
// 先创建顶级菜单(排除所有子菜单)
const topMenus = menus.filter((m) => !allSubMenuCodes.includes(m.code));
for (const menu of topMenus) {
await prisma.menu.upsert({
where: { code: menu.code },
update: menu,
create: menu,
});
}
// 获取系统管理菜单的 ID
const systemMenu = await prisma.menu.findUnique({
where: { code: 'system' },
});
// 创建系统管理子菜单
if (systemMenu) {
const subMenus = menus.filter((m) => systemSubMenuCodes.includes(m.code));
for (const menu of subMenus) {
await prisma.menu.upsert({
where: { code: menu.code },
update: { ...menu, parentId: systemMenu.id },
create: { ...menu, parentId: systemMenu.id },
});
}
}
// 获取教学管理菜单的 ID
const teachingMenu = await prisma.menu.findUnique({
where: { code: 'teaching' },
});
// 创建教学管理子菜单
if (teachingMenu) {
const subMenus = menus.filter((m) => teachingSubMenuCodes.includes(m.code));
for (const menu of subMenus) {
await prisma.menu.upsert({
where: { code: menu.code },
update: { ...menu, parentId: teachingMenu.id },
create: { ...menu, parentId: teachingMenu.id },
});
}
}
console.log(`已创建 ${menus.length} 个菜单`);
// 4. 清空所有角色的权限和菜单(不分配任何默认权限和菜单)
console.log('清空角色权限和菜单...');
const adminRole = await prisma.role.findUnique({
where: { code: 'admin' },
});
const userRole = await prisma.role.findUnique({
where: { code: 'user' },
});
if (adminRole) {
await prisma.rolePermission.deleteMany({
where: { roleId: adminRole.id },
});
await prisma.roleMenu.deleteMany({
where: { roleId: adminRole.id },
});
console.log('已清空管理员角色的权限和菜单');
}
if (userRole) {
await prisma.rolePermission.deleteMany({
where: { roleId: userRole.id },
});
await prisma.roleMenu.deleteMany({
where: { roleId: userRole.id },
});
console.log('已清空普通用户角色的权限和菜单');
}
// 5. 创建超级管理员用户
console.log('创建超级管理员用户...');
const superAdminPassword = await bcrypt.hash('admin123', 10);
// 由于复合唯一约束包含 deletedAt无法使用 upsert改用 findFirst + update/create
let superAdminUser = await prisma.user.findFirst({
where: { email: 'admin@seclusion.dev' },
});
if (superAdminUser) {
superAdminUser = await prisma.user.update({
where: { id: superAdminUser.id },
data: { isSuperAdmin: true },
});
} else {
superAdminUser = await prisma.user.create({
data: {
email: 'admin@seclusion.dev',
password: superAdminPassword,
name: '超级管理员',
isSuperAdmin: true,
},
});
}
// 为超管用户分配超管角色
const superAdminRole = await prisma.role.findUnique({
where: { code: 'super_admin' },
});
if (superAdminRole) {
await prisma.userRole.upsert({
where: {
userId_roleId: {
userId: superAdminUser.id,
roleId: superAdminRole.id,
},
},
update: {},
create: {
userId: superAdminUser.id,
roleId: superAdminRole.id,
},
});
}
console.log('超级管理员用户创建完成');
console.log(' 邮箱: admin@seclusion.dev');
console.log(' 密码: admin123');
// 6. 创建教学管理演示数据
console.log('\n创建教学管理演示数据...');
// 6.1 创建教师
const teachersData = [
{ teacherNo: 'T001', name: '张明', gender: 'male', phone: '13800000001', email: 'zhangming@school.edu', subject: '语文' },
{ teacherNo: 'T002', name: '李华', gender: 'female', phone: '13800000002', email: 'lihua@school.edu', subject: '数学' },
{ teacherNo: 'T003', name: '王芳', gender: 'female', phone: '13800000003', email: 'wangfang@school.edu', subject: '英语' },
{ teacherNo: 'T004', name: '赵强', gender: 'male', phone: '13800000004', email: 'zhaoqiang@school.edu', subject: '物理' },
{ teacherNo: 'T005', name: '陈静', gender: 'female', phone: '13800000005', email: 'chenjing@school.edu', subject: '化学' },
{ teacherNo: 'T006', name: '刘伟', gender: 'male', phone: '13800000006', email: 'liuwei@school.edu', subject: '生物' },
{ teacherNo: 'T007', name: '周敏', gender: 'female', phone: '13800000007', email: 'zhoumin@school.edu', subject: '历史' },
{ teacherNo: 'T008', name: '吴刚', gender: 'male', phone: '13800000008', email: 'wugang@school.edu', subject: '体育' },
];
const teachers: Record<string, { id: string }> = {};
for (const teacher of teachersData) {
const created = await prisma.teacher.upsert({
where: { teacherNo: teacher.teacherNo },
update: teacher,
create: teacher,
});
teachers[teacher.teacherNo] = created;
}
console.log(`已创建 ${teachersData.length} 名教师`);
// 6.2 创建班级
const classesData = [
{ code: 'C2024-01', name: '高一(1)班', grade: '高一', headTeacherNo: 'T001' },
{ code: 'C2024-02', name: '高一(2)班', grade: '高一', headTeacherNo: 'T002' },
{ code: 'C2024-03', name: '高二(1)班', grade: '高二', headTeacherNo: 'T003' },
{ code: 'C2024-04', name: '高三(1)班', grade: '高三', headTeacherNo: 'T004' },
];
const classes: Record<string, { id: string }> = {};
for (const cls of classesData) {
const headTeacher = teachers[cls.headTeacherNo];
const created = await prisma.class.upsert({
where: { code: cls.code },
update: { name: cls.name, grade: cls.grade, headTeacherId: headTeacher?.id },
create: { code: cls.code, name: cls.name, grade: cls.grade, headTeacherId: headTeacher?.id },
});
classes[cls.code] = created;
}
console.log(`已创建 ${classesData.length} 个班级`);
// 6.3 分配任课教师(多对多)
const classTeacherAssignments = [
{ classCode: 'C2024-01', teacherNos: ['T001', 'T002', 'T003', 'T004', 'T008'] },
{ classCode: 'C2024-02', teacherNos: ['T002', 'T003', 'T005', 'T006', 'T008'] },
{ classCode: 'C2024-03', teacherNos: ['T003', 'T004', 'T005', 'T007', 'T008'] },
{ classCode: 'C2024-04', teacherNos: ['T001', 'T002', 'T004', 'T006', 'T007'] },
];
for (const assignment of classTeacherAssignments) {
const classEntity = classes[assignment.classCode];
if (!classEntity) continue;
// 先删除该班级的所有任课教师关系
await prisma.classTeacher.deleteMany({
where: { classId: classEntity.id },
});
// 创建新的任课教师关系
for (const teacherNo of assignment.teacherNos) {
const teacher = teachers[teacherNo];
if (!teacher) continue;
await prisma.classTeacher.create({
data: {
classId: classEntity.id,
teacherId: teacher.id,
},
});
}
}
console.log('已分配班级任课教师');
// 6.4 创建学生
const studentsData = [
{ studentNo: 'S2024001', name: '小明', gender: 'male', phone: '13900000001', classCode: 'C2024-01' },
{ studentNo: 'S2024002', name: '小红', gender: 'female', phone: '13900000002', classCode: 'C2024-01' },
{ studentNo: 'S2024003', name: '小刚', gender: 'male', phone: '13900000003', classCode: 'C2024-01' },
{ studentNo: 'S2024004', name: '小丽', gender: 'female', phone: '13900000004', classCode: 'C2024-01' },
{ studentNo: 'S2024005', name: '小华', gender: 'male', phone: '13900000005', classCode: 'C2024-02' },
{ studentNo: 'S2024006', name: '小芳', gender: 'female', phone: '13900000006', classCode: 'C2024-02' },
{ studentNo: 'S2024007', name: '小强', gender: 'male', phone: '13900000007', classCode: 'C2024-02' },
{ studentNo: 'S2024008', name: '小敏', gender: 'female', phone: '13900000008', classCode: 'C2024-03' },
{ studentNo: 'S2024009', name: '小伟', gender: 'male', phone: '13900000009', classCode: 'C2024-03' },
{ studentNo: 'S2024010', name: '小静', gender: 'female', phone: '13900000010', classCode: 'C2024-03' },
{ studentNo: 'S2024011', name: '小杰', gender: 'male', phone: '13900000011', classCode: 'C2024-04' },
{ studentNo: 'S2024012', name: '小雪', gender: 'female', phone: '13900000012', classCode: 'C2024-04' },
{ studentNo: 'S2024013', name: '小龙', gender: 'male', phone: '13900000013', classCode: 'C2024-04' },
{ studentNo: 'S2024014', name: '小燕', gender: 'female', phone: '13900000014', classCode: 'C2024-04' },
{ studentNo: 'S2024015', name: '小峰', gender: 'male', phone: '13900000015', classCode: 'C2024-04' },
];
for (const student of studentsData) {
const classEntity = classes[student.classCode];
await prisma.student.upsert({
where: { studentNo: student.studentNo },
update: {
name: student.name,
gender: student.gender,
phone: student.phone,
classId: classEntity?.id,
},
create: {
studentNo: student.studentNo,
name: student.name,
gender: student.gender,
phone: student.phone,
classId: classEntity?.id,
},
});
}
console.log(`已创建 ${studentsData.length} 名学生`);
console.log('\n种子数据初始化完成');
}
main()
.catch((e) => {
console.error('种子数据初始化失败:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});