mbkauthe 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,121 @@
1
- # mbkauthe
1
+ # mbkauthe
2
+
3
+ [![Publish to npm](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/publish.yml) [![CodeQL Advanced](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/codeql.yml)
4
+
5
+ ## Table of Contents
6
+
7
+ - [Introduction](#mbkauth)
8
+ - [Features](#features)
9
+ - [Installation](#installation)
10
+ - [Usage](#usage)
11
+ - [Basic Setup](#basic-setup)
12
+ - [API Endpoints](#api-endpoints)
13
+ - [Login](#login)
14
+ - [Logout](#logout)
15
+ - [Terminate All Sessions](#terminate-all-sessions)
16
+ - [Database Structure](#database-structure)
17
+ - [License](#license)
18
+ - [Contact & Support](#contact--support)
19
+
20
+ `mbkAuthe` is a reusable authentication system for Node.js applications, designed to simplify session management, user authentication, and role-based access control. It integrates seamlessly with PostgreSQL and supports features like Two-Factor Authentication (2FA), session restoration, and reCAPTCHA verification.
21
+
22
+ ## Features
23
+
24
+ - **Session Management**: Secure session handling using `express-session` and `connect-pg-simple`.
25
+ - **Role-Based Access Control**: Validate user roles and permissions with ease.
26
+ - **Two-Factor Authentication (2FA)**: Optional 2FA support for enhanced security.
27
+ - **reCAPTCHA Integration**: Protect login endpoints with Google reCAPTCHA.
28
+ - **Cookie Management**: Configurable cookie expiration and domain settings.
29
+ - **PostgreSQL Integration**: Uses a connection pool for efficient database interactions.
30
+
31
+ ## Installation
32
+
33
+ Install the package via npm:
34
+
35
+ ```bash
36
+ npm install mbkauthe
37
+ ```
38
+
39
+ ## Usage
40
+ ### Basic Setup
41
+ 1. Import and configure the router in your Express application:
42
+ ```javascript
43
+ import express from "express";
44
+ import mbkAuthRouter from "mbkauthe";
45
+
46
+ const app = express();
47
+
48
+ app.use(mbkAuthRouter);
49
+
50
+ app.listen(3000, () => {
51
+ console.log("Server is running on port 3000");
52
+ });
53
+ ```
54
+ 2. Ensure your ``.env` file is properly configured. Refer to the [Configuration Guide(env.md)](env.md) for details.
55
+
56
+ Example `.env` file:
57
+ ```code
58
+ RECAPTCHA_SECRET_KEY=your-recaptcha-secret-key
59
+ SESSION_SECRET_KEY=your-session-secret-key
60
+ LOGIN_DB=postgres://username:password@host:port/database
61
+ DOMAIN=yourdomain.com
62
+ IS_DEPLOYED=true
63
+ MBKAUTH_TWO_FA_ENABLE=false
64
+ COOKIE_EXPIRE_TIME=2
65
+ ```
66
+
67
+ ## API Endpoints
68
+
69
+ ### Login
70
+
71
+ **POST** `/mbkauth/api/login`
72
+ - Request Body:
73
+ - `username`: User's username.
74
+ - `password`: User's password.
75
+ - `token`: (Optional) 2FA token.
76
+ - `recaptcha`: reCAPTCHA response.
77
+
78
+ - Response:
79
+ - `200`: Login successful.
80
+ - `400`: Missing or invalid input.
81
+ - `401`: Unauthorized (e.g., invalid credentials or 2FA token).
82
+ - `500`: Internal server error.
83
+
84
+ ### Logout
85
+
86
+ **POST** `/mbkauth/api/logout`
87
+ - Response:
88
+ - `200`: Login successful.
89
+ - `400`: User not logged in.
90
+ - `500`: Internal server error.
91
+
92
+ ### Terminate All Sessions
93
+
94
+ **POST** `/mbkauth/api/terminateAllSessions`
95
+ - Authentication: Requires a valid `Main_SECRET_TOKEN` in the `Authorization` header.
96
+ - Response:
97
+ - `200`: All sessions terminated successfully.
98
+ - `500`: Internal server error.
99
+ -
100
+
101
+
102
+ ## Database Structure
103
+
104
+ This project utilizes three primary tables:
105
+
106
+ 1. **User**: Stores the main user information.
107
+ 2. **sess**: Contains session-related data for users.
108
+ 3. **TwoFA**: Saves the Two-Factor Authentication (2FA) secrets for users.
109
+
110
+ For detailed information about table columns, schema, and queries to create these tables, refer to the [Database Guide (docs/db.md)](docs/db.md).
111
+
112
+ ## License
113
+ This project is licensed under the `Mozilla Public License 2.0`. See the [LICENSE](./LICENSE) file for details.
114
+
115
+
116
+
117
+ ## Contact & Support
118
+
119
+ For questions or contributions, please contact Muhammad Bin Khalid at [mbktechstudio.com/Support](https://mbktechstudio.com/Support/), [support@mbktechstudio.com](mailto:support@mbktechstudio.com) or [chmuhammadbinkhalid28.com](mailto:chmuhammadbinkhalid28.com).
120
+
121
+ **Developed by [Muhammad Bin Khalid](https://github.com/MIbnEKhalid)**
package/docs/db.md ADDED
@@ -0,0 +1,90 @@
1
+ ## Database structure
2
+
3
+ [<- Back](README.md)
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Users Table](#users-table)
8
+ 2. [Session Table](#session-table)
9
+ 3. [Two-Factor Authentication Table](#two-factor-authentication-table)
10
+ 4. [Query to Add a User](#query-to-add-a-user)
11
+
12
+
13
+ ### Users Table
14
+
15
+ - **Columns:**
16
+
17
+ - `id` (INTEGER, auto-increment, primary key): Unique identifier for each user.
18
+ - `UserName` (TEXT): The username of the user.
19
+ - `Password` (TEXT): The hashed password of the user.
20
+ - `Role` (ENUM): The role of the user. Possible values: `SuperAdmin`, `NormalUser`, `Guest`.
21
+ - `Active` (BOOLEAN): Indicates whether the user account is active.
22
+ - `HaveMailAccount` (BOOLEAN)(optional): Indicates if the user has a linked mail account.
23
+ - `SessionId` (TEXT): The session ID associated with the user.
24
+ - `GuestRole` (JSONB): Stores additional guest-specific role information in binary JSON format.
25
+
26
+ - **Schema:**
27
+ ```sql
28
+ CREATE TABLE "Users" (
29
+ id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
30
+ "UserName" TEXT NOT NULL,
31
+ "Password" TEXT NOT NULL,
32
+ "Role" TEXT CHECK("Role" IN ('SuperAdmin', 'NormalUser', 'Guest')) NOT NULL DEFAULT 'NormalUser'::text,
33
+ "Active" BOOLEAN NOT NULL DEFAULT true,
34
+ "HaveMailAccount" BOOLEAN NOT NULL DEFAULT false,
35
+ "SessionId" TEXT,
36
+ "GuestRole" JSONB DEFAULT '{"allowPages": [""], "NotallowPages": [""]}'::jsonb
37
+ );
38
+ ```
39
+
40
+ ### Session Table
41
+
42
+ - **Columns:**
43
+
44
+ - `sid` (VARCHAR, primary key): Unique session identifier.
45
+ - `sess` (JSON): Session data stored in JSON format.
46
+ - `expire` (TIMESTAMP): Expiration timestamp for the session.
47
+
48
+ - **Schema:**
49
+ ```sql
50
+ CREATE TABLE session (
51
+ sid VARCHAR PRIMARY KEY,
52
+ sess JSON NOT NULL,
53
+ expire TIMESTAMP NOT NULL
54
+ );
55
+ ```
56
+
57
+ ### Two-Factor Authentication Table
58
+
59
+ - **Columns:**
60
+
61
+ - `UserName` (TEXT): The username of the user.
62
+ - `TwoFAStatus` (TEXT): The status of two-factor authentication (e.g., enabled, disabled).
63
+ - `TwoFASecret` (TEXT): The secret key used for two-factor authentication.
64
+
65
+ - **Schema:**
66
+ ```sql
67
+ CREATE TABLE "TwoFA" (
68
+ "UserName" TEXT NOT NULL PRIMARY KEY,
69
+ "TwoFAStatus" TEXT NOT NULL DEFAULT false,
70
+ "TwoFASecret" TEXT NOT NULL
71
+ );
72
+ ```
73
+
74
+ ### Query to Add a User
75
+
76
+ To add new users to the `Users` table, use the following SQL queries:
77
+
78
+ ```sql
79
+ INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
80
+ VALUES ('support', '12345678', 'SuperAdmin', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
81
+
82
+ INSERT INTO "Users" ("UserName", "Password", "Role", "Active", "HaveMailAccount", "SessionId", "GuestRole")
83
+ VALUES ('test', '12345678', 'NormalUser', true, false, NULL, '{"allowPages": [""], "NotallowPages": [""]}'::jsonb);
84
+ ```
85
+
86
+ - Replace `support` and `test` with the desired usernames.
87
+ - Replace `12345678` with the actual passwords.
88
+ - Adjust the `Role` values as needed (`SuperAdmin`, `NormalUser`, or `Guest`).
89
+ - Modify the `Active` and `HaveMailAccount` values as required.
90
+ - Update the `GuestRole` JSON object if specific permissions are required(this functionality is under construction).
package/env.md ADDED
@@ -0,0 +1,55 @@
1
+ # Configuration Guide
2
+
3
+ [<- Back](README.md)
4
+
5
+ ## reCAPTCHA Settings
6
+ ```properties
7
+ RECAPTCHA_SECRET_KEY=123
8
+ ```
9
+ > Note: Obtain your secret key from Google reCAPTCHA Admin Console.
10
+
11
+
12
+ ## Session Settings
13
+ ```properties
14
+ SESSION_SECRET_KEY=123
15
+ IS_DEPLOYED=true
16
+ DOMAIN=mbktechstudio.com
17
+ ```
18
+ > **SESSION_SECRET_KEY**: Generate a secure key using [Generate Secret](https://generate-secret.vercel.app/32).
19
+
20
+ > **IS_DEPLOYED**:
21
+
22
+ > - `true`: For deployed environments. Sessions are shared across all subDOMAINs of `.mbktechstudio.com` or the DOMAIN specified in `DOMAIN`.
23
+
24
+ > - `false`: For local development.
25
+
26
+ > - Important: If set to `true`, login functionality will not work on `localhost`. Use a valid DOMAIN for proper operation.
27
+
28
+ > **DOMAIN**:
29
+
30
+ > - Set `DOMAIN` to your DOMAIN
31
+
32
+ > - If you don't have a DOMAIN, set `IS_DEPLOYED=false`.
33
+
34
+
35
+ ## Database Settings
36
+
37
+ ```properties
38
+ LOGIN_DB=postgresql://username:password@server.DOMAIN/db_name
39
+ ```
40
+ > Replace the placeholder with your PostgreSQL connection string.
41
+
42
+
43
+ ## Two-Factor Authentication (2FA)
44
+ ```properties
45
+ MBKAUTH_TWO_FA_ENABLE=false
46
+ ```
47
+ > MBKAUTH_TWO_FA_ENABLE: Set to `true` to enable Two-Factor Authentication.
48
+
49
+
50
+ ## Cookie Settings
51
+
52
+ ```properties
53
+ COOKIE_EXPIRE_TIME=5
54
+ ```
55
+ > Cookie expiration time in days. Default is `2 days`.
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import router from "./lib/main.js";
2
1
  import dotenv from "dotenv";
3
- import Joi from "joi";
2
+ import Joi from "joi";
3
+ import router from "./lib/main.js";
4
4
  dotenv.config();
5
5
 
6
6
  const envSchema = Joi.object({
@@ -17,7 +17,7 @@ const { error } = envSchema.validate(process.env);
17
17
  if (error) {
18
18
  throw new Error(`Environment variable validation error: ${error.message}`);
19
19
  }
20
-
21
- console.log("Hello, World!");
22
-
20
+ export { validateSession, checkRolePermission, validateSessionAndRole, getUserData } from "./lib/validateSessionAndRole.js";
21
+ export { authenticate } from "./lib/auth.js";
22
+ export { dblogin } from "./lib/pool.js";
23
23
  export default router;
package/lib/auth.js ADDED
@@ -0,0 +1,13 @@
1
+ export const authenticate = (authentication) => {
2
+ return (req, res, next) => {
3
+ const token = req.headers["authorization"];
4
+ console.log(`Received token: ${token}`);
5
+ if (token === authentication) {
6
+ console.log("Authentication successful");
7
+ next();
8
+ } else {
9
+ console.log("Authentication failed");
10
+ res.status(401).send("Unauthorized");
11
+ }
12
+ };
13
+ };
package/lib/main.js CHANGED
@@ -1,9 +1,319 @@
1
- import express from 'express';
1
+ import express from "express";
2
+ import crypto from "crypto";
3
+ import session from "express-session";
4
+ import pgSession from "connect-pg-simple";
5
+ const PgSession = pgSession(session);
6
+ import dotenv from "dotenv";
7
+ import { dblogin } from "./pool.js";
8
+ import { authenticate } from "./auth.js";
9
+ import fetch from 'node-fetch';
10
+ import cookieParser from "cookie-parser"; // Import cookie-parser
2
11
 
12
+ dotenv.config();
3
13
  const router = express.Router();
14
+ let COOKIE_EXPIRE_TIME = 2 * 24 * 60 * 60 * 1000; //2 days
4
15
 
5
- router.get('/mbkauthe/', (req, res) => {
6
- res.send('Welcome to the main page!');
16
+ try {
17
+ const parsedExpireTime = parseInt(process.env.COOKIE_EXPIRE_TIME, 10);
18
+ if (!isNaN(parsedExpireTime) && parsedExpireTime > 0) {
19
+ COOKIE_EXPIRE_TIME = parsedExpireTime * 24 * 60 * 60 * 1000; // Convert days to milliseconds
20
+ } else {
21
+ console.warn("Invalid COOKIE_EXPIRE_TIME in environment variables, using default value");
22
+ }
23
+ console.log(`Cookie expiration time set to ${COOKIE_EXPIRE_TIME} days for deployed environment`);
24
+ } catch (error) {
25
+ console.log("Error parsing COOKIE_EXPIRE_TIME:", error);
26
+ }
27
+
28
+ router.use(express.json());
29
+ router.use(express.urlencoded({ extended: true }));
30
+
31
+ router.use(
32
+ session({
33
+ store: new PgSession({
34
+ pool: dblogin, // Connection pool
35
+ tableName: "session", // Use another table-name than the default "session" one
36
+ }),
37
+ secret: process.env.SESSION_SECRET_KEY, // Replace with your secret key
38
+ resave: false,
39
+ saveUninitialized: false,
40
+ cookie: {
41
+ maxAge: COOKIE_EXPIRE_TIME,
42
+ DOMAIN: process.env.IS_DEPLOYED === 'true' ? `.${process.env.DOMAIN}` : undefined, // Use root DOMAIN for subDOMAIN sharing
43
+ httpOnly: true,
44
+ secure: process.env.IS_DEPLOYED === 'true', // Use secure cookies in production
45
+ },
46
+ })
47
+ );
48
+
49
+
50
+
51
+ router.use(cookieParser()); // Use cookie-parser middleware
52
+
53
+ router.use((req, res, next) => {
54
+ if (req.session && req.session.user) {
55
+ const userAgent = req.headers["user-agent"];
56
+ const userIp =
57
+ req.headers["x-forwarded-for"] || req.connection.remoteAddress;
58
+ const formattedIp = userIp === "::1" ? "127.0.0.1" : userIp;
59
+
60
+ req.session.otherInfo = {
61
+ ip: formattedIp,
62
+ browser: userAgent,
63
+ };
64
+
65
+ next();
66
+ } else {
67
+ next();
68
+ }
69
+ });
70
+
71
+ // Save the username in a cookie, the cookie user name is use
72
+ // for displaying user name in profile menu. This cookie is not use anyelse where.
73
+ // So it is safe to use.
74
+ router.use(async (req, res, next) => {
75
+ if (req.session && req.session.user) {
76
+ try {
77
+
78
+ res.cookie("username", req.session.user.username, {
79
+ maxAge: COOKIE_EXPIRE_TIME,
80
+ });
81
+
82
+ const query = `SELECT "Role" FROM "Users" WHERE "UserName" = $1`;
83
+ const result = await dblogin.query(query, [req.session.user.username]);
84
+
85
+ if (result.rows.length > 0) {
86
+ req.session.user.role = result.rows[0].Role;
87
+ res.cookie("userRole", req.session.user.role, {
88
+ maxAge: COOKIE_EXPIRE_TIME,
89
+ });
90
+ } else {
91
+ req.session.user.role = null;
92
+ }
93
+ } catch (error) {
94
+ console.log("Error fetching user role:", error.message);
95
+ req.session.user.role = null; // Fallback to null role
96
+ }
97
+ }
98
+ next();
99
+ });
100
+
101
+ router.use(async (req, res, next) => {
102
+ // Check for sessionId cookie if session is not initialized
103
+ if (!req.session.user && req.cookies && req.cookies.sessionId) {
104
+ console.log("Restoring session from sessionId cookie"); // Log session restoration
105
+ const sessionId = req.cookies.sessionId;
106
+ const query = `SELECT * FROM "Users" WHERE "SessionId" = $1`;
107
+ const result = await dblogin.query(query, [sessionId]);
108
+
109
+ if (result.rows.length > 0) {
110
+ const user = result.rows[0];
111
+ req.session.user = {
112
+ id: user.id,
113
+ username: user.UserName,
114
+ sessionId,
115
+ };
116
+ console.log(`Session restored for user: ${user.UserName}`); // Log successful session restoration
117
+ } else {
118
+ console.warn("No matching session found for sessionId"); // Log if no session is found
119
+ }
120
+ }
121
+ next();
122
+ });
123
+
124
+ //Invoke-RestMethod -Uri http://localhost:3030/terminateAllSessions -Method POST
125
+ // Terminate all sessions route
126
+ router.post("/mbkauthe/api/terminateAllSessions", authenticate(process.env.Main_SECRET_TOKEN), async (req, res) => {
127
+ try {
128
+ await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL`);
129
+
130
+ // Clear the session table
131
+ await dblogin.query('DELETE FROM "session"');
132
+
133
+ // Destroy all sessions on the server
134
+ req.session.destroy((err) => {
135
+ if (err) {
136
+ console.log("Error destroying session:", err);
137
+ return res
138
+ .status(500)
139
+ .json({ success: false, message: "Failed to terminate sessions" });
140
+ }
141
+ console.log("All sessions terminated successfully");
142
+ res.status(200).json({
143
+ success: true,
144
+ message: "All sessions terminated successfully",
145
+ });
146
+ });
147
+ } catch (err) {
148
+ console.log("Database query error during session termination:", err);
149
+ res
150
+ .status(500)
151
+ .json({ success: false, message: "Internal Server Error" });
152
+ }
153
+ }
154
+ );
155
+
156
+ router.post("/mbkauthe/api/login", async (req, res) => {
157
+ console.log("Login request received"); // Log when login is initiated
158
+
159
+ const { username, password, token, recaptcha } = req.body;
160
+ console.log(`Login attempt for username: ${username}`); // Log username
161
+
162
+ const secretKey = process.env.RECAPTCHA_SECRET_KEY;
163
+ const verificationUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${recaptcha}`;
164
+
165
+ // Bypass recaptcha for specific users
166
+ if (username !== "ibnekhalid" && username !== "maaz.waheed" && username !== "support") {
167
+ try {
168
+ const response = await fetch(verificationUrl, { method: 'POST' });
169
+ const body = await response.json();
170
+ console.log("reCAPTCHA verification response:", body); // Log reCAPTCHA response
171
+
172
+ if (!body.success) {
173
+ console.log("Failed reCAPTCHA verification");
174
+ return res.status(400).json({ success: false, message: "Failed reCAPTCHA verification" });
175
+ }
176
+ } catch (err) {
177
+ console.log("Error during reCAPTCHA verification:", err);
178
+ return res.status(500).json({ success: false, message: "Internal Server Error" });
179
+ }
180
+ }
181
+
182
+ if (!username || !password) {
183
+ console.log("Missing username or password");
184
+ return res.status(400).json({
185
+ success: false,
186
+ message: "Username and password are required",
187
+ });
188
+ }
189
+
190
+ console.log("RECAPTCHA_SECRET_KEY:", process.env.RECAPTCHA_SECRET_KEY); // Log reCAPTCHA secret key
191
+ console.log("SESSION_SECRET_KEY:", process.env.SESSION_SECRET_KEY); // Log reCAPTCHA secret key
192
+ console.log("LOGIN_DB:", process.env.LOGIN_DB); // Log reCAPTCHA secret key
193
+ console.log("COOKIE_EXPIRE_TIME:", process.env.COOKIE_EXPIRE_TIME); // Log reCAPTCHA secret key
194
+ console.log("DOMAIN:", process.env.DOMAIN); // Log reCAPTCHA secret key
195
+ console.log("IS_DEPLOYED:", process.env.IS_DEPLOYED); // Log reCAPTCHA secret key
196
+ console.log("MBKAUTH_TWO_FA_ENABLE:", process.env.MBKAUTH_TWO_FA_ENABLE); // Log reCAPTCHA secret key
197
+
198
+ try {
199
+ // Query to check if the username exists
200
+ const userQuery = `SELECT * FROM "Users" WHERE "UserName" = $1`;
201
+ const userResult = await dblogin.query(userQuery, [username]);
202
+ console.log("User query result:", userResult.rows); // Log user query result
203
+
204
+ if (userResult.rows.length === 0) {
205
+ console.log(`Username does not exist: ${username}`);
206
+ return res.status(404).json({ success: false, message: "Username does not exist" });
207
+ }
208
+
209
+ const user = userResult.rows[0];
210
+
211
+ // Check if the password matches
212
+ if (user.Password !== password) {
213
+ console.log(`Incorrect password for username: ${username}`);
214
+ return res.status(401).json({ success: false, message: "Incorrect password" });
215
+ }
216
+
217
+ // Check if the account is inactive
218
+ if (!user.Active) {
219
+ console.log(`Inactive account for username: ${username}`);
220
+ return res.status(403).json({ success: false, message: "Account is inactive" });
221
+ }
222
+
223
+ if ((process.env.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
224
+ let sharedSecret;
225
+ const query = `SELECT "TwoFAStatus", "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
226
+ const twoFAResult = await dblogin.query(query, [username]);
227
+ console.log("TwoFA query result:", twoFAResult.rows); // Log TwoFA query result
228
+
229
+ sharedSecret = twoFAResult.rows[0]?.TwoFASecret;
230
+ if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus && !token) {
231
+ console.log("2FA code required but not provided");
232
+ return res.status(401).json({ success: false, message: "Please Enter 2FA code" });
233
+ }
234
+
235
+ if (token && twoFAResult.rows[0]?.TwoFAStatus) {
236
+ const tokenValidates = speakeasy.totp.verify({
237
+ secret: sharedSecret,
238
+ encoding: "base32",
239
+ token: token,
240
+ window: 1, // Allows a margin for clock drift, optional
241
+ });
242
+
243
+ if (!tokenValidates) {
244
+ console.log(`Invalid 2FA code for username: ${username}`);
245
+ return res.status(401).json({ success: false, message: "Invalid 2FA code" });
246
+ }
247
+ }
248
+ }
249
+
250
+ // Generate session ID
251
+ const sessionId = crypto.randomBytes(256).toString("hex");
252
+ console.log(`Generated session ID for username: ${username}`); // Log session ID
253
+
254
+ await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [
255
+ sessionId,
256
+ user.id,
257
+ ]);
258
+
259
+ // Store session ID in session
260
+ req.session.user = {
261
+ id: user.id,
262
+ username: user.UserName,
263
+ sessionId,
264
+ };
265
+ console.log(`Session stored for user: ${user.UserName}, sessionId: ${sessionId}`); // Log session storage
266
+
267
+ // Set a cookie accessible across subDOMAINs
268
+ res.cookie("sessionId", sessionId, {
269
+ maxAge: COOKIE_EXPIRE_TIME,
270
+ DOMAIN: process.env.IS_DEPLOYED === 'true' ? `.${process.env.DOMAIN}` : undefined, // Use DOMAIN only in production
271
+ httpOnly: true,
272
+ secure: process.env.IS_DEPLOYED === 'true', // Use secure cookies in production
273
+ });
274
+ console.log(`Cookie set for user: ${user.UserName}, sessionId: ${sessionId}`); // Log cookie setting
275
+
276
+ console.log(`User "${username}" logged in successfully`);
277
+ res.status(200).json({
278
+ success: true,
279
+ message: "Login successful",
280
+ sessionId,
281
+ });
282
+ } catch (err) {
283
+ console.log("Error during login process:", err);
284
+ res.status(500).json({ success: false, message: "Internal Server Error" });
285
+ }
286
+ });
287
+
288
+ router.post("/mbkauthe/api/logout", async (req, res) => {
289
+ if (req.session.user) {
290
+ try {
291
+ const { id, username } = req.session.user;
292
+ const query = `SELECT "Active" FROM "Users" WHERE "id" = $1`;
293
+ const result = await dblogin.query(query, [id]);
294
+
295
+ if (result.rows.length > 0 && !result.rows[0].Active) {
296
+ console.log("Account is inactive during logout");
297
+ }
298
+
299
+ req.session.destroy((err) => {
300
+ if (err) {
301
+ console.log("Error destroying session:", err);
302
+ return res.status(500).json({ success: false, message: "Logout failed" });
303
+ }
304
+ // Clear both session cookies
305
+ res.clearCookie("connect.sid");
306
+ res.clearCookie("sessionId"); // Clear the sessionId cookie used for restoration
307
+ console.log(`User "${username}" logged out successfully`);
308
+ res.status(200).json({ success: true, message: "Logout successful" });
309
+ });
310
+ } catch (err) {
311
+ console.log("Database query error during logout:", err);
312
+ res.status(500).json({ success: false, message: "Internal Server Error" });
313
+ }
314
+ } else {
315
+ res.status(400).json({ success: false, message: "Not logged in" });
316
+ }
7
317
  });
8
318
 
9
319
  export default router;
package/lib/pool.js ADDED
@@ -0,0 +1,27 @@
1
+ import pkg from "pg";
2
+ const { Pool } = pkg;
3
+ import dotenv from "dotenv";
4
+
5
+ dotenv.config();
6
+
7
+ // PostgreSQL connection pool for pool
8
+ const poolConfig = {
9
+ connectionString: process.env.LOGIN_DB,
10
+ ssl: {
11
+ rejectUnauthorized: true,
12
+ },
13
+
14
+ };
15
+
16
+ export const dblogin = new Pool(poolConfig);
17
+
18
+ // Test connection for pool
19
+ (async () => {
20
+ try {
21
+ const client = await dblogin.connect();
22
+ console.log("Connected to PostgreSQL database (pool)!");
23
+ client.release();
24
+ } catch (err) {
25
+ console.error("Database connection error (pool):", err);
26
+ }
27
+ })();
@@ -0,0 +1,152 @@
1
+ import { dblogin } from "./pool.js";
2
+
3
+ async function validateSession(req, res, next) {
4
+ if (!req.session.user) {
5
+ return res.render("templates/Error/NotLoggedIn.handlebars", {
6
+ currentUrl: req.originalUrl,
7
+ });
8
+ }
9
+
10
+ try {
11
+ const { id, sessionId } = req.session.user;
12
+ const query = `SELECT "SessionId", "Active" FROM "Users" WHERE "id" = $1`;
13
+ const result = await dblogin.query(query, [id]);
14
+
15
+ // Check if user exists and session ID matches
16
+ if (result.rows.length === 0 || result.rows[0].SessionId !== sessionId) {
17
+ console.log(
18
+ `Session invalidated for user \"${req.session.user.username}\"`
19
+ );
20
+ req.session.destroy();
21
+ // ...existing code...
22
+ return res.render("templates/Error/SessionExpire.handlebars", {
23
+ currentUrl: req.originalUrl,
24
+ });
25
+ // ...existing code...
26
+ }
27
+
28
+ // Check if the user account is inactive
29
+ if (!result.rows[0].Active) {
30
+ console.log(
31
+ `Account is inactive for user \"${req.session.user.username}\"`
32
+ );
33
+ req.session.destroy();
34
+ res.clearCookie("connect.sid");
35
+ return res.render("templates/Error/AccountInactive.handlebars", {
36
+ currentUrl: req.originalUrl,
37
+ });
38
+ }
39
+
40
+ next(); // Proceed if everything is valid
41
+ } catch (err) {
42
+ console.error("Session validation error:", err);
43
+ res.status(500).json({ success: false, message: "Internal Server Error" });
44
+ }
45
+ }
46
+
47
+ const checkRolePermission = (requiredRole) => {
48
+ return async (req, res, next) => {
49
+ try {
50
+ if (!req.session || !req.session.user || !req.session.user.id) {
51
+ console.log("User not authenticated");
52
+ console.log(req.session);
53
+ return res.render("templates/Error/NotLoggedIn.handlebars", {
54
+ currentUrl: req.originalUrl,
55
+ });
56
+ }
57
+
58
+ if (requiredRole === "Any" || requiredRole === "any") {
59
+ return next();
60
+ }
61
+
62
+ const userId = req.session.user.id;
63
+
64
+ const query = `SELECT "Role" FROM "Users" WHERE "id" = $1`;
65
+ const result = await dblogin.query(query, [userId]);
66
+
67
+ if (result.rows.length === 0) {
68
+ return res
69
+ .status(401)
70
+ .json({ success: false, message: "User not found" });
71
+ }
72
+
73
+ const userRole = result.rows[0].Role;
74
+ if (userRole !== requiredRole) {
75
+ return res.render("templates/Error/AccessDenied.handlebars", {
76
+ currentRole: userRole,
77
+ requiredRole: requiredRole,
78
+ });
79
+ }
80
+
81
+ next();
82
+ } catch (err) {
83
+ console.error("Permission check error:", err);
84
+ res
85
+ .status(500)
86
+ .json({ success: false, message: "Internal Server Error" });
87
+ }
88
+ };
89
+ };
90
+
91
+ const validateSessionAndRole = (requiredRole) => {
92
+ return async (req, res, next) => {
93
+ await validateSession(req, res, async () => {
94
+ await checkRolePermission(requiredRole)(req, res, next);
95
+ });
96
+ };
97
+ };
98
+
99
+ async function getUserData(UserName, parameters) {
100
+ try {
101
+ if (!parameters || parameters.length === 0) {
102
+ throw new Error("Parameters are required to fetch user data");
103
+ }
104
+
105
+ // Dynamically select fields from Users table based on `parameters`
106
+ const userFields = [
107
+ "Password", "UserName", "Role", "Active", "GuestRole", "HaveMailAccount",
108
+ ];
109
+ const profileFields = [
110
+ "FullName", "email", "Image", "ProjectLinks", "SocialAccounts", "Bio",
111
+ ];
112
+
113
+ let userParameters = [];
114
+ let profileParameters = [];
115
+
116
+ if (parameters === "profiledata") {
117
+ userParameters = userFields.filter(field => field !== "Password");
118
+ profileParameters = profileFields;
119
+ } else {
120
+ userParameters = userFields.filter(field => parameters.includes(field));
121
+ profileParameters = profileFields.filter(field => parameters.includes(field));
122
+ }
123
+
124
+ // Prepare queries based on required fields
125
+ let userResult = {};
126
+ if (userParameters.length > 0) {
127
+ const userQuery = `SELECT ${userParameters.map(field => `"${field}"`).join(", ")}
128
+ FROM "Users" WHERE "UserName" = $1`;
129
+ const userQueryResult = await dblogin.query(userQuery, [UserName]);
130
+ if (userQueryResult.rows.length === 0) return { error: "User not found" };
131
+ userResult = userQueryResult.rows[0];
132
+ }
133
+
134
+ let profileResult = {};
135
+ if (profileParameters.length > 0) {
136
+ const profileQuery = `SELECT ${profileParameters.map(field => `"${field}"`).join(", ")}
137
+ FROM profiledata WHERE "UserName" = $1`;
138
+ const profileQueryResult = await dblogin.query(profileQuery, [UserName]);
139
+ if (profileQueryResult.rows.length === 0) return { error: "Profile data not found" };
140
+ profileResult = profileQueryResult.rows[0];
141
+ }
142
+
143
+ // Combine results
144
+ const combinedResult = { ...userResult, ...profileResult };
145
+ return combinedResult;
146
+ } catch (err) {
147
+ console.error("Error fetching user data:", err.message);
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ export { validateSession, checkRolePermission, validateSessionAndRole, getUserData };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "MBKTechStudio's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
+ "type": "module",
6
7
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
+ "test": "node index.js"
8
9
  },
9
10
  "repository": {
10
11
  "type": "git",