1
17import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
18import type { Request, Response } from "express";
19import { ApiError } from "../../common/errors.js";
20import { logger } from "../../common/logger.js";
21
22export const SCIM_ERROR_SCHEMAS = ["urn:ietf:params:scim:api:errors:2.0"];
23
24export class ScimError extends Error {
25 constructor(
26 public readonly status: number,
27 public readonly scimType: string | undefined,
28 message: string,
29 public readonly extra: Record<string, unknown> = {}
30 ) {
31 super(message);
32 this.name = "ScimError";
33 }
34
35 static unauthorized(detail = "Invalid authentication credentials"): ScimError {
36 return new ScimError(401, "invalidCredentials", detail);
37 }
38 static forbidden(detail = "Forbidden."): ScimError {
39 return new ScimError(403, "forbidden", detail);
40 }
41 static notFound(detail = "Resource not found."): ScimError {
42 return new ScimError(404, undefined, detail);
43 }
44 static invalidFilter(detail: string): ScimError {
45 return new ScimError(400, "invalidFilter", detail);
46 }
47 static invalidValue(detail: string): ScimError {
48 return new ScimError(400, "invalidValue", detail);
49 }
50 static duplicate(detail: string): ScimError {
51 return new ScimError(409, "duplicate", detail);
52 }
53 static preconditionFailed(detail = "Precondition Failed"): ScimError {
54 return new ScimError(412, undefined, detail);
55 }
56}
57
58function envelope(status: number, detail: string, scimType?: string, extra: Record<string, unknown> = {}) {
59 const body: Record<string, unknown> = {
60 schemas: SCIM_ERROR_SCHEMAS,
61 status: String(status),
62 detail
63 };
64 if (scimType) body.scimType = scimType;
65 Object.assign(body, extra);
66 return body;
67}
68
69@Catch()
70export class ScimExceptionsFilter implements ExceptionFilter {
71 catch(exception: unknown, host: ArgumentsHost): void {
72 const res = host.switchToHttp().getResponse<Response>();
73 const req = host.switchToHttp().getRequest<Request>();
74 res.setHeader("content-type", "application/scim+json; charset=utf-8");
75 if (exception instanceof ScimError) {
76 res.status(exception.status).json(
77 envelope(exception.status, exception.message, exception.scimType, exception.extra)
78 );
79 return;
80 }
81
82
83
84
85 if (exception instanceof ApiError) {
86 if (exception.code === "VALIDATION_ERROR") {
87 const details = (exception.details ?? []).map((d) => d.issue ?? d.field ?? d.reason).filter(Boolean);
88 res.status(400).json(
89 envelope(400, details.length ? details.join("; ") : exception.message, "invalidValue")
90 );
91 return;
92 }
93 if (exception.httpStatus === 404) {
94 res.status(404).json(envelope(404, "Resource not found."));
95 return;
96 }
97 res.status(exception.httpStatus).json(envelope(exception.httpStatus, exception.message));
98 return;
99 }
100 if (exception instanceof HttpException) {
101 const status = exception.getStatus();
102 if (status === 400) {
103 const body = exception.getResponse() as { message?: string | string[] } | string;
104 const detail =
105 typeof body === "string" ? body : Array.isArray(body.message) ? body.message.join("; ") : body.message ?? "Invalid request.";
106 res.status(400).json(envelope(400, detail, "invalidValue"));
107 return;
108 }
109 if (status === 404) {
110 res.status(404).json(envelope(404, "Resource not found."));
111 return;
112 }
113 if (status === 409) {
114 const body = exception.getResponse() as { message?: string };
115 res.status(409).json(envelope(409, body.message ?? "Conflict.", "duplicate"));
116 return;
117 }
118 if (status === 412) {
119 res.status(412).json(envelope(412, "Precondition Failed"));
120 return;
121 }
122 const body = exception.getResponse() as { message?: string };
123 const detail = typeof body === "string" ? body : body.message ?? "Request failed.";
124 res.status(status).json(envelope(status, detail));
125 return;
126 }
127
128 const message = exception instanceof Error ? exception.message : "Internal Server Error";
129 logger.warn(`scim 500 on ${req.method} ${req.path}: ${message}`);
130 res.status(500).json(envelope(500, "Internal Server Error"));
131 }
132}
133