144 lines
7.0 KiB
TypeScript
144 lines
7.0 KiB
TypeScript
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 }));
|
||
}
|
||
}
|