@mustafahmed1997/learnify-backend 1.2.0 → 1.4.0

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.
package/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # @mustafahmed1997/learnify-backend
2
2
 
3
3
  Shared backend utilities for Learnify microservices: Clerk-based auth/role
4
- middlewares, error handling utilities, and the Express `Request` type
5
- augmentation for `req.user` — all importable from the package root.
4
+ middlewares, Zod-based request validation, error handling utilities, response
5
+ shaping helpers, and the Express `Request` type augmentation for `req.user` —
6
+ all importable from the package root.
7
+
8
+ ## What this package solves
9
+
10
+ Every Learnify microservice needs the same handful of cross-cutting pieces:
11
+ who is the caller (Clerk auth), are they allowed to do this (role checks),
12
+ is their request well-formed (Zod validation), and what shape should errors
13
+ and responses take. This package centralizes all of that so each service
14
+ doesn't reimplement or subtly diverge on it.
6
15
 
7
16
  ## Installation
8
17
 
@@ -12,29 +21,62 @@ npm install @mustafahmed1997/learnify-backend
12
21
 
13
22
  ### Peer dependencies
14
23
 
15
- This package expects the consuming service to provide its own `express` and
16
- `@clerk/express` installs (so there is a single shared instance of each at
17
- runtime, and Clerk's request-scoped auth context works correctly):
24
+ This package expects the consuming service to provide its own `express`,
25
+ `@clerk/express`, and `zod` installs (so there is a single shared instance of
26
+ each at runtime, and Clerk's request-scoped auth context works correctly):
18
27
 
19
28
  ```bash
20
- npm install express @clerk/express
29
+ npm install express @clerk/express zod
21
30
  ```
22
31
 
23
- ## Setup: `clerkMiddleware()` before `checkAuth`
32
+ ## Setup: `clerkMiddleware()` before `checkAuth` / `checkRole`
24
33
 
25
- `checkAuth` reads Clerk auth state via `getAuth(req)`, which requires
26
- `clerkMiddleware()` from `@clerk/express` to have already run on the request:
34
+ Both `checkAuth` and `checkRole` read Clerk auth state via `getAuth(req)`,
35
+ which requires `clerkMiddleware()` from `@clerk/express` to have already run
36
+ on the request:
27
37
 
28
38
  ```ts
29
39
  import express from "express";
30
40
  import { clerkMiddleware } from "@clerk/express";
31
- import { checkAuth, checkRole, globalErrorHandler } from "@mustafahmed1997/learnify-backend";
41
+ import {
42
+ checkAuth,
43
+ checkRole,
44
+ validationMiddleware,
45
+ globalErrorHandler,
46
+ } from "@mustafahmed1997/learnify-backend";
32
47
 
33
48
  const app = express();
34
49
 
50
+ app.use(express.json());
35
51
  app.use(clerkMiddleware());
36
52
  ```
37
53
 
54
+ ## Public exports
55
+
56
+ | Export | Kind | Purpose |
57
+ | --- | --- | --- |
58
+ | `checkAuth` | middleware | Requires Clerk authentication, attaches `req.user` |
59
+ | `checkRole` | middleware factory | Role-based authorization (`Any` / `All` / `Exact`) |
60
+ | `validationMiddleware` | middleware factory | Validates `body` / `params` / `query` against Zod schemas |
61
+ | `validRoles`, `Role` | const / type | The canonical list of roles and their type |
62
+ | `AppError` | class | Base operational error carrying an HTTP status code |
63
+ | `ValidationError` | class | `AppError` subclass carrying structured field errors |
64
+ | `ValidationErrorItem` | type | Shape of one entry in `ValidationError.errors` |
65
+ | `globalErrorHandler` | middleware | Express error handler; register last |
66
+ | `handlePrismaError` | function | Maps a Prisma error to `{ status, message }` |
67
+ | `getErrorMsg`, `getSuccessMsg` | function | Consistent error/success message builders |
68
+ | `getJsonResponse`, `JsonResponseOptions` | function / type | Builds a sparse, consistent response body |
69
+ | `checkUnique` | function | Checks an array has no duplicate values |
70
+ | `checkValidRoles` | function | Checks every value in an array is a valid `Role` |
71
+ | `requiredFieldMsg`, `minLengthMsg`, `maxLengthMsg`, `invalidSchemaFormatMsg`, `invalidEmailMsg`, `invalidPasswordMsg`, `invalidDateFormatMsg`, `futureDateRequiredMsg`, `invalidNumberMsg`, `positiveNumberMsg`, `invalidUrlMsg` | function | Reusable validation error message builders (pair well with custom Zod `.refine()`/`.check()` messages) |
72
+ | `ClerkUser` | type | Shape of `req.user` |
73
+ | `Location` | type | `"body" \| "params" \| "query"` |
74
+ | `CheckRoleOptions`, `RoleCheckMode` | type | Options accepted by `checkRole` |
75
+
76
+ Importing anything from the package root also loads the Express `Request`
77
+ type augmentation (`req.user?: ClerkUser`), so no extra `express.d.ts` is
78
+ needed in the consuming service.
79
+
38
80
  ## `checkAuth`
39
81
 
40
82
  Verifies the request is authenticated via Clerk and attaches a typed
@@ -48,13 +90,20 @@ app.get("/me", checkAuth, (req, res) => {
48
90
  });
49
91
  ```
50
92
 
51
- If the request is not authenticated, `checkAuth` responds `401` with
52
- `{ error: "Unauthenticated. Login first please." }` and does not call `next()`.
93
+ If the request is not authenticated, `checkAuth` responds `401` with:
94
+
95
+ ```json
96
+ { "error": "Unauthenticated. Login first please." }
97
+ ```
98
+
99
+ and does not call `next()`. If reading the Clerk auth context throws, the
100
+ error is passed to `next(error)` for `globalErrorHandler` to handle.
53
101
 
54
102
  ## `checkRole`
55
103
 
56
- Authorizes a request based on the role in the Clerk session claims
57
- (`sessionClaims.userMetadata.role`). Takes an array of allowed roles.
104
+ Authorizes a request based on the caller's roles in the Clerk session claims
105
+ (`sessionClaims.userMetadata.roles`, an array). Takes an array of allowed
106
+ roles and an optional `{ mode }`.
58
107
 
59
108
  ```ts
60
109
  import { checkRole } from "@mustafahmed1997/learnify-backend";
@@ -66,8 +115,46 @@ app.delete(
66
115
  );
67
116
  ```
68
117
 
69
- If the caller's role is not in the allowed list, it responds `403` with
70
- `{ message: "Unauthorized !!" }` and does not call `next()`.
118
+ ### Modes
119
+
120
+ `mode` defaults to `"Any"` when omitted.
121
+
122
+ - **`Any`** — the caller needs at least one of `allowedRoles`. Extra roles on
123
+ either side are ignored.
124
+
125
+ ```ts
126
+ // Passes if the caller has ADMIN or INSTRUCTOR (or both)
127
+ checkRole(["ADMIN", "INSTRUCTOR"], { mode: "Any" });
128
+ ```
129
+
130
+ - **`All`** — the caller must have every role in `allowedRoles`, but may have
131
+ additional roles beyond that.
132
+
133
+ ```ts
134
+ // Passes only if the caller has both ADMIN and INSTRUCTOR (extra roles OK)
135
+ checkRole(["ADMIN", "INSTRUCTOR"], { mode: "All" });
136
+ ```
137
+
138
+ - **`Exact`** — the caller's role set must match `allowedRoles` exactly, with
139
+ no missing or extra roles.
140
+
141
+ ```ts
142
+ // Passes only if the caller's roles are exactly {ADMIN, INSTRUCTOR}
143
+ checkRole(["ADMIN", "INSTRUCTOR"], { mode: "Exact" });
144
+ ```
145
+
146
+ ### Responses
147
+
148
+ - Empty or duplicate `allowedRoles`, or duplicate/invalid roles coming back
149
+ from Clerk's session claims, are treated as misconfiguration/upstream data
150
+ problems: the middleware calls `next(error)` with a plain `Error`, which
151
+ `globalErrorHandler` turns into a `500`.
152
+ - No `sessionClaims`, no `userMetadata.roles`, or an empty roles array
153
+ responds `403` with `{ "message": "User roles not found" }` and does not
154
+ call `next()`.
155
+ - A caller whose roles don't satisfy the selected mode responds `403` with
156
+ `{ "message": "Forbidden" }` and does not call `next()`.
157
+ - An authorized caller calls `next()` with no arguments.
71
158
 
72
159
  ### Combined route example
73
160
 
@@ -80,20 +167,75 @@ app.patch(
80
167
  );
81
168
  ```
82
169
 
83
- ## `validRoles` and `Role`
170
+ ## `validationMiddleware`
171
+
172
+ Validates `req.body`, `req.params`, and/or `req.query` against Zod schemas.
173
+ Any subset of the three locations may be supplied — omitted locations are
174
+ skipped.
84
175
 
85
176
  ```ts
86
- import { validRoles } from "@mustafahmed1997/learnify-backend";
87
- import type { Role } from "@mustafahmed1997/learnify-backend";
177
+ import { z } from "zod";
178
+ import { validationMiddleware } from "@mustafahmed1997/learnify-backend";
88
179
 
89
- console.log(validRoles); // ["STUDENT", "INSTRUCTOR", "ADMIN"]
180
+ const createCourseSchema = z.object({
181
+ title: z.string().min(3),
182
+ price: z.number().positive(),
183
+ });
90
184
 
91
- function isValidRole(value: string): value is Role {
92
- return (validRoles as readonly string[]).includes(value);
93
- }
185
+ const courseIdParamsSchema = z.object({
186
+ id: z.string().uuid(),
187
+ });
188
+
189
+ const paginationQuerySchema = z.object({
190
+ page: z.coerce.number().int().positive().default(1),
191
+ });
192
+
193
+ app.post(
194
+ "/courses",
195
+ validationMiddleware({ body: createCourseSchema }),
196
+ createCourseController,
197
+ );
198
+
199
+ app.get(
200
+ "/courses/:id",
201
+ validationMiddleware({ params: courseIdParamsSchema }),
202
+ getCourseController,
203
+ );
204
+
205
+ app.get(
206
+ "/courses",
207
+ validationMiddleware({ query: paginationQuerySchema }),
208
+ listCoursesController,
209
+ );
210
+
211
+ // All three at once:
212
+ app.patch(
213
+ "/courses/:id",
214
+ validationMiddleware({
215
+ params: courseIdParamsSchema,
216
+ body: createCourseSchema.partial(),
217
+ query: paginationQuerySchema,
218
+ }),
219
+ updateCourseController,
220
+ );
94
221
  ```
95
222
 
96
- ## `AppError`
223
+ - On success, `validationMiddleware` calls `next()` with no arguments **and**
224
+ writes each schema's parsed/transformed output back onto `req.body`,
225
+ `req.params`, and/or `req.query`. This means transforms like
226
+ `z.coerce.number()` or `.default(...)` are reflected in what the
227
+ controller sees — e.g. `req.query.page` above is a `number`, not the raw
228
+ query string.
229
+ - On failure, it calls `next(validationError)` with a `ValidationError`
230
+ (never touches `res` directly) collecting **every** failing field across
231
+ **all** provided locations in one pass — it does not stop at the first
232
+ invalid location.
233
+ - Register `globalErrorHandler` after your routes to turn that
234
+ `ValidationError` into the `400` response documented below.
235
+
236
+ ## Error classes
237
+
238
+ ### `AppError`
97
239
 
98
240
  A typed operational error carrying an HTTP status code.
99
241
 
@@ -105,6 +247,33 @@ if (!course) {
105
247
  }
106
248
  ```
107
249
 
250
+ ### `ValidationError`
251
+
252
+ An `AppError` subclass (status code always `400`) that also carries the list
253
+ of field-level failures. This is what `validationMiddleware` throws, but you
254
+ can also construct and throw it directly for manual validation:
255
+
256
+ ```ts
257
+ import { ValidationError } from "@mustafahmed1997/learnify-backend";
258
+ import type { ValidationErrorItem } from "@mustafahmed1997/learnify-backend";
259
+
260
+ const errors: ValidationErrorItem[] = [
261
+ { location: "body", field: "role", message: "Invalid role" },
262
+ ];
263
+
264
+ throw new ValidationError("Validation failed", errors);
265
+ ```
266
+
267
+ `ValidationErrorItem` shape:
268
+
269
+ ```ts
270
+ type ValidationErrorItem = {
271
+ location: "body" | "params" | "query";
272
+ field: string;
273
+ message: string;
274
+ };
275
+ ```
276
+
108
277
  ## `globalErrorHandler`
109
278
 
110
279
  Express error-handling middleware. Register it **last**, after all routes:
@@ -115,9 +284,140 @@ import { globalErrorHandler } from "@mustafahmed1997/learnify-backend";
115
284
  app.use(globalErrorHandler);
116
285
  ```
117
286
 
118
- - An `AppError` produces `{ error: err.message }` with `err.statusCode`.
119
- - Any other thrown value produces `{ error: "Internal server error" }` with
120
- status `500`.
287
+ Behavior, checked in this order:
288
+
289
+ 1. **`ValidationError`** (including whatever `validationMiddleware` throws)
290
+ → `400` with the field-level errors:
291
+
292
+ ```json
293
+ {
294
+ "error": "Validation failed",
295
+ "errors": [
296
+ { "location": "body", "field": "role", "message": "Invalid role" },
297
+ { "location": "params", "field": "userId", "message": "Invalid user ID" }
298
+ ]
299
+ }
300
+ ```
301
+
302
+ 2. Any other **`AppError`** → its own `statusCode` with `{ "error": err.message }`.
303
+ 3. Anything else (an unexpected `Error`, a rejected promise with a non-Error
304
+ value, etc.) → `500` with a fixed, generic body that never leaks the
305
+ underlying error's message or stack:
306
+
307
+ ```json
308
+ { "error": "Internal server error" }
309
+ ```
310
+
311
+ The original error is still logged via `console.error` for observability
312
+ — it just isn't sent to the client.
313
+
314
+ ## `handlePrismaError`
315
+
316
+ Maps a Prisma error (by `error.code` / `error.name`) to a plain
317
+ `{ status, message }` pair, for services that want to translate Prisma
318
+ failures into an `AppError` themselves:
319
+
320
+ ```ts
321
+ import { handlePrismaError, AppError } from "@mustafahmed1997/learnify-backend";
322
+
323
+ try {
324
+ await prisma.course.create({ data });
325
+ } catch (err) {
326
+ const { status, message } = handlePrismaError(err);
327
+ throw new AppError(message, status);
328
+ }
329
+ ```
330
+
331
+ Handles `P2002` (unique constraint), `P2003` (invalid foreign key), `P2025`
332
+ (record not found), `P2014` (invalid relation), `P2000` (value too long),
333
+ `PrismaClientValidationError`, and falls back to a generic `500` message for
334
+ anything else — including `null`/`undefined`/non-object inputs.
335
+
336
+ ## `validRoles` and `Role`
337
+
338
+ ```ts
339
+ import { validRoles } from "@mustafahmed1997/learnify-backend";
340
+ import type { Role } from "@mustafahmed1997/learnify-backend";
341
+
342
+ console.log(validRoles); // ["STUDENT", "INSTRUCTOR", "ADMIN"]
343
+
344
+ function isValidRole(value: string): value is Role {
345
+ return (validRoles as readonly string[]).includes(value);
346
+ }
347
+ ```
348
+
349
+ ## Role-checking utilities
350
+
351
+ Low-level building blocks used internally by `checkRole`, exported for
352
+ services that need custom role-array validation outside of the middleware:
353
+
354
+ ```ts
355
+ import { checkUnique, checkValidRoles } from "@mustafahmed1997/learnify-backend";
356
+
357
+ checkUnique(["ADMIN", "ADMIN"]); // false
358
+ checkUnique(["ADMIN", "STUDENT"]); // true
359
+
360
+ checkValidRoles(["ADMIN", "STUDENT"]); // true
361
+ checkValidRoles(["ADMIN", "SUPERADMIN"]); // false — not in validRoles
362
+ ```
363
+
364
+ ## Response utilities
365
+
366
+ ### `getJsonResponse`
367
+
368
+ Builds a response body containing only the fields you actually pass —
369
+ `undefined` fields are omitted rather than serialized:
370
+
371
+ ```ts
372
+ import { getJsonResponse } from "@mustafahmed1997/learnify-backend";
373
+
374
+ res.status(200).json(
375
+ getJsonResponse({
376
+ message: "Course created",
377
+ data: course,
378
+ }),
379
+ );
380
+ // { "message": "Course created", "data": { ... } }
381
+ ```
382
+
383
+ ### `getErrorMsg` / `getSuccessMsg`
384
+
385
+ Small helpers for consistent, grammatically-correct singular/plural messages:
386
+
387
+ ```ts
388
+ import { getErrorMsg, getSuccessMsg } from "@mustafahmed1997/learnify-backend";
389
+
390
+ getErrorMsg("Course", "was", "not found"); // "Course was not found"
391
+ getErrorMsg("Courses", "were", "not found"); // "Courses were not found"
392
+
393
+ getSuccessMsg("Course", "has", "created"); // "Course has been created successfully"
394
+ getSuccessMsg("Courses", "have", "created"); // "Courses have been created successfully"
395
+ ```
396
+
397
+ ## Validation error message utilities
398
+
399
+ Reusable message builders for custom Zod validators (e.g. inside
400
+ `.refine()`), so wording stays consistent across services:
401
+
402
+ ```ts
403
+ import { z } from "zod";
404
+ import {
405
+ requiredFieldMsg,
406
+ minLengthMsg,
407
+ invalidEmailMsg,
408
+ } from "@mustafahmed1997/learnify-backend";
409
+
410
+ const schema = z.object({
411
+ email: z.string().email(invalidEmailMsg()),
412
+ password: z.string().min(8, minLengthMsg(8)),
413
+ });
414
+ ```
415
+
416
+ Available: `requiredFieldMsg(field)`, `minLengthMsg(min)`,
417
+ `maxLengthMsg(max)`, `invalidSchemaFormatMsg(field, format)`,
418
+ `invalidEmailMsg()`, `invalidPasswordMsg()`, `invalidDateFormatMsg()`,
419
+ `futureDateRequiredMsg()`, `invalidNumberMsg(field)`,
420
+ `positiveNumberMsg(field)`, `invalidUrlMsg()`.
121
421
 
122
422
  ## `ClerkUser` and `req.user` typing
123
423
 
@@ -154,14 +454,81 @@ type ClerkUser = {
154
454
  };
155
455
  ```
156
456
 
457
+ > Note: `checkAuth` attaches `req.user` from `sessionClaims.userMetadata`
458
+ > as-is (singular `role`). `checkRole` performs its own authorization check
459
+ > directly against `sessionClaims.userMetadata.roles` (plural, an array) —
460
+ > it does not read `req.user`. If your Clerk session claims only populate
461
+ > `roles`, use `checkRole` for authorization decisions rather than
462
+ > `req.user.userMetadata.role`.
463
+
157
464
  ## TypeScript usage
158
465
 
159
466
  The package ships its own `.d.ts` declarations — no `@types` package needed.
160
467
  It targets ESM (`"type": "module"`) and expects a `moduleResolution` setting
161
468
  that supports package `exports` (e.g. `node16`, `nodenext`, or `bundler`).
162
469
 
470
+ ## Using this package in the Learnify microservices
471
+
472
+ A typical service wires the pieces together in this order:
473
+
474
+ ```ts
475
+ import express from "express";
476
+ import { clerkMiddleware } from "@clerk/express";
477
+ import {
478
+ checkAuth,
479
+ checkRole,
480
+ validationMiddleware,
481
+ globalErrorHandler,
482
+ } from "@mustafahmed1997/learnify-backend";
483
+
484
+ const app = express();
485
+
486
+ app.use(express.json());
487
+ app.use(clerkMiddleware()); // must run before checkAuth/checkRole
488
+
489
+ app.post(
490
+ "/courses",
491
+ checkAuth,
492
+ checkRole(["INSTRUCTOR", "ADMIN"]),
493
+ validationMiddleware({ body: createCourseSchema }),
494
+ createCourseController,
495
+ );
496
+
497
+ // ...all other routes...
498
+
499
+ app.use(globalErrorHandler); // must be registered last
500
+ ```
501
+
502
+ Controllers can then `throw new AppError(...)` / `throw new ValidationError(...)`
503
+ (or pass them to `next(...)`) and rely on `globalErrorHandler` to produce a
504
+ consistent response shape across every microservice.
505
+
506
+ ## Running tests
507
+
508
+ ```bash
509
+ npm test # runs the full suite once (vitest run)
510
+ npm run test:watch
511
+ ```
512
+
513
+ ## Type checking
514
+
515
+ ```bash
516
+ npm run typecheck
517
+ ```
518
+
519
+ ## Building
520
+
521
+ ```bash
522
+ npm run build
523
+ ```
524
+
525
+ Compiles `src/` to `dist/` (via `tsconfig.build.json`), producing the ESM
526
+ output and `.d.ts` declarations referenced by `main`/`types` in
527
+ `package.json`.
528
+
163
529
  ## What this package does not do
164
530
 
165
- - Does not depend on Prisma or any database.
531
+ - Does not depend on Prisma or any database (`handlePrismaError` only maps
532
+ error shapes — it never imports `@prisma/client`).
166
533
  - Does not call the Auth Service (or any other service) over HTTP.
167
534
  - Does not manage Clerk session creation — that's handled by `clerkMiddleware()`.
@@ -2,7 +2,7 @@ export class AppError extends Error {
2
2
  constructor(message, statusCode) {
3
3
  super(message);
4
4
  this.statusCode = statusCode;
5
- Object.setPrototypeOf(this, AppError.prototype);
5
+ Object.setPrototypeOf(this, new.target.prototype);
6
6
  }
7
7
  }
8
8
  //# sourceMappingURL=AppError.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AppError.js","sourceRoot":"","sources":["../../src/errors/AppError.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGjC,YAAY,OAAe,EAAE,UAAkB;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAE7B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClD,CAAC;CACF"}
1
+ {"version":3,"file":"AppError.js","sourceRoot":"","sources":["../../src/errors/AppError.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGjC,YAAY,OAAe,EAAE,UAAkB;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAE7B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF"}
@@ -1,6 +1,13 @@
1
1
  import { AppError } from "./AppError.js";
2
+ import { ValidationError } from "./validationError.js";
2
3
  export function globalErrorHandler(err, req, res, next) {
3
4
  console.error(err);
5
+ if (err instanceof ValidationError) {
6
+ return res.status(err.statusCode).json({
7
+ error: err.message,
8
+ errors: err.errors,
9
+ });
10
+ }
4
11
  if (err instanceof AppError) {
5
12
  return res.status(err.statusCode).json({
6
13
  error: err.message,
@@ -10,4 +17,24 @@ export function globalErrorHandler(err, req, res, next) {
10
17
  error: "Internal server error",
11
18
  });
12
19
  }
20
+ /*
21
+ * Error json due to validation error :
22
+
23
+ {
24
+ "error": "Validation failed",
25
+ "errors": [
26
+ {
27
+ "location": "body",
28
+ "field": "role",
29
+ "message": "Invalid role"
30
+ },
31
+ {
32
+ "location": "params",
33
+ "field": "userId",
34
+ "message": "Invalid user ID"
35
+ }
36
+ ]
37
+ }
38
+
39
+ */
13
40
  //# sourceMappingURL=globalErrorHandler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"globalErrorHandler.js","sourceRoot":"","sources":["../../src/errors/globalErrorHandler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,UAAU,kBAAkB,CAChC,GAAY,EACZ,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEnB,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,KAAK,EAAE,GAAG,CAAC,OAAO;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QAC1B,KAAK,EAAE,uBAAuB;KAC/B,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"globalErrorHandler.js","sourceRoot":"","sources":["../../src/errors/globalErrorHandler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,UAAU,kBAAkB,CAChC,GAAY,EACZ,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEnB,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,KAAK,EAAE,GAAG,CAAC,OAAO;YAClB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;IACL,CAAC;IAED,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACrC,KAAK,EAAE,GAAG,CAAC,OAAO;SACnB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QAC1B,KAAK,EAAE,uBAAuB;KAC/B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;EAmBE"}
@@ -0,0 +1,11 @@
1
+ import { Location } from "../types/locationType.js";
2
+ import { AppError } from "./AppError.js";
3
+ export type ValidationErrorItem = {
4
+ location: Location;
5
+ field: string;
6
+ message: string;
7
+ };
8
+ export declare class ValidationError extends AppError {
9
+ errors: ValidationErrorItem[];
10
+ constructor(message: string, errors: ValidationErrorItem[]);
11
+ }
@@ -0,0 +1,8 @@
1
+ import { AppError } from "./AppError.js";
2
+ export class ValidationError extends AppError {
3
+ constructor(message, errors) {
4
+ super(message, 400);
5
+ this.errors = errors;
6
+ }
7
+ }
8
+ //# sourceMappingURL=validationError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validationError.js","sourceRoot":"","sources":["../../src/errors/validationError.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAQzC,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAC3C,YACE,OAAe,EACR,MAA6B;QAEpC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAFb,WAAM,GAAN,MAAM,CAAuB;IAGtC,CAAC;CACF"}
package/dist/index.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  import "./types/express.js";
2
2
  export { checkAuth } from "./middlewares/checkAuth.js";
3
3
  export { checkRole } from "./middlewares/checkRole.js";
4
+ export type { CheckRoleOptions, RoleCheckMode, } from "./middlewares/checkRole.js";
4
5
  export { validationMiddleware } from "./middlewares/validationMiddleware.js";
5
6
  export { validRoles } from "./constants/validRoles.js";
6
7
  export type { Role } from "./constants/validRoles.js";
7
8
  export { AppError } from "./errors/AppError.js";
9
+ export { ValidationError } from "./errors/validationError.js";
10
+ export type { ValidationErrorItem } from "./errors/validationError.js";
8
11
  export { globalErrorHandler } from "./errors/globalErrorHandler.js";
9
12
  export { handlePrismaError } from "./errors/handlePrismaError.js";
10
13
  export { getErrorMsg } from "./utils/getErrorMsg.js";
11
14
  export { getSuccessMsg } from "./utils/getSuccessMsg.js";
12
15
  export { getJsonResponse } from "./utils/getJsonResponse.js";
13
16
  export type { JsonResponseOptions } from "./utils/getJsonResponse.js";
17
+ export { checkUnique } from "./utils/checkUnique.js";
18
+ export { checkValidRoles } from "./utils/checkValidRoles.js";
14
19
  export { requiredFieldMsg, minLengthMsg, maxLengthMsg, invalidSchemaFormatMsg, invalidEmailMsg, invalidPasswordMsg, invalidDateFormatMsg, futureDateRequiredMsg, invalidNumberMsg, positiveNumberMsg, invalidUrlMsg, } from "./utils/validationErrorMessages.js";
15
20
  export type { ClerkUser } from "./types/ClerkUser.js";
21
+ export type { Location } from "./types/locationType.js";
package/dist/index.js CHANGED
@@ -4,10 +4,13 @@ export { checkRole } from "./middlewares/checkRole.js";
4
4
  export { validationMiddleware } from "./middlewares/validationMiddleware.js";
5
5
  export { validRoles } from "./constants/validRoles.js";
6
6
  export { AppError } from "./errors/AppError.js";
7
+ export { ValidationError } from "./errors/validationError.js";
7
8
  export { globalErrorHandler } from "./errors/globalErrorHandler.js";
8
9
  export { handlePrismaError } from "./errors/handlePrismaError.js";
9
10
  export { getErrorMsg } from "./utils/getErrorMsg.js";
10
11
  export { getSuccessMsg } from "./utils/getSuccessMsg.js";
11
12
  export { getJsonResponse } from "./utils/getJsonResponse.js";
13
+ export { checkUnique } from "./utils/checkUnique.js";
14
+ export { checkValidRoles } from "./utils/checkValidRoles.js";
12
15
  export { requiredFieldMsg, minLengthMsg, maxLengthMsg, invalidSchemaFormatMsg, invalidEmailMsg, invalidPasswordMsg, invalidDateFormatMsg, futureDateRequiredMsg, invalidNumberMsg, positiveNumberMsg, invalidUrlMsg, } from "./utils/validationErrorMessages.js";
13
16
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAGvD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG7D,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,GACd,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAKvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAGvD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,GACd,MAAM,oCAAoC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"checkAuth.js","sourceRoot":"","sources":["../../src/middlewares/checkAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAIzC,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,IAAI,CAAC;QACH,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAEhE,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,KAAK,EAAE,sCAAsC;aAC9C,CAAC,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC;QACvB,MAAM,YAAY,GAAG,aAAa,EAAE,YAAyC,CAAC;QAC9E,MAAM,KAAK,GAAG,aAAa,EAAE,KAA2B,CAAC;QACzD,MAAM,SAAS,GAAG,aAAa,EAAE,SAA+B,CAAC;QACjE,MAAM,QAAQ,GAAG,aAAa,EAAE,QAA8B,CAAC;QAC/D,MAAM,QAAQ,GAAG,aAAa,EAAE,QAA8B,CAAC;QAE/D,MAAM,IAAI,GAAc;YACtB,OAAO;YACP,KAAK;YACL,SAAS;YACT,QAAQ;YACR,QAAQ;YACR,YAAY;SACb,CAAC;QAEF,yBAAyB;QACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAEhB,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"checkAuth.js","sourceRoot":"","sources":["../../src/middlewares/checkAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAIzC,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAY,EACZ,GAAa,EACb,IAAkB;IAElB,IAAI,CAAC;QACH,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAEhE,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,KAAK,EAAE,sCAAsC;aAC9C,CAAC,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC;QACvB,MAAM,YAAY,GAChB,aAAa,EAAE,YAAyC,CAAC;QAC3D,MAAM,KAAK,GAAG,aAAa,EAAE,KAA2B,CAAC;QACzD,MAAM,SAAS,GAAG,aAAa,EAAE,SAA+B,CAAC;QACjE,MAAM,QAAQ,GAAG,aAAa,EAAE,QAA8B,CAAC;QAC/D,MAAM,QAAQ,GAAG,aAAa,EAAE,QAA8B,CAAC;QAE/D,MAAM,IAAI,GAAc;YACtB,OAAO;YACP,KAAK;YACL,SAAS;YACT,QAAQ;YACR,QAAQ;YACR,YAAY;SACb,CAAC;QAEF,yBAAyB;QACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAEhB,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -1,3 +1,28 @@
1
1
  import { NextFunction, Request, Response } from "express";
2
2
  import { validRoles } from "../constants/validRoles.js";
3
- export declare function checkRole(roles: (typeof validRoles)[number][]): (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
3
+ type Role = (typeof validRoles)[number];
4
+ declare const ROLE_CHECK_MODES: readonly ["All", "Exact", "Any"];
5
+ export type RoleCheckMode = (typeof ROLE_CHECK_MODES)[number];
6
+ export type CheckRoleOptions = {
7
+ mode: RoleCheckMode;
8
+ };
9
+ /**
10
+ * Used as a middleware to check whether the user has permission to access this endpoint
11
+ * based on their role.
12
+ *
13
+ * @param allowedRoles
14
+ * @param options
15
+ *
16
+ * Options : mode : "All" | "Exact" | "Any"
17
+ *
18
+ * - "All" : All roles in allowedRoles are required. They must exist in the user's roles
19
+ * even if user has more roles.
20
+ *
21
+ * - "Exact" : All roles in allowedRoles are required. They must exist in the user's roles
22
+ * exactly without any other roles.
23
+ *
24
+ * - "Any" : At least one role in allowedRoles is required. One role in the allowedRoles
25
+ * must exist in the user's roles regardless of other roles in user's roles array and allowed roles array.
26
+ */
27
+ export declare function checkRole(allowedRoles: Role[], options?: CheckRoleOptions): (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
28
+ export {};
@@ -1,11 +1,68 @@
1
1
  import { getAuth } from "@clerk/express";
2
- export function checkRole(roles) {
3
- return async (req, res, next) => {
2
+ import { checkUnique } from "../utils/checkUnique.js";
3
+ import { checkValidRoles } from "../utils/checkValidRoles.js";
4
+ const ROLE_CHECK_MODES = ["All", "Exact", "Any"];
5
+ /**
6
+ * Used as a middleware to check whether the user has permission to access this endpoint
7
+ * based on their role.
8
+ *
9
+ * @param allowedRoles
10
+ * @param options
11
+ *
12
+ * Options : mode : "All" | "Exact" | "Any"
13
+ *
14
+ * - "All" : All roles in allowedRoles are required. They must exist in the user's roles
15
+ * even if user has more roles.
16
+ *
17
+ * - "Exact" : All roles in allowedRoles are required. They must exist in the user's roles
18
+ * exactly without any other roles.
19
+ *
20
+ * - "Any" : At least one role in allowedRoles is required. One role in the allowedRoles
21
+ * must exist in the user's roles regardless of other roles in user's roles array and allowed roles array.
22
+ */
23
+ export function checkRole(allowedRoles, options = { mode: "Any" }) {
24
+ return (req, res, next) => {
25
+ if (allowedRoles.length === 0) {
26
+ return next(new Error("Allowed roles cannot be empty in checkRole middleware !!"));
27
+ }
28
+ if (!checkUnique(allowedRoles)) {
29
+ return next(new Error("Allowed roles array should have unique values !!"));
30
+ }
4
31
  const { sessionClaims } = getAuth(req);
5
- const userMetaData = sessionClaims?.userMetadata;
6
- const role = userMetaData?.role;
7
- if (!roles.includes(role))
8
- return res.status(403).json({ message: "Unauthorized !!" });
32
+ const userMetadata = sessionClaims?.userMetadata;
33
+ const userRoles = userMetadata?.roles;
34
+ if (!userRoles || userRoles.length === 0) {
35
+ return res.status(403).json({
36
+ message: "User roles not found",
37
+ });
38
+ }
39
+ if (!checkUnique(userRoles)) {
40
+ return next(new Error("User roles from clerk array should have unique values !!"));
41
+ }
42
+ if (!checkValidRoles(userRoles)) {
43
+ return next(new Error("User roles from clerk contains invalid roles !!"));
44
+ }
45
+ let isAuthorized = false;
46
+ switch (options.mode) {
47
+ case "Any":
48
+ isAuthorized = allowedRoles.some((role) => userRoles.includes(role));
49
+ break;
50
+ case "Exact":
51
+ isAuthorized =
52
+ allowedRoles.every((role) => userRoles.includes(role)) &&
53
+ allowedRoles.length === userRoles.length;
54
+ break;
55
+ case "All":
56
+ isAuthorized = allowedRoles.every((role) => userRoles.includes(role));
57
+ break;
58
+ default:
59
+ break;
60
+ }
61
+ if (!isAuthorized) {
62
+ return res.status(403).json({
63
+ message: "Forbidden",
64
+ });
65
+ }
9
66
  next();
10
67
  };
11
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"checkRole.js","sourceRoot":"","sources":["../../src/middlewares/checkRole.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAIzC,MAAM,UAAU,SAAS,CAAC,KAAoC;IAC5D,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,YAAY,GAAG,aAAa,EAAE,YAGnC,CAAC;QACF,MAAM,IAAI,GAAG,YAAY,EAAE,IAAmC,CAAC;QAE/D,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YACvB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAE9D,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"checkRole.js","sourceRoot":"","sources":["../../src/middlewares/checkRole.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGzC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAI9D,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAU,CAAC;AAQ1D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS,CACvB,YAAoB,EACpB,UAA4B,EAAE,IAAI,EAAE,KAAK,EAAE;IAE3C,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,IAAI,CACT,IAAI,KAAK,CAAC,0DAA0D,CAAC,CACtE,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CACT,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAC9D,CAAC;QACJ,CAAC;QAED,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAEvC,MAAM,YAAY,GAAG,aAAa,EAAE,YAKvB,CAAC;QAEd,MAAM,SAAS,GAAG,YAAY,EAAE,KAAK,CAAC;QAEtC,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,sBAAsB;aAChC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,OAAO,IAAI,CACT,IAAI,KAAK,CAAC,0DAA0D,CAAC,CACtE,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC,CAAC;QAC5E,CAAC;QAED,IAAI,YAAY,GAAG,KAAK,CAAC;QAEzB,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,KAAK,KAAK;gBACR,YAAY,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;gBACrE,MAAM;YAER,KAAK,OAAO;gBACV,YAAY;oBACV,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;wBACtD,YAAY,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,CAAC;gBAC3C,MAAM;YAER,KAAK,KAAK;gBACR,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;gBACtE,MAAM;YAER;gBACE,MAAM;QACV,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,WAAW;aACrB,CAAC,CAAC;QACL,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -1,3 +1,9 @@
1
1
  import { NextFunction, Request, Response } from "express";
2
2
  import z from "zod";
3
- export declare function validationMiddleware(validaitonSchema: z.ZodType): (req: Request, res: Response, next: NextFunction) => void;
3
+ type validationSchemaType = {
4
+ body?: z.ZodType;
5
+ query?: z.ZodType;
6
+ params?: z.ZodType;
7
+ };
8
+ export declare function validationMiddleware(validaitonSchema: validationSchemaType): (req: Request, res: Response, next: NextFunction) => void;
9
+ export {};
@@ -1,26 +1,37 @@
1
- import z from "zod";
1
+ import { ValidationError, } from "../errors/validationError.js";
2
2
  export function validationMiddleware(validaitonSchema) {
3
3
  return (req, res, next) => {
4
- try {
5
- const result = validaitonSchema.safeParse(req.body);
4
+ const validationErrors = [];
5
+ for (const [location, schema] of Object.entries(validaitonSchema)) {
6
+ if (!schema)
7
+ continue;
8
+ const result = schema.safeParse(req[location]);
6
9
  if (!result.success) {
7
- const formattedErrors = result.error.issues.map((issue) => ({
10
+ validationErrors.push(...result.error.issues.map((issue) => ({
11
+ location: location,
8
12
  field: issue.path.join("."),
9
13
  message: issue.message,
10
- }));
11
- req.validationErrorArr = formattedErrors;
12
- return next(new Error(""));
14
+ })));
13
15
  }
14
16
  else {
15
- return next();
17
+ switch (location) {
18
+ case "body":
19
+ req.body = result.data;
20
+ break;
21
+ case "params":
22
+ req.params = result.data;
23
+ break;
24
+ case "query":
25
+ req.query = result.data;
26
+ break;
27
+ }
16
28
  }
17
29
  }
18
- catch (error) {
19
- if (error instanceof z.ZodError) {
20
- req.validationErrorArr = error.issues;
21
- return next(error.issues);
22
- }
23
- return next(error);
30
+ if (validationErrors.length > 0) {
31
+ return next(new ValidationError("Validation failed", validationErrors));
32
+ }
33
+ else {
34
+ return next();
24
35
  }
25
36
  };
26
37
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validationMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/validationMiddleware.ts"],"names":[],"mappings":"AACA,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,MAAM,UAAU,oBAAoB,CAAC,gBAA2B;IAC9D,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,gBAAgB,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBAC1D,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;oBAC3B,OAAO,EAAE,KAAK,CAAC,OAAO;iBACvB,CAAC,CAAC,CAAC;gBAEJ,GAAG,CAAC,kBAAkB,GAAG,eAAe,CAAC;gBAEzC,OAAO,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7B,CAAC;iBAAM,CAAC;gBACN,OAAO,IAAI,EAAE,CAAC;YAChB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAC/B,GAAW,CAAC,kBAAkB,GAAG,KAAK,CAAC,MAAM,CAAC;gBAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,CAAC;YACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"validationMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/validationMiddleware.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,eAAe,GAEhB,MAAM,8BAA8B,CAAC;AAQtC,MAAM,UAAU,oBAAoB,CAAC,gBAAsC;IACzE,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,MAAM,gBAAgB,GAA0B,EAAE,CAAC;QAEnD,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,MAAM;gBAAE,SAAS;YAEtB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAyB,CAAC,CAAC,CAAC;YAEhE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,gBAAgB,CAAC,IAAI,CACnB,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACrC,QAAQ,EAAE,QAAoB;oBAC9B,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;oBAC3B,OAAO,EAAE,KAAK,CAAC,OAAO;iBACvB,CAAC,CAAC,CACJ,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,QAAQ,QAAoB,EAAE,CAAC;oBAC7B,KAAK,MAAM;wBACT,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;wBACvB,MAAM;oBACR,KAAK,QAAQ;wBACX,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,IAAyB,CAAC;wBAC9C,MAAM;oBACR,KAAK,OAAO;wBACV,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,IAAwB,CAAC;wBAC5C,MAAM;gBACV,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,IAAI,eAAe,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -3,10 +3,6 @@ declare global {
3
3
  namespace Express {
4
4
  interface Request {
5
5
  user?: ClerkUser;
6
- validationErrorArr?: {
7
- field: string;
8
- message: string;
9
- }[];
10
6
  }
11
7
  }
12
8
  }
@@ -0,0 +1 @@
1
+ export type Location = "body" | "params" | "query";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=locationType.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locationType.js","sourceRoot":"","sources":["../../src/types/locationType.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export declare function checkUnique(arr: string[]): boolean;
@@ -0,0 +1,4 @@
1
+ export function checkUnique(arr) {
2
+ return arr.every((item, index) => arr.indexOf(item) === index);
3
+ }
4
+ //# sourceMappingURL=checkUnique.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkUnique.js","sourceRoot":"","sources":["../../src/utils/checkUnique.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,GAAa;IACvC,OAAO,GAAG,CAAC,KAAK,CACd,CAAC,IAAY,EAAE,KAAa,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,KAAK,CAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function checkValidRoles(arr: string[]): boolean;
@@ -0,0 +1,5 @@
1
+ import { validRoles } from "../constants/validRoles.js";
2
+ export function checkValidRoles(arr) {
3
+ return arr.every((item) => validRoles.includes(item));
4
+ }
5
+ //# sourceMappingURL=checkValidRoles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkValidRoles.js","sourceRoot":"","sources":["../../src/utils/checkValidRoles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE9D,MAAM,UAAU,eAAe,CAAC,GAAa;IAC3C,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAY,CAAC,CAAC,CAAC;AACxE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mustafahmed1997/learnify-backend",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Shared Clerk-based auth middlewares, role checking, error utilities, and Express types for Learnify backend microservices.",
5
5
  "keywords": [
6
6
  "express",