Files
aurak/server/src/user/user.controller.ts
T
Developer 7e741651db test: 系统性测试142项全通过 + 修复GET /users/:id缺失
测试架构(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>
2026-06-09 10:01:04 +08:00

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);
}
}