feat(admin): Complete tenant management and module access control system

Major Features:
- Tenant management CRUD (list, create, edit, delete, module configuration)
- Dynamic module management system (modules table with 8 modules)
- Multi-tenant module permission merging (ModuleService)
- Module access control middleware (requireModule)
- User module permission API (GET /api/v1/auth/me/modules)
- Frontend module permission filtering (HomePage + TopNavigation)

Module Integration:
- RVW module integrated with PromptService (editorial + methodology)
- All modules (RVW/PKB/ASL/DC) added authenticate + requireModule middleware
- Fixed ReviewTask foreign key constraint (cross-schema issue)
- Removed all MOCK_USER_ID, unified to request.user?.userId

Prompt Management Enhancements:
- Module names displayed in Chinese (RVW -> 智能审稿)
- Enhanced version history with view content and rollback features
- List page shows both activeVersion and draftVersion columns

Database Changes:
- Added platform_schema.modules table
- Modified tenant_modules table (added index and UUID)
- Removed ReviewTask foreign key to public.users (cross-schema fix)
- Seeded 8 modules: RVW, PKB, ASL, DC, IIT, AIA, SSA, ST

Documentation Updates:
- Updated ADMIN module development status
- Updated TODO checklist (89% progress)
- Updated Prompt management plan (Phase 3.5.5 completed)
- Added module authentication specification

Files Changed: 80+
Status: All features tested and verified locally
Next: User management module development
This commit is contained in:
2026-01-13 07:34:30 +08:00
parent 5523ef36ea
commit d595037316
51 changed files with 3550 additions and 287 deletions

View File

