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 { BeginTotpLoginDto, IdentifyDto, LdapLoginDto, LoginDto, PasswordlessOtpDto, PasswordlessVerifyDto, PasswordLoginDto, RefreshSessionDto, RegisterDto, VerifyPinDto, VerifyTotpLoginDto } from '../dto/auth.dto'; import { resolveAuthorizedPayload, verifyAccessToken } from '../session-auth'; @ApiTags('Аутентификация') @Controller('auth') 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) { return this.core.auth.Login(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, @Ip() ipAddress?: string) { return this.core.auth.SendOtp({ recipient: dto.recipient, channel: dto.channel, ipAddress }); } @Post('otp/verify') @ApiOperation({ summary: 'Проверить OTP для входа', description: 'Если пользователь не существует, создает его. Если пароль не задан, сразу возвращает JWT. Если пароль задан, возвращает requiresPassword=true и tempAuthToken.' }) @ApiBody({ type: PasswordlessVerifyDto }) verifyOtp(@Body() dto: PasswordlessVerifyDto) { return this.core.auth.VerifyOtp(dto); } @Post('login/password') @ApiOperation({ summary: 'Войти по паролю', description: 'Identifier-first парольный шаг. Принимает login+password, либо tempAuthToken+password для совместимости, затем выдает JWT.' }) @ApiBody({ type: PasswordLoginDto }) loginWithPassword(@Body() dto: PasswordLoginDto) { return this.core.auth.LoginWithPassword(dto); } @Post('ldap/login') @ApiOperation({ summary: 'Войти через LDAP/LDAPS', description: 'Аутентификация через корпоративный LDAP-сервер. Требует включённой настройки LDAP_ENABLED.' }) @ApiBody({ type: LdapLoginDto }) loginWithLdap(@Body() dto: LdapLoginDto) { return this.core.auth.LoginWithLdap(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) { 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 }) 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 })); } }