feat(admin): Add user management and upgrade to module permission system
Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
This commit is contained in:
@@ -55,6 +55,7 @@ export interface UserInfoResponse {
|
||||
departmentName?: string | null;
|
||||
isDefaultPassword: boolean;
|
||||
permissions: string[];
|
||||
modules: string[]; // 用户可访问的模块代码列表
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +78,7 @@ export class AuthService {
|
||||
const { phone, password } = request;
|
||||
|
||||
// 1. 查找用户
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { phone },
|
||||
include: {
|
||||
tenants: true,
|
||||
@@ -103,8 +104,9 @@ export class AuthService {
|
||||
throw new Error('账号已被禁用,请联系管理员');
|
||||
}
|
||||
|
||||
// 4. 获取用户权限
|
||||
// 4. 获取用户权限和模块列表
|
||||
const permissions = await this.getUserPermissions(user.role);
|
||||
const modules = await this.getUserModules(user.id);
|
||||
|
||||
// 5. 生成 JWT
|
||||
const jwtPayload: JWTPayload = {
|
||||
@@ -119,9 +121,9 @@ export class AuthService {
|
||||
const tokens = jwtService.generateTokens(jwtPayload);
|
||||
|
||||
// 6. 更新最后登录时间
|
||||
await prisma.User.update({
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { updatedAt: new Date() },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
|
||||
logger.info('用户登录成功(密码方式)', {
|
||||
@@ -129,6 +131,7 @@ export class AuthService {
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
modules: modules.length,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -145,6 +148,7 @@ export class AuthService {
|
||||
departmentName: user.departments?.name,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
permissions,
|
||||
modules, // 新增:返回模块列表
|
||||
},
|
||||
tokens,
|
||||
};
|
||||
@@ -180,7 +184,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
// 3. 查找用户
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { phone },
|
||||
include: {
|
||||
tenants: true,
|
||||
@@ -199,8 +203,9 @@ export class AuthService {
|
||||
throw new Error('账号已被禁用,请联系管理员');
|
||||
}
|
||||
|
||||
// 5. 获取用户权限
|
||||
// 5. 获取用户权限和模块列表
|
||||
const permissions = await this.getUserPermissions(user.role);
|
||||
const modules = await this.getUserModules(user.id);
|
||||
|
||||
// 6. 生成 JWT
|
||||
const jwtPayload: JWTPayload = {
|
||||
@@ -218,6 +223,7 @@ export class AuthService {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
modules: modules.length,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -234,6 +240,7 @@ export class AuthService {
|
||||
departmentName: user.departments?.name,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
permissions,
|
||||
modules, // 新增:返回模块列表
|
||||
},
|
||||
tokens,
|
||||
};
|
||||
@@ -243,7 +250,7 @@ export class AuthService {
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
async getCurrentUser(userId: string): Promise<UserInfoResponse> {
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
tenants: true,
|
||||
@@ -289,7 +296,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 2. 获取用户
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
@@ -309,7 +316,7 @@ export class AuthService {
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// 5. 更新密码
|
||||
await prisma.User.update({
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
@@ -326,7 +333,7 @@ export class AuthService {
|
||||
*/
|
||||
async sendVerificationCode(phone: string, type: 'LOGIN' | 'RESET_PASSWORD'): Promise<{ expiresIn: number }> {
|
||||
// 1. 检查用户是否存在
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { phone },
|
||||
});
|
||||
|
||||
@@ -375,7 +382,7 @@ export class AuthService {
|
||||
*/
|
||||
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||
return jwtService.refreshToken(refreshToken, async (userId) => {
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { tenants: true },
|
||||
});
|
||||
@@ -407,11 +414,59 @@ export class AuthService {
|
||||
return rolePermissions.map(rp => rp.permissions.code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户可访问的模块列表
|
||||
*
|
||||
* 逻辑:
|
||||
* 1. 查询用户所有租户关系
|
||||
* 2. 对每个租户,检查租户订阅的模块
|
||||
* 3. 如果用户有自定义模块权限,使用自定义权限
|
||||
* 4. 否则继承租户的全部模块权限
|
||||
* 5. 去重后返回所有可访问模块
|
||||
*/
|
||||
private async getUserModules(userId: string): Promise<string[]> {
|
||||
// 获取用户的所有租户关系
|
||||
const tenantMembers = await prisma.tenant_members.findMany({
|
||||
where: { user_id: userId },
|
||||
});
|
||||
|
||||
const allAccessibleModules = new Set<string>();
|
||||
|
||||
for (const tm of tenantMembers) {
|
||||
// 获取租户订阅的模块
|
||||
const tenantModules = await prisma.tenant_modules.findMany({
|
||||
where: {
|
||||
tenant_id: tm.tenant_id,
|
||||
is_enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取用户在该租户的自定义模块权限
|
||||
const userModules = await prisma.user_modules.findMany({
|
||||
where: {
|
||||
user_id: userId,
|
||||
tenant_id: tm.tenant_id,
|
||||
is_enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userModules.length > 0) {
|
||||
// 有自定义权限,使用自定义权限
|
||||
userModules.forEach(um => allAccessibleModules.add(um.module_code));
|
||||
} else {
|
||||
// 无自定义权限,继承租户所有模块
|
||||
tenantModules.forEach(tm => allAccessibleModules.add(tm.module_code));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(allAccessibleModules).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID获取JWT Payload(用于刷新Token)
|
||||
*/
|
||||
async getUserPayloadById(userId: string): Promise<JWTPayload | null> {
|
||||
const user = await prisma.User.findUnique({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { tenants: true },
|
||||
});
|
||||
|
||||
@@ -190,3 +190,5 @@ export const jwtService = new JWTService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -319,6 +319,8 @@ export function getBatchItems<T>(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -104,3 +104,5 @@ export function getAllFallbackCodes(): string[] {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,5 @@ export interface VariableValidation {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -194,3 +194,5 @@ export function createOpenAIStreamAdapter(
|
||||
return new OpenAIStreamAdapter(reply, model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -200,3 +200,5 @@ export async function streamChat(
|
||||
return service.streamGenerate(messages, callbacks);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,5 @@ export type {
|
||||
|
||||
export { THINKING_TAGS } from './types';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -93,3 +93,5 @@ export type SSEEventType =
|
||||
| 'error'
|
||||
| 'done';
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user