收口 Phase 0 三处「声明未生效」: - 三级鉴权落地:权限集存 Redis(key 前缀隔离多项目),@RequirePermissions 挂到全部受控接口,super_admin 角色绕过权限码与 data_scope - 审计/登录日志自动落库:@AuditLog 装饰器 + 拦截器异步写入(写操作+登录登出),登录成功/失败均记 login_log(脱敏) - seed 脚本:一键初始化租户 touyun/用户 touyun/超管 super_admin/全量资源树(48 资源全量授权) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
182 lines
9.8 KiB
TypeScript
182 lines
9.8 KiB
TypeScript
import { PrismaClient } from '@prisma/client';
|
||
import bcrypt from 'bcryptjs';
|
||
import { Permissions } from '../src/common/permissions.js';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
const TENANT = 'touyun';
|
||
const SUPER_ROLE = 'super_admin';
|
||
const ADMIN_USERNAME = 'touyun';
|
||
const ADMIN_PASSWORD = 'Touyun@123';
|
||
|
||
/** 菜单资源(导航)。 */
|
||
const MENUS = [
|
||
{ code: 'dashboard', name: '工作台', parent: null, path: '/dashboard', component: 'dashboard/index', sort: 1 },
|
||
{ code: 'system', name: '系统管理', parent: null, path: '/system', component: 'Layout', sort: 2 },
|
||
{ code: 'tenant', name: '租户管理', parent: 'system', path: '/system/tenant', component: 'system/tenant/index', sort: 1 },
|
||
{ code: 'user', name: '用户管理', parent: 'system', path: '/system/user', component: 'system/user/index', sort: 2 },
|
||
{ code: 'role', name: '角色管理', parent: 'system', path: '/system/role', component: 'system/role/index', sort: 3 },
|
||
{ code: 'resource', name: '资源管理', parent: 'system', path: '/system/resource', component: 'system/resource/index', sort: 4 },
|
||
{ code: 'org', name: '组织管理', parent: 'system', path: '/system/org', component: 'system/org/index', sort: 5 },
|
||
{ code: 'dict', name: '字典管理', parent: 'system', path: '/system/dict', component: 'system/dict/index', sort: 6 },
|
||
{ code: 'config', name: '参数配置', parent: 'system', path: '/system/config', component: 'system/config/index', sort: 7 },
|
||
{ code: 'monitor', name: '系统监控', parent: null, path: '/monitor', component: 'Layout', sort: 3 },
|
||
{ code: 'audit', name: '操作日志', parent: 'monitor', path: '/monitor/audit', component: 'monitor/audit/index', sort: 1 },
|
||
{ code: 'login-log', name: '登录日志', parent: 'monitor', path: '/monitor/login-log', component: 'monitor/login-log/index', sort: 2 },
|
||
];
|
||
|
||
/** 接口资源(每个受控接口一个,code = 权限码)。 */
|
||
const APIS: { code: string; name: string; method: string; path: string; parent: string }[] = [
|
||
{ code: Permissions.tenant.list, name: '租户列表', method: 'GET', path: '/api/v1/tenants', parent: 'tenant' },
|
||
{ code: Permissions.tenant.create, name: '创建租户', method: 'POST', path: '/api/v1/tenants', parent: 'tenant' },
|
||
{ code: Permissions.tenant.update, name: '更新租户', method: 'PATCH', path: '/api/v1/tenants/:id', parent: 'tenant' },
|
||
{ code: Permissions.tenant.delete, name: '删除租户', method: 'DELETE', path: '/api/v1/tenants/:id', parent: 'tenant' },
|
||
{ code: Permissions.user.list, name: '用户列表', method: 'GET', path: '/api/v1/users', parent: 'user' },
|
||
{ code: Permissions.user.create, name: '创建用户', method: 'POST', path: '/api/v1/users', parent: 'user' },
|
||
{ code: Permissions.user.update, name: '更新用户', method: 'PATCH', path: '/api/v1/users/:id', parent: 'user' },
|
||
{ code: Permissions.user.delete, name: '删除用户', method: 'DELETE', path: '/api/v1/users/:id', parent: 'user' },
|
||
{ code: Permissions.user.resetPwd, name: '重置密码', method: 'PATCH', path: '/api/v1/users/:id/password', parent: 'user' },
|
||
{ code: Permissions.role.list, name: '角色列表', method: 'GET', path: '/api/v1/roles', parent: 'role' },
|
||
{ code: Permissions.role.create, name: '创建角色', method: 'POST', path: '/api/v1/roles', parent: 'role' },
|
||
{ code: Permissions.role.update, name: '更新角色', method: 'PATCH', path: '/api/v1/roles/:id', parent: 'role' },
|
||
{ code: Permissions.role.delete, name: '删除角色', method: 'DELETE', path: '/api/v1/roles/:id', parent: 'role' },
|
||
{ code: Permissions.role.assignResource, name: '分配资源', method: 'PUT', path: '/api/v1/roles/:id/resources', parent: 'role' },
|
||
{ code: Permissions.resource.list, name: '资源列表', method: 'GET', path: '/api/v1/resources', parent: 'resource' },
|
||
{ code: Permissions.resource.create, name: '创建资源', method: 'POST', path: '/api/v1/resources', parent: 'resource' },
|
||
{ code: Permissions.resource.update, name: '更新资源', method: 'PATCH', path: '/api/v1/resources/:id', parent: 'resource' },
|
||
{ code: Permissions.resource.delete, name: '删除资源', method: 'DELETE', path: '/api/v1/resources/:id', parent: 'resource' },
|
||
{ code: Permissions.org.list, name: '组织列表', method: 'GET', path: '/api/v1/orgs', parent: 'org' },
|
||
{ code: Permissions.org.create, name: '创建组织', method: 'POST', path: '/api/v1/orgs', parent: 'org' },
|
||
{ code: Permissions.org.update, name: '更新组织', method: 'PATCH', path: '/api/v1/orgs/:id', parent: 'org' },
|
||
{ code: Permissions.org.delete, name: '删除组织', method: 'DELETE', path: '/api/v1/orgs/:id', parent: 'org' },
|
||
{ code: Permissions.dict.list, name: '字典列表', method: 'GET', path: '/api/v1/dict-types', parent: 'dict' },
|
||
{ code: Permissions.dict.create, name: '创建字典', method: 'POST', path: '/api/v1/dict-types', parent: 'dict' },
|
||
{ code: Permissions.dict.update, name: '更新字典', method: 'PATCH', path: '/api/v1/dict-types/:id', parent: 'dict' },
|
||
{ code: Permissions.dict.delete, name: '删除字典', method: 'DELETE', path: '/api/v1/dict-types/:id', parent: 'dict' },
|
||
{ code: Permissions.config.list, name: '配置列表', method: 'GET', path: '/api/v1/configs', parent: 'config' },
|
||
{ code: Permissions.config.create, name: '创建配置', method: 'POST', path: '/api/v1/configs', parent: 'config' },
|
||
{ code: Permissions.config.update, name: '更新配置', method: 'PATCH', path: '/api/v1/configs/:id', parent: 'config' },
|
||
{ code: Permissions.config.delete, name: '删除配置', method: 'DELETE', path: '/api/v1/configs/:id', parent: 'config' },
|
||
{ code: Permissions.audit.list, name: '操作日志列表', method: 'GET', path: '/api/v1/audit-logs', parent: 'audit' },
|
||
{ code: Permissions.loginLog.list, name: '登录日志列表', method: 'GET', path: '/api/v1/login-logs', parent: 'login-log' },
|
||
];
|
||
|
||
/** 按钮资源(纯前端按钮,无独立受控接口)。 */
|
||
const BUTTONS = [
|
||
{ code: 'user:export', name: '用户导出', parent: 'user' },
|
||
{ code: 'role:export', name: '角色导出', parent: 'role' },
|
||
{ code: 'audit:export', name: '操作日志导出', parent: 'audit' },
|
||
{ code: 'login-log:export', name: '登录日志导出', parent: 'login-log' },
|
||
];
|
||
|
||
async function main(): Promise<void> {
|
||
// 1. 租户
|
||
await prisma.tenant.upsert({
|
||
where: { code: TENANT },
|
||
update: { name: '投云', status: 'enabled' },
|
||
create: { code: TENANT, name: '投云', status: 'enabled' },
|
||
});
|
||
|
||
// 2. 根组织
|
||
await prisma.org.upsert({
|
||
where: { tenantCode_code: { tenantCode: TENANT, code: TENANT } },
|
||
update: { name: '投云总部' },
|
||
create: { tenantCode: TENANT, code: TENANT, name: '投云总部', parentCode: null, sort: 0, status: 'enabled' },
|
||
});
|
||
|
||
// 3. 超管角色
|
||
await prisma.role.upsert({
|
||
where: { tenantCode_code: { tenantCode: TENANT, code: SUPER_ROLE } },
|
||
update: { name: '超级管理员', dataScope: 'all', status: 'enabled' },
|
||
create: { tenantCode: TENANT, code: SUPER_ROLE, name: '超级管理员', dataScope: 'all', status: 'enabled' },
|
||
});
|
||
|
||
// 4. 管理员用户
|
||
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||
const user = await prisma.user.upsert({
|
||
where: { tenantCode_username: { tenantCode: TENANT, username: ADMIN_USERNAME } },
|
||
update: { passwordHash, nickname: '管理员', status: 'enabled' },
|
||
create: {
|
||
tenantCode: TENANT,
|
||
username: ADMIN_USERNAME,
|
||
passwordHash,
|
||
nickname: '管理员',
|
||
status: 'enabled',
|
||
orgCode: TENANT,
|
||
},
|
||
});
|
||
|
||
// 5. 绑定超管角色
|
||
await prisma.userRole.upsert({
|
||
where: { tenantCode_userId_roleCode: { tenantCode: TENANT, userId: user.id, roleCode: SUPER_ROLE } },
|
||
update: {},
|
||
create: { tenantCode: TENANT, userId: user.id, roleCode: SUPER_ROLE },
|
||
});
|
||
|
||
// 6. 资源树(菜单 + 接口 + 按钮)
|
||
const allResourceCodes: string[] = [];
|
||
for (const m of MENUS) {
|
||
allResourceCodes.push(m.code);
|
||
await prisma.resource.upsert({
|
||
where: { tenantCode_code: { tenantCode: TENANT, code: m.code } },
|
||
update: { name: m.name, parentCode: m.parent, path: m.path, component: m.component, sort: m.sort, type: 'menu' },
|
||
create: {
|
||
tenantCode: TENANT,
|
||
code: m.code,
|
||
name: m.name,
|
||
parentCode: m.parent,
|
||
type: 'menu',
|
||
path: m.path,
|
||
component: m.component,
|
||
sort: m.sort,
|
||
status: 'enabled',
|
||
},
|
||
});
|
||
}
|
||
for (const a of APIS) {
|
||
allResourceCodes.push(a.code);
|
||
await prisma.resource.upsert({
|
||
where: { tenantCode_code: { tenantCode: TENANT, code: a.code } },
|
||
update: { name: a.name, parentCode: a.parent, method: a.method, apiPath: a.path, type: 'api' },
|
||
create: {
|
||
tenantCode: TENANT,
|
||
code: a.code,
|
||
name: a.name,
|
||
parentCode: a.parent,
|
||
type: 'api',
|
||
method: a.method,
|
||
apiPath: a.path,
|
||
status: 'enabled',
|
||
},
|
||
});
|
||
}
|
||
for (const b of BUTTONS) {
|
||
allResourceCodes.push(b.code);
|
||
await prisma.resource.upsert({
|
||
where: { tenantCode_code: { tenantCode: TENANT, code: b.code } },
|
||
update: { name: b.name, parentCode: b.parent, type: 'button' },
|
||
create: { tenantCode: TENANT, code: b.code, name: b.name, parentCode: b.parent, type: 'button', status: 'enabled' },
|
||
});
|
||
}
|
||
|
||
// 7. 超管全量授权(菜单 + 接口 + 按钮)
|
||
for (const resourceCode of allResourceCodes) {
|
||
await prisma.roleResource.upsert({
|
||
where: { tenantCode_roleCode_resourceCode: { tenantCode: TENANT, roleCode: SUPER_ROLE, resourceCode } },
|
||
update: {},
|
||
create: { tenantCode: TENANT, roleCode: SUPER_ROLE, resourceCode },
|
||
});
|
||
}
|
||
|
||
console.log(`seed 完成:租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}、资源 ${allResourceCodes.length} 个(已全量授权)。`);
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|