- 后端:基于 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>
509 lines
16 KiB
TypeScript
509 lines
16 KiB
TypeScript
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();
|
||
});
|