@mustafahmed1997/learnify-backend 1.0.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 ADDED
@@ -0,0 +1,167 @@
1
+ # @mustafahmed1997/learnify-backend
2
+
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.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @mustafahmed1997/learnify-backend
11
+ ```
12
+
13
+ ### Peer dependencies
14
+
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):
18
+
19
+ ```bash
20
+ npm install express @clerk/express
21
+ ```
22
+
23
+ ## Setup: `clerkMiddleware()` before `checkAuth`
24
+
25
+ `checkAuth` reads Clerk auth state via `getAuth(req)`, which requires
26
+ `clerkMiddleware()` from `@clerk/express` to have already run on the request:
27
+
28
+ ```ts
29
+ import express from "express";
30
+ import { clerkMiddleware } from "@clerk/express";
31
+ import { checkAuth, checkRole, globalErrorHandler } from "@mustafahmed1997/learnify-backend";
32
+
33
+ const app = express();
34
+
35
+ app.use(clerkMiddleware());
36
+ ```
37
+
38
+ ## `checkAuth`
39
+
40
+ Verifies the request is authenticated via Clerk and attaches a typed
41
+ `req.user` (a `ClerkUser`) for downstream handlers.
42
+
43
+ ```ts
44
+ import { checkAuth } from "@mustafahmed1997/learnify-backend";
45
+
46
+ app.get("/me", checkAuth, (req, res) => {
47
+ res.json(req.user);
48
+ });
49
+ ```
50
+
51
+ If the request is not authenticated, `checkAuth` responds `401` with
52
+ `{ error: "Unauthenticated. Login first please." }` and does not call `next()`.
53
+
54
+ ## `checkRole`
55
+
56
+ Authorizes a request based on the role in the Clerk session claims
57
+ (`sessionClaims.userMetadata.role`). Takes an array of allowed roles.
58
+
59
+ ```ts
60
+ import { checkRole } from "@mustafahmed1997/learnify-backend";
61
+
62
+ app.delete(
63
+ "/courses/:id",
64
+ checkRole(["ADMIN", "INSTRUCTOR"]),
65
+ deleteCourseController,
66
+ );
67
+ ```
68
+
69
+ If the caller's role is not in the allowed list, it responds `403` with
70
+ `{ message: "Unauthorized !!" }` and does not call `next()`.
71
+
72
+ ### Combined route example
73
+
74
+ ```ts
75
+ app.patch(
76
+ "/users/:id/role",
77
+ checkAuth,
78
+ checkRole(["ADMIN"]),
79
+ updateRoleController,
80
+ );
81
+ ```
82
+
83
+ ## `validRoles` and `Role`
84
+
85
+ ```ts
86
+ import { validRoles } from "@mustafahmed1997/learnify-backend";
87
+ import type { Role } from "@mustafahmed1997/learnify-backend";
88
+
89
+ console.log(validRoles); // ["STUDENT", "INSTRUCTOR", "ADMIN"]
90
+
91
+ function isValidRole(value: string): value is Role {
92
+ return (validRoles as readonly string[]).includes(value);
93
+ }
94
+ ```
95
+
96
+ ## `AppError`
97
+
98
+ A typed operational error carrying an HTTP status code.
99
+
100
+ ```ts
101
+ import { AppError } from "@mustafahmed1997/learnify-backend";
102
+
103
+ if (!course) {
104
+ throw new AppError("Course not found", 404);
105
+ }
106
+ ```
107
+
108
+ ## `globalErrorHandler`
109
+
110
+ Express error-handling middleware. Register it **last**, after all routes:
111
+
112
+ ```ts
113
+ import { globalErrorHandler } from "@mustafahmed1997/learnify-backend";
114
+
115
+ app.use(globalErrorHandler);
116
+ ```
117
+
118
+ - An `AppError` produces `{ error: err.message }` with `err.statusCode`.
119
+ - Any other thrown value produces `{ error: "Internal server error" }` with
120
+ status `500`.
121
+
122
+ ## `ClerkUser` and `req.user` typing
123
+
124
+ Importing anything from this package's root automatically loads the Express
125
+ `Request` augmentation, so `req.user` is typed as `ClerkUser | undefined`
126
+ wherever `express`'s `Request` type is used — no extra `express.d.ts` needed
127
+ in the consuming service.
128
+
129
+ ```ts
130
+ import type { Request, Response } from "express";
131
+ import type { ClerkUser } from "@mustafahmed1997/learnify-backend";
132
+
133
+ function exampleController(req: Request, res: Response) {
134
+ const clerkId = req.user?.clerkId;
135
+ const role = req.user?.userMetadata.role;
136
+
137
+ res.json({ clerkId, role });
138
+ }
139
+ ```
140
+
141
+ `ClerkUser` shape:
142
+
143
+ ```ts
144
+ type ClerkUser = {
145
+ clerkId: string;
146
+ userMetadata: {
147
+ role: "STUDENT" | "INSTRUCTOR" | "ADMIN";
148
+ dbUserId: string;
149
+ };
150
+ email?: string;
151
+ firstName?: string;
152
+ lastName?: string;
153
+ imageURL?: string;
154
+ };
155
+ ```
156
+
157
+ ## TypeScript usage
158
+
159
+ The package ships its own `.d.ts` declarations — no `@types` package needed.
160
+ It targets ESM (`"type": "module"`) and expects a `moduleResolution` setting
161
+ that supports package `exports` (e.g. `node16`, `nodenext`, or `bundler`).
162
+
163
+ ## What this package does not do
164
+
165
+ - Does not depend on Prisma or any database.
166
+ - Does not call the Auth Service (or any other service) over HTTP.
167
+ - Does not manage Clerk session creation — that's handled by `clerkMiddleware()`.
@@ -0,0 +1,2 @@
1
+ export declare const validRoles: readonly ["STUDENT", "INSTRUCTOR", "ADMIN"];
2
+ export type Role = (typeof validRoles)[number];
@@ -0,0 +1,2 @@
1
+ export const validRoles = ["STUDENT", "INSTRUCTOR", "ADMIN"];
2
+ //# sourceMappingURL=validRoles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validRoles.js","sourceRoot":"","sources":["../../src/constants/validRoles.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,CAAU,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare class AppError extends Error {
2
+ statusCode: number;
3
+ constructor(message: string, statusCode: number);
4
+ }
@@ -0,0 +1,8 @@
1
+ export class AppError extends Error {
2
+ constructor(message, statusCode) {
3
+ super(message);
4
+ this.statusCode = statusCode;
5
+ Object.setPrototypeOf(this, AppError.prototype);
6
+ }
7
+ }
8
+ //# sourceMappingURL=AppError.js.map
@@ -0,0 +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"}
@@ -0,0 +1,2 @@
1
+ import { NextFunction, Request, Response } from "express";
2
+ export declare function globalErrorHandler(err: unknown, req: Request, res: Response, next: NextFunction): Response<any, Record<string, any>>;
@@ -0,0 +1,13 @@
1
+ import { AppError } from "./AppError.js";
2
+ export function globalErrorHandler(err, req, res, next) {
3
+ console.error(err);
4
+ if (err instanceof AppError) {
5
+ return res.status(err.statusCode).json({
6
+ error: err.message,
7
+ });
8
+ }
9
+ return res.status(500).json({
10
+ error: "Internal server error",
11
+ });
12
+ }
13
+ //# sourceMappingURL=globalErrorHandler.js.map
@@ -0,0 +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"}
@@ -0,0 +1,8 @@
1
+ import "./types/express.js";
2
+ export { checkAuth } from "./middlewares/checkAuth.js";
3
+ export { checkRole } from "./middlewares/checkRole.js";
4
+ export { validRoles } from "./constants/validRoles.js";
5
+ export type { Role } from "./constants/validRoles.js";
6
+ export { AppError } from "./errors/AppError.js";
7
+ export { globalErrorHandler } from "./errors/globalErrorHandler.js";
8
+ export type { ClerkUser } from "./types/ClerkUser.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import "./types/express.js";
2
+ export { checkAuth } from "./middlewares/checkAuth.js";
3
+ export { checkRole } from "./middlewares/checkRole.js";
4
+ export { validRoles } from "./constants/validRoles.js";
5
+ export { AppError } from "./errors/AppError.js";
6
+ export { globalErrorHandler } from "./errors/globalErrorHandler.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAGvD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { NextFunction, Request, Response } from "express";
2
+ export declare function checkAuth(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
@@ -0,0 +1,32 @@
1
+ import { getAuth } from "@clerk/express";
2
+ export async function checkAuth(req, res, next) {
3
+ try {
4
+ const { isAuthenticated, userId, sessionClaims } = getAuth(req);
5
+ if (!isAuthenticated || !userId) {
6
+ return res.status(401).json({
7
+ error: "Unauthenticated. Login first please.",
8
+ });
9
+ }
10
+ const clerkId = userId;
11
+ const userMetadata = sessionClaims?.userMetadata;
12
+ const email = sessionClaims?.email;
13
+ const firstName = sessionClaims?.firstName;
14
+ const lastName = sessionClaims?.lastName;
15
+ const imageURL = sessionClaims?.imageURL;
16
+ const user = {
17
+ clerkId,
18
+ email,
19
+ firstName,
20
+ lastName,
21
+ imageURL,
22
+ userMetadata,
23
+ };
24
+ // Attach user to request
25
+ req.user = user;
26
+ next();
27
+ }
28
+ catch (error) {
29
+ next(error);
30
+ }
31
+ }
32
+ //# sourceMappingURL=checkAuth.js.map
@@ -0,0 +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"}
@@ -0,0 +1,3 @@
1
+ import { NextFunction, Request, Response } from "express";
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>;
@@ -0,0 +1,12 @@
1
+ import { getAuth } from "@clerk/express";
2
+ export function checkRole(roles) {
3
+ return async (req, res, next) => {
4
+ 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 !!" });
9
+ next();
10
+ };
11
+ }
12
+ //# sourceMappingURL=checkRole.js.map
@@ -0,0 +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"}
@@ -0,0 +1,12 @@
1
+ import { validRoles } from "../constants/validRoles.js";
2
+ export type ClerkUser = {
3
+ clerkId: string;
4
+ userMetadata: {
5
+ role: (typeof validRoles)[number];
6
+ dbUserId: string;
7
+ };
8
+ email?: string;
9
+ firstName?: string;
10
+ lastName?: string;
11
+ imageURL?: string;
12
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ClerkUser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClerkUser.js","sourceRoot":"","sources":["../../src/types/ClerkUser.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import type { ClerkUser } from "./ClerkUser.js";
2
+ declare global {
3
+ namespace Express {
4
+ interface Request {
5
+ user?: ClerkUser;
6
+ }
7
+ }
8
+ }
9
+ export {};
@@ -0,0 +1,8 @@
1
+ // types/express.ts
2
+ //
3
+ // Augments the global Express namespace so `req.user` is typed everywhere.
4
+ // This must be a real module (imported for its side effect from index.ts,
5
+ // not merely referenced) so the augmentation survives declaration emission
6
+ // and is automatically loaded by any consumer importing from the package root.
7
+ export {};
8
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/types/express.ts"],"names":[],"mappings":"AAAA,mBAAmB;AACnB,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,+EAA+E"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mustafahmed1997/learnify-backend",
3
+ "version": "1.0.0",
4
+ "description": "Shared Clerk-based auth middlewares, role checking, error utilities, and Express types for Learnify backend microservices.",
5
+ "keywords": [
6
+ "express",
7
+ "clerk",
8
+ "middleware",
9
+ "learnify"
10
+ ],
11
+ "license": "ISC",
12
+ "author": "Mostafa Ahmed",
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/mustafaahmed-f/learnify-backend-package.git"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.build.json",
34
+ "typecheck": "tsc -p tsconfig.json",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest"
37
+ },
38
+ "peerDependencies": {
39
+ "@clerk/express": "^1.7.74",
40
+ "express": "^5.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@clerk/express": "^1.7.74",
44
+ "@types/express": "^5.0.3",
45
+ "@types/node": "^20.19.39",
46
+ "express": "^5.1.0",
47
+ "typescript": "^5.1.6",
48
+ "vitest": "^3.0.0"
49
+ }
50
+ }