mbkauthe 4.7.1 → 4.8.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 +3 -3
- package/docs/api.md +12 -12
- package/docs/db.md +3 -1
- package/docs/db.sql +6 -0
- package/docs/env.md +18 -6
- package/index.d.ts +8 -4
- package/index.js +1 -1
- package/lib/config/cookies.js +2 -0
- package/lib/config/index.js +8 -5
- package/lib/middleware/auth.js +53 -0
- package/lib/pool.js +16 -191
- package/lib/routes/auth.js +14 -8
- package/lib/routes/dbLogs.js +46 -8
- package/lib/routes/misc.js +14 -8
- package/lib/routes/oauth.js +361 -355
- package/lib/utils/dbQueryLogger.js +324 -0
- package/package.json +1 -1
- package/public/main.css +109 -28
- package/public/main.js +6 -2
- package/test.spec.js +14 -2
- package/views/head.handlebars +12 -0
- package/views/pages/accountSwitch.handlebars +37 -16
- package/views/pages/dbLogs.handlebars +433 -155
- package/views/pages/errorCodes.handlebars +30 -26
- package/views/pages/info_mbkauthe.handlebars +15 -15
- package/views/pages/loginmbkauthe.handlebars +15 -11
- package/views/pages/test.handlebars +27 -15
- package/views/profilemenu.handlebars +53 -57
- package/views/sharedStyles.handlebars +1 -1
- package/views/showmessage.handlebars +52 -30
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
- PostgreSQL session management
|
|
23
23
|
- Multi-session support (configurable concurrent sessions per user)
|
|
24
24
|
- Optional TOTP-based 2FA with trusted devices
|
|
25
|
-
-
|
|
25
|
+
- Social login (GitHub App & Google OAuth)
|
|
26
26
|
- Role-based access: SuperAdmin, NormalUser, Guest
|
|
27
27
|
- CSRF protection & rate limiting
|
|
28
28
|
- Easy Express.js integration
|
|
@@ -101,9 +101,9 @@ These are only mounted when `process.env.env === "dev"`:
|
|
|
101
101
|
|
|
102
102
|
Enable via `MBKAUTH_TWO_FA_ENABLE=true`. Trusted devices can skip 2FA for a set duration.
|
|
103
103
|
|
|
104
|
-
## 🔄
|
|
104
|
+
## 🔄 Social Login Integration
|
|
105
105
|
|
|
106
|
-
**GitHub / Google OAuth:** Configure
|
|
106
|
+
**GitHub App / Google OAuth:** Configure credentials via `.env` or `mbkautheVar`. Users must link accounts before login.
|
|
107
107
|
|
|
108
108
|
## 🎨 Customization
|
|
109
109
|
|
package/docs/api.md
CHANGED
|
@@ -179,7 +179,7 @@ Renders the main login page.
|
|
|
179
179
|
**Response:** HTML page with login form
|
|
180
180
|
|
|
181
181
|
**Template Variables:**
|
|
182
|
-
- `githubLoginEnabled` - Whether GitHub
|
|
182
|
+
- `githubLoginEnabled` - Whether GitHub App login is enabled
|
|
183
183
|
- `googleLoginEnabled` - Whether Google OAuth is enabled
|
|
184
184
|
- `customURL` - Redirect URL after login
|
|
185
185
|
- `userLoggedIn` - Whether user is already authenticated
|
|
@@ -317,8 +317,8 @@ The endpoints below are active in the router but are not fully expanded above. U
|
|
|
317
317
|
|
|
318
318
|
**OAuth:**
|
|
319
319
|
|
|
320
|
-
- `GET /mbkauthe/api/github/login` - Starts GitHub
|
|
321
|
-
- `GET /mbkauthe/api/github/login/callback` - GitHub
|
|
320
|
+
- `GET /mbkauthe/api/github/login` - Starts GitHub App login flow.
|
|
321
|
+
- `GET /mbkauthe/api/github/login/callback` - GitHub App callback.
|
|
322
322
|
- `GET /mbkauthe/api/google/login` - Starts Google OAuth login flow.
|
|
323
323
|
- `GET /mbkauthe/api/google/login/callback` - Google OAuth callback.
|
|
324
324
|
|
|
@@ -1222,11 +1222,11 @@ GET /mbkauthe/test
|
|
|
1222
1222
|
|
|
1223
1223
|
### OAuth Endpoints
|
|
1224
1224
|
|
|
1225
|
-
#### GitHub
|
|
1225
|
+
#### GitHub App
|
|
1226
1226
|
|
|
1227
1227
|
##### `GET /mbkauthe/api/github/login`
|
|
1228
1228
|
|
|
1229
|
-
Initiates the GitHub
|
|
1229
|
+
Initiates the GitHub App authentication flow.
|
|
1230
1230
|
|
|
1231
1231
|
**Rate Limit:** 10 requests per 5 minutes
|
|
1232
1232
|
|
|
@@ -1235,11 +1235,11 @@ Initiates the GitHub OAuth authentication flow.
|
|
|
1235
1235
|
**Query Parameters:**
|
|
1236
1236
|
- `redirect` (optional) - Relative URL to redirect after successful authentication (must start with `/` to prevent open redirect attacks)
|
|
1237
1237
|
|
|
1238
|
-
**Response:** Redirects to GitHub
|
|
1238
|
+
**Response:** Redirects to GitHub authorization page
|
|
1239
1239
|
|
|
1240
1240
|
**Prerequisites:**
|
|
1241
1241
|
- `GITHUB_LOGIN_ENABLED=true` in environment
|
|
1242
|
-
- Valid `
|
|
1242
|
+
- Valid `GITHUB_APP_CLIENT_ID` and `GITHUB_APP_CLIENT_SECRET` configured
|
|
1243
1243
|
- User's GitHub account must be linked to an MBKAuth account in `user_github` table
|
|
1244
1244
|
|
|
1245
1245
|
**Example:**
|
|
@@ -1250,9 +1250,9 @@ GET /mbkauthe/api/github/login?redirect=/dashboard
|
|
|
1250
1250
|
**Workflow:**
|
|
1251
1251
|
1. User clicks "Login with GitHub"
|
|
1252
1252
|
2. CSRF token generated and stored in session
|
|
1253
|
-
3. Redirects to GitHub
|
|
1254
|
-
4. GitHub redirects back to callback URL
|
|
1255
|
-
5. System verifies
|
|
1253
|
+
3. Redirects to GitHub authorization page
|
|
1254
|
+
4. GitHub redirects back to callback URL with authorization `code`
|
|
1255
|
+
5. System verifies `github_id` is linked
|
|
1256
1256
|
6. If 2FA enabled, prompts for 2FA
|
|
1257
1257
|
7. Creates session and redirects to specified URL
|
|
1258
1258
|
|
|
@@ -1260,7 +1260,7 @@ GET /mbkauthe/api/github/login?redirect=/dashboard
|
|
|
1260
1260
|
|
|
1261
1261
|
##### `GET /mbkauthe/api/github/login/callback`
|
|
1262
1262
|
|
|
1263
|
-
Handles the
|
|
1263
|
+
Handles the callback from GitHub after user authorization.
|
|
1264
1264
|
|
|
1265
1265
|
**Rate Limit:** Inherited from OAuth rate limiter (10 requests per 5 minutes)
|
|
1266
1266
|
|
|
@@ -1277,7 +1277,7 @@ Handles the OAuth callback from GitHub after user authorization.
|
|
|
1277
1277
|
- **GitHub Not Linked**: Returns error if GitHub account is not in `user_github` table
|
|
1278
1278
|
- **Account Inactive**: Returns error if user account is deactivated
|
|
1279
1279
|
- **Not Authorized**: Returns error if user is not allowed to access the application
|
|
1280
|
-
- **GitHub Auth Error**: Returns error for
|
|
1280
|
+
- **GitHub Auth Error**: Returns error for provider authentication failures
|
|
1281
1281
|
|
|
1282
1282
|
**Success Flow:**
|
|
1283
1283
|
```
|
package/docs/db.md
CHANGED
|
@@ -85,7 +85,9 @@ CREATE TABLE IF NOT EXISTS user_github (
|
|
|
85
85
|
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
86
86
|
github_id VARCHAR(255) UNIQUE,
|
|
87
87
|
github_username VARCHAR(255),
|
|
88
|
-
|
|
88
|
+
installation_id BIGINT,
|
|
89
|
+
installation_target_type VARCHAR(32),
|
|
90
|
+
access_token VARCHAR(255),
|
|
89
91
|
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
90
92
|
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
91
93
|
);
|
package/docs/db.sql
CHANGED
|
@@ -49,11 +49,17 @@ CREATE TABLE IF NOT EXISTS user_github (
|
|
|
49
49
|
user_name VARCHAR(50) REFERENCES "Users"("UserName"),
|
|
50
50
|
github_id VARCHAR(255) UNIQUE,
|
|
51
51
|
github_username VARCHAR(255),
|
|
52
|
+
installation_id BIGINT,
|
|
53
|
+
installation_target_type VARCHAR(32),
|
|
52
54
|
access_token TEXT,
|
|
53
55
|
created_at TimeStamp WITH TIME ZONE DEFAULT NOW(),
|
|
54
56
|
updated_at TimeStamp WITH TIME ZONE DEFAULT NOW()
|
|
55
57
|
);
|
|
56
58
|
|
|
59
|
+
ALTER TABLE user_github
|
|
60
|
+
ADD COLUMN IF NOT EXISTS installation_id BIGINT,
|
|
61
|
+
ADD COLUMN IF NOT EXISTS installation_target_type VARCHAR(32);
|
|
62
|
+
|
|
57
63
|
-- Add indexes for performance optimization
|
|
58
64
|
CREATE INDEX IF NOT EXISTS idx_user_github_github_id ON user_github (github_id);
|
|
59
65
|
CREATE INDEX IF NOT EXISTS idx_user_github_user_name ON user_github (user_name);
|
package/docs/env.md
CHANGED
|
@@ -95,14 +95,26 @@ This document describes the environment variables MBKAuth expects and keeps brie
|
|
|
95
95
|
- Required: No
|
|
96
96
|
|
|
97
97
|
- GITHUB_LOGIN_ENABLED / GOOGLE_LOGIN_ENABLED
|
|
98
|
-
- Description: Enable
|
|
98
|
+
- Description: Enable social login providers.
|
|
99
99
|
- Default: `false`
|
|
100
|
-
- If `true`,
|
|
100
|
+
- If `GOOGLE_LOGIN_ENABLED=true`, `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are required.
|
|
101
|
+
- If `GITHUB_LOGIN_ENABLED=true`, GitHub App client credentials are required.
|
|
101
102
|
|
|
102
|
-
-
|
|
103
|
-
- Description:
|
|
104
|
-
- Required
|
|
105
|
-
- Create
|
|
103
|
+
- GITHUB_APP_SLUG
|
|
104
|
+
- Description: GitHub App slug (optional for login flow in this package; useful for install/link flows handled elsewhere).
|
|
105
|
+
- Required: No
|
|
106
|
+
- Create GitHub App: https://github.com/settings/apps
|
|
107
|
+
|
|
108
|
+
- GITHUB_APP_CLIENT_ID / GITHUB_APP_CLIENT_SECRET
|
|
109
|
+
- Description: GitHub App OAuth credentials used for user sign-in.
|
|
110
|
+
- Required when `GITHUB_LOGIN_ENABLED=true`.
|
|
111
|
+
|
|
112
|
+
- GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
|
|
113
|
+
- Description: Legacy fallback keys if app-prefixed keys are not provided.
|
|
114
|
+
|
|
115
|
+
- GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
|
|
116
|
+
- Description: Google OAuth credentials.
|
|
117
|
+
- Required when `GOOGLE_LOGIN_ENABLED=true`.
|
|
106
118
|
- Create Google OAuth: https://console.cloud.google.com/
|
|
107
119
|
|
|
108
120
|
---
|
package/index.d.ts
CHANGED
|
@@ -55,8 +55,9 @@ declare module 'mbkauthe' {
|
|
|
55
55
|
COOKIE_EXPIRE_TIME?: number;
|
|
56
56
|
DEVICE_TRUST_DURATION_DAYS?: number;
|
|
57
57
|
GITHUB_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
GITHUB_APP_SLUG?: string;
|
|
59
|
+
GITHUB_APP_CLIENT_ID?: string;
|
|
60
|
+
GITHUB_APP_CLIENT_SECRET?: string;
|
|
60
61
|
GOOGLE_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
61
62
|
GOOGLE_CLIENT_ID?: string;
|
|
62
63
|
GOOGLE_CLIENT_SECRET?: string;
|
|
@@ -66,8 +67,9 @@ declare module 'mbkauthe' {
|
|
|
66
67
|
|
|
67
68
|
export interface OAuthConfig {
|
|
68
69
|
GITHUB_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
GITHUB_APP_SLUG?: string;
|
|
71
|
+
GITHUB_APP_CLIENT_ID?: string;
|
|
72
|
+
GITHUB_APP_CLIENT_SECRET?: string;
|
|
71
73
|
GOOGLE_LOGIN_ENABLED?: 'true' | 'false' | 'f';
|
|
72
74
|
GOOGLE_CLIENT_ID?: string;
|
|
73
75
|
GOOGLE_CLIENT_SECRET?: string;
|
|
@@ -132,6 +134,8 @@ declare module 'mbkauthe' {
|
|
|
132
134
|
user_name: string;
|
|
133
135
|
github_id: string;
|
|
134
136
|
github_username: string;
|
|
137
|
+
installation_id?: number;
|
|
138
|
+
installation_target_type?: string;
|
|
135
139
|
access_token: string;
|
|
136
140
|
created_at: Date;
|
|
137
141
|
updated_at: Date;
|
package/index.js
CHANGED
package/lib/config/cookies.js
CHANGED
|
@@ -149,6 +149,8 @@ export const clearSessionCookies = (res) => {
|
|
|
149
149
|
res.clearCookie("mbkauthe.sid", cachedClearCookieOptions);
|
|
150
150
|
res.clearCookie("sessionId", cachedClearCookieOptions);
|
|
151
151
|
res.clearCookie("fullName", cachedClearCookieOptions);
|
|
152
|
+
res.clearCookie("profileImageUrl", cachedClearCookieOptions);
|
|
153
|
+
res.clearCookie("profileImageUser", cachedClearCookieOptions);
|
|
152
154
|
res.clearCookie("device_token", cachedClearCookieOptions);
|
|
153
155
|
};
|
|
154
156
|
|
package/lib/config/index.js
CHANGED
|
@@ -64,7 +64,7 @@ function validateConfiguration() {
|
|
|
64
64
|
const keysToCheck = [
|
|
65
65
|
"APP_NAME", "DEVICE_TRUST_DURATION_DAYS", "EncPass", "Main_SECRET_TOKEN", "SESSION_SECRET_KEY",
|
|
66
66
|
"IS_DEPLOYED", "LOGIN_DB", "MBKAUTH_TWO_FA_ENABLE", "COOKIE_EXPIRE_TIME", "DOMAIN", "loginRedirectURL",
|
|
67
|
-
"GITHUB_LOGIN_ENABLED", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "GOOGLE_LOGIN_ENABLED", "GOOGLE_CLIENT_ID",
|
|
67
|
+
"GITHUB_LOGIN_ENABLED", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID", "GITHUB_APP_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "GOOGLE_LOGIN_ENABLED", "GOOGLE_CLIENT_ID",
|
|
68
68
|
"GOOGLE_CLIENT_SECRET", "MAX_SESSIONS_PER_USER"
|
|
69
69
|
];
|
|
70
70
|
|
|
@@ -145,11 +145,14 @@ function validateConfiguration() {
|
|
|
145
145
|
|
|
146
146
|
// Validate GitHub login configuration
|
|
147
147
|
if (mbkautheVar.GITHUB_LOGIN_ENABLED === "true") {
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const hasGithubClientId = !!(mbkautheVar.GITHUB_APP_CLIENT_ID || mbkautheVar.GITHUB_CLIENT_ID);
|
|
149
|
+
const hasGithubClientSecret = !!(mbkautheVar.GITHUB_APP_CLIENT_SECRET || mbkautheVar.GITHUB_CLIENT_SECRET);
|
|
150
|
+
|
|
151
|
+
if (!hasGithubClientId) {
|
|
152
|
+
errors.push("mbkautheVar.GITHUB_APP_CLIENT_ID (or GITHUB_CLIENT_ID) is required when GITHUB_LOGIN_ENABLED is 'true'");
|
|
150
153
|
}
|
|
151
|
-
if (!
|
|
152
|
-
errors.push("mbkautheVar.GITHUB_CLIENT_SECRET is required when GITHUB_LOGIN_ENABLED is 'true'");
|
|
154
|
+
if (!hasGithubClientSecret) {
|
|
155
|
+
errors.push("mbkautheVar.GITHUB_APP_CLIENT_SECRET (or GITHUB_CLIENT_SECRET) is required when GITHUB_LOGIN_ENABLED is 'true'");
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
package/lib/middleware/auth.js
CHANGED
|
@@ -6,6 +6,32 @@ import { ErrorCodes, createErrorResponse } from "../utils/errors.js";
|
|
|
6
6
|
import { hashApiToken } from "#config.js";
|
|
7
7
|
import { canAccessMethod } from "#config.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Decide if the incoming request should return JSON errors instead of HTML.
|
|
11
|
+
* Non-browser clients (API calls / AJAX) should get JSON.
|
|
12
|
+
*/
|
|
13
|
+
function isJsonRequest(req) {
|
|
14
|
+
if (!req || !req.headers) return false;
|
|
15
|
+
const accept = (req.headers.accept || "").toLowerCase();
|
|
16
|
+
const xRequestedWith = (req.headers["x-requested-with"] || "").toLowerCase();
|
|
17
|
+
const userAgent = (req.headers["user-agent"] || "").toLowerCase();
|
|
18
|
+
const url = (req.originalUrl || req.url || "").toLowerCase();
|
|
19
|
+
const path = (req.path || "").toLowerCase();
|
|
20
|
+
|
|
21
|
+
const isApiPath = url.startsWith("/mbkauthe/api/") || url.startsWith("/api/") || path.startsWith("/mbkauthe/api/") || path.startsWith("/api/");
|
|
22
|
+
const isAcceptJson = accept.includes("application/json") || accept.includes("json") || accept.includes("*/*");
|
|
23
|
+
|
|
24
|
+
const nonBrowserAgent = /curl|wget|httpie|python-requests|python|go-http-client|java\/|php|node-fetch|axios|postman|insomnia|okhttp/;
|
|
25
|
+
const browserAgent = /mozilla|applewebkit|chrome|safari|firefox|edg|msie|trident|opera/;
|
|
26
|
+
|
|
27
|
+
if (isApiPath || xRequestedWith === "xmlhttprequest") return true;
|
|
28
|
+
if (isAcceptJson && !accept.includes("text/html")) return true;
|
|
29
|
+
|
|
30
|
+
if (nonBrowserAgent.test(userAgent) && !browserAgent.test(userAgent)) return true;
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
/**
|
|
10
36
|
* Validates a Bearer token (API Token or Session UUID)
|
|
11
37
|
* Returns a user object if valid, or null/error object
|
|
@@ -160,6 +186,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
160
186
|
if (!req.session.user) {
|
|
161
187
|
console.log("[mbkauthe] User not authenticated");
|
|
162
188
|
console.log("[mbkauthe]: ", req.session.user);
|
|
189
|
+
if (isJsonRequest(req)) {
|
|
190
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
191
|
+
}
|
|
163
192
|
return renderError(res, req, {
|
|
164
193
|
code: 401,
|
|
165
194
|
error: "Not Logged In",
|
|
@@ -177,6 +206,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
177
206
|
console.warn(`[mbkauthe] Missing sessionId for user "${req.session.user.username}"`);
|
|
178
207
|
req.session.destroy();
|
|
179
208
|
clearSessionCookies(res);
|
|
209
|
+
if (isJsonRequest(req)) {
|
|
210
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
211
|
+
}
|
|
180
212
|
return renderError(res, req, {
|
|
181
213
|
code: 401,
|
|
182
214
|
error: "Session Expired",
|
|
@@ -200,6 +232,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
200
232
|
console.log(`[mbkauthe] Session not found for user "${req.session.user.username}"`);
|
|
201
233
|
req.session.destroy();
|
|
202
234
|
clearSessionCookies(res);
|
|
235
|
+
if (isJsonRequest(req)) {
|
|
236
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
237
|
+
}
|
|
203
238
|
return renderError(res, req, {
|
|
204
239
|
code: 401,
|
|
205
240
|
error: "Session Expired",
|
|
@@ -217,6 +252,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
217
252
|
// destroy and clear cookies
|
|
218
253
|
req.session.destroy();
|
|
219
254
|
clearSessionCookies(res);
|
|
255
|
+
if (isJsonRequest(req)) {
|
|
256
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_EXPIRED));
|
|
257
|
+
}
|
|
220
258
|
return renderError(res, req, {
|
|
221
259
|
code: 401,
|
|
222
260
|
error: "Session Expired",
|
|
@@ -231,6 +269,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
231
269
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
232
270
|
req.session.destroy();
|
|
233
271
|
clearSessionCookies(res);
|
|
272
|
+
if (isJsonRequest(req)) {
|
|
273
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.ACCOUNT_INACTIVE));
|
|
274
|
+
}
|
|
234
275
|
return renderError(res, req, {
|
|
235
276
|
code: 401,
|
|
236
277
|
error: "Account Inactive",
|
|
@@ -247,6 +288,9 @@ async function validateSession(req, res, next, strictTokenValidation = false) {
|
|
|
247
288
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
248
289
|
req.session.destroy();
|
|
249
290
|
clearSessionCookies(res);
|
|
291
|
+
if (isJsonRequest(req)) {
|
|
292
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.APP_NOT_AUTHORIZED));
|
|
293
|
+
}
|
|
250
294
|
return renderError(res, req, {
|
|
251
295
|
code: 401,
|
|
252
296
|
error: "Unauthorized",
|
|
@@ -444,6 +488,9 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
444
488
|
try {
|
|
445
489
|
if (!req.session || !req.session.user || !req.session.user.id) {
|
|
446
490
|
console.log("[mbkauthe] User not authenticated");
|
|
491
|
+
if (isJsonRequest(req)) {
|
|
492
|
+
return res.status(401).json(createErrorResponse(401, ErrorCodes.SESSION_NOT_FOUND));
|
|
493
|
+
}
|
|
447
494
|
return renderError(res, req, {
|
|
448
495
|
code: 401,
|
|
449
496
|
error: "Not Logged In",
|
|
@@ -458,6 +505,9 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
458
505
|
|
|
459
506
|
// Check notAllowed role
|
|
460
507
|
if (notAllowed && userRole === notAllowed) {
|
|
508
|
+
if (isJsonRequest(req)) {
|
|
509
|
+
return res.status(403).json(createErrorResponse(403, ErrorCodes.ROLE_NOT_ALLOWED));
|
|
510
|
+
}
|
|
461
511
|
return renderError(res, req, {
|
|
462
512
|
code: 403,
|
|
463
513
|
error: "Access Denied",
|
|
@@ -477,6 +527,9 @@ const checkRolePermission = (requiredRoles, notAllowed) => {
|
|
|
477
527
|
|
|
478
528
|
// Check if user role is in allowed roles
|
|
479
529
|
if (!rolesArray.includes(userRole)) {
|
|
530
|
+
if (isJsonRequest(req)) {
|
|
531
|
+
return res.status(403).json(createErrorResponse(403, ErrorCodes.INSUFFICIENT_PERMISSIONS));
|
|
532
|
+
}
|
|
480
533
|
return renderError(res, req, {
|
|
481
534
|
code: 403,
|
|
482
535
|
error: "Access Denied",
|
package/lib/pool.js
CHANGED
|
@@ -1,211 +1,35 @@
|
|
|
1
1
|
import pkg from "pg";
|
|
2
2
|
const { Pool } = pkg;
|
|
3
3
|
import { mbkautheVar } from "#config.js";
|
|
4
|
+
import { attachDevQueryLogger, runWithRequestContext, getRequestContext } from "./utils/dbQueryLogger.js";
|
|
4
5
|
import dotenv from "dotenv";
|
|
5
6
|
dotenv.config();
|
|
6
7
|
|
|
8
|
+
export { runWithRequestContext, getRequestContext };
|
|
9
|
+
|
|
7
10
|
const poolConfig = {
|
|
8
11
|
connectionString: mbkautheVar.LOGIN_DB,
|
|
9
12
|
ssl: {
|
|
10
13
|
rejectUnauthorized: true,
|
|
11
14
|
},
|
|
12
|
-
|
|
13
|
-
// Connection pool tuning for serverless/ephemeral environments (Vercel)
|
|
14
|
-
// - keep max small to avoid exhausting DB connections
|
|
15
|
-
// - reduce idle time so connections are returned sooner
|
|
16
|
-
// - set a short connection timeout to fail fast
|
|
17
|
-
max: 10,
|
|
15
|
+
max: 3,
|
|
18
16
|
idleTimeoutMillis: 10000,
|
|
19
|
-
connectionTimeoutMillis:
|
|
17
|
+
connectionTimeoutMillis: 10000,
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
export const dblogin = new Pool(poolConfig);
|
|
23
21
|
|
|
22
|
+
// Keep pool.js focused on pool setup; attach dev-only query logger from dedicated module.
|
|
23
|
+
attachDevQueryLogger(dblogin);
|
|
24
24
|
|
|
25
|
+
/*
|
|
26
|
+
attachDevQueryLogger([
|
|
27
|
+
{ pool: dblogin, name: "loginDB" },
|
|
28
|
+
{ pool: dblogin1, name: "loginDB1" },
|
|
29
|
+
]);
|
|
30
|
+
*/
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import path from "path";
|
|
29
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
30
|
-
|
|
31
|
-
const isDev = process.env.env === 'dev';
|
|
32
|
-
const requestContext = isDev ? new AsyncLocalStorage() : null;
|
|
33
|
-
|
|
34
|
-
export const runWithRequestContext = (req, fn) => {
|
|
35
|
-
if (!isDev || !requestContext) return fn();
|
|
36
|
-
return requestContext.run({ req }, fn);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const getRequestContext = () => {
|
|
40
|
-
if (!isDev || !requestContext) return undefined;
|
|
41
|
-
return requestContext.getStore();
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
if (isDev) {
|
|
45
|
-
|
|
46
|
-
// Simple counter for all DB requests made via this pool. This is intentionally
|
|
47
|
-
// lightweight.
|
|
48
|
-
let _dbQueryCount = 0;
|
|
49
|
-
const _dbQueryLog = [];
|
|
50
|
-
const _MAX_QUERY_LOG_ENTRIES = 1000;
|
|
51
|
-
|
|
52
|
-
const _origQuery = dblogin.query.bind(dblogin);
|
|
53
|
-
|
|
54
|
-
dblogin.query = (...args) => {
|
|
55
|
-
_dbQueryCount++;
|
|
56
|
-
|
|
57
|
-
// Track query text for debugging/metrics.
|
|
58
|
-
// `pg` supports (text, values, callback) or (config, callback).
|
|
59
|
-
let queryText = '';
|
|
60
|
-
let queryName = '';
|
|
61
|
-
let queryValues;
|
|
62
|
-
try {
|
|
63
|
-
if (typeof args[0] === 'string') {
|
|
64
|
-
queryText = args[0];
|
|
65
|
-
queryValues = Array.isArray(args[1]) ? args[1] : undefined;
|
|
66
|
-
} else if (args[0] && typeof args[0] === 'object') {
|
|
67
|
-
queryText = args[0].text || '';
|
|
68
|
-
queryName = args[0].name || '';
|
|
69
|
-
queryValues = Array.isArray(args[0].values) ? args[0].values : undefined;
|
|
70
|
-
}
|
|
71
|
-
} catch {
|
|
72
|
-
queryText = '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!queryText) {
|
|
76
|
-
return _origQuery(...args);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const startTime = process.hrtime.bigint();
|
|
80
|
-
const toWorkspacePath = (filePath) => {
|
|
81
|
-
const rel = path.relative(process.cwd(), filePath) || filePath;
|
|
82
|
-
return rel.replace(/\\/g, '/');
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const buildCallsite = () => {
|
|
86
|
-
try {
|
|
87
|
-
const stack = new Error().stack || '';
|
|
88
|
-
const lines = stack.split('\n').map(l => l.trim());
|
|
89
|
-
// Skip frames from this wrapper and node internals; pick first app frame.
|
|
90
|
-
const frame = lines.find((line) =>
|
|
91
|
-
line.startsWith('at ') &&
|
|
92
|
-
!line.includes('/lib/pool.js') &&
|
|
93
|
-
!line.includes('node:internal') &&
|
|
94
|
-
!line.includes('internal/process')
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
if (!frame) return null;
|
|
98
|
-
|
|
99
|
-
const withFunc = /^at\s+([^\s(]+)\s+\((.+):([0-9]+):([0-9]+)\)$/.exec(frame);
|
|
100
|
-
const noFunc = /^at\s+(.+):([0-9]+):([0-9]+)$/.exec(frame);
|
|
101
|
-
|
|
102
|
-
if (withFunc) {
|
|
103
|
-
return {
|
|
104
|
-
function: withFunc[1],
|
|
105
|
-
file: toWorkspacePath(withFunc[2]),
|
|
106
|
-
line: Number(withFunc[3]),
|
|
107
|
-
column: Number(withFunc[4])
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (noFunc) {
|
|
111
|
-
return {
|
|
112
|
-
function: null,
|
|
113
|
-
file: toWorkspacePath(noFunc[1]),
|
|
114
|
-
line: Number(noFunc[2]),
|
|
115
|
-
column: Number(noFunc[3])
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const buildRequestContext = () => {
|
|
125
|
-
const store = getRequestContext();
|
|
126
|
-
const req = store?.req;
|
|
127
|
-
if (!req) return null;
|
|
128
|
-
|
|
129
|
-
const user = req.session?.user || null;
|
|
130
|
-
return {
|
|
131
|
-
method: req.method,
|
|
132
|
-
url: req.originalUrl || req.url,
|
|
133
|
-
ip: req.ip,
|
|
134
|
-
userId: user?.id || null,
|
|
135
|
-
username: user?.username || null
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const callsiteSnapshot = buildCallsite();
|
|
140
|
-
|
|
141
|
-
const recordLog = (success, error) => {
|
|
142
|
-
const durationMs = Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
143
|
-
const request = buildRequestContext();
|
|
144
|
-
|
|
145
|
-
_dbQueryLog.push({
|
|
146
|
-
time: new Date().toISOString(),
|
|
147
|
-
query: queryText,
|
|
148
|
-
name: queryName || undefined,
|
|
149
|
-
values: queryValues,
|
|
150
|
-
durationMs,
|
|
151
|
-
success,
|
|
152
|
-
error: error ? { message: error.message, code: error.code } : undefined,
|
|
153
|
-
request,
|
|
154
|
-
pool: {
|
|
155
|
-
total: dblogin.totalCount,
|
|
156
|
-
idle: dblogin.idleCount,
|
|
157
|
-
waiting: dblogin.waitingCount
|
|
158
|
-
},
|
|
159
|
-
callsite: callsiteSnapshot
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (_dbQueryLog.length > _MAX_QUERY_LOG_ENTRIES) {
|
|
163
|
-
_dbQueryLog.shift();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const result = _origQuery(...args);
|
|
169
|
-
if (result && typeof result.then === 'function') {
|
|
170
|
-
return result
|
|
171
|
-
.then((res) => {
|
|
172
|
-
recordLog(true, null);
|
|
173
|
-
return res;
|
|
174
|
-
})
|
|
175
|
-
.catch((err) => {
|
|
176
|
-
recordLog(false, err);
|
|
177
|
-
throw err;
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
recordLog(true, null);
|
|
182
|
-
return result;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
recordLog(false, err);
|
|
185
|
-
throw err;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Public helpers
|
|
190
|
-
|
|
191
|
-
dblogin.getQueryCount = () => _dbQueryCount;
|
|
192
|
-
dblogin.resetQueryCount = () => {
|
|
193
|
-
_dbQueryCount = 0;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
dblogin.getQueryLog = (options = {}) => {
|
|
197
|
-
const { limit } = options;
|
|
198
|
-
if (typeof limit === 'number') {
|
|
199
|
-
return _dbQueryLog.slice(-limit);
|
|
200
|
-
}
|
|
201
|
-
return [..._dbQueryLog];
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
dblogin.resetQueryLog = () => {
|
|
205
|
-
_dbQueryLog.length = 0;
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
32
|
+
/*
|
|
209
33
|
(async () => {
|
|
210
34
|
try {
|
|
211
35
|
const client = await dblogin.connect();
|
|
@@ -213,4 +37,5 @@ if (isDev) {
|
|
|
213
37
|
} catch (err) {
|
|
214
38
|
console.error("[mbkauthe] Database connection error (pool):", err);
|
|
215
39
|
}
|
|
216
|
-
})();
|
|
40
|
+
})();
|
|
41
|
+
*/
|
package/lib/routes/auth.js
CHANGED
|
@@ -19,10 +19,13 @@ const router = express.Router();
|
|
|
19
19
|
|
|
20
20
|
// Helper function to clear profile picture cache
|
|
21
21
|
function clearProfilePicCache(req, username) {
|
|
22
|
-
if (req.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (!req || !req.res || !username) return;
|
|
23
|
+
|
|
24
|
+
const cookieUsername = req.cookies?.profileImageUser;
|
|
25
|
+
if (cookieUsername && cookieUsername !== username) return;
|
|
26
|
+
|
|
27
|
+
req.res.clearCookie('profileImageUrl', cachedClearCookieOptions);
|
|
28
|
+
req.res.clearCookie('profileImageUser', cachedClearCookieOptions);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
// Rate limiters for auth routes
|
|
@@ -285,6 +288,9 @@ export async function completeLoginProcess(req, res, user, redirectUrl = null, t
|
|
|
285
288
|
}
|
|
286
289
|
// Cache display name client-side to avoid extra DB lookups
|
|
287
290
|
res.cookie("fullName", req.session.user.fullname || username, { ...cachedCookieOptions, httpOnly: false });
|
|
291
|
+
const profileImageForCookie = loginProfileImage && typeof loginProfileImage === 'string' ? loginProfileImage : 'default';
|
|
292
|
+
res.cookie('profileImageUrl', profileImageForCookie, { ...cachedCookieOptions, httpOnly: false });
|
|
293
|
+
res.cookie('profileImageUser', username, { ...cachedCookieOptions, httpOnly: false });
|
|
288
294
|
// Record which method was used to login (client-visible badge)
|
|
289
295
|
if (method && typeof method === 'string') {
|
|
290
296
|
try {
|
|
@@ -561,9 +567,6 @@ router.post("/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
|
|
|
561
567
|
const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
|
|
562
568
|
|
|
563
569
|
try {
|
|
564
|
-
// Use cached allowedApps from preAuthUser to avoid extra database join
|
|
565
|
-
const cachedAllowedApps = req.session.preAuthUser?.allowedApps;
|
|
566
|
-
|
|
567
570
|
const query = `SELECT tfa."TwoFASecret" FROM "TwoFA" tfa WHERE tfa."UserName" = $1`;
|
|
568
571
|
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
569
572
|
|
|
@@ -574,7 +577,7 @@ router.post("/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
|
|
|
574
577
|
}
|
|
575
578
|
|
|
576
579
|
const sharedSecret = twoFAResult.rows[0].TwoFASecret;
|
|
577
|
-
const allowedApps =
|
|
580
|
+
const allowedApps = req.session.preAuthUser?.allowedApps;
|
|
578
581
|
const tokenValidates = speakeasy.totp.verify({
|
|
579
582
|
secret: sharedSecret,
|
|
580
583
|
encoding: "base32",
|
|
@@ -771,6 +774,9 @@ router.post("/api/switch-session", LoginLimit, async (req, res) => {
|
|
|
771
774
|
|
|
772
775
|
// Sync sessionId cookie and remember list
|
|
773
776
|
res.cookie('fullName', fullName, { ...cachedCookieOptions, httpOnly: false });
|
|
777
|
+
const switchProfileForCookie = switchProfileImage && typeof switchProfileImage === 'string' ? switchProfileImage : 'default';
|
|
778
|
+
res.cookie('profileImageUrl', switchProfileForCookie, { ...cachedCookieOptions, httpOnly: false });
|
|
779
|
+
res.cookie('profileImageUser', row.UserName, { ...cachedCookieOptions, httpOnly: false });
|
|
774
780
|
const encryptedSid = encryptSessionId(row.sid);
|
|
775
781
|
if (encryptedSid) {
|
|
776
782
|
res.cookie('sessionId', encryptedSid, cachedCookieOptions);
|