From e019829fa3d098741bf2bb9380f488e5b9fe809f Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:41:26 +0100 Subject: [PATCH 1/8] fix: harden runtime and local development setup --- Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index cbc8e05..5b6ff13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,10 +9,9 @@ FROM node:22-alpine RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app ENV NODE_ENV=production -COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/dist ./dist -COPY --from=build /app/prisma ./prisma -RUN chown -R appuser:appgroup /app +COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules +COPY --from=build --chown=appuser:appgroup /app/dist ./dist +COPY --from=build --chown=appuser:appgroup /app/prisma ./prisma USER appuser EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ From 6bf065ca3216a3bd9c3d9df25f2c4ae893a69406 Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:41:34 +0100 Subject: [PATCH 2/8] fix: harden runtime and local development setup --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 630002b..d5b3360 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,10 @@ is safe. It creates: - **alice@example.com** (attendee) and **organiser@example.com** (organiser), both with password `Password123!` - A demo organisation **Stellar Events Demo** -- A draft event **StellarFest 2027** with General Admission and VIP ticket types +- A published event **StellarFest 2027** with General Admission and VIP ticket types + +The production Docker image runs as the unprivileged `appuser` account rather +than root. Seed data can be wiped and recreated with: From b5962767a23603e67e880fc82e42a41a93e10fbc Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:41:41 +0100 Subject: [PATCH 3/8] fix: harden runtime and local development setup --- docs/API.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/API.md b/docs/API.md index 49c65b2..05da365 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,5 +1,12 @@ # API reference +## Interactive OpenAPI documentation + +When `NODE_ENV` is not `production`, Swagger UI is available at `/docs` and +the generated OpenAPI document at `/docs-json`. The schema is generated from +controllers and DTOs at startup. Swagger is disabled in production so the +interactive schema is not exposed publicly. + Every route below is real: paths come from the controllers, request bodies from the `class-validator` DTOs, and response bodies from what the services return. When they drift, the generated Bruno collection under From 965eb6cc38d2da75aa23d47dae07000f9cdb117b Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:41:48 +0100 Subject: [PATCH 4/8] fix: harden runtime and local development setup --- docs/ERROR_HANDLING.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index 588e8f6..98a1684 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -1,14 +1,29 @@ # Error handling -Nest's built-in HTTP exception filter handles everything — services -throw `NotFoundException`, `ForbiddenException`, `ConflictException`, -`BadRequestException`, etc. from `@nestjs/common`, and Nest serializes -them to `{ statusCode, message, error }` automatically. +`GlobalExceptionFilter` is registered in `src/main.ts` and handles every +uncaught exception at the HTTP boundary. Known Nest HTTP exceptions retain +Nest's `{ statusCode, message, error }` response shape. Unexpected errors are +sanitised and returned as: + +```json +{ + "statusCode": 500, + "code": "INTERNAL_ERROR", + "message": "Internal server error", + "error": "Internal Server Error", + "requestId": "" +} +``` + +The request id is taken from `X-Request-Id` when supplied; otherwise the +filter generates one. Unexpected errors are logged server-side with the +request method and URL, but their internal message and stack are never +exposed to callers. `ValidationPipe` (registered globally in `main.ts`) rejects any request body that doesn't match its DTO's `class-validator` decorators before the request ever reaches a controller method. -There's no custom global exception filter yet — see -`docs/OBSERVABILITY.md` for what's still missing before a production -deploy (structured error logging in particular). +Prisma and domain errors are mapped to stable status codes and public error +messages by the same filter. See `docs/OBSERVABILITY.md` for production +monitoring guidance. From 79b7d3c6f0d1eb39b92709bdc2fefba1c2505ff9 Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:41:55 +0100 Subject: [PATCH 5/8] fix: harden runtime and local development setup --- prisma/seed.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index b284f14..634c8e1 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,7 +1,7 @@ /** * prisma/seed.ts — local development seed * - * Creates a demo user, organiser, event and two ticket types so new + * Creates a demo user, organiser, published event and two ticket types so new * contributors have a populated DB immediately after running * `npx prisma migrate dev`. * @@ -12,7 +12,13 @@ * running the script more than once is safe. */ -import { PrismaClient, Industry, UserRole, OrgMemberRole } from '@prisma/client'; +import { + PrismaClient, + EventStatus, + Industry, + UserRole, + OrgMemberRole, +} from '@prisma/client'; import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient(); @@ -87,11 +93,17 @@ async function main() { organizationId: org.id, name: 'StellarFest 2027', category: Industry.CONCERTS, + status: EventStatus.PUBLISHED, venue: 'The Stellar Arena, Cape Town', startsAt: new Date('2027-03-15T19:00:00Z'), endsAt: new Date('2027-03-15T23:00:00Z'), }, }); + } else if (event.status !== EventStatus.PUBLISHED) { + event = await prisma.event.update({ + where: { id: event.id }, + data: { status: EventStatus.PUBLISHED }, + }); } // ── 6. Ticket types ────────────────────────────────────────────────────── From cfe90f3625f99f38ac0dcef248e4aed22abd9d82 Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:42:01 +0100 Subject: [PATCH 6/8] fix: harden runtime and local development setup --- src/common/filters/global-exception.filter.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/common/filters/global-exception.filter.spec.ts b/src/common/filters/global-exception.filter.spec.ts index f637d3c..6a1b90d 100644 --- a/src/common/filters/global-exception.filter.spec.ts +++ b/src/common/filters/global-exception.filter.spec.ts @@ -23,6 +23,7 @@ describe('GlobalExceptionFilter', () => { const request = { method: 'GET', url: '/test', + headers: { 'x-request-id': 'req-test-1' }, }; const argumentsHost = { switchToHttp: () => ({ @@ -216,6 +217,8 @@ describe('GlobalExceptionFilter', () => { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, code: 'INTERNAL_ERROR', message: 'Internal server error', + error: 'Internal Server Error', + requestId: 'req-test-1', }); }); }); @@ -233,6 +236,8 @@ describe('GlobalExceptionFilter', () => { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, code: 'INTERNAL_ERROR', message: 'Internal server error', + error: 'Internal Server Error', + requestId: 'req-test-1', }); }); @@ -248,6 +253,8 @@ describe('GlobalExceptionFilter', () => { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, code: 'INTERNAL_ERROR', message: 'Internal server error', + error: 'Internal Server Error', + requestId: 'req-test-1', }); }); }); From aeec0f1bb42708d8f951f2aea4002fc3765c824d Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:42:08 +0100 Subject: [PATCH 7/8] fix: harden runtime and local development setup --- src/common/filters/global-exception.filter.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/common/filters/global-exception.filter.ts b/src/common/filters/global-exception.filter.ts index ee9832f..adddac8 100644 --- a/src/common/filters/global-exception.filter.ts +++ b/src/common/filters/global-exception.filter.ts @@ -7,6 +7,7 @@ import { Logger, } from '@nestjs/common'; import type { Request, Response } from 'express'; +import { randomUUID } from 'node:crypto'; import { Prisma } from '@prisma/client'; import { DomainError } from '../errors/domain.error'; @@ -73,11 +74,21 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); } - response.status(statusCode).json({ + const responseBody = { statusCode, code, message, - }); + ...(code === 'INTERNAL_ERROR' && { + error: 'Internal Server Error', + requestId: this.getRequestId(request), + }), + }; + response.status(statusCode).json(responseBody); + } + + private getRequestId(request: Request): string { + const header = request.headers['x-request-id']; + return (Array.isArray(header) ? header[0] : header) ?? randomUUID(); } private getPrismaErrorMessage( From 9a28247643fab0b149cdf97c1c5601a45670e525 Mon Sep 17 00:00:00 2001 From: Mekussdev Date: Fri, 16 Oct 2026 16:42:16 +0100 Subject: [PATCH 8/8] fix: harden runtime and local development setup --- src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 8bc7794..8ea7a70 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,7 +36,9 @@ async function bootstrap() { .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); - SwaggerModule.setup('docs', app, document); + SwaggerModule.setup('docs', app, document, { + jsonDocumentUrl: 'docs-json', + }); } await app.listen(config.getOrThrow('PORT'));