import { Body, Controller, Delete, ForbiddenException, Get, Headers, Param, Patch, 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 { getAuthorizedUserId } from '../document-access'; import { ChangePasswordDto, PasswordVerificationDto, SendPasswordVerificationOtpDto, SetPasswordDto, UpdateAvatarDto, UpdateContactsDto, UpdateProfileDto } from '../dto/profile.dto'; @ApiTags('Профиль и биометрия') @ApiBearerAuth() @Controller('profile/users/:userId') export class ProfileController { constructor( private readonly core: CoreGrpcService, private readonly jwt: JwtService ) {} private async assertSelfAccess(authorization: string | undefined, userId: string) { const requesterId = await getAuthorizedUserId(this.jwt, this.core, authorization); if (requesterId !== userId) { throw new ForbiddenException('Можно изменять только свой профиль'); } return requesterId; } @Get() @ApiOperation({ summary: 'Получить профиль пользователя', description: 'Возвращает публичный профиль, аватар и основные/резервные контакты пользователя.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiResponse({ status: 200, description: 'Профиль пользователя успешно получен' }) @ApiResponse({ status: 404, description: 'Пользователь не найден' }) getProfile(@Param('userId') userId: string) { return this.core.profile.GetProfile({ userId }); } @Patch('avatar') @ApiOperation({ summary: 'Обновить аватар', description: 'Сохраняет URL или ключ объекта MinIO как аватар пользователя.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: UpdateAvatarDto }) @ApiResponse({ status: 200, description: 'Аватар обновлен' }) updateAvatar(@Param('userId') userId: string, @Body() dto: UpdateAvatarDto) { return this.core.profile.UpdateAvatar({ userId, avatarUrl: dto.avatarUrl, avatarStorageKey: dto.avatarStorageKey }); } @Patch() @ApiOperation({ summary: 'Обновить профиль', description: 'Обновляет firstName, lastName, bio, age и gender в UserProfile, а displayName синхронизирует с именем и фамилией.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: UpdateProfileDto }) @ApiResponse({ status: 200, description: 'Профиль обновлен' }) updateProfile(@Param('userId') userId: string, @Body() dto: UpdateProfileDto) { return this.core.profile.UpdateProfile({ userId, ...dto }); } @Patch('contacts') @ApiOperation({ summary: 'Обновить контакты', description: 'Обновляет основную и резервную почту/телефон в модели User.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: UpdateContactsDto }) @ApiResponse({ status: 200, description: 'Контакты обновлены' }) @ApiResponse({ status: 400, description: 'Переданы некорректные контактные данные' }) updateContacts(@Param('userId') userId: string, @Body() dto: UpdateContactsDto) { return this.core.profile.UpdateContacts({ userId, ...dto }); } @Post('password') @ApiOperation({ summary: 'Установить пароль', description: 'Устанавливает пароль для аккаунта без ранее заданного пароля.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiBody({ type: SetPasswordDto }) @ApiResponse({ status: 201, description: 'Пароль установлен' }) async setPassword( @Headers('authorization') authorization: string | undefined, @Param('userId') userId: string, @Body() dto: SetPasswordDto ) { await this.assertSelfAccess(authorization, userId); return this.core.profile.SetPassword({ userId, password: dto.password }); } @Post('password/otp') @ApiOperation({ summary: 'Отправить OTP для смены пароля' }) async sendPasswordVerificationOtp( @Headers('authorization') authorization: string | undefined, @Param('userId') userId: string, @Body() dto: SendPasswordVerificationOtpDto ) { await this.assertSelfAccess(authorization, userId); return this.core.profile.SendPasswordVerificationOtp({ userId, channel: dto.channel }); } @Patch('password') @ApiOperation({ summary: 'Сменить пароль с подтверждением личности' }) async changePassword( @Headers('authorization') authorization: string | undefined, @Param('userId') userId: string, @Body() dto: ChangePasswordDto ) { await this.assertSelfAccess(authorization, userId); return this.core.profile.ChangePassword({ userId, ...dto }); } @Delete('password') @ApiOperation({ summary: 'Удалить пароль с подтверждением личности' }) async removePassword( @Headers('authorization') authorization: string | undefined, @Param('userId') userId: string, @Body() dto: PasswordVerificationDto ) { await this.assertSelfAccess(authorization, userId); return this.core.profile.RemovePassword({ userId, ...dto }); } @Post('self-delete') @ApiOperation({ summary: 'Запросить удаление своего профиля', description: 'Планирует удаление аккаунта через период ожидания ACCOUNT_DELETE_GRACE_DAYS. До истечения срока пользователь может отменить удаление.' }) @ApiParam({ name: 'userId', description: 'ID пользователя' }) @ApiResponse({ status: 201, description: 'Удаление запланировано' }) async selfDelete(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) { await this.assertSelfAccess(authorization, userId); return this.core.profile.RequestAccountDeletion({ userId }); } @Post('self-delete/cancel') @ApiOperation({ summary: 'Отменить запланированное удаление профиля' }) async cancelSelfDelete(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) { await this.assertSelfAccess(authorization, userId); return this.core.profile.CancelAccountDeletion({ userId }); } @Get('self-delete/status') @ApiOperation({ summary: 'Статус запланированного удаления профиля' }) async selfDeleteStatus(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) { await this.assertSelfAccess(authorization, userId); return this.core.profile.GetAccountDeletionStatus({ userId }); } @Get('e2e-public-key') @ApiOperation({ summary: 'Получить публичный E2E-ключ пользователя' }) async getE2EPublicKey(@Param('userId') userId: string) { return this.core.profile.GetE2EPublicKey({ userId }); } @Patch('e2e-public-key') @ApiOperation({ summary: 'Сохранить свой публичный E2E-ключ' }) async setE2EPublicKey( @Headers('authorization') authorization: string | undefined, @Param('userId') userId: string, @Body() dto: { publicKey: string } ) { await this.assertSelfAccess(authorization, userId); return this.core.profile.SetE2EPublicKey({ userId, publicKey: dto.publicKey }); } }