more fix and update
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Headers, Ip, 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 { QrSessionDto, WebAuthnDto } from '../dto/identity.dto';
|
||||
import { resolveAuthorizedPayload } from '../session-auth';
|
||||
|
||||
@ApiTags('Биометрия и QR-вход')
|
||||
@Controller('auth/advanced')
|
||||
export class AdvancedAuthController {
|
||||
constructor(private readonly core: CoreGrpcService) {}
|
||||
constructor(
|
||||
private readonly core: CoreGrpcService,
|
||||
private readonly jwt: JwtService
|
||||
) {}
|
||||
|
||||
@Post('webauthn/register/challenge')
|
||||
@ApiOperation({ summary: 'Challenge регистрации WebAuthn', description: 'Создает challenge для регистрации лица/отпечатка. Endpoint готов для подключения настоящего WebAuthn attestation.' })
|
||||
@@ -28,15 +33,31 @@ export class AdvancedAuthController {
|
||||
@ApiOperation({ summary: 'Создать QR-сессию', description: 'Создает временную QR-сессию для входа с другого устройства.' })
|
||||
@ApiBody({ type: QrSessionDto })
|
||||
@ApiResponse({ status: 201, description: 'QR-сессия создана' })
|
||||
createQr(@Body() dto: QrSessionDto) {
|
||||
return this.core.advancedAuth.CreateQrSession(dto);
|
||||
createQr(@Body() dto: QrSessionDto, @Ip() ipAddress?: string, @Headers('user-agent') userAgent?: string) {
|
||||
return this.core.advancedAuth.CreateQrSession({
|
||||
deviceName: dto.deviceName,
|
||||
fingerprint: dto.fingerprint,
|
||||
deviceType: dto.deviceType ?? 'WEB',
|
||||
ipAddress,
|
||||
userAgent
|
||||
});
|
||||
}
|
||||
|
||||
@Get('qr/session/:sessionId')
|
||||
@ApiOperation({ summary: 'Проверить QR-сессию', description: 'Возвращает текущий статус QR-сессии: PENDING/CONFIRMED/EXPIRED.' })
|
||||
@ApiOperation({ summary: 'Проверить QR-сессию', description: 'Возвращает текущий статус QR-сессии: PENDING/APPROVED/EXPIRED.' })
|
||||
@ApiParam({ name: 'sessionId', description: 'ID QR-сессии' })
|
||||
@ApiResponse({ status: 200, description: 'Статус QR-сессии получен' })
|
||||
pollQr(@Param('sessionId') sessionId: string) {
|
||||
return this.core.advancedAuth.PollQrSession({ sessionId });
|
||||
}
|
||||
|
||||
@Post('qr/session/:sessionId/approve')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Подтвердить QR-вход', description: 'Подтверждает QR-сессию с мобильного приложения уже авторизованным пользователем.' })
|
||||
@ApiParam({ name: 'sessionId', description: 'ID QR-сессии' })
|
||||
@ApiResponse({ status: 201, description: 'QR-сессия подтверждена' })
|
||||
async approveQr(@Param('sessionId') sessionId: string, @Headers('authorization') authorization?: string) {
|
||||
const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization);
|
||||
return this.core.advancedAuth.ApproveQrSession({ sessionId, userId: payload.sub });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Body, Controller, Get, Headers, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Headers, Ip, Post } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { IdentifyDto, LdapLoginDto, LoginDto, PasswordlessOtpDto, PasswordlessVerifyDto, PasswordLoginDto, RefreshSessionDto, RegisterDto, VerifyPinDto } from '../dto/auth.dto';
|
||||
import { BeginTotpLoginDto, IdentifyDto, LdapLoginDto, LoginDto, PasswordlessOtpDto, PasswordlessVerifyDto, PasswordLoginDto, RefreshSessionDto, RegisterDto, VerifyPinDto, VerifyTotpLoginDto } from '../dto/auth.dto';
|
||||
import { resolveAuthorizedPayload, verifyAccessToken } from '../session-auth';
|
||||
|
||||
@ApiTags('Аутентификация')
|
||||
@@ -38,8 +38,8 @@ export class AuthController {
|
||||
@Post('otp/send')
|
||||
@ApiOperation({ summary: 'Отправить OTP для входа', description: 'Passwordless-first вход: пользователь вводит почту или телефон, сервер создает 6-значный код и пишет его в console.log.' })
|
||||
@ApiBody({ type: PasswordlessOtpDto })
|
||||
sendOtp(@Body() dto: PasswordlessOtpDto) {
|
||||
return this.core.auth.SendOtp({ recipient: dto.recipient, channel: dto.channel });
|
||||
sendOtp(@Body() dto: PasswordlessOtpDto, @Ip() ipAddress?: string) {
|
||||
return this.core.auth.SendOtp({ recipient: dto.recipient, channel: dto.channel, ipAddress });
|
||||
}
|
||||
|
||||
@Post('otp/verify')
|
||||
@@ -70,6 +70,20 @@ export class AuthController {
|
||||
return this.core.auth.VerifyPin(dto);
|
||||
}
|
||||
|
||||
@Post('totp/begin')
|
||||
@ApiOperation({ summary: 'Начать вход по TOTP', description: 'Создаёт challenge для входа через приложение-аутентификатор вместо SMS/email OTP.' })
|
||||
@ApiBody({ type: BeginTotpLoginDto })
|
||||
beginTotpLogin(@Body() dto: BeginTotpLoginDto) {
|
||||
return this.core.auth.BeginTotpLogin(dto);
|
||||
}
|
||||
|
||||
@Post('totp/verify')
|
||||
@ApiOperation({ summary: 'Подтвердить TOTP при входе', description: 'Завершает вход после проверки кода из Google Authenticator или аналога.' })
|
||||
@ApiBody({ type: VerifyTotpLoginDto })
|
||||
verifyTotpLogin(@Body() dto: VerifyTotpLoginDto) {
|
||||
return this.core.auth.VerifyTotpLogin(dto);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@ApiOperation({ summary: 'Обновить access token', description: 'Обновляет JWT по refresh token. Если сессия заблокирована PIN-кодом, возвращает requiresPin=true без выхода из аккаунта.' })
|
||||
@ApiBody({ type: RefreshSessionDto })
|
||||
|
||||
@@ -1,130 +1,148 @@
|
||||
import { Body, Controller, Delete, ForbiddenException, Get, Headers, Param, Patch, Post } from '@nestjs/common';
|
||||
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
|
||||
import { getAuthorizedUserId } from '../document-access';
|
||||
|
||||
import { AddFamilyMemberDto, CreateFamilyGroupDto, RespondFamilyInviteDto, SendFamilyInviteDto, UpdateFamilyGroupDto } from '../dto/identity.dto';
|
||||
|
||||
|
||||
|
||||
@ApiTags('Семья')
|
||||
|
||||
@ApiBearerAuth()
|
||||
|
||||
@Controller('family')
|
||||
|
||||
export class FamilyController {
|
||||
|
||||
constructor(
|
||||
|
||||
private readonly core: CoreGrpcService,
|
||||
|
||||
private readonly jwt: JwtService
|
||||
|
||||
) {}
|
||||
|
||||
|
||||
|
||||
private async auth(authorization?: string) {
|
||||
|
||||
return getAuthorizedUserId(this.jwt, this.core, authorization);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Post('groups')
|
||||
|
||||
@ApiOperation({ summary: 'Создать семейную группу' })
|
||||
|
||||
@ApiBody({ type: CreateFamilyGroupDto })
|
||||
|
||||
async createGroup(@Headers('authorization') authorization: string | undefined, @Body() dto: CreateFamilyGroupDto) {
|
||||
|
||||
const userId = await this.auth(authorization);
|
||||
|
||||
if (dto.ownerId !== userId) {
|
||||
|
||||
throw new ForbiddenException('Можно создавать семью только от своего имени');
|
||||
|
||||
}
|
||||
|
||||
return firstValueFrom(this.core.family.CreateFamilyGroup(dto));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Get('users/:userId/groups')
|
||||
|
||||
@ApiOperation({ summary: 'Список семей пользователя' })
|
||||
|
||||
async listGroups(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) {
|
||||
|
||||
const requesterId = await this.auth(authorization);
|
||||
|
||||
if (requesterId !== userId) {
|
||||
|
||||
throw new ForbiddenException('Можно просматривать только свои семьи');
|
||||
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
|
||||
this.core.family.ListFamilyGroups({ userId }).pipe(
|
||||
|
||||
map((response) => {
|
||||
|
||||
const payload = response as { groups?: unknown[] };
|
||||
|
||||
return { groups: payload.groups ?? [] };
|
||||
|
||||
})
|
||||
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Get('groups/:groupId')
|
||||
|
||||
@ApiOperation({ summary: 'Получить семейную группу' })
|
||||
|
||||
async getGroup(@Headers('authorization') authorization: string | undefined, @Param('groupId') groupId: string) {
|
||||
|
||||
const requesterId = await this.auth(authorization);
|
||||
|
||||
return firstValueFrom(this.core.family.GetFamilyGroup({ requesterId, groupId }));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Patch('groups/:groupId')
|
||||
|
||||
@ApiOperation({ summary: 'Обновить семейную группу' })
|
||||
|
||||
async updateGroup(
|
||||
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
|
||||
@Param('groupId') groupId: string,
|
||||
|
||||
@Body() dto: UpdateFamilyGroupDto
|
||||
|
||||
) {
|
||||
|
||||
const requesterId = await this.auth(authorization);
|
||||
|
||||
import { Body, Controller, Delete, ForbiddenException, Get, Headers, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { getAuthorizedUserId } from '../document-access';
|
||||
import { AddFamilyMemberDto, CreateFamilyGroupDto, RespondFamilyInviteDto, SendFamilyInviteDto, UpdateFamilyGroupDto } from '../dto/identity.dto';
|
||||
|
||||
@ApiTags('Семья')
|
||||
@ApiBearerAuth()
|
||||
@Controller('family')
|
||||
export class FamilyController {
|
||||
constructor(
|
||||
private readonly core: CoreGrpcService,
|
||||
private readonly jwt: JwtService
|
||||
) {}
|
||||
|
||||
private async auth(authorization?: string) {
|
||||
return getAuthorizedUserId(this.jwt, this.core, authorization);
|
||||
}
|
||||
|
||||
@Post('groups')
|
||||
@ApiOperation({ summary: 'Создать семейную группу' })
|
||||
@ApiBody({ type: CreateFamilyGroupDto })
|
||||
async createGroup(@Headers('authorization') authorization: string | undefined, @Body() dto: CreateFamilyGroupDto) {
|
||||
const userId = await this.auth(authorization);
|
||||
if (dto.ownerId !== userId) {
|
||||
throw new ForbiddenException('Можно создавать семью только от своего имени');
|
||||
}
|
||||
return firstValueFrom(this.core.family.CreateFamilyGroup(dto));
|
||||
}
|
||||
|
||||
@Get('users/:userId/groups')
|
||||
@ApiOperation({ summary: 'Список семей пользователя' })
|
||||
async listGroups(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
if (requesterId !== userId) {
|
||||
throw new ForbiddenException('Можно просматривать только свои семьи');
|
||||
}
|
||||
return firstValueFrom(
|
||||
this.core.family.ListFamilyGroups({ userId }).pipe(
|
||||
map((response) => {
|
||||
const payload = response as { groups?: unknown[] };
|
||||
return { groups: payload.groups ?? [] };
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Get('groups/:groupId')
|
||||
@ApiOperation({ summary: 'Получить семейную группу' })
|
||||
async getGroup(@Headers('authorization') authorization: string | undefined, @Param('groupId') groupId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.GetFamilyGroup({ requesterId, groupId }));
|
||||
}
|
||||
|
||||
@Patch('groups/:groupId')
|
||||
@ApiOperation({ summary: 'Обновить семейную группу' })
|
||||
async updateGroup(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Param('groupId') groupId: string,
|
||||
@Body() dto: UpdateFamilyGroupDto
|
||||
) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.UpdateFamilyGroup({ requesterId, groupId, name: dto.name }));
|
||||
}
|
||||
|
||||
@Delete('groups/:groupId')
|
||||
@ApiOperation({ summary: 'Удалить семейную группу' })
|
||||
async deleteGroup(@Headers('authorization') authorization: string | undefined, @Param('groupId') groupId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.DeleteFamilyGroup({ requesterId, groupId }));
|
||||
}
|
||||
|
||||
@Post('groups/:groupId/members')
|
||||
@ApiOperation({ summary: 'Добавить участника семьи' })
|
||||
async addMember(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Param('groupId') groupId: string,
|
||||
@Body() dto: AddFamilyMemberDto
|
||||
) {
|
||||
await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.AddFamilyMember({ groupId, ...dto }));
|
||||
}
|
||||
|
||||
@Delete('members/:memberId')
|
||||
@ApiOperation({ summary: 'Удалить участника семьи' })
|
||||
async removeMember(@Headers('authorization') authorization: string | undefined, @Param('memberId') memberId: string) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.RemoveFamilyMember({ requesterId, memberId }));
|
||||
}
|
||||
|
||||
@Get('groups/:groupId/invite-search')
|
||||
@ApiOperation({ summary: 'Поиск пользователей для приглашения в семью' })
|
||||
async searchInviteUsers(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Param('groupId') groupId: string,
|
||||
@Query('q') query: string
|
||||
) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.SearchFamilyInviteUsers({ requesterId, groupId, query: query ?? '' }));
|
||||
}
|
||||
|
||||
@Post('groups/:groupId/invites')
|
||||
@ApiOperation({ summary: 'Пригласить участника в семью' })
|
||||
async sendInvite(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Param('groupId') groupId: string,
|
||||
@Body() dto: SendFamilyInviteDto
|
||||
) {
|
||||
const requesterId = await this.auth(authorization);
|
||||
return firstValueFrom(
|
||||
this.core.family.SendFamilyInvite({
|
||||
requesterId,
|
||||
groupId,
|
||||
target: dto.target,
|
||||
inviteeUserId: dto.inviteeUserId
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Get('invites')
|
||||
@ApiOperation({ summary: 'Список приглашений' })
|
||||
async listInvites(@Headers('authorization') authorization: string | undefined) {
|
||||
const userId = await this.auth(authorization);
|
||||
return firstValueFrom(
|
||||
this.core.family.ListFamilyInvites({ userId }).pipe(
|
||||
map((response) => {
|
||||
const payload = response as { invites?: unknown[] };
|
||||
return { invites: payload.invites ?? [] };
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Post('invites/:inviteId/respond')
|
||||
@ApiOperation({ summary: 'Принять или отклонить приглашение' })
|
||||
async respondInvite(
|
||||
@Headers('authorization') authorization: string | undefined,
|
||||
@Param('inviteId') inviteId: string,
|
||||
@Body() dto: RespondFamilyInviteDto
|
||||
) {
|
||||
const userId = await this.auth(authorization);
|
||||
return firstValueFrom(this.core.family.RespondFamilyInvite({ userId, inviteId, accept: dto.accept }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Body, Controller, Get, Headers, Param, Post, Query, Res } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { BadRequestException, Body, Controller, ForbiddenException, Get, Headers, Param, Post, Query, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { getAuthorizedUserId } from '../document-access';
|
||||
@@ -44,6 +45,49 @@ export class MediaController {
|
||||
return getAuthorizedUserId(this.jwt, this.core, authorization);
|
||||
}
|
||||
|
||||
@Post('upload')
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Загрузить файл через API', description: 'Принимает файл по uploadToken из upload-url ответа. Обходит прямой доступ браузера к MinIO.' })
|
||||
@ApiResponse({ status: 201, description: 'Файл загружен' })
|
||||
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 50 * 1024 * 1024 } }))
|
||||
async uploadObject(
|
||||
@Headers('x-upload-token') uploadToken: string | undefined,
|
||||
@UploadedFile() file: { buffer: Buffer; mimetype: string } | undefined
|
||||
) {
|
||||
if (!uploadToken) {
|
||||
throw new BadRequestException('Не передан uploadToken');
|
||||
}
|
||||
if (!file) {
|
||||
throw new BadRequestException('Файл не передан');
|
||||
}
|
||||
|
||||
let payload: { purpose?: string; storageKey?: string; contentType?: string };
|
||||
try {
|
||||
payload = await this.jwt.verifyAsync(uploadToken, {
|
||||
secret: process.env.JWT_ACCESS_SECRET ?? 'docker-access-secret',
|
||||
issuer: 'id.lendry.ru'
|
||||
});
|
||||
} catch {
|
||||
throw new ForbiddenException('Ссылка загрузки недействительна или истекла');
|
||||
}
|
||||
|
||||
if (payload.purpose !== 'media-upload' || !payload.storageKey) {
|
||||
throw new ForbiddenException('Ссылка загрузки недействительна');
|
||||
}
|
||||
|
||||
const bucket = process.env.MINIO_BUCKET ?? 'lendry-id';
|
||||
await this.getS3Client().send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: payload.storageKey,
|
||||
Body: file.buffer,
|
||||
ContentType: file.mimetype || payload.contentType || 'application/octet-stream'
|
||||
})
|
||||
);
|
||||
|
||||
return { ok: true, storageKey: payload.storageKey };
|
||||
}
|
||||
|
||||
@Post('avatars/upload-url')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Получить URL для загрузки аватара', description: 'Возвращает presigned URL MinIO для загрузки изображения аватара.' })
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
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, VerifySecurityPinDto } from '../dto/security.dto';
|
||||
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) {}
|
||||
constructor(
|
||||
private readonly core: CoreGrpcService,
|
||||
private readonly jwt: JwtService
|
||||
) {}
|
||||
|
||||
@Get('users/:userId/devices')
|
||||
@ApiOperation({ summary: 'Активные устройства', description: 'Показывает устройства пользователя и связанные активные сессии.' })
|
||||
@ApiOperation({ summary: 'Активные устройства', description: 'Показывает устройства пользователя и связанные активные сессии. Текущее устройство не отображается.' })
|
||||
@ApiParam({ name: 'userId', description: 'ID пользователя' })
|
||||
@ApiResponse({ status: 200, description: 'Список устройств получен' })
|
||||
listDevices(@Param('userId') userId: string) {
|
||||
return this.core.security.ListActiveDevices({ userId });
|
||||
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')
|
||||
@@ -33,6 +39,36 @@ export class SecurityController {
|
||||
return 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 пользователя' })
|
||||
@@ -102,10 +138,11 @@ export class SecurityController {
|
||||
}
|
||||
|
||||
@Post('users/:userId/revoke-all-sessions')
|
||||
@ApiOperation({ summary: 'Выйти везде', description: 'Отзывает все активные и PIN-заблокированные сессии пользователя.' })
|
||||
@ApiOperation({ summary: 'Выйти везде', description: 'Отзывает все активные и PIN-заблокированные сессии пользователя, кроме текущей.' })
|
||||
@ApiParam({ name: 'userId', description: 'ID пользователя' })
|
||||
@ApiResponse({ status: 201, description: 'Все сессии отозваны' })
|
||||
revokeAll(@Param('userId') userId: string) {
|
||||
return this.core.security.RevokeAllSessions({ userId });
|
||||
@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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user