161 lines
7.8 KiB
TypeScript
161 lines
7.8 KiB
TypeScript
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 });
|
||
}
|
||
}
|
||
|