-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add error logging across services, fix ci script, and silence t… #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||
| import { AuthScheme, Header } from "@/common/constants/header"; | ||||||||||||||||||||||||||||||
| import { Injectable } from "@nestjs/common"; | ||||||||||||||||||||||||||||||
| import { ThrottlerGuard as _ThrottlerGuard } from "@nestjs/throttler"; | ||||||||||||||||||||||||||||||
| import type { Request } from "express"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Rate-limiting guard that keys each throttle bucket on the combination of the | ||||||||||||||||||||||||||||||
| * client IP address and Bearer token. | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
| * Using both dimensions means: | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
| * - Different IPs with the same token are counted separately (IP-level limit). | ||||||||||||||||||||||||||||||
| * - The same IP using different tokens is also counted separately (token-level | ||||||||||||||||||||||||||||||
| * limit). | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
| * If no token is present the key degrades gracefully to `<ip>:` so that | ||||||||||||||||||||||||||||||
| * unauthenticated probing is also throttled. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||||||||
| class ThrottlerGuard extends _ThrottlerGuard { | ||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Builds the throttle storage key for the incoming request. | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
| * @param {Request} request - The raw Express request object. | ||||||||||||||||||||||||||||||
| * @returns {Promise<string>} A string key of the form `<ip>:<token>`. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| protected override async getTracker(request: Request): Promise<string> { | ||||||||||||||||||||||||||||||
| const ip: string = request.ip ?? request.socket?.remoteAddress ?? ""; | ||||||||||||||||||||||||||||||
| const authHeader: string = request.headers?.[Header.AUTHORIZATION] ?? ""; | ||||||||||||||||||||||||||||||
| const token: string = authHeader.startsWith(AuthScheme.BEARER_PREFIX) | ||||||||||||||||||||||||||||||
| ? authHeader.slice(7) | ||||||||||||||||||||||||||||||
| : ""; | ||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+32
|
||||||||||||||||||||||||||||||
| const authHeader: string = request.headers?.[Header.AUTHORIZATION] ?? ""; | |
| const token: string = authHeader.startsWith(AuthScheme.BEARER_PREFIX) | |
| ? authHeader.slice(7) | |
| : ""; | |
| const authHeaderValue = request.headers?.[Header.AUTHORIZATION]; | |
| const authHeader: string = Array.isArray(authHeaderValue) | |
| ? authHeaderValue[0] ?? "" | |
| : authHeaderValue ?? ""; | |
| const bearerPrefix: string = AuthScheme.BEARER_PREFIX; | |
| const token: string = | |
| authHeader.slice(0, bearerPrefix.length).toLowerCase() === | |
| bearerPrefix.toLowerCase() | |
| ? authHeader.slice(bearerPrefix.length) | |
| : ""; |
Copilot
AI
Apr 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The throttle key currently includes the raw Bearer token. This stores credentials in the throttler backend (typically Redis) and can leak tokens via key inspection/metrics. Prefer hashing the token (e.g., SHA-256) or using a non-reversible fingerprint before building the tracker key.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { | |||||||||
| Controller, | ||||||||||
| Get, | ||||||||||
| HttpStatus, | ||||||||||
| Logger, | ||||||||||
| Param, | ||||||||||
| ParseUUIDPipe, | ||||||||||
| UseGuards, | ||||||||||
|
|
@@ -34,6 +35,8 @@ import { | |||||||||
| @UseGuards(BearerGuard) | ||||||||||
| @Controller(Endpoint.JOBS) | ||||||||||
| class JobsController { | ||||||||||
| private readonly logger: Logger = new Logger(JobsController.name); | ||||||||||
|
|
||||||||||
| constructor(private readonly jobsService: JobsService) {} | ||||||||||
|
|
||||||||||
| /** | ||||||||||
|
|
@@ -51,10 +54,21 @@ class JobsController { | |||||||||
| description: "Unauthorized.", | ||||||||||
| }) | ||||||||||
| @ApiResponse({ status: HttpStatus.NOT_FOUND, description: "Job not found." }) | ||||||||||
| @ApiResponse({ | ||||||||||
| status: 429, | ||||||||||
| description: "Too Many Requests.", | ||||||||||
| }) | ||||||||||
| async getJobStatus( | ||||||||||
| @Param("jobId", new ParseUUIDPipe()) jobId: string, | ||||||||||
| ): Promise<JobStatusResponse> { | ||||||||||
| return this.jobsService.getJobStatus(jobId); | ||||||||||
| this.logger.debug(`GET /jobs/${jobId}`); | ||||||||||
|
|
||||||||||
| const result: JobStatusResponse = | ||||||||||
| await this.jobsService.getJobStatus(jobId); | ||||||||||
|
|
||||||||||
| this.logger.debug(`Job status — ${JSON.stringify(result)}`); | ||||||||||
|
||||||||||
| this.logger.debug(`Job status — ${JSON.stringify(result)}`); | |
| this.logger.debug( | |
| `Job status retrieved — jobId=${jobId} status=${result.status}`, | |
| ); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -49,6 +49,18 @@ const bootstrap = async (): Promise<void> => { | |||||
| .build(); | ||||||
|
|
||||||
| SwaggerModule.setup("api", app, SwaggerModule.createDocument(app, config)); | ||||||
|
|
||||||
| const logger: Logger = app.get(Logger); | ||||||
| await app.listen(configService.port); | ||||||
| logger.log( | ||||||
| `Swagger UI: http://localhost:${configService.port}/api`, | ||||||
| "Bootstrap", | ||||||
| ); | ||||||
| logger.log( | ||||||
| `Bearer token: ${configService.clientBearerToken}`, | ||||||
|
||||||
| `Bearer token: ${configService.clientBearerToken}`, | |
| `Bearer token configured: ${Boolean(configService.clientBearerToken)}`, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| import { AppConfigService } from "@/common/config/config.service"; | ||
| import { type Metadata } from "@/metadata/metadata.types"; | ||
| import { Injectable, OnModuleDestroy } from "@nestjs/common"; | ||
| import { Injectable, Logger, OnModuleDestroy } from "@nestjs/common"; | ||
| import Redis from "ioredis"; | ||
| import { ResultAsync } from "neverthrow"; | ||
|
|
||
| /** | ||
| * Redis key prefix used to namespace metadata entries. Prevents collisions | ||
|
|
@@ -28,6 +29,7 @@ const METADATA_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days. | |
| */ | ||
| @Injectable() | ||
| class MetadataService implements OnModuleDestroy { | ||
| private readonly logger = new Logger(MetadataService.name); | ||
| private readonly redis: Redis; | ||
|
|
||
| constructor(private readonly configService: AppConfigService) { | ||
|
|
@@ -42,7 +44,7 @@ class MetadataService implements OnModuleDestroy { | |
| }); | ||
| // Prevent unhandled error events from crashing the process. | ||
| this.redis.on("error", (error: Error) => { | ||
| console.error("[MetadataService] Redis error:", error.message); | ||
| this.logger.error("Redis connection error", error.message); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -73,22 +75,28 @@ class MetadataService implements OnModuleDestroy { | |
| * | ||
| * @param {string} keyIdentifier - Application-assigned stable key | ||
| * identifier. | ||
| * @returns {Promise<Metadata | null>} The stored `Metadata`, or `null` if | ||
| * not found or expired. | ||
| * @returns {ResultAsync<Metadata | null, Error>} The stored `Metadata`, or | ||
| * `null` if not found or expired. Errors when the stored value cannot be | ||
| * parsed (corrupted entry). | ||
| */ | ||
| async retrieve(keyIdentifier: string): Promise<Metadata | null> { | ||
| retrieve(keyIdentifier: string): ResultAsync<Metadata | null, Error> { | ||
| const redisKey: string = `${METADATA_REDIS_PREFIX}:${keyIdentifier}`; | ||
| const serialized: string | null = await this.redis.get(redisKey); | ||
| if (!serialized) return null; | ||
|
|
||
| try { | ||
| return JSON.parse(serialized) as Metadata; | ||
| } catch { | ||
| throw new Error( | ||
| `Corrupted metadata for '${keyIdentifier}'. ` + | ||
| `Delete and re-run key generation.`, | ||
| ); | ||
| } | ||
| return ResultAsync.fromPromise( | ||
| this.redis.get(redisKey).then((serialized) => { | ||
| if (!serialized) return null; | ||
| return JSON.parse(serialized) as Metadata; | ||
| }), | ||
| (cause) => { | ||
| this.logger.error( | ||
| `Corrupted metadata for key identifier '${keyIdentifier}' — value cannot be parsed as JSON`, | ||
| ); | ||
| return new Error( | ||
| `Corrupted metadata for '${keyIdentifier}'. ` + | ||
| `Delete and re-run key generation.`, | ||
| { cause }, | ||
| ); | ||
| }, | ||
|
Comment on lines
+84
to
+98
|
||
| ); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rate limiting is now enabled globally via ThrottlerModule + APP_GUARD, but there are no tests asserting 429 behavior (e.g., repeated requests from same IP/token). Since the repo already has e2e coverage for auth and endpoints, consider adding an e2e test to ensure throttling works and is keyed as intended.