fix and update

This commit is contained in:
lendry
2026-07-07 09:01:49 +03:00
parent 881e5d764b
commit 29306eb2ec
14 changed files with 551 additions and 60 deletions

View File

@@ -1,11 +1,13 @@
import { Body, Controller, Get, Headers, Param, Post, Req } from '@nestjs/common';
import { Body, Controller, ForbiddenException, Get, Headers, Param, Post, Req } from '@nestjs/common';
import type { Request } from 'express';
import { firstValueFrom } from 'rxjs';
import { enrichAuthClientMeta } from '../client-request.util';
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';
import { resolveFrontendUrl } from '../lib/oauth-issuer';
@ApiTags('Биометрия и QR-вход')
@Controller('auth/advanced')
@@ -53,6 +55,30 @@ export class AdvancedAuthController {
return this.core.advancedAuth.PollQrSession({ sessionId });
}
@Post('qr/session/:sessionId/claim')
@ApiOperation({
summary: 'Привязать QR-сессию к устройству',
description: 'Новое устройство подтверждает сканирование QR-кода для подключения из раздела «Безопасность».'
})
@ApiParam({ name: 'sessionId', description: 'ID QR-сессии' })
@ApiBody({ type: QrSessionDto })
@ApiResponse({ status: 201, description: 'QR-сессия привязана к устройству' })
claimQr(@Param('sessionId') sessionId: string, @Body() dto: QrSessionDto, @Req() req: Request) {
const enriched = enrichAuthClientMeta(req, {
deviceName: dto.deviceName,
fingerprint: dto.fingerprint,
deviceType: dto.deviceType ?? 'WEB'
});
return this.core.advancedAuth.ClaimQrSession({
sessionId,
deviceName: enriched.deviceName,
fingerprint: enriched.fingerprint,
deviceType: enriched.deviceType,
ipAddress: enriched.ipAddress,
userAgent: enriched.userAgent
});
}
@Post('qr/session/:sessionId/approve')
@ApiBearerAuth()
@ApiOperation({ summary: 'Подтвердить QR-вход', description: 'Подтверждает QR-сессию с мобильного приложения уже авторизованным пользователем.' })

View File

@@ -31,6 +31,7 @@ import {
resolveFedcmEndpoints
} from '../lib/fedcm-config';
import { resolveFedcmSessionFromRequest, setFedcmSessionCookie } from '../lib/fedcm-cookie';
import { resolveFedcmSessionPinState } from '../lib/fedcm-session';
import { verifyAccessToken } from '../session-auth';
type FedcmAccountsResponse = {
@@ -128,12 +129,17 @@ export class FedcmController {
applyFedcmCorsHeaders(res, origin);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const session = await resolveFedcmSessionFromRequest(this.jwt, req.headers.cookie);
const { session, requiresPin } = await resolveFedcmSessionPinState(this.jwt, this.core, req.headers.cookie);
if (!session) {
applyFedcmLoginStatus(res, false);
return { accounts: [] };
}
if (requiresPin) {
applyFedcmLoginStatus(res, false);
return { accounts: [] };
}
try {
const result = (await firstValueFrom(
this.core.fedcm.GetAccounts({ userId: session.sub, sessionId: session.sessionId })
@@ -180,11 +186,16 @@ export class FedcmController {
applyFedcmCorsHeaders(res, rpOrigin);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const session = await resolveFedcmSessionFromRequest(this.jwt, req.headers.cookie);
const { session, requiresPin } = await resolveFedcmSessionPinState(this.jwt, this.core, req.headers.cookie);
if (!session) {
throw new UnauthorizedException('Сессия FedCM не найдена');
}
if (requiresPin) {
applyFedcmLoginStatus(res, false);
throw new UnauthorizedException('Требуется подтверждение PIN-кода');
}
const body = (req.body ?? {}) as Record<string, unknown>;
const clientId = String(body.client_id ?? body.clientId ?? '').trim();
const accountId = String(body.account_id ?? body.accountId ?? '').trim();
@@ -298,8 +309,8 @@ export class FedcmController {
'Минимальная HTML-страница на API-домене: Set-Login + navigator.login.setStatus для Chrome FedCM (origin login_url).'
})
async loginStatus(@Req() req: Request, @Res() res: Response) {
const session = await resolveFedcmSessionFromRequest(this.jwt, req.headers.cookie);
const loggedIn = Boolean(session);
const { session, requiresPin } = await resolveFedcmSessionPinState(this.jwt, this.core, req.headers.cookie);
const loggedIn = Boolean(session && !requiresPin);
applyFedcmLoginStatus(res, loggedIn);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');

View File

@@ -1,9 +1,11 @@
import { Body, Controller, Get, Headers, Param, Post } from '@nestjs/common';
import { Body, Controller, ForbiddenException, Get, Headers, Param, Post } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { OptionalPinDto, PinDto, TotpCodeDto, VerifySecurityPinDto } from '../dto/security.dto';
import { resolveAuthorizedPayload } from '../session-auth';
import { resolveFrontendUrl } from '../lib/oauth-issuer';
@ApiTags('Безопасность')
@ApiBearerAuth()
@@ -148,4 +150,20 @@ export class SecurityController {
const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization);
return this.core.security.RevokeAllSessions({ userId, exceptSessionId: payload.sessionId });
}
@Post('users/:userId/device-link/session')
@ApiOperation({
summary: 'Создать QR для подключения устройства',
description: 'Генерирует QR-код (5 минут) для входа на новом устройстве через раздел «Безопасность».'
})
@ApiParam({ name: 'userId', description: 'ID пользователя' })
@ApiResponse({ status: 201, description: 'QR-сессия для подключения устройства создана' })
async createDeviceLinkSession(@Param('userId') userId: string, @Headers('authorization') authorization?: string) {
const payload = await resolveAuthorizedPayload(this.jwt, this.core, authorization);
if (payload.sub !== userId) {
throw new ForbiddenException('Недостаточно прав для подключения устройства');
}
const frontendUrl = await resolveFrontendUrl(this.core);
return firstValueFrom(this.core.advancedAuth.CreateDeviceLinkSession({ userId, frontendUrl }));
}
}