@@ -0,0 +1,273 @@
/**
* 模块权限服务
*
* 管理用户可访问的模块,支持多租户模块权限合并
*/
import { prisma } from '../../config/database.js';
import { logger } from '../logging/index.js';
/**
* 模块信息
*/
export interface ModuleInfo {
code: string;
name: string;
description?: string | null;
icon?: string | null;
}
/**
* 用户模块访问结果
*/
export interface UserModulesResult {
modules: string[]; // 可访问的模块代码列表
moduleDetails: ModuleInfo[]; // 模块详细信息
tenantModules: { // 按租户分组的模块
tenantId: string;
tenantName: string;
isPrimary: boolean;
modules: string[];
}[];
}
class ModuleService {
/**
* 获取所有可用模块
*/
async getAllModules(): Promise<ModuleInfo[]> {
const modules = await prisma.modules.findMany({
where: { is_active: true },
orderBy: { sort_order: 'asc' },
select: {
code: true,
name: true,
description: true,
icon: true,
},
});
return modules;
}
/**
* 获取用户可访问的所有模块
* 合并用户所属的所有租户的模块权限
*/
async getUserModules(userId: string): Promise<UserModulesResult> {
try {
// 1. 获取用户的主租户
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
tenant_id: true,
tenants: {
select: {
id: true,
name: true,
},
},
},
});
if (!user) {
logger.warn('[ModuleService] 用户不存在', { userId });
return { modules: [], moduleDetails: [], tenantModules: [] };
}
// 2. 获取用户加入的其他租户
const memberships = await prisma.tenant_members.findMany({
where: { user_id: userId },
select: {
tenant_id: true,
tenants: {
select: {
id: true,
name: true,
},
},
},
});
// 3. 构建租户列表(主租户 + 额外加入的租户)
const tenantMap = new Map<string, { name: string; isPrimary: boolean }>();
// 主租户
if (user.tenant_id && user.tenants) {
tenantMap.set(user.tenant_id, {
name: user.tenants.name,
isPrimary: true,
});
}
// 额外加入的租户
for (const m of memberships) {
if (!tenantMap.has(m.tenant_id)) {
tenantMap.set(m.tenant_id, {
name: m.tenants.name,
isPrimary: false,
});
}
}
const tenantIds = Array.from(tenantMap.keys());
if (tenantIds.length === 0) {
logger.warn('[ModuleService] 用户没有关联任何租户', { userId });
return { modules: [], moduleDetails: [], tenantModules: [] };
}
// 4. 查询所有租户的已开通模块
const tenantModulesData = await prisma.tenant_modules.findMany({
where: {
tenant_id: { in: tenantIds },
is_enabled: true,
OR: [
{ expires_at: null },
{ expires_at: { gt: new Date() } },
],
},
select: {
tenant_id: true,
module_code: true,
},
});
// 5. 按租户分组模块
const tenantModulesGrouped: UserModulesResult['tenantModules'] = [];
const modulesByTenant = new Map<string, string[]>();
for (const tm of tenantModulesData) {
if (!modulesByTenant.has(tm.tenant_id)) {
modulesByTenant.set(tm.tenant_id, []);
}
modulesByTenant.get(tm.tenant_id)!.push(tm.module_code);
}
Array.from(tenantMap.entries()).forEach(([tenantId, tenantInfo]) => {
tenantModulesGrouped.push({
tenantId,
tenantName: tenantInfo.name,
isPrimary: tenantInfo.isPrimary,
modules: modulesByTenant.get(tenantId) || [],
});
});
// 6. 合并所有模块(去重)
const moduleSet = new Set(tenantModulesData.map(tm => tm.module_code));
const allModuleCodes = Array.from(moduleSet);
// 7. 获取模块详细信息
const moduleDetails = await prisma.modules.findMany({
where: {
code: { in: allModuleCodes },
is_active: true,
},
orderBy: { sort_order: 'asc' },
select: {
code: true,
name: true,
description: true,
icon: true,
},
});
logger.debug('[ModuleService] 获取用户模块成功', {
userId,
tenantCount: tenantIds.length,
moduleCount: allModuleCodes.length,
});
return {
modules: allModuleCodes,
moduleDetails,
tenantModules: tenantModulesGrouped,
};
} catch (error) {
logger.error('[ModuleService] 获取用户模块失败', { userId, error });
return { modules: [], moduleDetails: [], tenantModules: [] };
}
}
/**
* 检查用户是否有权访问指定模块
*/
async canAccessModule(userId: string, moduleCode: string): Promise<boolean> {
try {
const { modules } = await this.getUserModules(userId);
return modules.includes(moduleCode);
} catch (error) {
logger.error('[ModuleService] 检查模块权限失败', { userId, moduleCode, error });
return false;
}
}
/**
* 获取租户的已开通模块
*/
async getTenantModules(tenantId: string): Promise<string[]> {
const modules = await prisma.tenant_modules.findMany({
where: {
tenant_id: tenantId,
is_enabled: true,
OR: [
{ expires_at: null },
{ expires_at: { gt: new Date() } },
],
},
select: { module_code: true },
});
return modules.map(m => m.module_code);
}
/**
* 设置租户的模块配置
*/
async setTenantModules(
tenantId: string,
moduleConfigs: { code: string; enabled: boolean; expiresAt?: Date | null }[]
): Promise<void> {
for (const config of moduleConfigs) {
// 先查询是否存在
const existing = await prisma.tenant_modules.findUnique({
where: {
tenant_id_module_code: {
tenant_id: tenantId,
module_code: config.code,
},
},
});
if (existing) {
// 更新
await prisma.tenant_modules.update({
where: { id: existing.id },
data: {
is_enabled: config.enabled,
expires_at: config.expiresAt,
},
});
} else {
// 创建(使用 crypto.randomUUID 生成 id
const { randomUUID } = await import('crypto');
await prisma.tenant_modules.create({
data: {
id: randomUUID(),
tenant_id: tenantId,
module_code: config.code,
is_enabled: config.enabled,
expires_at: config.expiresAt,
},
});
}
}
logger.info('[ModuleService] 更新租户模块配置', {
tenantId,
moduleCount: moduleConfigs.length,
});
}
}
// 导出单例
export const moduleService = new ModuleService();