liuyun 78e537fa73 feat(rbac): 用户-角色分配 + 权限码下发接口
- users: GET/PUT /users/:id/roles(事务化替换 + 过滤无效角色 + 清权限缓存)
- auth: GET /auth/permissions 返回 {super, permissions}
- permissions/seed: 新增 user:assign-role 权限码与资源

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:19:11 +08:00

260 lines
13 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 bcrypt from 'bcryptjs';
import { Permissions } from '../src/common/permissions.js';
import { PLATFORM_ROLE_CODE, PLATFORM_TENANT_CODE } from '../src/common/constants.js';
const prisma = new PrismaClient();
const TENANT = PLATFORM_TENANT_CODE;
const SUPER_ROLE = PLATFORM_ROLE_CODE;
const ADMIN_USERNAME = 'touyun';
const ADMIN_PASSWORD = 'Touyun@123';
/** 菜单资源(导航)。 */
const MENUS = [
{ code: 'dashboard', name: '工作台', parent: null, path: '/', component: 'dashboard/index', icon: 'mdi:view-dashboard', sort: 1 },
{ code: 'permission', name: '权限管理', parent: null, path: '/permission', component: 'Layout', icon: 'mdi:shield-account', sort: 2 },
{ code: 'user', name: '用户管理', parent: 'permission', path: '/system/user', component: 'system/user/index', icon: 'mdi:account-group', sort: 1 },
{ code: 'role', name: '角色管理', parent: 'permission', path: '/system/role', component: 'system/role/index', icon: 'mdi:account-key', sort: 2 },
{ code: 'resource', name: '资源管理', parent: 'permission', path: '/system/resource', component: 'system/resource/index', icon: 'mdi:view-list', sort: 3 },
{ code: 'org', name: '组织管理', parent: 'permission', path: '/system/org', component: 'system/org/index', icon: 'mdi:sitemap', sort: 4 },
{ code: 'system', name: '系统管理', parent: null, path: '/system', component: 'Layout', icon: 'mdi:cog', sort: 3 },
{ code: 'tenant', name: '租户管理', parent: 'system', path: '/system/tenant', component: 'system/tenant/index', icon: 'mdi:domain', sort: 1 },
{ code: 'dict', name: '字典管理', parent: 'system', path: '/system/dict', component: 'system/dict/index', icon: 'mdi:book-open-variant', sort: 2 },
{ code: 'config', name: '参数配置', parent: 'system', path: '/system/config', component: 'system/config/index', icon: 'mdi:tune', sort: 3 },
{ code: 'monitor', name: '系统监控', parent: null, path: '/monitor', component: 'Layout', icon: 'mdi:monitor-dashboard', sort: 4 },
{ code: 'audit', name: '操作日志', parent: 'monitor', path: '/monitor/audit', component: 'monitor/audit/index', icon: 'mdi:clipboard-text', sort: 1 },
{ code: 'login-log', name: '登录日志', parent: 'monitor', path: '/monitor/login-log', component: 'monitor/login-log/index', icon: 'mdi:login', 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.user.assignRole, name: '分配角色', method: 'PUT', path: '/api/v1/users/:id/roles', 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' },
];
/** 内置枚举字典(值对齐 src/common/enums.ts,仅展示层 label/颜色/排序可改)。 */
const DICTS: { code: string; name: string; items: { label: string; value: string; colorType: string; sort: number }[] }[] = [
{
code: 'common_status',
name: '通用状态',
items: [
{ label: '启用', value: 'enabled', colorType: 'success', sort: 1 },
{ label: '停用', value: 'disabled', colorType: 'info', sort: 2 },
],
},
{
code: 'user_status',
name: '用户状态',
items: [
{ label: '启用', value: 'enabled', colorType: 'success', sort: 1 },
{ label: '停用', value: 'disabled', colorType: 'info', sort: 2 },
{ label: '锁定', value: 'locked', colorType: 'danger', sort: 3 },
],
},
{
code: 'resource_type',
name: '资源类型',
items: [
{ label: '菜单', value: 'menu', colorType: 'primary', sort: 1 },
{ label: '按钮', value: 'button', colorType: 'warning', sort: 2 },
{ label: '接口', value: 'api', colorType: 'info', sort: 3 },
],
},
{
code: 'data_scope',
name: '数据权限',
items: [
{ label: '全部', value: 'all', colorType: 'success', sort: 1 },
{ label: '本部门', value: 'dept', colorType: 'info', sort: 2 },
{ label: '本部门及以下', value: 'dept_and_child', colorType: 'warning', sort: 3 },
{ label: '本人', value: 'self', colorType: 'danger', sort: 4 },
{ label: '自定义', value: 'custom', colorType: 'primary', sort: 5 },
],
},
{
code: 'login_status',
name: '登录状态',
items: [
{ label: '成功', value: 'success', colorType: 'success', sort: 1 },
{ label: '失败', value: 'fail', colorType: 'danger', sort: 2 },
],
},
];
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. 管理员用户(update 分支不重置密码/状态,避免重跑 seed 重置管理员密码或重新启用账号)
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
const user = await prisma.user.upsert({
where: { tenantCode_username: { tenantCode: TENANT, username: ADMIN_USERNAME } },
update: { nickname: '管理员' },
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, icon: m.icon, 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,
icon: m.icon,
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 },
});
}
// 8. 内置字典(类型 + 项,幂等)
for (const d of DICTS) {
await prisma.dictType.upsert({
where: { tenantCode_code: { tenantCode: TENANT, code: d.code } },
update: { name: d.name, status: 'enabled', builtIn: true },
create: { tenantCode: TENANT, code: d.code, name: d.name, status: 'enabled', builtIn: true },
});
for (const it of d.items) {
await prisma.dictItem.upsert({
where: { tenantCode_dictTypeCode_value: { tenantCode: TENANT, dictTypeCode: d.code, value: it.value } },
update: { label: it.label, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true },
create: {
tenantCode: TENANT,
dictTypeCode: d.code,
label: it.label,
value: it.value,
colorType: it.colorType,
sort: it.sort,
status: 'enabled',
builtIn: true,
},
});
}
}
console.log(`seed 完成:租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}、资源 ${allResourceCodes.length} 个(已全量授权)、内置字典 ${DICTS.length} 个。`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});