Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Codefied-CodePix/Karokar-backend/llms.txt

Use this file to discover all available pages before exploring further.

KaroKar secures its API with JSON Web Tokens (JWT) via @nestjs/jwt and passport-jwt. Every request to a protected endpoint must carry a signed JWT in the Authorization header. The token encodes both the caller’s identity (userId) and their organizational scope (organizationId) — together these two claims drive every authorization and multi-tenancy decision made at the API boundary.
There is no login endpoint in this codebase. KaroKar does not currently expose a POST /auth/login or any other token-issuance route. JWT tokens must be obtained through an external identity provider or issued directly during development. A built-in authentication endpoint is not yet implemented.

JWT Payload Shape

The JWT payload must conform to the JwtPayload interface defined in src/shared/types/authenticated-request.interface.ts:
export interface JwtPayload {
  sub: string;
  userId?: string;
  organizationId: string;
}

export interface AuthenticatedUser {
  userId: string;
  organizationId: string;
}

export interface AuthenticatedRequest extends Request {
  user?: AuthenticatedUser | JwtPayload;
  organizationId?: string;
}
sub
string
required
The standard JWT subject claim. Used as the fallback userId when the explicit userId field is absent.
userId
string
Explicit user identifier. When present, takes precedence over sub. The OrganizationContextInterceptor normalises both fields into a single AuthenticatedUser shape before handlers run.
organizationId
string
required
The organization the caller is acting on behalf of. This claim is required on every protected request. Requests without it will receive 401 Unauthorized.

Passing the Token

Include the JWT as a Bearer token in the Authorization header on every request:
Authorization: Bearer <your-jwt-token>

Example — Create a Booking

curl -X POST http://localhost:3000/bookings \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <your-jwt-token>' \
  -d '{"vehicleId": "vehicle-uuid-here", "organizationId": "org-uuid-here"}'

Example — Retrieve a Booking

curl -X GET http://localhost:3000/bookings/booking-uuid-here \
  -H 'Authorization: Bearer <your-jwt-token>'

Organization Context Resolution

Before any guard or handler runs, the OrganizationContextInterceptor normalises the request. Its full implementation is:
@Injectable()
export class OrganizationContextInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const user = request.user as JwtPayload | undefined;

    if (user?.organizationId) {
      request.organizationId = user.organizationId;

      if (!('userId' in user) || !user.userId) {
        request.user = {
          userId: user.sub ?? user.userId ?? '',
          organizationId: user.organizationId,
        };
      }
    }

    return next.handle();
  }
}
What this does:
  1. Reads the decoded JWT payload from request.user (populated by Passport after signature verification).
  2. If organizationId is present in the payload, it is promoted to request.organizationId so downstream guards and handlers can read it without inspecting the raw JWT shape.
  3. If the payload only carries a sub claim (no explicit userId), the interceptor normalises request.user into an AuthenticatedUser using sub as the userId.
The interceptor does not throw on missing claims — it simply skips normalisation when organizationId is absent. Identity validation and rejection occur in the PermissionGuard on protected routes.

Permission Guard

Protected routes are decorated with @RequirePermission('some.permission'). The PermissionGuard enforces access control on every such route:
@Injectable()
export class PermissionGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    @Inject(AUTHORIZATION_SERVICE)
    private readonly authorizationService: IAuthorizationService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const permission = this.reflector.getAllAndOverride<string | undefined>(
      PERMISSION_KEY,
      [context.getHandler(), context.getClass()],
    );

    if (!permission) {
      return true;
    }

    const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
    const user = request.user;

    const userId = user && 'userId' in user ? user.userId : undefined;
    const organizationId =
      request.organizationId ??
      (user && 'organizationId' in user ? user.organizationId : undefined);

    if (!userId || !organizationId) {
      throw new UnauthorizedException();
    }

    const allowed = await this.authorizationService.can(
      userId,
      organizationId,
      permission,
    );

    if (!allowed) {
      throw new ForbiddenException();
    }

    return true;
  }
}

Guard Logic — Step by Step

1

Read the required permission

The guard uses NestJS’s Reflector to read the permission string set by the @RequirePermission decorator on the handler or controller class. If no permission is set, the guard returns true immediately — the route is publicly accessible.
2

Extract identity claims

userId is read from request.user.userId. organizationId is read first from request.organizationId (set by the interceptor), falling back to request.user.organizationId in the raw JWT payload.
3

Validate identity completeness

If either userId or organizationId cannot be resolved, a 401 Unauthorized exception is thrown. This happens when the JWT is missing, expired, or lacks the required claims.
4

Evaluate the permission

AuthorizationService.can(userId, organizationId, permission) is called. If it returns false, a 403 Forbidden exception is thrown.

Permission Naming Convention

Following ADR-010 and ADR-007, permissions are named as <domain>.<action> strings — not role names:
booking.create
booking.approve
vehicle.manage
assignment.accept
This keeps authorization logic decoupled from role assignments, allowing fine-grained access policies per organization.

Public vs. Protected Endpoints

Endpoints that are not decorated with @RequirePermission are publicly accessible — the guard returns true without evaluating any claims. Integrators should check individual endpoint documentation to confirm whether authentication is required.
Even on public endpoints, the OrganizationContextInterceptor still runs. If a valid JWT is present in the Authorization header, request.organizationId will be populated — this is useful for endpoints that behave differently for authenticated vs. anonymous callers.

Authorization Service — Integration Required

AuthorizationService.can() is not yet implemented. The current stub throws a NotImplementedException. Any request that reaches the permission-check step (Step 4 above) will receive a 500 Internal Server Error until a concrete implementation of the IAuthorizationService interface is provided via the AUTHORIZATION_SERVICE injection token. Routes decorated with @RequirePermission are therefore non-functional in the current codebase.

Environment Configuration

The JWT signing secret is configured via the JWT_SECRET environment variable (see .env.example):
JWT_SECRET=change-me-in-production
The default value change-me-in-production must be replaced with a strong secret before deploying to any non-local environment. Tokens signed with the default secret are trivially forgeable.

Future Authentication Support

ADR-010 designates Phase 01 authentication as JWT-only (web and mobile clients). Future phases are planned to add OAuth 2.0, OpenID Connect, SAML, and Enterprise SSO support. The API design is intentionally structured so these authentication layers can be added without requiring changes to resource endpoints or authorization logic.

Error Reference

ScenarioHTTP StatusCause
Missing Authorization header on a protected route401 UnauthorizedNo JWT supplied
JWT is expired or signature is invalid401 UnauthorizedPassport rejects the token before the guard runs
JWT missing userId/sub or organizationId401 UnauthorizedGuard cannot resolve identity claims
Permission check returns false403 ForbiddenUser lacks the required permission in their organization
AuthorizationService.can() not implemented500 Internal Server ErrorStub throws NotImplementedException

Build docs developers (and LLMs) love