response-standardizer 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { Request, Response } from "express";
2
+ import { ErrorFields } from "./types";
3
+ export declare class ServiceException extends Error {
4
+ statusCode: number;
5
+ constructor(statusCode: number, message: string);
6
+ }
7
+ export declare class UnknownException extends ServiceException {
8
+ constructor(statusCode: number, message: string);
9
+ }
10
+ export declare class NotFoundException extends ServiceException {
11
+ constructor(message: string);
12
+ }
13
+ export declare class UnauthorizedException extends ServiceException {
14
+ constructor(message: string);
15
+ }
16
+ export declare class AccessDeniedException extends ServiceException {
17
+ constructor(message: string);
18
+ }
19
+ export declare class InternalServerException extends ServiceException {
20
+ constructor(message: string);
21
+ }
22
+ export declare class ValidationException extends ServiceException {
23
+ errors?: ErrorFields;
24
+ constructor(message: string, errors?: ErrorFields);
25
+ }
26
+ export declare const handleControllerException: (req: Request, res: Response, err: Error) => void;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleControllerException = exports.ValidationException = exports.InternalServerException = exports.AccessDeniedException = exports.UnauthorizedException = exports.NotFoundException = exports.UnknownException = exports.ServiceException = void 0;
4
+ const _1 = require(".");
5
+ class ServiceException extends Error {
6
+ constructor(statusCode, message) {
7
+ super(message);
8
+ this.statusCode = statusCode;
9
+ }
10
+ }
11
+ exports.ServiceException = ServiceException;
12
+ class UnknownException extends ServiceException {
13
+ constructor(statusCode, message) {
14
+ super(statusCode, message);
15
+ Object.setPrototypeOf(this, new.target.prototype);
16
+ }
17
+ }
18
+ exports.UnknownException = UnknownException;
19
+ class NotFoundException extends ServiceException {
20
+ constructor(message) {
21
+ super(404, message);
22
+ Object.setPrototypeOf(this, new.target.prototype);
23
+ }
24
+ }
25
+ exports.NotFoundException = NotFoundException;
26
+ class UnauthorizedException extends ServiceException {
27
+ constructor(message) {
28
+ super(401, message);
29
+ Object.setPrototypeOf(this, new.target.prototype);
30
+ }
31
+ }
32
+ exports.UnauthorizedException = UnauthorizedException;
33
+ class AccessDeniedException extends ServiceException {
34
+ constructor(message) {
35
+ super(403, message);
36
+ Object.setPrototypeOf(this, new.target.prototype);
37
+ }
38
+ }
39
+ exports.AccessDeniedException = AccessDeniedException;
40
+ class InternalServerException extends ServiceException {
41
+ constructor(message) {
42
+ super(500, message);
43
+ Object.setPrototypeOf(this, new.target.prototype);
44
+ }
45
+ }
46
+ exports.InternalServerException = InternalServerException;
47
+ class ValidationException extends ServiceException {
48
+ constructor(message, errors) {
49
+ super(400, message);
50
+ this.errors = errors;
51
+ Object.setPrototypeOf(this, new.target.prototype);
52
+ }
53
+ }
54
+ exports.ValidationException = ValidationException;
55
+ const handleControllerException = (req, res, err) => {
56
+ if (err instanceof UnauthorizedException) {
57
+ return _1.RestResponse.unauthorized(req, res, err.message);
58
+ }
59
+ else if (err instanceof AccessDeniedException) {
60
+ return _1.RestResponse.accessDenied(req, res, err.message);
61
+ }
62
+ else if (err instanceof ValidationException) {
63
+ return _1.RestResponse.validationError(req, res, err?.errors, err?.message);
64
+ }
65
+ else if (err instanceof InternalServerException) {
66
+ return _1.RestResponse.exceptionError(req, res, err.message);
67
+ }
68
+ else if (err instanceof NotFoundException) {
69
+ return _1.RestResponse.notFound(req, res, err.message);
70
+ }
71
+ return _1.RestResponse.exceptionError(req, res, err.message);
72
+ };
73
+ exports.handleControllerException = handleControllerException;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Request, Response } from "express";
2
- import { ErrorFields, RestResponseFunctions, RestMiddlewareFunctions } from "./types";
1
+ import { RestResponseFunctions, RestMiddlewareFunctions } from "./types";
3
2
  declare global {
4
3
  namespace Express {
5
4
  interface Response {
@@ -23,17 +22,3 @@ export declare const error: (message: string, meta?: any) => void;
23
22
  export declare const warn: (message: string, meta?: any) => void;
24
23
  export declare const info: (message: string, meta?: any) => void;
25
24
  export declare const log: (level: "INFO" | "WARN" | "ERROR", message: string, meta?: any) => void;
26
- export declare const response: (req: Request, res: Response, response: ServiceResponse, handler?: (eq: Request, res: Response, data?: any) => void) => void;
27
- export declare const handleServiceException: (message: string, err: any) => ServiceResponse;
28
- export interface ServiceResponse {
29
- }
30
- export declare class ServiceException implements ServiceResponse {
31
- code: number;
32
- message: string;
33
- errors?: ErrorFields | undefined;
34
- constructor(code: number, message: string, errors?: ErrorFields | undefined);
35
- }
36
- export declare class ServiceResponseSuccess implements ServiceResponse {
37
- entity: any;
38
- constructor(entity?: any);
39
- }
package/dist/index.js CHANGED
@@ -3,11 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ServiceResponseSuccess = exports.ServiceException = exports.handleServiceException = exports.response = exports.log = exports.info = exports.warn = exports.error = exports.RestMiddleware = exports.RestResponse = exports.protect = exports.initKeycloak = void 0;
6
+ exports.log = exports.info = exports.warn = exports.error = exports.RestMiddleware = exports.RestResponse = exports.protect = exports.initKeycloak = void 0;
7
7
  const utils_1 = require("./utils");
8
8
  const axios_1 = __importDefault(require("axios"));
9
9
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
10
- const zod_1 = require("zod");
11
10
  let KEYCLOAK_PUBLIC_KEY = null;
12
11
  const initKeycloak = async (config) => {
13
12
  const KEYCLOAK_SERVICE = config.service ?? "localhost";
@@ -71,6 +70,9 @@ const created = (req, res, data) => {
71
70
  const deleted = (req, res) => {
72
71
  res.status(204).send();
73
72
  };
73
+ const noContent = (req, res) => {
74
+ res.status(204).send();
75
+ };
74
76
  const validationError = (req, res, errors, message = "Validation error") => {
75
77
  res.status(400).json({ errors, message });
76
78
  };
@@ -94,6 +96,7 @@ exports.RestResponse = {
94
96
  paginate,
95
97
  created,
96
98
  deleted,
99
+ noContent,
97
100
  validationError,
98
101
  exceptionError,
99
102
  unauthorized,
@@ -166,68 +169,3 @@ const log = (level, message, meta) => {
166
169
  console.log(`${color}[${timestamp}][${level}] ${message}${metaStr}${colors.RESET}`);
167
170
  };
168
171
  exports.log = log;
169
- const response = (req, res, response, handler = (eq, res, data = null) => { }) => {
170
- if (response instanceof ServiceException) {
171
- if (response?.code === 401) {
172
- return exports.RestResponse.unauthorized(req, res, response?.message);
173
- }
174
- else if (response?.code === 403) {
175
- return exports.RestResponse.accessDenied(req, res, response?.message);
176
- }
177
- else if (response?.code === 400) {
178
- return exports.RestResponse.validationError(req, res, response?.errors);
179
- }
180
- else if (response?.code === 500) {
181
- return exports.RestResponse.exceptionError(req, res, response.message);
182
- }
183
- else if (response?.code === 404) {
184
- return exports.RestResponse.notFound(req, res, response.message);
185
- }
186
- return exports.RestResponse.exceptionError(req, res, response.message);
187
- }
188
- return handler(req, res, response.entity);
189
- };
190
- exports.response = response;
191
- const handleServiceException = (message, err) => {
192
- if (err instanceof zod_1.ZodError) {
193
- const zodError = err;
194
- if (zodError.issues) {
195
- const validationErrors = {};
196
- zodError.issues.forEach((i) => {
197
- const key = i.path[0];
198
- if (!validationErrors[key])
199
- validationErrors[key] = [];
200
- validationErrors[key].push(i.message);
201
- });
202
- return new ServiceException(400, "Validation error", validationErrors);
203
- }
204
- }
205
- else if (err?.isAxiosError && err?.response && err.response.status) {
206
- (0, exports.error)(message, {
207
- status: err.response.status,
208
- statusText: err.response.statusText,
209
- error: err.response.data
210
- });
211
- return new ServiceException(err.response.status, err.response.data?.error_description);
212
- }
213
- else if (err instanceof Error) {
214
- (0, exports.error)(message, { error: err });
215
- return new ServiceException(500, err?.message);
216
- }
217
- return new ServiceException(500, "Internal server error");
218
- };
219
- exports.handleServiceException = handleServiceException;
220
- class ServiceException {
221
- constructor(code, message, errors) {
222
- this.code = code;
223
- this.message = message;
224
- this.errors = errors;
225
- }
226
- }
227
- exports.ServiceException = ServiceException;
228
- class ServiceResponseSuccess {
229
- constructor(entity = null) {
230
- this.entity = entity;
231
- }
232
- }
233
- exports.ServiceResponseSuccess = ServiceResponseSuccess;
package/dist/types.d.ts CHANGED
@@ -28,6 +28,7 @@ export interface RestResponseFunctions {
28
28
  paginate: <T>(req: Request, res: Response, data: Pagination<T>) => void;
29
29
  created: <T>(req: Request, res: Response, data: T) => void;
30
30
  deleted: (req: Request, res: Response) => void;
31
+ noContent: (req: Request, res: Response) => void;
31
32
  validationError: (req: Request, res: Response, errors?: ErrorFields, message?: string) => void;
32
33
  exceptionError: (req: Request, res: Response, errors: any, message?: string) => void;
33
34
  unauthorized: (req: Request, res: Response, message?: string) => void;
package/package.json CHANGED
@@ -1,38 +1,38 @@
1
- {
2
- "name": "response-standardizer",
3
- "version": "1.3.3",
4
- "description": "Express middleware to standardize API responses",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "type": "commonjs",
8
- "scripts": {
9
- "build": "tsc",
10
- "prepare": "npm run build"
11
- },
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://gitlab.com/dezhnevesht-archive-software/response-standardizer.git"
15
- },
16
- "author": "Hamid Atyabi",
17
- "license": "ISC",
18
- "bugs": {
19
- "url": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer/issues"
20
- },
21
- "homepage": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer#readme",
22
- "peerDependencies": {
23
- "express": "^4 || ^5"
24
- },
25
- "dependencies": {
26
- "axios": "^1.13.2",
27
- "jsonwebtoken": "^9.0.2",
28
- "uuid": "^9.0.1",
29
- "zod": "^4.1.12"
30
- },
31
- "devDependencies": {
32
- "@types/express": "^5.0.5",
33
- "@types/jsonwebtoken": "^9.0.10",
34
- "@types/uuid": "^10.0.0",
35
- "ts-node-dev": "^2.0.0",
36
- "typescript": "^5.9.3"
37
- }
38
- }
1
+ {
2
+ "name": "response-standardizer",
3
+ "version": "1.3.5",
4
+ "description": "Express middleware to standardize API responses",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "commonjs",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "prepare": "npm run build"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://gitlab.com/dezhnevesht-archive-software/response-standardizer.git"
15
+ },
16
+ "author": "Hamid Atyabi",
17
+ "license": "ISC",
18
+ "bugs": {
19
+ "url": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer/issues"
20
+ },
21
+ "homepage": "https://gitlab.com/dezhnevesht-archive-software/response-standardizer#readme",
22
+ "peerDependencies": {
23
+ "express": "^4 || ^5"
24
+ },
25
+ "dependencies": {
26
+ "axios": "^1.13.2",
27
+ "jsonwebtoken": "^9.0.2",
28
+ "uuid": "^9.0.1",
29
+ "zod": "^4.1.12"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "^5.0.5",
33
+ "@types/jsonwebtoken": "^9.0.10",
34
+ "@types/uuid": "^10.0.0",
35
+ "ts-node-dev": "^2.0.0",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
@@ -0,0 +1,65 @@
1
+ import { Request, Response } from "express";
2
+ import { RestResponse } from ".";
3
+ import { ErrorFields } from "./types";
4
+ export class ServiceException extends Error{
5
+ statusCode: number;
6
+ constructor(statusCode: number, message: string) {
7
+ super(message);
8
+ this.statusCode = statusCode;
9
+ }
10
+ }
11
+ export class UnknownException extends ServiceException {
12
+ constructor(statusCode: number, message: string) {
13
+ super(statusCode, message);
14
+ Object.setPrototypeOf(this, new.target.prototype);
15
+ }
16
+ }
17
+ export class NotFoundException extends ServiceException {
18
+ constructor(message: string) {
19
+ super(404, message);
20
+ Object.setPrototypeOf(this, new.target.prototype);
21
+ }
22
+ }
23
+ export class UnauthorizedException extends ServiceException {
24
+ constructor(message: string) {
25
+ super(401, message);
26
+ Object.setPrototypeOf(this, new.target.prototype);
27
+ }
28
+ }
29
+ export class AccessDeniedException extends ServiceException {
30
+ constructor(message: string) {
31
+ super(403, message);
32
+ Object.setPrototypeOf(this, new.target.prototype);
33
+ }
34
+ }
35
+ export class InternalServerException extends ServiceException {
36
+ constructor(message: string) {
37
+ super(500, message);
38
+ Object.setPrototypeOf(this, new.target.prototype);
39
+ }
40
+ }
41
+ export class ValidationException extends ServiceException {
42
+ errors?: ErrorFields
43
+
44
+ constructor(message: string, errors?: ErrorFields) {
45
+ super(400, message);
46
+ this.errors = errors;
47
+ Object.setPrototypeOf(this, new.target.prototype);
48
+ }
49
+ }
50
+
51
+ export const handleControllerException = (req: Request, res: Response, err: Error) => {
52
+ if(err instanceof UnauthorizedException){
53
+ return RestResponse.unauthorized(req, res, err.message);
54
+ }else if(err instanceof AccessDeniedException){
55
+ return RestResponse.accessDenied(req, res, err.message);
56
+ }else if(err instanceof ValidationException){
57
+ return RestResponse.validationError(req, res, (err as any)?.errors, (err as any)?.message);
58
+ }else if(err instanceof InternalServerException){
59
+ return RestResponse.exceptionError(req, res, err.message);
60
+ }else if(err instanceof NotFoundException){
61
+ return RestResponse.notFound(req, res, err.message);
62
+ }
63
+
64
+ return RestResponse.exceptionError(req, res, err.message);
65
+ }
package/src/index.ts CHANGED
@@ -1,272 +1,219 @@
1
- import { Request, Response, NextFunction } from "express";
2
- import { ErrorFields, StandardResponse, RestResponseFunctions, RestMiddlewareFunctions, AuthRequest, Pagination } from "./types";
3
- import { generateRequestId, getTimestamp } from "./utils";
4
- import axios from "axios";
5
- import jwt, { TokenExpiredError } from "jsonwebtoken";
6
- import path from "path";
7
- import { ZodError } from "zod";
8
- declare global {
9
- namespace Express {
10
- interface Response {
11
- json: (data: any) => Response;
12
- send: (data?: any) => Response;
13
- locals: {
14
- requestId: string;
15
- [key: string]: any;
16
- };
17
- }
18
- }
19
- }
20
- let KEYCLOAK_PUBLIC_KEY: any = null;
21
- export const initKeycloak = async (config: { service?: string; realm?: string }) => {
22
- const KEYCLOAK_SERVICE = config.service ?? "localhost";
23
- const KEYCLOAK_REALM = config.realm ?? "master";
24
- const realmUrl = `http://${KEYCLOAK_SERVICE}/realms/${KEYCLOAK_REALM}`;
25
- info(`Keycloak PublicKey Url: ${realmUrl}`);
26
- const resp = await axios.get(realmUrl);
27
- const key = resp.data.public_key;
28
- KEYCLOAK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----\n${key.match(/.{1,64}/g)?.join("\n")}\n-----END PUBLIC KEY-----`;
29
- };
30
-
31
- export const protect = (allowedRoles?: string[]) => {
32
- return (req: any, res: any, next: any) => {
33
- const authHeader = req.headers["authorization"];
34
- if (!authHeader)
35
- return RestResponse.unauthorized(req, res)
36
-
37
- const token = authHeader.split(" ")[1];
38
- if (!token)
39
- return RestResponse.unauthorized(req, res, "Token malformed")
40
-
41
- try {
42
- const decoded = jwt.verify(token, KEYCLOAK_PUBLIC_KEY, { algorithms: ["RS256"] });
43
- (req as AuthRequest).user = decoded;
44
- (req as AuthRequest).token = token;
45
- if(allowedRoles)
46
- return role(req, res, next, allowedRoles);
47
-
48
- next();
49
- } catch (err) {
50
- error((err as any)?.message)
51
- return RestResponse.unauthorized(req, res, "Token is not valid")
52
- }
53
- }
54
-
55
- }
56
- const role = (req: any, res: any, next: any, allowedRoles: string[]) => {
57
- const user = req.user;
58
- if (!user) {
59
- return RestResponse.unauthorized(req, res);
60
- }
61
- const realmRoles: string[] = (user?.realm_access?.roles ?? []).map((r: string) => r.toUpperCase());
62
- const clientRoles: string[] = Object.values(user?.resource_access ?? {})
63
- .flatMap((r: any) => r.roles ?? [])
64
- .map((r: string) => r.toUpperCase());
65
- const allowedUpper = allowedRoles.map(r => r === "super-admin"?"ADMIN":`DEZH-${r.toUpperCase()}`);
66
- const allRoles = [...realmRoles, ...clientRoles];
67
- const hasAccess: boolean = allowedUpper.some((role: string) =>
68
- allRoles.includes(role)
69
- );
70
- if (!hasAccess) {
71
- return RestResponse.accessDenied(req, res)
72
- }
73
- next()
74
- }
75
- const success = <T>(req: Request, res: Response, data: T) => {
76
- res.status(200).json(data);
77
- };
78
-
79
- const paginate = <T>(
80
- req: Request,
81
- res: Response,
82
- data: Pagination<T>
83
- ) => {
84
- res.status(200).json(data);
85
- };
86
-
87
- const created = <T>(req: Request, res: Response, data: T) => {
88
- res.status(201).json(data);
89
- };
90
-
91
- const deleted = (req: Request, res: Response) => {
92
- res.status(204).send();
93
- };
94
-
95
- const validationError = (
96
- req: Request,
97
- res: Response,
98
- errors?: ErrorFields,
99
- message = "Validation error"
100
- ) => {
101
- res.status(400).json({ errors, message });
102
- };
103
-
104
- const exceptionError = (
105
- req: Request,
106
- res: Response,
107
- errors: any,
108
- message = "Internal server error"
109
- ) => {
110
- res.status(500).json({
111
- message,
112
- errors
113
- });
114
- };
115
-
116
- const unauthorized = (req: Request, res: Response, message = "Unauthorized") => {
117
- res.status(401).json({ message });
118
- };
119
-
120
- const accessDenied = (req: Request, res: Response, message = "Access denied") => {
121
- res.status(403).json({ message });
122
- };
123
-
124
- const notFound = (req: Request, res: Response, message = "Not found") => {
125
- res.status(404).json({ message });
126
- };
127
-
128
- export const RestResponse: RestResponseFunctions = {
129
- success,
130
- paginate,
131
- created,
132
- deleted,
133
- validationError,
134
- exceptionError,
135
- unauthorized,
136
- accessDenied,
137
- notFound
138
- };
139
-
140
-
141
- const responseHandlerMiddleware = (
142
- req: Request,
143
- res: Response,
144
- next: NextFunction
145
- ) => {
146
-
147
- const oldJson = res.json.bind(res);
148
- const oldSend = res.send.bind(res);
149
-
150
- res.json = function (data: any) {
151
- if(res.statusCode > 204){
152
- const response: StandardResponse = {
153
- code: res.statusCode,
154
- message: data?.message || res.locals.message || (res.statusCode < 300 ? "OK" : "Error"),
155
- errors: data?.errors ?? null,
156
- meta: {
157
- requestId: res.locals.requestId || generateRequestId(),
158
- timestamp: getTimestamp()
159
- }
160
- };
161
-
162
- return oldJson(response);
163
- }
164
- return oldJson(data);
165
- };
166
-
167
- res.send = function (data: any) {
168
- if (res.statusCode === 204) {
169
- return oldSend();
170
- }
171
- return oldSend(data);
172
- };
173
-
174
- res.locals.requestId = generateRequestId();
175
- next();
176
- };
177
-
178
-
179
- export const RestMiddleware: RestMiddlewareFunctions = {
180
- responseHandlerMiddleware
181
- }
182
-
183
-
184
- const colors = {
185
- RESET: "\x1b[0m",
186
- RED: "\x1b[31m",
187
- GREEN: "\x1b[32m",
188
- YELLOW: "\x1b[33m"
189
- };
190
-
191
- const isProduction = process.env.NODE_ENV === "production";
192
- export const error = (message: string, meta?: any) => {
193
- log("ERROR", message, meta)
194
- }
195
- export const warn = (message: string, meta?: any) => {
196
- log("WARN", message, meta)
197
- }
198
- export const info = (message: string, meta?: any) => {
199
- log("INFO", message, meta)
200
- }
201
- export const log = (level: "INFO" | "WARN" | "ERROR", message: string, meta?: any) => {
202
- const timestamp = new Date().toISOString();
203
-
204
- // انتخاب رنگ فقط در dev
205
- let color = colors.RESET;
206
- if (!isProduction) {
207
- if (level === "INFO") color = colors.GREEN;
208
- if (level === "WARN") color = colors.YELLOW;
209
- if (level === "ERROR") color = colors.RED;
210
- }
211
- const metaStr = meta ? `. ${JSON.stringify(meta)}` : "";
212
-
213
- // چاپ لاگ
214
- console.log(`${color}[${timestamp}][${level}] ${message}${metaStr}${colors.RESET}`);
215
- };
216
- export const response = (
217
- req: Request,
218
- res: Response,
219
- response: ServiceResponse,
220
- handler = (eq: Request, res: Response, data: any = null) => {}
221
- ) => {
222
- if(response instanceof ServiceException){
223
- if(response?.code === 401){
224
- return RestResponse.unauthorized(req, res, response?.message)
225
- }else if(response?.code === 403){
226
- return RestResponse.accessDenied(req, res, response?.message)
227
- }else if(response?.code === 400){
228
- return RestResponse.validationError(req, res, response?.errors)
229
- }else if(response?.code === 500){
230
- return RestResponse.exceptionError(req, res, response.message)
231
- }else if(response?.code === 404){
232
- return RestResponse.notFound(req, res, response.message)
233
- }
234
- return RestResponse.exceptionError(req, res, response.message)
235
- }
236
- return handler(req, res, (response as ServiceResponseSuccess).entity)
237
- }
238
- export const handleServiceException = (message: string, err: any) : ServiceResponse => {
239
- if(err instanceof ZodError){
240
- const zodError = err as ZodError;
241
- if (zodError.issues) {
242
- const validationErrors: any = {};
243
- zodError.issues.forEach((i) => {
244
- const key = i.path[0];
245
- if (!validationErrors[key]) validationErrors[key] = [];
246
- validationErrors[key].push(i.message);
247
- });
248
- return new ServiceException(400, "Validation error", validationErrors)
249
- }
250
- }else if((err as any)?.isAxiosError && (err as any)?.response && (err as any).response.status){
251
- error(message, {
252
- status: (err as any).response.status,
253
- statusText: (err as any).response.statusText,
254
- error: (err as any).response.data
255
- })
256
- return new ServiceException((err as any).response.status, (err as any).response.data?.error_description);
257
- }else if(err instanceof Error){
258
- error(message, { error: err })
259
- return new ServiceException(500, err?.message);
260
- }
261
- return new ServiceException(500, "Internal server error");
262
- }
263
-
264
- export interface ServiceResponse{
265
-
266
- }
267
- export class ServiceException implements ServiceResponse {
268
- constructor(public code: number, public message: string, public errors?: ErrorFields) {}
269
- }
270
- export class ServiceResponseSuccess implements ServiceResponse {
271
- constructor(public entity: any = null) {}
272
- }
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { ErrorFields, StandardResponse, RestResponseFunctions, RestMiddlewareFunctions, AuthRequest, Pagination } from "./types";
3
+ import { generateRequestId, getTimestamp } from "./utils";
4
+ import axios from "axios";
5
+ import jwt, { TokenExpiredError } from "jsonwebtoken";
6
+ import path from "path";
7
+ import { ZodError } from "zod";
8
+ declare global {
9
+ namespace Express {
10
+ interface Response {
11
+ json: (data: any) => Response;
12
+ send: (data?: any) => Response;
13
+ locals: {
14
+ requestId: string;
15
+ [key: string]: any;
16
+ };
17
+ }
18
+ }
19
+ }
20
+ let KEYCLOAK_PUBLIC_KEY: any = null;
21
+ export const initKeycloak = async (config: { service?: string; realm?: string }) => {
22
+ const KEYCLOAK_SERVICE = config.service ?? "localhost";
23
+ const KEYCLOAK_REALM = config.realm ?? "master";
24
+ const realmUrl = `http://${KEYCLOAK_SERVICE}/realms/${KEYCLOAK_REALM}`;
25
+ info(`Keycloak PublicKey Url: ${realmUrl}`);
26
+ const resp = await axios.get(realmUrl);
27
+ const key = resp.data.public_key;
28
+ KEYCLOAK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----\n${key.match(/.{1,64}/g)?.join("\n")}\n-----END PUBLIC KEY-----`;
29
+ };
30
+
31
+ export const protect = (allowedRoles?: string[]) => {
32
+ return (req: any, res: any, next: any) => {
33
+ const authHeader = req.headers["authorization"];
34
+ if (!authHeader)
35
+ return RestResponse.unauthorized(req, res)
36
+
37
+ const token = authHeader.split(" ")[1];
38
+ if (!token)
39
+ return RestResponse.unauthorized(req, res, "Token malformed")
40
+
41
+ try {
42
+ const decoded = jwt.verify(token, KEYCLOAK_PUBLIC_KEY, { algorithms: ["RS256"] });
43
+ (req as AuthRequest).user = decoded;
44
+ (req as AuthRequest).token = token;
45
+ if(allowedRoles)
46
+ return role(req, res, next, allowedRoles);
47
+
48
+ next();
49
+ } catch (err) {
50
+ error((err as any)?.message)
51
+ return RestResponse.unauthorized(req, res, "Token is not valid")
52
+ }
53
+ }
54
+
55
+ }
56
+ const role = (req: any, res: any, next: any, allowedRoles: string[]) => {
57
+ const user = req.user;
58
+ if (!user) {
59
+ return RestResponse.unauthorized(req, res);
60
+ }
61
+ const realmRoles: string[] = (user?.realm_access?.roles ?? []).map((r: string) => r.toUpperCase());
62
+ const clientRoles: string[] = Object.values(user?.resource_access ?? {})
63
+ .flatMap((r: any) => r.roles ?? [])
64
+ .map((r: string) => r.toUpperCase());
65
+ const allowedUpper = allowedRoles.map(r => r === "super-admin"?"ADMIN":`DEZH-${r.toUpperCase()}`);
66
+ const allRoles = [...realmRoles, ...clientRoles];
67
+ const hasAccess: boolean = allowedUpper.some((role: string) =>
68
+ allRoles.includes(role)
69
+ );
70
+ if (!hasAccess) {
71
+ return RestResponse.accessDenied(req, res)
72
+ }
73
+ next()
74
+ }
75
+ const success = <T>(req: Request, res: Response, data: T) => {
76
+ res.status(200).json(data);
77
+ };
78
+
79
+ const paginate = <T>(
80
+ req: Request,
81
+ res: Response,
82
+ data: Pagination<T>
83
+ ) => {
84
+ res.status(200).json(data);
85
+ };
86
+
87
+ const created = <T>(req: Request, res: Response, data: T) => {
88
+ res.status(201).json(data);
89
+ };
90
+
91
+ const deleted = (req: Request, res: Response) => {
92
+ res.status(204).send();
93
+ };
94
+ const noContent = (req: Request, res: Response) => {
95
+ res.status(204).send();
96
+ };
97
+
98
+ const validationError = (
99
+ req: Request,
100
+ res: Response,
101
+ errors?: ErrorFields,
102
+ message = "Validation error"
103
+ ) => {
104
+ res.status(400).json({ errors, message });
105
+ };
106
+
107
+ const exceptionError = (
108
+ req: Request,
109
+ res: Response,
110
+ errors: any,
111
+ message = "Internal server error"
112
+ ) => {
113
+ res.status(500).json({
114
+ message,
115
+ errors
116
+ });
117
+ };
118
+
119
+ const unauthorized = (req: Request, res: Response, message = "Unauthorized") => {
120
+ res.status(401).json({ message });
121
+ };
122
+
123
+ const accessDenied = (req: Request, res: Response, message = "Access denied") => {
124
+ res.status(403).json({ message });
125
+ };
126
+
127
+ const notFound = (req: Request, res: Response, message = "Not found") => {
128
+ res.status(404).json({ message });
129
+ };
130
+
131
+ export const RestResponse: RestResponseFunctions = {
132
+ success,
133
+ paginate,
134
+ created,
135
+ deleted,
136
+ noContent,
137
+ validationError,
138
+ exceptionError,
139
+ unauthorized,
140
+ accessDenied,
141
+ notFound
142
+ };
143
+
144
+
145
+ const responseHandlerMiddleware = (
146
+ req: Request,
147
+ res: Response,
148
+ next: NextFunction
149
+ ) => {
150
+
151
+ const oldJson = res.json.bind(res);
152
+ const oldSend = res.send.bind(res);
153
+
154
+ res.json = function (data: any) {
155
+ if(res.statusCode > 204){
156
+ const response: StandardResponse = {
157
+ code: res.statusCode,
158
+ message: data?.message || res.locals.message || (res.statusCode < 300 ? "OK" : "Error"),
159
+ errors: data?.errors ?? null,
160
+ meta: {
161
+ requestId: res.locals.requestId || generateRequestId(),
162
+ timestamp: getTimestamp()
163
+ }
164
+ };
165
+
166
+ return oldJson(response);
167
+ }
168
+ return oldJson(data);
169
+ };
170
+
171
+ res.send = function (data: any) {
172
+ if (res.statusCode === 204) {
173
+ return oldSend();
174
+ }
175
+ return oldSend(data);
176
+ };
177
+
178
+ res.locals.requestId = generateRequestId();
179
+ next();
180
+ };
181
+
182
+
183
+ export const RestMiddleware: RestMiddlewareFunctions = {
184
+ responseHandlerMiddleware
185
+ }
186
+
187
+
188
+ const colors = {
189
+ RESET: "\x1b[0m",
190
+ RED: "\x1b[31m",
191
+ GREEN: "\x1b[32m",
192
+ YELLOW: "\x1b[33m"
193
+ };
194
+
195
+ const isProduction = process.env.NODE_ENV === "production";
196
+ export const error = (message: string, meta?: any) => {
197
+ log("ERROR", message, meta)
198
+ }
199
+ export const warn = (message: string, meta?: any) => {
200
+ log("WARN", message, meta)
201
+ }
202
+ export const info = (message: string, meta?: any) => {
203
+ log("INFO", message, meta)
204
+ }
205
+ export const log = (level: "INFO" | "WARN" | "ERROR", message: string, meta?: any) => {
206
+ const timestamp = new Date().toISOString();
207
+
208
+ // انتخاب رنگ فقط در dev
209
+ let color = colors.RESET;
210
+ if (!isProduction) {
211
+ if (level === "INFO") color = colors.GREEN;
212
+ if (level === "WARN") color = colors.YELLOW;
213
+ if (level === "ERROR") color = colors.RED;
214
+ }
215
+ const metaStr = meta ? `. ${JSON.stringify(meta)}` : "";
216
+
217
+ // چاپ لاگ
218
+ console.log(`${color}[${timestamp}][${level}] ${message}${metaStr}${colors.RESET}`);
219
+ };
package/src/types.ts CHANGED
@@ -1,60 +1,61 @@
1
- import { Request, Response, NextFunction } from "express";
2
-
3
- export interface StandardResponse<T = any> {
4
- code: number;
5
- message: string;
6
- errors: Record<string, string[]> | null;
7
- meta: {
8
- requestId: string;
9
- timestamp: string;
10
- };
11
- }
12
- export interface PaginationMeta{
13
- total: number;
14
- page: number;
15
- limit: number;
16
- totalPages: number;
17
- }
18
- export interface Pagination<T>{
19
- data: T[];
20
- meta: PaginationMeta
21
- }
22
-
23
-
24
- export type ErrorFields = Record<string, string[]>;
25
-
26
- export type Middleware = (req: Request, res: Response, next: NextFunction) => void;
27
-
28
- export interface RestMiddlewareFunctions {
29
- responseHandlerMiddleware: <T>(req: Request, res: Response, next: NextFunction) => void
30
- }
31
- export interface RestResponseFunctions {
32
- success: <T>(req: Request, res: Response, data: T) => void;
33
- paginate: <T>(
34
- req: Request,
35
- res: Response,
36
- data: Pagination<T>
37
- ) => void;
38
- created: <T>(req: Request, res: Response, data: T) => void;
39
- deleted: (req: Request, res: Response) => void;
40
- validationError: (
41
- req: Request,
42
- res: Response,
43
- errors?: ErrorFields,
44
- message?: string
45
- ) => void;
46
- exceptionError: (
47
- req: Request,
48
- res: Response,
49
- errors: any,
50
- message?: string
51
- ) => void;
52
- unauthorized: (req: Request, res: Response, message?: string) => void;
53
- accessDenied: (req: Request, res: Response, message?: string) => void;
54
- notFound: (req: Request, res: Response, message?: string) => void;
55
- }
56
-
57
- export interface AuthRequest extends Request {
58
- user?: any;
59
- token?: string
1
+ import { Request, Response, NextFunction } from "express";
2
+
3
+ export interface StandardResponse<T = any> {
4
+ code: number;
5
+ message: string;
6
+ errors: Record<string, string[]> | null;
7
+ meta: {
8
+ requestId: string;
9
+ timestamp: string;
10
+ };
11
+ }
12
+ export interface PaginationMeta{
13
+ total: number;
14
+ page: number;
15
+ limit: number;
16
+ totalPages: number;
17
+ }
18
+ export interface Pagination<T>{
19
+ data: T[];
20
+ meta: PaginationMeta
21
+ }
22
+
23
+
24
+ export type ErrorFields = Record<string, string[]>;
25
+
26
+ export type Middleware = (req: Request, res: Response, next: NextFunction) => void;
27
+
28
+ export interface RestMiddlewareFunctions {
29
+ responseHandlerMiddleware: <T>(req: Request, res: Response, next: NextFunction) => void
30
+ }
31
+ export interface RestResponseFunctions {
32
+ success: <T>(req: Request, res: Response, data: T) => void;
33
+ paginate: <T>(
34
+ req: Request,
35
+ res: Response,
36
+ data: Pagination<T>
37
+ ) => void;
38
+ created: <T>(req: Request, res: Response, data: T) => void;
39
+ deleted: (req: Request, res: Response) => void;
40
+ noContent: (req: Request, res: Response) => void;
41
+ validationError: (
42
+ req: Request,
43
+ res: Response,
44
+ errors?: ErrorFields,
45
+ message?: string
46
+ ) => void;
47
+ exceptionError: (
48
+ req: Request,
49
+ res: Response,
50
+ errors: any,
51
+ message?: string
52
+ ) => void;
53
+ unauthorized: (req: Request, res: Response, message?: string) => void;
54
+ accessDenied: (req: Request, res: Response, message?: string) => void;
55
+ notFound: (req: Request, res: Response, message?: string) => void;
56
+ }
57
+
58
+ export interface AuthRequest extends Request {
59
+ user?: any;
60
+ token?: string
60
61
  }
package/src/utils.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { v4 as uuidv4 } from "uuid";
2
-
3
- export const generateRequestId = () => {
4
- return uuidv4();
5
- }
6
-
7
- export const getTimestamp = () => {
8
- return new Date().toISOString();
9
- }
1
+ import { v4 as uuidv4 } from "uuid";
2
+
3
+ export const generateRequestId = () => {
4
+ return uuidv4();
5
+ }
6
+
7
+ export const getTimestamp = () => {
8
+ return new Date().toISOString();
9
+ }
package/tsconfig.json CHANGED
@@ -1,15 +1,15 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "CommonJS",
5
- "moduleResolution": "Node",
6
- "rootDir": "src",
7
- "outDir": "dist",
8
- "strict": true,
9
- "declaration": true,
10
- "esModuleInterop": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "skipLibCheck": true
13
- },
14
- "include": ["src/**/*"]
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src/**/*"]
15
15
  }