diff --git a/paperless-backend/src/auth/api-key.guard.ts b/paperless-backend/src/auth/api-key.guard.ts index 8bdef25..d9ef63b 100644 --- a/paperless-backend/src/auth/api-key.guard.ts +++ b/paperless-backend/src/auth/api-key.guard.ts @@ -1,19 +1,25 @@ -import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { CanActivate, ExecutionContext, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { ApiKeysService } from './api-keys.service'; @Injectable() export class ApiKeyGuard implements CanActivate { + private readonly logger = new Logger(ApiKeyGuard.name); + constructor(private readonly apiKeysService: ApiKeysService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - + const method: string = request.method; + const url: string = request.url; + // Check header (X-API-Key) let apiKey = request.headers['x-api-key'] || request.headers['X-API-Key']; + let source = 'X-API-Key header'; // Fallback to query parameter (apiKey) if (!apiKey) { apiKey = request.query['apiKey']; + if (apiKey) source = 'apiKey query param'; } // Fallback to Authorization: Bearer (used by SSE clients that can't set X-API-Key) @@ -21,24 +27,28 @@ export class ApiKeyGuard implements CanActivate { const auth: string | undefined = request.headers['authorization']; if (auth?.startsWith('Bearer ')) { apiKey = auth.slice(7); + source = 'Authorization: Bearer'; } } + this.logger.debug( + `[${method} ${url}] key source: ${apiKey ? source : 'NONE'} | ` + + `headers: ${JSON.stringify(Object.keys(request.headers))} | ` + + `key prefix: ${apiKey ? String(apiKey).slice(0, 8) + '…' : 'n/a'}`, + ); + if (!apiKey) { + this.logger.warn(`[${method} ${url}] rejected – no API key found`); throw new UnauthorizedException('API Key missing'); } try { const keyEntry = await this.apiKeysService.validateKey(apiKey as string); - - // Attach metadata to request if needed later - request.apiKeyMetadata = { - id: keyEntry.id, - name: keyEntry.name, - }; - + this.logger.debug(`[${method} ${url}] accepted – key "${keyEntry.name}" (id=${keyEntry.id})`); + request.apiKeyMetadata = { id: keyEntry.id, name: keyEntry.name }; return true; } catch (err) { + this.logger.warn(`[${method} ${url}] rejected – validation failed: ${err.message}`); throw new UnauthorizedException(err.message || 'Invalid API Key'); } } diff --git a/paperless-backend/src/auth/jwt-or-apikey.guard.ts b/paperless-backend/src/auth/jwt-or-apikey.guard.ts index e31d396..657c5c2 100644 --- a/paperless-backend/src/auth/jwt-or-apikey.guard.ts +++ b/paperless-backend/src/auth/jwt-or-apikey.guard.ts @@ -1,28 +1,31 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common'; import { JwtAuthGuard } from './jwt-auth.guard'; import { ApiKeyGuard } from './api-key.guard'; import { lastValueFrom, isObservable } from 'rxjs'; -/** - * Combined guard that accepts either a valid JWT Bearer token - * or a valid API key (X-API-Key header / apiKey query param). - * Tries JWT first, falls back to API key. - */ @Injectable() export class JwtOrApiKeyGuard implements CanActivate { + private readonly logger = new Logger(JwtOrApiKeyGuard.name); + constructor( private readonly jwtGuard: JwtAuthGuard, private readonly apiKeyGuard: ApiKeyGuard, ) {} async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const tag = `[${req.method} ${req.url}]`; + // Try JWT first try { const result = this.jwtGuard.canActivate(context); const jwtOk = isObservable(result) ? await lastValueFrom(result) : await result; - if (jwtOk) return true; - } catch { - // JWT failed, try API key + if (jwtOk) { + this.logger.debug(`${tag} authenticated via JWT`); + return true; + } + } catch (err) { + this.logger.debug(`${tag} JWT failed (${err.message}), trying API key…`); } // Fall back to API key