import { BadRequestException, Injectable, NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { createHash, randomBytes } from 'node:crypto'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { AuthenticatedDeviceContextValue, RequestContextService, } from '../../infrastructure/request-context/request-context.service'; import { OwnerContext } from '../users/owner-context.service'; @Injectable() export class DeviceAuthService { constructor( private readonly prismaService: PrismaService, private readonly requestContext: RequestContextService, private readonly ownerContext: OwnerContext, ) {} generateDeviceAccessToken(): string { return randomBytes(32).toString('base64url'); } hashDeviceAccessToken(token: string): string { return createHash('sha256').update(token).digest('hex'); } async authenticateAuthorizationHeader( authorizationHeader: string | string[], ): Promise { const token = this.extractBearerToken(authorizationHeader); const tokenHash = this.hashDeviceAccessToken(token); const device = await this.prismaService.device.findUnique({ where: { tokenHash, }, select: { id: true, userId: true, tokenRevokedAt: true, }, }); if (!device || device.tokenRevokedAt) { throw new UnauthorizedException('Invalid device access token'); } await this.prismaService.device.update({ where: { id: device.id, }, data: { tokenLastUsedAt: new Date(), }, }); const authenticatedDevice = { deviceId: device.id, userId: device.userId, }; this.requestContext.setAuthenticatedDevice(authenticatedDevice); return authenticatedDevice; } async resolveCurrentDevice( legacyDeviceId?: string, ): Promise { const authenticatedDevice = this.requestContext.getAuthenticatedDevice(); if (authenticatedDevice) { return authenticatedDevice; } if (!legacyDeviceId) { throw new BadRequestException( 'deviceId is required when Authorization is missing.', ); } const owner = await this.ownerContext.resolve(); const device = await this.prismaService.device.findUnique({ where: { id: legacyDeviceId, }, select: { id: true, userId: true, }, }); if (!device || device.userId !== owner.userId) { throw new NotFoundException('Device not found'); } return { deviceId: device.id, userId: device.userId, }; } private extractBearerToken(authorizationHeader: string | string[]): string { const normalizedHeader = Array.isArray(authorizationHeader) ? authorizationHeader[0] : authorizationHeader; if (!normalizedHeader) { throw new UnauthorizedException('Invalid device access token'); } const match = normalizedHeader.match(/^Bearer\s+(.+)$/i); if (!match || !match[1]?.trim()) { throw new UnauthorizedException('Invalid device access token'); } return match[1].trim(); } }