@mustafahmed1997/learnify-backend 1.3.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 +395 -28
- package/dist/errors/AppError.js +1 -1
- package/dist/errors/AppError.js.map +1 -1
- package/dist/errors/globalErrorHandler.js +27 -0
- package/dist/errors/globalErrorHandler.js.map +1 -1
- package/dist/errors/validationError.d.ts +11 -0
- package/dist/errors/validationError.js +8 -0
- package/dist/errors/validationError.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/middlewares/checkAuth.js.map +1 -1
- package/dist/middlewares/checkRole.d.ts +26 -1
- package/dist/middlewares/checkRole.js +63 -6
- package/dist/middlewares/checkRole.js.map +1 -1
- package/dist/middlewares/validationMiddleware.d.ts +7 -1
- package/dist/middlewares/validationMiddleware.js +25 -14
- package/dist/middlewares/validationMiddleware.js.map +1 -1
- package/dist/types/express.d.ts +0 -4
- package/dist/types/locationType.d.ts +1 -0
- package/dist/types/locationType.js +2 -0
- package/dist/types/locationType.js.map +1 -0
- package/dist/utils/checkUnique.d.ts +1 -0
- package/dist/utils/checkUnique.js +4 -0
- package/dist/utils/checkUnique.js.map +1 -0
- package/dist/utils/checkValidRoles.d.ts +1 -0
- package/dist/utils/checkValidRoles.js +5 -0
- package/dist/utils/checkValidRoles.js.map +1 -0
- package/package.json +1 -1
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,
|
|
5
|
-
augmentation for `req.user` —
|
|
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
|
|
16
|
-
`@clerk/express` installs (so there is a single shared instance of
|
|
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`
|
|
26
|
-
`clerkMiddleware()` from `@clerk/express` to have already run
|
|
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 {
|
|
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
|
-
|
|
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
|
|
57
|
-
(`sessionClaims.userMetadata.
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
## `
|
|
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 {
|
|
87
|
-
import
|
|
177
|
+
import { z } from "zod";
|
|
178
|
+
import { validationMiddleware } from "@mustafahmed1997/learnify-backend";
|
|
88
179
|
|
|
89
|
-
|
|
180
|
+
const createCourseSchema = z.object({
|
|
181
|
+
title: z.string().min(3),
|
|
182
|
+
price: z.number().positive(),
|
|
183
|
+
});
|
|
90
184
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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()`.
|
package/dist/errors/AppError.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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;
|
|
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 @@
|
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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
|
|
6
|
-
const
|
|
7
|
-
if (!
|
|
8
|
-
return res.status(403).json({
|
|
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;
|
|
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
|
-
|
|
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
|
|
1
|
+
import { ValidationError, } from "../errors/validationError.js";
|
|
2
2
|
export function validationMiddleware(validaitonSchema) {
|
|
3
3
|
return (req, res, next) => {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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":"
|
|
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"}
|
package/dist/types/express.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Location = "body" | "params" | "query";
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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