import { Body, Controller, Get, Headers, Param, Post } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { CoreGrpcService } from '../core-grpc.service'; import { OptionalPinDto, PinDto, TotpCodeDto, VerifySecurityPinDto } from '../dto/security.dto'; import { resolveAuthorizedPayload } from '../session-auth'; @ApiTags('Безопасность') @ApiBearerAuth() @Controller('security') export class SecurityController { constructor( private readonly core: CoreGrpcService, private readonly jwt: JwtService ) {} @Get('users/:userId/devices') @ApiOperation({ summary: 'Активные устройства', description: 'Показывает устройства пользователя и связанные активные сессии. Текущее устройство не отображается.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiResponse({ status: 200, description: 'Список устройств получен' }) async listDevices(@Param('userId') userId: string, @Headers('authorization') authorization?: string) { const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization); return this.core.security.ListActiveDevices({ userId, exceptSessionId: payload.sessionId }); } @Get('users/:userId/sessions') @ApiOperation({ summary: 'Активные сессии', description: 'Возвращает ACTIVE и LOCKED сессии пользователя для управления устройствами.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiResponse({ status: 200, description: 'Список активных сессий получен' }) async listSessions(@Param('userId') userId: string, @Headers('authorization') authorization?: string) { await resolveAuthorizedPayload(this.jwt, this.core, authorization); return this.core.security.ListActiveSessions({ userId }); } @Get('users/:userId/sign-in-history') @ApiOperation({ summary: 'История входов', description: 'Показывает последние попытки входа и причины отказов.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiResponse({ status: 200, description: 'История входов получена' }) listHistory(@Param('userId') userId: string, @Headers('authorization') authorization?: string) { return resolveAuthorizedPayload(this.jwt, this.core, authorization).then(() => this.core.security.ListSignInHistory({ userId }) ); } @Get('users/:userId/totp/status') @ApiOperation({ summary: 'Статус TOTP', description: 'Показывает, включена ли двухфакторная аутентификация через приложение.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) getTotpStatus(@Param('userId') userId: string) { return this.core.security.GetTotpStatus({ userId }); } @Post('users/:userId/totp/setup') @ApiOperation({ summary: 'Настроить TOTP', description: 'Генерирует секрет и otpauth URL для Google Authenticator и аналогов.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) setupTotp(@Param('userId') userId: string) { return this.core.security.SetupTotp({ userId }); } @Post('users/:userId/totp/enable') @ApiOperation({ summary: 'Включить TOTP', description: 'Подтверждает код из приложения и включает двухфакторную аутентификацию.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: TotpCodeDto }) enableTotp(@Param('userId') userId: string, @Body() dto: TotpCodeDto) { return this.core.security.EnableTotp({ userId, code: dto.code }); } @Post('users/:userId/totp/disable') @ApiOperation({ summary: 'Отключить TOTP', description: 'Отключает двухфакторную аутентификацию после проверки кода.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: TotpCodeDto }) disableTotp(@Param('userId') userId: string, @Body() dto: TotpCodeDto) { return this.core.security.DisableTotp({ userId, code: dto.code }); } @Post('users/:userId/pin/setup') @ApiOperation({ summary: 'Настроить PIN-код', description: 'Создает или включает PIN-код пользователя. PIN хранится только в виде bcrypt hash.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: PinDto }) @ApiResponse({ status: 201, description: 'PIN-код настроен' }) setupPin(@Param('userId') userId: string, @Body() dto: PinDto) { return this.core.security.SetupPin({ userId, pin: dto.pin }); } @Post('users/:userId/pin/update') @ApiOperation({ summary: 'Обновить PIN-код', description: 'Меняет PIN-код и переводит активные сессии пользователя в LOCKED до повторной проверки.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: PinDto }) @ApiResponse({ status: 201, description: 'PIN-код обновлен' }) updatePin(@Param('userId') userId: string, @Body() dto: PinDto) { return this.core.security.UpdatePin({ userId, pin: dto.pin }); } @Post('users/:userId/pin/delete-request') @ApiOperation({ summary: 'Запросить удаление PIN-кода', description: 'Запускает период ожидания перед отключением PIN-кода согласно SystemSetting.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: OptionalPinDto }) requestPinDeletion(@Param('userId') userId: string, @Body() dto: OptionalPinDto) { return this.core.security.RequestPinDeletion({ userId, pin: dto.pin ?? '' }); } @Post('users/:userId/pin/delete-cancel') @ApiOperation({ summary: 'Отменить удаление PIN-кода', description: 'Отменяет запланированное удаление PIN-кода.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) cancelPinDeletion(@Param('userId') userId: string) { return this.core.security.CancelPinDeletion({ userId }); } @Post('pin/verify') @ApiOperation({ summary: 'Проверить PIN-код', description: 'Проверяет PIN-код и возвращает access token с pinVerified=true для указанной сессии.' }) @ApiBody({ type: VerifySecurityPinDto }) @ApiResponse({ status: 201, description: 'PIN-код проверен' }) verifyPin(@Body() dto: VerifySecurityPinDto) { return this.core.security.VerifyPinCode(dto); } @Post('users/:userId/sessions/:sessionId/lock') @ApiOperation({ summary: 'Заблокировать сессию', description: 'Переводит активную сессию в LOCKED и сбрасывает pinVerified=false.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiParam({ name: 'sessionId', description: 'ID сессии' }) @ApiResponse({ status: 201, description: 'Сессия заблокирована' }) lockSession(@Param('userId') userId: string, @Param('sessionId') sessionId: string) { return this.core.security.LockSession({ userId, sessionId }); } @Post('users/:userId/sessions/:sessionId/revoke') @ApiOperation({ summary: 'Выйти с устройства', description: 'Отзывает конкретную сессию пользователя и завершает доступ с выбранного устройства.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiParam({ name: 'sessionId', description: 'ID сессии' }) @ApiResponse({ status: 201, description: 'Сессия отозвана' }) revokeSession(@Param('userId') userId: string, @Param('sessionId') sessionId: string) { return this.core.security.RevokeSession({ userId, sessionId }); } @Post('users/:userId/devices/:deviceId/revoke') @ApiOperation({ summary: 'Завершить сессии устройства', description: 'Отзывает все активные сессии выбранного устройства пользователя.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiParam({ name: 'deviceId', description: 'ID устройства' }) @ApiResponse({ status: 201, description: 'Сессии устройства завершены' }) revokeDevice(@Param('userId') userId: string, @Param('deviceId') deviceId: string) { return this.core.security.RevokeDevice({ userId, deviceId }); } @Post('users/:userId/revoke-all-sessions') @ApiOperation({ summary: 'Выйти везде', description: 'Отзывает все активные и PIN-заблокированные сессии пользователя, кроме текущей.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiResponse({ status: 201, description: 'Остальные сессии отозваны' }) async revokeAll(@Param('userId') userId: string, @Headers('authorization') authorization?: string) { const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization); return this.core.security.RevokeAllSessions({ userId, exceptSessionId: payload.sessionId }); } }