@mhiliger/auth-be 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,140 @@
1
+ # @your-org/auth-be
2
+
3
+ Shared authentication backend for Express.js applications. This package provides:
4
+ - Secure JWT (JSON Web Token) issuance and verification.
5
+ - User registration workflow (Sign up -> Verify Email -> Admin Approve -> Set Password).
6
+ - Role-based access control middleware.
7
+ - Database adapter pattern for user management.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @your-org/auth-be express jsonwebtoken express-rate-limit pg-promise
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ ### 1. Configure Database Adapter
18
+
19
+ Create a database adapter that implements the required methods. The package provides a standard `createPostgresAdapter` that works with a specific schema.
20
+
21
+ **Database Schema:**
22
+
23
+ The adapter expects tables: `users`, `roles`, `permissions`, `UserRoles`, `RolePerms`, `registration_tokens`.
24
+ See migration scripts in your project for exact schema.
25
+
26
+ **Initialize Adapter:**
27
+
28
+ ```javascript
29
+ // db/index.js
30
+ const pgp = require("pg-promise")();
31
+ const db = pgp({
32
+ host: process.env.DB_HOST,
33
+ port: process.env.DB_PORT,
34
+ database: "SysAccess",
35
+ user: process.env.DB_USER,
36
+ password: process.env.DB_PASSWORD
37
+ });
38
+
39
+ const { createPostgresAdapter } = require("@your-org/auth-be");
40
+ const authAdapter = createPostgresAdapter(db);
41
+
42
+ module.exports = authAdapter;
43
+ ```
44
+
45
+ ### 2. Configure Email Service
46
+
47
+ You need an email service to send verification links. Implement an object with these methods:
48
+
49
+ ```javascript
50
+ const emailService = {
51
+ sendVerificationEmail: async (user, token) => { ... },
52
+ sendAdminNotification: async (user, adminEmail) => { ... },
53
+ sendApprovalEmail: async (user, token) => { ... },
54
+ sendRejectionEmail: async (user, reason) => { ... },
55
+ sendPasswordResetEmail: async (user, token) => { ... }
56
+ };
57
+ ```
58
+
59
+ ### 3. Initialize Express App
60
+
61
+ Mount the auth and registration routers.
62
+
63
+ ```javascript
64
+ const express = require("express");
65
+ const cookieParser = require("cookie-parser");
66
+ const { createAuthRouter, createRegistrationRouter, createVerifyJWT } = require("@your-org/auth-be");
67
+ const authAdapter = require("./db"); // Your adapter
68
+ const emailService = require("./services/email"); // Your service
69
+
70
+ const app = express();
71
+ app.use(express.json());
72
+ app.use(cookieParser());
73
+
74
+ // Secrets Configuration
75
+ const authConfig = {
76
+ accessTokenSecret: process.env.ACCESS_TOKEN_SECRET,
77
+ refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET,
78
+ accessTokenLife: "15m",
79
+ refreshTokenLife: "1d",
80
+ loginPath: "/auth", // Endpoint for login
81
+ };
82
+
83
+ // 1. Mount Auth Routes (Login, Refresh, Logout)
84
+ app.use("/", createAuthRouter({
85
+ db: authAdapter,
86
+ config: authConfig
87
+ }));
88
+
89
+ // 2. Configure JWT Verification Middleware
90
+ const verifyJWT = createVerifyJWT({
91
+ accessTokenSecret: authConfig.accessTokenSecret,
92
+ onVerifySuccess: (req, decoded) => {
93
+ // Attach user info to request
94
+ req.user = decoded;
95
+ req.permissions = decoded.permissions;
96
+ }
97
+ });
98
+
99
+ // 3. Mount Registration Routes (Public + Admin)
100
+ app.use("/", createRegistrationRouter({
101
+ db: authAdapter,
102
+ verifyJWT: verifyJWT, // Protect admin routes
103
+ config: {
104
+ verificationTokenLife: "24h",
105
+ passwordSetupTokenLife: "48h",
106
+ // Callbacks for email notifications
107
+ onRegistrationSubmit: (user, token) => emailService.sendVerificationEmail(user, token),
108
+ onEmailVerified: (user) => emailService.sendAdminNotification(user, "admin@example.com"),
109
+ onApproval: (user, token) => emailService.sendApprovalEmail(user, token),
110
+ onRejection: (user, reason) => emailService.sendRejectionEmail(user, reason),
111
+ onPasswordReset: (user, token) => emailService.sendPasswordResetEmail(user, token),
112
+ }
113
+ }));
114
+
115
+ // 4. Protect Your API Routes
116
+ app.get("/api/protected-resource", verifyJWT, (req, res) => {
117
+ res.json({ message: "You are authorized!", user: req.user });
118
+ });
119
+
120
+ app.listen(3000, () => console.log("Server running"));
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### `createAuthRouter({ db, config })`
126
+ Creates routes:
127
+ - `POST /auth` (Login)
128
+ - `GET /refresh` (Refresh Token)
129
+ - `POST /logout` (Logout)
130
+
131
+ ### `createRegistrationRouter({ db, verifyJWT, config })`
132
+ Creates routes:
133
+ - `POST /register/submit`
134
+ - `GET /register/verify/:token`
135
+ - `GET /register/setup/:token`
136
+ - `POST /register/setup/:token`
137
+ - `POST /register/forgot-password`
138
+
139
+ ### `createVerifyJWT({ accessTokenSecret, onVerifySuccess })`
140
+ Middleware that verifies the Bearer token in the `Authorization` header.
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@mhiliger/auth-be",
3
+ "version": "1.0.0",
4
+ "description": "Shared authentication and JWT management for Express.js",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "dependencies": {
13
+ "jsonwebtoken": "^9.0.0",
14
+ "express-rate-limit": "^7.0.0"
15
+ },
16
+ "peerDependencies": {
17
+ "express": "^4.0.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=16.0.0"
21
+ }
22
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * A standard Postgres adapter for the authentication library.
3
+ * Assumes a schema with 'users', 'roles', 'permissions', 'UserRoles', and 'RolePerms' tables.
4
+ *
5
+ * @param {Object} db - A database connection object (e.g., pg-promise instance).
6
+ * @returns {Object} An auth adapter implementation.
7
+ */
8
+ const createPostgresAdapter = (db) => ({
9
+ /**
10
+ * Validates user credentials using PostgreSQL crypt function.
11
+ */
12
+ validateUser: async (email, password) => {
13
+ const userRecord = await db.manyOrNone(
14
+ "SELECT id, email, first, last, status FROM users WHERE email=$1 AND PASSWORD = crypt($2, PASSWORD)",
15
+ [email, password]
16
+ );
17
+ return userRecord.length === 1 ? userRecord[0] : null;
18
+ },
19
+
20
+ /**
21
+ * Retrieves user permissions IDs.
22
+ */
23
+ getUserPermissions: async (email) => {
24
+ const result = await db.manyOrNone(
25
+ `SELECT DISTINCT p.perm_key FROM users AS u
26
+ JOIN "UserRoles" AS ur ON u.id = ur.userid
27
+ JOIN roles AS r ON r.id = ur.roleid
28
+ JOIN "RolePerms" AS rp ON r.id = rp.roleid
29
+ JOIN permissions AS p ON p.id = rp.permid
30
+ WHERE u.email = $1`,
31
+ [email]
32
+ );
33
+ return result.map((perm) => ({ perm_key: perm.perm_key }));
34
+ },
35
+
36
+ /**
37
+ * Saves the refresh token for a user.
38
+ */
39
+ saveRefreshToken: async (userId, refreshtoken) => {
40
+ return await db.none("UPDATE users SET refreshtoken = $1 WHERE id = $2", [refreshtoken, userId]);
41
+ },
42
+
43
+ /**
44
+ * Finds a user by their refresh token.
45
+ */
46
+ findUserByRefreshToken: async (refreshToken) => {
47
+ const result = await db.manyOrNone(
48
+ "SELECT id, email, first, last, status FROM users WHERE refreshtoken = $1",
49
+ [refreshToken]
50
+ );
51
+ return result.length > 0 ? result[0] : null;
52
+ },
53
+
54
+ /**
55
+ * Clears the refresh token for a user.
56
+ */
57
+ clearRefreshToken: async (userId) => {
58
+ return await db.none("UPDATE users SET refreshtoken = '' WHERE id = $1", [userId]);
59
+ },
60
+
61
+ // ==================== Registration Workflow Methods ====================
62
+
63
+ /**
64
+ * Finds a user by their email address.
65
+ * @param {string} email - The email to search for.
66
+ * @returns {Object|null} The user record or null if not found.
67
+ */
68
+ findUserByEmail: async (email) => {
69
+ const result = await db.manyOrNone(
70
+ "SELECT id, email, first, last, status, request_note, admin_rejection_note FROM users WHERE email = $1",
71
+ [email]
72
+ );
73
+ return result.length > 0 ? result[0] : null;
74
+ },
75
+
76
+ /**
77
+ * Creates a new registration request (user with PENDING_VERIFICATION status).
78
+ * @param {string} email - User's email address.
79
+ * @param {string} first - User's first name.
80
+ * @param {string} last - User's last name.
81
+ * @param {string} requestNote - User's note explaining why they need access.
82
+ * @returns {Object} The created user record.
83
+ */
84
+ createRegistrationRequest: async (email, first, last, requestNote) => {
85
+ const result = await db.one(
86
+ `INSERT INTO users (email, first, last, status, request_note)
87
+ VALUES ($1, $2, $3, 'PENDING_VERIFICATION', $4)
88
+ RETURNING id, email, first, last, status, request_note`,
89
+ [email, first, last, requestNote || "Requesting login id to system"]
90
+ );
91
+ return result;
92
+ },
93
+
94
+ /**
95
+ * Updates a user's status.
96
+ * @param {number} userId - The user's ID.
97
+ * @param {string} status - The new status value.
98
+ */
99
+ updateUserStatus: async (userId, status) => {
100
+ return await db.none("UPDATE users SET status = $1 WHERE id = $2", [status, userId]);
101
+ },
102
+
103
+ /**
104
+ * Saves a registration token (email verification or password setup).
105
+ * @param {number} userId - The user's ID.
106
+ * @param {string} tokenHash - SHA-256 hash of the token.
107
+ * @param {string} tokenType - 'email_verification' or 'password_setup'.
108
+ * @param {Date} expiresAt - Token expiration timestamp.
109
+ * @returns {Object} The created token record.
110
+ */
111
+ saveRegistrationToken: async (userId, tokenHash, tokenType, expiresAt) => {
112
+ const result = await db.one(
113
+ `INSERT INTO registration_tokens (user_id, token_hash, token_type, expires_at)
114
+ VALUES ($1, $2, $3, $4)
115
+ RETURNING id, user_id, token_type, expires_at, created_at`,
116
+ [userId, tokenHash, tokenType, expiresAt]
117
+ );
118
+ return result;
119
+ },
120
+
121
+ /**
122
+ * Finds a valid (unexpired, unused) token by its hash.
123
+ * @param {string} tokenHash - SHA-256 hash of the token.
124
+ * @param {string} tokenType - 'email_verification' or 'password_setup'.
125
+ * @returns {Object|null} The token record with user info, or null if not found/invalid.
126
+ */
127
+ findValidToken: async (tokenHash, tokenType) => {
128
+ const result = await db.manyOrNone(
129
+ `SELECT t.id, t.user_id, t.token_type, t.expires_at, t.used_at,
130
+ u.email, u.first, u.last, u.status, u.request_note
131
+ FROM registration_tokens t
132
+ JOIN users u ON t.user_id = u.id
133
+ WHERE t.token_hash = $1
134
+ AND t.token_type = $2
135
+ AND t.expires_at > NOW()
136
+ AND t.used_at IS NULL`,
137
+ [tokenHash, tokenType]
138
+ );
139
+ return result.length > 0 ? result[0] : null;
140
+ },
141
+
142
+ /**
143
+ * Marks a token as used.
144
+ * @param {number} tokenId - The token's ID.
145
+ */
146
+ markTokenUsed: async (tokenId) => {
147
+ return await db.none("UPDATE registration_tokens SET used_at = NOW() WHERE id = $1", [tokenId]);
148
+ },
149
+
150
+ /**
151
+ * Invalidates all tokens of a specific type for a user.
152
+ * @param {number} userId - The user's ID.
153
+ * @param {string} tokenType - 'email_verification' or 'password_setup'.
154
+ */
155
+ invalidateUserTokens: async (userId, tokenType) => {
156
+ return await db.none(
157
+ "UPDATE registration_tokens SET used_at = NOW() WHERE user_id = $1 AND token_type = $2 AND used_at IS NULL",
158
+ [userId, tokenType]
159
+ );
160
+ },
161
+
162
+ /**
163
+ * Gets all users with PENDING_APPROVAL status.
164
+ * @returns {Array} List of pending registration requests.
165
+ */
166
+ getPendingRegistrations: async () => {
167
+ const result = await db.manyOrNone(
168
+ `SELECT id, email, first, last, status, request_note, admin_rejection_note
169
+ FROM users
170
+ WHERE status = 'PENDING_APPROVAL'
171
+ ORDER BY id DESC`
172
+ );
173
+ return result;
174
+ console.log("Pending registrations retrieved:");
175
+ },
176
+
177
+ /**
178
+ * Gets a single registration by user ID.
179
+ * @param {number} id - The user's ID.
180
+ * @returns {Object|null} The user record or null if not found.
181
+ */
182
+ getRegistrationById: async (id) => {
183
+ const result = await db.manyOrNone(
184
+ `SELECT id, email, first, last, status, request_note, admin_rejection_note
185
+ FROM users
186
+ WHERE id = $1`,
187
+ [id]
188
+ );
189
+ return result.length > 0 ? result[0] : null;
190
+ },
191
+
192
+ /**
193
+ * Sets the admin rejection note for a user.
194
+ * @param {number} userId - The user's ID.
195
+ * @param {string} note - The rejection reason.
196
+ */
197
+ setAdminRejectionNote: async (userId, note) => {
198
+ return await db.none("UPDATE users SET admin_rejection_note = $1 WHERE id = $2", [note, userId]);
199
+ },
200
+
201
+ /**
202
+ * Sets the user's password using PostgreSQL crypt function.
203
+ * @param {number} userId - The user's ID.
204
+ * @param {string} password - The plain text password (will be hashed).
205
+ */
206
+ setUserPassword: async (userId, password) => {
207
+ return await db.none(
208
+ "UPDATE users SET password = crypt($1, gen_salt('bf')) WHERE id = $2",
209
+ [password, userId]
210
+ );
211
+ }
212
+ });
213
+
214
+ module.exports = createPostgresAdapter;
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ const createAuthRouter = require("./routes/authFactory");
2
+ const createRegistrationRouter = require("./routes/registrationFactory");
3
+ const createVerifyJWT = require("./middleware/verifyJWT");
4
+ const createPostgresAdapter = require("./adapters/PostgresAdapter");
5
+ const { createRegistrationLimiter, createVerificationLimiter } = require("./middleware/rateLimiter");
6
+ const { generateToken, hashToken } = require("./utils/tokenGenerator");
7
+
8
+ module.exports = {
9
+ createAuthRouter,
10
+ createRegistrationRouter,
11
+ createVerifyJWT,
12
+ createPostgresAdapter,
13
+ createRegistrationLimiter,
14
+ createVerificationLimiter,
15
+ generateToken,
16
+ hashToken,
17
+ };
@@ -0,0 +1,40 @@
1
+ const rateLimit = require("express-rate-limit");
2
+
3
+ /**
4
+ * Creates a rate limiter for registration endpoints.
5
+ * @param {Object} config - Configuration options.
6
+ * @param {number} [config.windowMs=3600000] - Time window in milliseconds (default: 1 hour).
7
+ * @param {number} [config.max=5] - Maximum requests per window (default: 5).
8
+ * @returns {Function} Express rate limiting middleware.
9
+ */
10
+ const createRegistrationLimiter = (config = {}) => {
11
+ return rateLimit({
12
+ windowMs: config.windowMs || 60 * 60 * 1000, // 1 hour default
13
+ max: config.max || 5,
14
+ message: { error: "Too many requests, please try again later" },
15
+ standardHeaders: true,
16
+ legacyHeaders: false,
17
+ });
18
+ };
19
+
20
+ /**
21
+ * Creates a rate limiter for verification endpoints (more lenient).
22
+ * @param {Object} config - Configuration options.
23
+ * @param {number} [config.windowMs=3600000] - Time window in milliseconds (default: 1 hour).
24
+ * @param {number} [config.max=10] - Maximum requests per window (default: 10).
25
+ * @returns {Function} Express rate limiting middleware.
26
+ */
27
+ const createVerificationLimiter = (config = {}) => {
28
+ return rateLimit({
29
+ windowMs: config.windowMs || 60 * 60 * 1000, // 1 hour default
30
+ max: config.max || 10,
31
+ message: { error: "Too many requests, please try again later" },
32
+ standardHeaders: true,
33
+ legacyHeaders: false,
34
+ });
35
+ };
36
+
37
+ module.exports = {
38
+ createRegistrationLimiter,
39
+ createVerificationLimiter,
40
+ };
@@ -0,0 +1,50 @@
1
+ const jwt = require("jsonwebtoken");
2
+
3
+ /**
4
+ * Factory to create verifyJWT middleware with custom configuration.
5
+ * @param {Object} config - Configuration object
6
+ * @param {string} config.accessTokenSecret - The secret used to verify the access token.
7
+ * @param {Function} [config.onVerifySuccess] - Optional callback when verification succeeds, receives (req, decoded).
8
+ * @param {Function} [config.onVerifyError] - Optional callback when verification fails, receives (res, error).
9
+ * @returns {Function} Express middleware
10
+ */
11
+ const createVerifyJWT = (config) => {
12
+ const { accessTokenSecret, onVerifySuccess, onVerifyError } = config;
13
+
14
+ if (!accessTokenSecret) {
15
+ throw new Error("accessTokenSecret is required for verifyJWT middleware");
16
+ }
17
+
18
+ return (req, res, next) => {
19
+ const authHeader = req?.headers["authorization"] || req?.headers["Authorization"];
20
+
21
+ if (!authHeader?.startsWith("Bearer ")) {
22
+ if (onVerifyError) return onVerifyError(res, new Error("No Bearer token provided"));
23
+ return res.status(401).json({ error: "no header to verify jwt" });
24
+ }
25
+
26
+ const token = authHeader.split(" ")[1];
27
+
28
+ try {
29
+ const decoded = jwt.verify(token, accessTokenSecret);
30
+
31
+ // Default behavior: attach common fields to req
32
+ req.user = decoded;
33
+ // Also attach specific fields for backward compatibility if needed or as configured
34
+ if (onVerifySuccess) {
35
+ onVerifySuccess(req, decoded);
36
+ } else {
37
+ req.email = decoded.email;
38
+ req.permissions = decoded.permissions;
39
+ req.userId = decoded.userId;
40
+ }
41
+
42
+ next();
43
+ } catch (error) {
44
+ if (onVerifyError) return onVerifyError(res, error);
45
+ return res.status(403).json({ error: "error verifying jwt" });
46
+ }
47
+ };
48
+ };
49
+
50
+ module.exports = createVerifyJWT;
@@ -0,0 +1,140 @@
1
+ const express = require("express");
2
+ const jwt = require("jsonwebtoken");
3
+
4
+ /**
5
+ * Creates an Express router with auth endpoints.
6
+ * @param {Object} options - Configuration options
7
+ * @param {Object} options.db - Database interface object
8
+ * @param {Object} options.queries - Object containing SQL queries or functions
9
+ * @param {Object} options.config - Config for secrets and tokens
10
+ * @returns {express.Router}
11
+ */
12
+ const createAuthRouter = ({ db, queries, config }) => {
13
+ const router = express.Router();
14
+
15
+ const {
16
+ accessTokenSecret,
17
+ refreshTokenSecret,
18
+ accessTokenLife,
19
+ refreshTokenLife,
20
+ cookieName = "jwt",
21
+ cookieOptions = {
22
+ httpOnly: true,
23
+ sameSite: "None",
24
+ secure: true,
25
+ maxAge: 24 * 60 * 60 * 1000,
26
+ },
27
+ loginPath = "/login",
28
+ } = config;
29
+
30
+ // Login Route
31
+ router.post(loginPath, async (req, res) => {
32
+ const { email, password } = req.body;
33
+
34
+ if (!email || !password) {
35
+ return res.status(400).json({ error: "missing user or password" });
36
+ }
37
+
38
+ try {
39
+ // 1. Validate User Credentials
40
+ // This should return user details if valid, or null/throw if not
41
+ const user = await db.validateUser(email, password);
42
+ if (!user) {
43
+ return res.status(401).json({ error: "Invalid credentials." });
44
+ }
45
+
46
+ // 2. Get Permissions
47
+ const permissions = await db.getUserPermissions(email);
48
+ if (!permissions || permissions.length === 0) {
49
+ return res.status(403).json({ error: "No permissions for user" });
50
+ }
51
+
52
+ // 3. Create Tokens
53
+ const payload = {
54
+ userId: user.id,
55
+ email: user.email,
56
+ first: user.first,
57
+ last: user.last,
58
+ status: user.status,
59
+ permissions: [...new Set(permissions)],
60
+ };
61
+
62
+ const accessToken = jwt.sign(payload, accessTokenSecret, { expiresIn: accessTokenLife });
63
+ const refreshToken = jwt.sign(payload, refreshTokenSecret, { expiresIn: refreshTokenLife });
64
+
65
+ // 4. Save Refresh Token to DB
66
+ await db.saveRefreshToken(user.id, refreshToken);
67
+
68
+ // 5. Respond
69
+ res.cookie(cookieName, refreshToken, cookieOptions);
70
+ res.status(200).json({ accessToken });
71
+ } catch (error) {
72
+ console.error("Login error:", error);
73
+ res.status(500).json({ error: "Internal server error during login" });
74
+ }
75
+ });
76
+
77
+ // Refresh Token Route
78
+ router.get("/refresh", async (req, res) => {
79
+ const cookies = req.cookies;
80
+ if (!cookies?.[cookieName]) return res.status(403).json({ error: "Missing refresh token" });
81
+
82
+ const refreshToken = cookies[cookieName];
83
+
84
+ try {
85
+ // 1. Find user by refresh token
86
+ const user = await db.findUserByRefreshToken(refreshToken);
87
+ if (!user) return res.status(403).json({ error: "Invalid refresh token" });
88
+
89
+ // 2. Verify token
90
+ const decoded = jwt.verify(refreshToken, refreshTokenSecret);
91
+ if (user.email !== decoded.email) {
92
+ return res.status(403).json({ error: "Token mismatch" });
93
+ }
94
+
95
+ // 3. Refresh Permissions
96
+ const permissions = await db.getUserPermissions(user.email);
97
+
98
+ // 4. Issue new Access Token
99
+ const payload = {
100
+ userId: user.id,
101
+ email: user.email,
102
+ first: user.first,
103
+ last: user.last,
104
+ status: user.status,
105
+ permissions: [...new Set(permissions)],
106
+ };
107
+
108
+ const accessToken = jwt.sign(payload, accessTokenSecret, { expiresIn: accessTokenLife });
109
+ res.json({ accessToken });
110
+ } catch (error) {
111
+ console.error("Refresh error:", error);
112
+ res.status(403).json({ error: "Refresh failed" });
113
+ }
114
+ });
115
+
116
+ // Logout Route
117
+ router.post("/logout", async (req, res) => {
118
+ const cookies = req.cookies;
119
+ if (!cookies?.[cookieName]) return res.sendStatus(204);
120
+
121
+ const refreshToken = cookies[cookieName];
122
+
123
+ try {
124
+ const user = await db.findUserByRefreshToken(refreshToken);
125
+ if (user) {
126
+ await db.clearRefreshToken(user.id);
127
+ }
128
+ } catch (error) {
129
+ // Log error but proceed to clear cookie
130
+ console.error("Logout DB error:", error);
131
+ }
132
+
133
+ res.clearCookie(cookieName, cookieOptions);
134
+ res.sendStatus(204);
135
+ });
136
+
137
+ return router;
138
+ };
139
+
140
+ module.exports = createAuthRouter;
@@ -0,0 +1,340 @@
1
+ const express = require("express");
2
+ const { generateToken, hashToken } = require("../utils/tokenGenerator");
3
+ const { createRegistrationLimiter, createVerificationLimiter } = require("../middleware/rateLimiter");
4
+
5
+ /**
6
+ * Parses a duration string (e.g., '24h', '48h') into milliseconds.
7
+ * @param {string} duration - Duration string like '24h', '1d', '30m'.
8
+ * @returns {number} Milliseconds.
9
+ */
10
+ const parseDuration = (duration) => {
11
+ const match = duration.match(/^(\d+)([hdm])$/);
12
+ if (!match) return 24 * 60 * 60 * 1000; // default 24h
13
+
14
+ const value = parseInt(match[1], 10);
15
+ const unit = match[2];
16
+
17
+ switch (unit) {
18
+ case "h":
19
+ return value * 60 * 60 * 1000;
20
+ case "d":
21
+ return value * 24 * 60 * 60 * 1000;
22
+ case "m":
23
+ return value * 60 * 1000;
24
+ default:
25
+ return 24 * 60 * 60 * 1000;
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Creates registration workflow routes.
31
+ * @param {Object} options
32
+ * @param {Object} options.db - Database adapter with registration methods.
33
+ * @param {Object} options.config - Configuration options.
34
+ * @param {string} [options.config.verificationTokenLife='24h'] - Verification token lifetime.
35
+ * @param {string} [options.config.passwordSetupTokenLife='48h'] - Password setup token lifetime.
36
+ * @param {Array<number>} [options.config.adminPermissionIds=["AllowUsers"]] - Permission IDs required for admin operations.
37
+ * @param {number} [options.config.rateLimitWindowMs] - Rate limit window in ms.
38
+ * @param {number} [options.config.rateLimitMax] - Rate limit max requests.
39
+ * @param {Function} [options.config.onRegistrationSubmit] - Callback when registration is submitted (user, token).
40
+ * @param {Function} [options.config.onEmailVerified] - Callback when email is verified (user).
41
+ * @param {Function} [options.config.onApproval] - Callback when admin approves (user, token).
42
+ * @param {Function} [options.config.onRejection] - Callback when admin rejects (user, reason).
43
+ * @param {Function} [options.verifyJWT] - JWT verification middleware for admin routes.
44
+ * @returns {express.Router}
45
+ */
46
+ const createRegistrationRouter = ({ db, config = {}, verifyJWT }) => {
47
+ const router = express.Router();
48
+
49
+ const {
50
+ verificationTokenLife = "24h",
51
+ passwordSetupTokenLife = "48h",
52
+ rateLimitWindowMs,
53
+ rateLimitMax,
54
+ onRegistrationSubmit,
55
+ onEmailVerified,
56
+ onApproval,
57
+ onRejection,
58
+ onPasswordReset,
59
+ } = config;
60
+
61
+ // Rate limiters
62
+ const submitLimiter = createRegistrationLimiter({
63
+ windowMs: rateLimitWindowMs,
64
+ max: rateLimitMax,
65
+ });
66
+ const verifyLimiter = createVerificationLimiter({
67
+ windowMs: rateLimitWindowMs,
68
+ max: rateLimitMax ? rateLimitMax * 2 : undefined,
69
+ });
70
+
71
+ // ==================== Public Routes ====================
72
+
73
+ /**
74
+ * Phase 1: Submit Registration
75
+ * POST /register/submit
76
+ */
77
+ router.post("/register/submit", submitLimiter, async (req, res) => {
78
+ try {
79
+ const { email, first, last, requestNote } = req.body;
80
+
81
+ // Validate required fields
82
+ if (!email || !first || !last) {
83
+ return res.status(400).json({ error: "Email, first name, and last name are required" });
84
+ }
85
+
86
+ // Basic email validation
87
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
88
+ if (!emailRegex.test(email)) {
89
+ return res.status(400).json({ error: "Invalid email format" });
90
+ }
91
+
92
+ // Validate request note length
93
+ if (requestNote && requestNote.length > 256) {
94
+ return res.status(400).json({ error: "Request note must be 256 characters or less" });
95
+ }
96
+
97
+ // Check if email already exists
98
+ const existingUser = await db.findUserByEmail(email);
99
+ if (existingUser) {
100
+ // Return generic success to prevent user enumeration
101
+ return res.json({
102
+ success: true,
103
+ message: "If this email is not already registered, you will receive a verification email shortly.",
104
+ });
105
+ }
106
+
107
+ // Create the registration request
108
+ const user = await db.createRegistrationRequest(email, first, last, requestNote);
109
+
110
+ // Generate verification token
111
+ const { token, tokenHash } = generateToken();
112
+ const expiresAt = new Date(Date.now() + parseDuration(verificationTokenLife));
113
+
114
+ await db.saveRegistrationToken(user.id, tokenHash, "email_verification", expiresAt);
115
+
116
+ // Send verification email via callback
117
+ if (onRegistrationSubmit) {
118
+ try {
119
+ await onRegistrationSubmit(user, token);
120
+ } catch (emailError) {
121
+ console.error("Failed to send verification email:", emailError);
122
+ // Don't fail the registration if email fails
123
+ }
124
+ }
125
+
126
+ res.json({
127
+ success: true,
128
+ message: "If this email is not already registered, you will receive a verification email shortly.",
129
+ });
130
+ } catch (error) {
131
+ console.error("Registration submit error:", error);
132
+ res.status(500).json({ error: "An error occurred during registration" });
133
+ }
134
+ });
135
+
136
+ /**
137
+ * Phase 2: Verify Email
138
+ * GET /register/verify/:token
139
+ */
140
+ router.get("/register/verify/:token", verifyLimiter, async (req, res) => {
141
+ try {
142
+ const { token } = req.params;
143
+
144
+ if (!token) {
145
+ return res.status(400).json({ error: "Token is required" });
146
+ }
147
+
148
+ const tokenHash = hashToken(token);
149
+ const tokenRecord = await db.findValidToken(tokenHash, "email_verification");
150
+
151
+ if (!tokenRecord) {
152
+ return res.status(400).json({ error: "Invalid or expired verification link" });
153
+ }
154
+
155
+ // Check if user is in correct state
156
+ if (tokenRecord.status !== "PENDING_VERIFICATION") {
157
+ return res.status(400).json({ error: "Email has already been verified" });
158
+ }
159
+
160
+ // Mark token as used
161
+ await db.markTokenUsed(tokenRecord.id);
162
+
163
+ // Update user status to PENDING_APPROVAL
164
+ await db.updateUserStatus(tokenRecord.user_id, "PENDING_APPROVAL");
165
+
166
+ // Notify admin via callback
167
+ if (onEmailVerified) {
168
+ try {
169
+ const user = {
170
+ id: tokenRecord.user_id,
171
+ email: tokenRecord.email,
172
+ first: tokenRecord.first,
173
+ last: tokenRecord.last,
174
+ request_note: tokenRecord.request_note,
175
+ };
176
+ await onEmailVerified(user);
177
+ } catch (notifyError) {
178
+ console.error("Failed to notify admin:", notifyError);
179
+ // Don't fail the verification if notification fails
180
+ }
181
+ }
182
+
183
+ res.json({
184
+ success: true,
185
+ message: "Email verified successfully. Your request is now pending administrator review.",
186
+ });
187
+ } catch (error) {
188
+ console.error("Email verification error:", error);
189
+ res.status(500).json({ error: "An error occurred during verification" });
190
+ }
191
+ });
192
+
193
+ /**
194
+ * Phase 4: Validate Password Setup Token
195
+ * GET /register/setup/:token
196
+ */
197
+ router.get("/register/setup/:token", verifyLimiter, async (req, res) => {
198
+ try {
199
+ const { token } = req.params;
200
+
201
+ if (!token) {
202
+ return res.status(400).json({ error: "Token is required" });
203
+ }
204
+
205
+ const tokenHash = hashToken(token);
206
+ const tokenRecord = await db.findValidToken(tokenHash, "password_setup");
207
+
208
+ if (!tokenRecord) {
209
+ return res.status(400).json({ error: "Invalid or expired setup link" });
210
+ }
211
+
212
+ // Check if user is in correct state
213
+ if (tokenRecord.status !== "APPROVED" && tokenRecord.status !== "ACTIVE") {
214
+ return res.status(400).json({ error: "Account is not in the correct state for password setup" });
215
+ }
216
+
217
+ res.json({
218
+ success: true,
219
+ valid: true,
220
+ email: tokenRecord.email,
221
+ first: tokenRecord.first,
222
+ });
223
+ } catch (error) {
224
+ console.error("Setup token validation error:", error);
225
+ res.status(500).json({ error: "An error occurred during validation" });
226
+ }
227
+ });
228
+
229
+ /**
230
+ * Phase 4: Set Password
231
+ * POST /register/setup/:token
232
+ */
233
+ router.post("/register/setup/:token", submitLimiter, async (req, res) => {
234
+ try {
235
+ const { token } = req.params;
236
+ const { password } = req.body;
237
+
238
+ if (!token) {
239
+ return res.status(400).json({ error: "Token is required" });
240
+ }
241
+
242
+ if (!password) {
243
+ return res.status(400).json({ error: "Password is required" });
244
+ }
245
+
246
+ // Validate password format (same as PWD_REGEX in frontend)
247
+ const pwdRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%]).{8,24}$/;
248
+ if (!pwdRegex.test(password)) {
249
+ return res.status(400).json({
250
+ error: "Password must be 8-24 characters with uppercase, lowercase, number, and special character (!@#$%)",
251
+ });
252
+ }
253
+
254
+ const tokenHash = hashToken(token);
255
+ const tokenRecord = await db.findValidToken(tokenHash, "password_setup");
256
+
257
+ if (!tokenRecord) {
258
+ return res.status(400).json({ error: "Invalid or expired setup link" });
259
+ }
260
+
261
+ // Check if user is in correct state
262
+ if (tokenRecord.status !== "APPROVED" && tokenRecord.status !== "ACTIVE") {
263
+ return res.status(400).json({ error: "Account is not in the correct state for password setup" });
264
+ }
265
+
266
+ // Mark token as used
267
+ await db.markTokenUsed(tokenRecord.id);
268
+
269
+ // Set password and update status to ACTIVE
270
+ await db.setUserPassword(tokenRecord.user_id, password);
271
+ await db.updateUserStatus(tokenRecord.user_id, "ACTIVE");
272
+
273
+ res.json({
274
+ success: true,
275
+ message: "Password set successfully. You can now log in.",
276
+ });
277
+ } catch (error) {
278
+ console.error("Password setup error:", error);
279
+ res.status(500).json({ error: "An error occurred during password setup" });
280
+ }
281
+ });
282
+
283
+ /**
284
+ * Phase 0: Request Password Reset (Public)
285
+ * POST /register/forgot-password
286
+ */
287
+ router.post("/register/forgot-password", submitLimiter, async (req, res) => {
288
+ try {
289
+ const { email } = req.body;
290
+
291
+ if (!email) {
292
+ return res.status(400).json({ error: "Email is required" });
293
+ }
294
+
295
+ const user = await db.findUserByEmail(email);
296
+
297
+ // We always return a success message to prevent user enumeration
298
+ const successResponse = {
299
+ success: true,
300
+ message: "If an account exists for this email, you will receive a password reset link shortly.",
301
+ };
302
+
303
+ // Only proceed if user exists and is ACTIVE or APPROVED (already has/had access)
304
+ if (!user || (user.status !== "ACTIVE" && user.status !== "APPROVED")) {
305
+ return res.json(successResponse);
306
+ }
307
+
308
+ // Invalidate any existing password setup tokens
309
+ await db.invalidateUserTokens(user.id, "password_setup");
310
+
311
+ // Generate password setup token
312
+ const { token, tokenHash } = generateToken();
313
+ const expiresAt = new Date(Date.now() + parseDuration(passwordSetupTokenLife));
314
+
315
+ await db.saveRegistrationToken(user.id, tokenHash, "password_setup", expiresAt);
316
+
317
+ // Send password reset email via callback
318
+ if (onPasswordReset) {
319
+ try {
320
+ await onPasswordReset(user, token);
321
+ } catch (emailError) {
322
+ console.error("Failed to send password reset email:", emailError);
323
+ // Don't fail the request if email fails
324
+ }
325
+ }
326
+
327
+ res.json(successResponse);
328
+ } catch (error) {
329
+ console.error("Forgot password error:", error);
330
+ res.status(500).json({ error: "An error occurred" });
331
+ }
332
+ });
333
+
334
+ // ==================== Admin Routes ====================
335
+ // Admin routes have been moved to the main application (Login-BE/routes/adminRegistrations.js)
336
+
337
+ return router;
338
+ };
339
+
340
+ module.exports = createRegistrationRouter;
@@ -0,0 +1,26 @@
1
+ const crypto = require("crypto");
2
+
3
+ /**
4
+ * Generates a secure random token and its hash.
5
+ * The plain token is sent to the user, while only the hash is stored in the database.
6
+ * @returns {{ token: string, tokenHash: string }}
7
+ */
8
+ const generateToken = () => {
9
+ const token = crypto.randomBytes(32).toString("base64url");
10
+ const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
11
+ return { token, tokenHash };
12
+ };
13
+
14
+ /**
15
+ * Hashes a token for comparison with stored hash.
16
+ * @param {string} token - The plain token to hash.
17
+ * @returns {string} The SHA-256 hash of the token.
18
+ */
19
+ const hashToken = (token) => {
20
+ return crypto.createHash("sha256").update(token).digest("hex");
21
+ };
22
+
23
+ module.exports = {
24
+ generateToken,
25
+ hashToken,
26
+ };