forked from hangshuo652/aurak
7e741651db
测试架构(10大类142项): ┌──────────────────────────────────────────────────────┐ │ 1. 环境准备 4项 环境可达性 + 残留清理 │ │ 2. 身份认证 15项 登录/错误密码/空/篡改JWT │ │ 3. 用户CRUD正常 11项 创建/查询/编辑/升降级/删除 │ │ 4. 用户CRUD异常 17项 重复/空/短密码/不存在/权限 │ │ 5. 边界测试 7项 并发/超长/空权限/幂等 │ │ 6. 权限矩阵RBAC 49项 3层角色权限/API校验/系统保护 │ │ 7. 租户隔离 1项 跨租户不可见 │ │ 8. 缺陷回归 5项 系统角色保护/幂等删除 │ │ 9. 前端UI一致 22项 登录/导航/Tab/弹窗/3角色 │ │ 10.用户故事完整 14项 SA/TA/USER闭环/升降级即时生效 │ └──────────────────────────────────────────────────────┘ 发现并修复: - 系统角色权限可被任意修改(isSystem 保护缺失) - GET /users/:id 端点不存在 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
269 lines
7.2 KiB
TypeScript
269 lines
7.2 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
ForbiddenException,
|
|
NotFoundException,
|
|
Get,
|
|
Param,
|
|
Post,
|
|
Put,
|
|
Request,
|
|
UseGuards,
|
|
Query,
|
|
} from '@nestjs/common';
|
|
import { UserService } from './user.service';
|
|
import { CombinedAuthGuard } from '../auth/combined-auth.guard';
|
|
import { CreateUserDto } from './dto/create-user.dto';
|
|
import { UpdateUserDto } from './dto/update-user.dto';
|
|
import { I18nService } from '../i18n/i18n.service';
|
|
import { UserRole } from './user-role.enum';
|
|
import { UserSettingService } from './user-setting.service';
|
|
import { Permission } from '../auth/permission/permission.decorator';
|
|
import { PermissionsGuard } from '../auth/permission/permission.guard';
|
|
|
|
@Controller('users')
|
|
@UseGuards(CombinedAuthGuard)
|
|
export class UserController {
|
|
constructor(
|
|
private readonly userService: UserService,
|
|
private readonly i18nService: I18nService,
|
|
private readonly userSettingService: UserSettingService,
|
|
) {}
|
|
|
|
// --- API Key Management ---
|
|
@Get('api-key')
|
|
async getApiKey(@Request() req) {
|
|
const apiKey = await this.userService.getOrCreateApiKey(req.user.id);
|
|
return { apiKey };
|
|
}
|
|
|
|
@Post('api-key/rotate')
|
|
async rotateApiKey(@Request() req) {
|
|
const apiKey = await this.userService.regenerateApiKey(req.user.id);
|
|
return { apiKey };
|
|
}
|
|
|
|
// --- Personal Settings ---
|
|
@Get('settings')
|
|
async getSettings(@Request() req) {
|
|
return this.userSettingService.getByUser(req.user.id);
|
|
}
|
|
|
|
@Put('settings/language')
|
|
async updateLanguage(@Request() req, @Body() body: { language: string }) {
|
|
if (!body.language) throw new BadRequestException('language is required');
|
|
return this.userSettingService.update(req.user.id, body.language);
|
|
}
|
|
|
|
// --- Profile ---
|
|
@Get('profile')
|
|
async getProfile(@Request() req: any) {
|
|
return this.userService.findOneById(req.user.id);
|
|
}
|
|
|
|
@Get('tenants')
|
|
async getMyTenants(@Request() req: any) {
|
|
return this.userService.getUserTenants(req.user.id);
|
|
}
|
|
|
|
@Get('me')
|
|
async getMe(@Request() req) {
|
|
const user = await this.userService.findOneById(req.user.id);
|
|
if (!user) throw new NotFoundException();
|
|
|
|
let isNotebookEnabled = true;
|
|
if (user.tenantId) {
|
|
const settings = await this.userService.getTenantSettings(user.tenantId);
|
|
isNotebookEnabled = settings?.isNotebookEnabled ?? true;
|
|
}
|
|
|
|
const tenantName = user.tenantMembers?.[0]?.tenant?.name || 'Default';
|
|
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
displayName: user.displayName,
|
|
role: user.isAdmin ? UserRole.SUPER_ADMIN : UserRole.USER,
|
|
tenantId: user.tenantId,
|
|
tenantName,
|
|
isAdmin: user.isAdmin,
|
|
isNotebookEnabled,
|
|
};
|
|
}
|
|
|
|
@Get(':id')
|
|
@UseGuards(PermissionsGuard)
|
|
@Permission('user:view')
|
|
async findOne(@Param('id') id: string) {
|
|
const user = await this.userService.findOneById(id);
|
|
if (!user) throw new NotFoundException(this.i18nService.getErrorMessage('userNotFound'));
|
|
return user;
|
|
}
|
|
|
|
@Get()
|
|
@UseGuards(PermissionsGuard)
|
|
@Permission('user:view')
|
|
async findAll(
|
|
@Request() req,
|
|
@Query('page') page?: string,
|
|
@Query('limit') limit?: string,
|
|
) {
|
|
const p = page ? parseInt(page) : undefined;
|
|
const l = limit ? parseInt(limit) : undefined;
|
|
|
|
if (req.user.role === UserRole.SUPER_ADMIN) {
|
|
return this.userService.findAll(p, l);
|
|
} else {
|
|
return this.userService.findByTenantId(req.user.tenantId, p, l);
|
|
}
|
|
}
|
|
|
|
@Put('password')
|
|
async changePassword(
|
|
@Request() req,
|
|
@Body() body: { currentPassword: string; newPassword: string },
|
|
) {
|
|
const { currentPassword, newPassword } = body;
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
throw new BadRequestException(
|
|
this.i18nService.getErrorMessage('passwordsRequired'),
|
|
);
|
|
}
|
|
|
|
if (newPassword.length < 6) {
|
|
throw new BadRequestException(
|
|
this.i18nService.getErrorMessage('newPasswordMinLength'),
|
|
);
|
|
}
|
|
|
|
return this.userService.changePassword(
|
|
req.user.id,
|
|
currentPassword,
|
|
newPassword,
|
|
);
|
|
}
|
|
|
|
@Post()
|
|
@UseGuards(PermissionsGuard)
|
|
@Permission('user:create')
|
|
async createUser(@Request() req, @Body() body: CreateUserDto) {
|
|
const { username, password } = body;
|
|
|
|
if (!username || !password) {
|
|
throw new BadRequestException(
|
|
this.i18nService.getErrorMessage('usernamePasswordRequired'),
|
|
);
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
throw new BadRequestException(
|
|
this.i18nService.getErrorMessage('passwordMinLength'),
|
|
);
|
|
}
|
|
|
|
// All new users default to non-admin.
|
|
let isAdmin = false;
|
|
|
|
// Pass the calculated params to the service
|
|
return this.userService.createUser(
|
|
username,
|
|
password,
|
|
isAdmin,
|
|
req.user.tenantId,
|
|
body.displayName,
|
|
);
|
|
}
|
|
|
|
@Put(':id')
|
|
@UseGuards(PermissionsGuard)
|
|
@Permission('user:edit')
|
|
async updateUser(
|
|
@Request() req,
|
|
@Body() body: UpdateUserDto,
|
|
@Param('id') id: string,
|
|
) {
|
|
const callerRole = req.user.role;
|
|
|
|
// Get user info to update
|
|
const userToUpdate = await this.userService.findOneById(id);
|
|
if (!userToUpdate) {
|
|
throw new NotFoundException(
|
|
this.i18nService.getErrorMessage('userNotFound'),
|
|
);
|
|
}
|
|
|
|
if (
|
|
callerRole === 'TENANT_ADMIN' &&
|
|
userToUpdate.tenantId !== req.user.tenantId
|
|
) {
|
|
throw new ForbiddenException('Cannot modify users outside your tenant');
|
|
}
|
|
|
|
// Prevent modifying the builtin admin account
|
|
if (userToUpdate.username === 'admin') {
|
|
throw new ForbiddenException(
|
|
this.i18nService.getErrorMessage('cannotModifyBuiltinAdmin'),
|
|
);
|
|
}
|
|
|
|
// Role modification is now obsolete on global level.
|
|
if (body.isAdmin !== undefined && userToUpdate.isAdmin !== body.isAdmin) {
|
|
if (callerRole !== UserRole.SUPER_ADMIN) {
|
|
throw new ForbiddenException(
|
|
'Only Super Admins can change user admin status.',
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate password length if provided
|
|
if (body.password && body.password.length < 6) {
|
|
throw new BadRequestException(
|
|
this.i18nService.getErrorMessage('passwordMinLength'),
|
|
);
|
|
}
|
|
|
|
return this.userService.updateUser(id, body);
|
|
}
|
|
|
|
@Delete(':id')
|
|
@UseGuards(PermissionsGuard)
|
|
@Permission('user:delete')
|
|
async deleteUser(@Request() req, @Param('id') id: string) {
|
|
const callerRole = req.user.role;
|
|
|
|
// Prevent admin from deleting themselves
|
|
if (req.user.id === id) {
|
|
throw new BadRequestException(
|
|
this.i18nService.getErrorMessage('cannotDeleteSelf'),
|
|
);
|
|
}
|
|
|
|
// Get user info to delete
|
|
const userToDelete = await this.userService.findOneById(id);
|
|
if (!userToDelete) {
|
|
throw new NotFoundException(
|
|
this.i18nService.getErrorMessage('userNotFound'),
|
|
);
|
|
}
|
|
|
|
if (
|
|
callerRole === 'TENANT_ADMIN' &&
|
|
userToDelete.tenantId !== req.user.tenantId
|
|
) {
|
|
throw new ForbiddenException('Cannot delete users outside your tenant');
|
|
}
|
|
|
|
// Block deletion of built-in admin account
|
|
if (userToDelete.username === 'admin') {
|
|
throw new ForbiddenException(
|
|
this.i18nService.getErrorMessage('cannotDeleteBuiltinAdmin'),
|
|
);
|
|
}
|
|
|
|
return this.userService.deleteUser(id);
|
|
}
|
|
}
|