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:
273
backend/src/common/auth/module.service.ts
Normal file
273
backend/src/common/auth/module.service.ts
Normal 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();
|
||||
|
||||
Reference in New Issue
Block a user