Files
IdP/apps/api-gateway/src/controllers/auth.controller.ts
2026-06-29 22:51:25 +03:00

148 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Body, Controller, Get, Headers, Post, Req, UseInterceptors } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Request } from 'express';
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { enrichAuthClientMeta } from '../client-request.util';
import { BeginTotpLoginDto, IdentifyDto, LdapLoginDto, LoginDto, PasswordlessOtpDto, PasswordlessVerifyDto, PasswordLoginDto, RefreshSessionDto, RegisterDto, VerifyPinDto, VerifyTotpLoginDto } from '../dto/auth.dto';
import { resolveAuthorizedPayload, verifyAccessToken } from '../session-auth';
import { FedcmCookieInterceptor } from '../interceptors/fedcm-cookie.interceptor';
@ApiTags('Аутентификация')
@Controller('auth')
@UseInterceptors(FedcmCookieInterceptor)
export class AuthController {
constructor(
private readonly core: CoreGrpcService,
private readonly jwt: JwtService
) {}
@Post('register')
@ApiOperation({ summary: 'Регистрация пользователя', description: 'Создает пользователя. Первый пользователь безопасно получает права super admin.' })
@ApiBody({ type: RegisterDto })
register(@Body() dto: RegisterDto) {
return this.core.auth.Register(dto);
}
@Post('login')
@ApiOperation({ summary: 'Вход по почте, телефону или логину', description: 'Возвращает JWT и refresh token. Если PIN включен, сессия создается в ограниченном режиме.' })
@ApiBody({ type: LoginDto })
login(@Body() dto: LoginDto, @Req() req: Request) {
return this.core.auth.Login(enrichAuthClientMeta(req, dto));
}
@Post('identify')
@ApiOperation({ summary: 'Проверить способ входа', description: 'Identifier-first шаг: проверяет, существует ли пользователь, задан ли пароль и включен ли PIN.' })
@ApiBody({ type: IdentifyDto })
identify(@Body() dto: IdentifyDto) {
return this.core.auth.Identify(dto);
}
@Post('otp/send')
@ApiOperation({ summary: 'Отправить OTP для входа', description: 'Passwordless-first вход: пользователь вводит почту или телефон, сервер создает 6-значный код и пишет его в console.log.' })
@ApiBody({ type: PasswordlessOtpDto })
sendOtp(@Body() dto: PasswordlessOtpDto, @Req() req: Request) {
return this.core.auth.SendOtp({ recipient: dto.recipient, channel: dto.channel, ipAddress: enrichAuthClientMeta(req, {}).ipAddress });
}
@Post('otp/verify')
@ApiOperation({ summary: 'Проверить OTP для входа', description: 'Если пользователь не существует, создает его. Если пароль не задан, сразу возвращает JWT. Если пароль задан, возвращает requiresPassword=true и tempAuthToken.' })
@ApiBody({ type: PasswordlessVerifyDto })
verifyOtp(@Body() dto: PasswordlessVerifyDto, @Req() req: Request) {
return this.core.auth.VerifyOtp(enrichAuthClientMeta(req, dto));
}
@Post('login/password')
@ApiOperation({ summary: 'Войти по паролю', description: 'Identifier-first парольный шаг. Принимает login+password, либо tempAuthToken+password для совместимости, затем выдает JWT.' })
@ApiBody({ type: PasswordLoginDto })
loginWithPassword(@Body() dto: PasswordLoginDto, @Req() req: Request) {
return this.core.auth.LoginWithPassword(enrichAuthClientMeta(req, dto));
}
@Post('ldap/login')
@ApiOperation({ summary: 'Войти через LDAP/LDAPS', description: 'Аутентификация через корпоративный LDAP-сервер. Требует включённой настройки LDAP_ENABLED.' })
@ApiBody({ type: LdapLoginDto })
loginWithLdap(@Body() dto: LdapLoginDto, @Req() req: Request) {
return this.core.auth.LoginWithLdap(enrichAuthClientMeta(req, dto));
}
@Post('pin/verify')
@ApiOperation({ summary: 'Подтвердить PIN-код', description: 'Разблокирует временную сессию и выдает JWT с pinVerified=true.' })
@ApiBody({ type: VerifyPinDto })
verifyPin(@Body() dto: VerifyPinDto) {
return this.core.auth.VerifyPin(dto);
}
@Post('totp/begin')
@ApiOperation({ summary: 'Начать вход по TOTP', description: 'Создаёт challenge для входа через приложение-аутентификатор вместо SMS/email OTP.' })
@ApiBody({ type: BeginTotpLoginDto })
beginTotpLogin(@Body() dto: BeginTotpLoginDto, @Req() req: Request) {
return this.core.auth.BeginTotpLogin(enrichAuthClientMeta(req, dto));
}
@Post('totp/verify')
@ApiOperation({ summary: 'Подтвердить TOTP при входе', description: 'Завершает вход после проверки кода из Google Authenticator или аналога.' })
@ApiBody({ type: VerifyTotpLoginDto })
verifyTotpLogin(@Body() dto: VerifyTotpLoginDto, @Req() req: Request) {
return this.core.auth.VerifyTotpLogin(enrichAuthClientMeta(req, dto));
}
@Post('refresh')
@ApiOperation({ summary: 'Обновить access token', description: 'Обновляет JWT по refresh token. Если сессия заблокирована PIN-кодом, возвращает requiresPin=true без выхода из аккаунта.' })
@ApiBody({ type: RefreshSessionDto })
refresh(@Body() dto: RefreshSessionDto) {
return this.core.auth.RefreshSession(dto);
}
@Get('session')
@ApiBearerAuth()
@ApiOperation({
summary: 'Состояние текущей сессии',
description: 'Возвращает профиль и флаг PIN-блокировки. Не выбрасывает пользователя из аккаунта при заблокированном PIN.'
})
async session(@Headers('authorization') authorization?: string) {
const payload = await verifyAccessToken(this.jwt, authorization);
if (!payload.sessionId) {
const user = await firstValueFrom(this.core.auth.GetMe({ userId: payload.sub }));
return { requiresPin: false, sessionId: null, pinVerified: true, user };
}
const validation = (await firstValueFrom(
this.core.auth.ValidateSession({
userId: payload.sub,
sessionId: payload.sessionId,
touchActivity: false
})
)) as { requiresPin: boolean; sessionId: string; pinVerified: boolean };
const user = await firstValueFrom(this.core.auth.GetMe({ userId: payload.sub }));
if (!validation.requiresPin) {
await firstValueFrom(
this.core.auth.ValidateSession({
userId: payload.sub,
sessionId: payload.sessionId,
touchActivity: true
})
);
}
return {
requiresPin: validation.requiresPin,
sessionId: validation.sessionId,
pinVerified: validation.pinVerified,
user
};
}
@Get('me')
@ApiBearerAuth()
@ApiOperation({ summary: 'Текущий пользователь', description: 'Возвращает профиль пользователя по access token.' })
async me(@Headers('authorization') authorization?: string) {
const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization);
return firstValueFrom(this.core.auth.GetMe({ userId: payload.sub }));
}
}