124 lines
3.1 KiB
TypeScript
124 lines
3.1 KiB
TypeScript
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<AuthenticatedDeviceContextValue> {
|
|
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<AuthenticatedDeviceContextValue> {
|
|
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();
|
|
}
|
|
}
|