ec-nest/src/common/tenant-template.ts
liuyun 9869974d4c feat(auth): 首登强制改密 + 待改密门禁
- User 新增 must_change_password;创建(默认强制)/重置/createTenant 置 true
- 登录响应返回 mustChangePassword,改密成功清除
- JwtAuthGuard 待改密门禁:仅放行 PATCH /auth/me/password

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

218 lines
14 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 bcrypt from 'bcryptjs';
import type { PrismaClient } from '@prisma/client';
import { Permissions } from './permissions.js';
/** 菜单资源(导航)。 */
export 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: 'session', name: '在线会话', parent: 'monitor', path: '/monitor/session', component: 'monitor/session/index', icon: 'mdi:account-multiple-check', sort: 3 },
];
/** 接口资源(每个受控接口一个,code = 权限码)。 */
export 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' },
{ code: Permissions.session.list, name: '会话列表', method: 'GET', path: '/api/v1/sessions', parent: 'session' },
{ code: Permissions.session.kick, name: '强退会话', method: 'POST', path: '/api/v1/sessions/:sid/kick', parent: 'session' },
{ code: Permissions.user.kickSessions, name: '踢全部会话', method: 'POST', path: '/api/v1/users/:id/kick-sessions', parent: 'user' },
{ code: Permissions.user.lock, name: '锁定用户', method: 'POST', path: '/api/v1/users/:id/lock', parent: 'user' },
{ code: Permissions.user.unlock, name: '解锁用户', method: 'POST', path: '/api/v1/users/:id/unlock', parent: 'user' },
];
/** 按钮资源(纯前端按钮,无独立受控接口)。 */
export 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' },
];
/** 内置枚举字典(值对齐 enums.ts,仅展示层 label/颜色/排序可改)。 */
export 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 },
] },
];
export interface InitTenantOptions {
prisma: PrismaClient;
tenantCode: string;
tenantName: string;
/** 顶层管理员角色码:平台=super_admin,业务租户=tenant_super_admin。 */
adminRoleCode: string;
adminRoleName: string;
adminUsername: string;
adminPassword: string;
/** 是否包含平台级权限(tenant:*)——仅平台租户为 true。 */
includePlatformPerms: boolean;
/** 管理员是否需首登强制改密(随机密码场景为 true)。 */
forcePasswordChange?: boolean;
}
export interface SyncTenantResourcesOptions {
prisma: PrismaClient;
tenantCode: string;
adminRoleCode: string;
includePlatformPerms: boolean;
}
/** 同步租户资源(菜单/接口/按钮)+ 内置字典 + 顶层角色全量授权(幂等;不触碰用户/密码)。 */
export async function syncTenantResources(opts: SyncTenantResourcesOptions): Promise<void> {
const { prisma, tenantCode, adminRoleCode, includePlatformPerms } = opts;
// 资源(菜单 + 接口 + 按钮;业务租户剥离 tenant:* 平台级接口)
const apis = includePlatformPerms ? APIS : APIS.filter((a) => !a.code.startsWith('tenant:'));
const allResourceCodes: string[] = [];
for (const m of MENUS) {
allResourceCodes.push(m.code);
await prisma.resource.upsert({
where: { tenantCode_code: { tenantCode, 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, 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, code: a.code } },
update: { name: a.name, parentCode: a.parent, method: a.method, apiPath: a.path, type: 'api' },
create: { tenantCode, 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, code: b.code } },
update: { name: b.name, parentCode: b.parent, type: 'button' },
create: { tenantCode, code: b.code, name: b.name, parentCode: b.parent, type: 'button', status: 'enabled' },
});
}
// 内置字典
for (const d of DICTS) {
await prisma.dictType.upsert({
where: { tenantCode_code: { tenantCode, code: d.code } },
update: { name: d.name, status: 'enabled', builtIn: true },
create: { tenantCode, 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, dictTypeCode: d.code, value: it.value } },
update: { label: it.label, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true },
create: { tenantCode, dictTypeCode: d.code, label: it.label, value: it.value, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true },
});
}
}
// 顶层角色全量授权
for (const code of allResourceCodes) {
await prisma.roleResource.upsert({
where: { tenantCode_roleCode_resourceCode: { tenantCode, roleCode: adminRoleCode, resourceCode: code } },
update: {},
create: { tenantCode, roleCode: adminRoleCode, resourceCode: code },
});
}
}
/** 初始化一个租户:根组织、顶层角色、管理员、全量资源、内置字典、授权(幂等)。 */
export async function initTenant(opts: InitTenantOptions): Promise<void> {
const { prisma, tenantCode, tenantName, adminRoleCode, adminRoleName, adminUsername, adminPassword, includePlatformPerms, forcePasswordChange = false } = opts;
// 1. 根组织
await prisma.org.upsert({
where: { tenantCode_code: { tenantCode, code: tenantCode } },
update: {},
create: { tenantCode, code: tenantCode, name: `${tenantName}总部`, parentCode: null, sort: 0, status: 'enabled' },
});
// 2. 顶层角色
await prisma.role.upsert({
where: { tenantCode_code: { tenantCode, code: adminRoleCode } },
update: {},
create: { tenantCode, code: adminRoleCode, name: adminRoleName, dataScope: 'all', status: 'enabled' },
});
// 3. 管理员账号(update 不重置密码/状态)
const passwordHash = await bcrypt.hash(adminPassword, 10);
const admin = await prisma.user.upsert({
where: { tenantCode_username: { tenantCode, username: adminUsername } },
update: {},
create: { tenantCode, username: adminUsername, passwordHash, nickname: '管理员', status: 'enabled', orgCode: tenantCode, mustChangePassword: forcePasswordChange },
});
// 4. 绑定管理员到顶层角色
await prisma.userRole.upsert({
where: { tenantCode_userId_roleCode: { tenantCode, userId: admin.id, roleCode: adminRoleCode } },
update: {},
create: { tenantCode, userId: admin.id, roleCode: adminRoleCode },
});
// 5. 资源 + 内置字典 + 全量授权
await syncTenantResources({ prisma, tenantCode, adminRoleCode, includePlatformPerms });
}