@javagt/express-easy-auth 1.0.2 → 1.0.4
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/demo/public/js/components/FreshAuthModal.js +110 -0
- package/package.json +1 -1
- package/src/client.js +17 -1
- package/src/db/init.js +3 -13
- package/src/index.js +1 -3
- package/src/middleware/auth.js +3 -2
- package/src/middleware/requestId.js +16 -0
- package/src/routes/auth.js +4 -1
- package/src/utils/logger.js +8 -7
- package/data-test-v2/users.db +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FreshAuthModal - A reference UI component for handling "Sudo Mode" (requireFreshAuth).
|
|
3
|
+
*
|
|
4
|
+
* This component listens for 403 FRESH_AUTH_REQUIRED errors and prompts the user
|
|
5
|
+
* to re-authenticate without leaving the current page.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class FreshAuthModal {
|
|
9
|
+
constructor(authClient) {
|
|
10
|
+
this.auth = authClient;
|
|
11
|
+
this.modal = null;
|
|
12
|
+
this._initStyles();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shows the re-authentication modal.
|
|
17
|
+
* @returns {Promise<boolean>} - Resolves to true if re-auth succeeded, false otherwise.
|
|
18
|
+
*/
|
|
19
|
+
async prompt() {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
this._createModal(resolve);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_initStyles() {
|
|
26
|
+
if (document.getElementById('fresh-auth-styles')) return;
|
|
27
|
+
const style = document.createElement('style');
|
|
28
|
+
style.id = 'fresh-auth-styles';
|
|
29
|
+
style.textContent = `
|
|
30
|
+
.fa-modal-overlay {
|
|
31
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
32
|
+
background: rgba(0,0,0,0.5); display: flex; align-items: center;
|
|
33
|
+
justify-content: center; z-index: 9999; font-family: sans-serif;
|
|
34
|
+
}
|
|
35
|
+
.fa-modal {
|
|
36
|
+
background: white; padding: 2rem; border-radius: 8px; width: 100%; max-width: 400px;
|
|
37
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
|
38
|
+
}
|
|
39
|
+
.fa-modal h2 { margin-top: 0; color: #333; }
|
|
40
|
+
.fa-modal p { color: #666; font-size: 0.9rem; margin-bottom: 1.5rem; }
|
|
41
|
+
.fa-modal input {
|
|
42
|
+
width: 100%; padding: 0.8rem; margin-bottom: 1rem; border: 1px solid #ddd;
|
|
43
|
+
border-radius: 4px; box-sizing: border-box;
|
|
44
|
+
}
|
|
45
|
+
.fa-modal button {
|
|
46
|
+
width: 100%; padding: 0.8rem; background: #007bff; color: white;
|
|
47
|
+
border: none; border-radius: 4px; cursor: pointer; font-weight: bold;
|
|
48
|
+
}
|
|
49
|
+
.fa-modal button:hover { background: #0056b3; }
|
|
50
|
+
.fa-modal .cancel {
|
|
51
|
+
background: none; color: #888; margin-top: 0.5rem; font-weight: normal;
|
|
52
|
+
}
|
|
53
|
+
.fa-error { color: #dc3545; font-size: 0.8rem; margin-bottom: 1rem; display: none; }
|
|
54
|
+
`;
|
|
55
|
+
document.head.appendChild(style);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_createModal(resolve) {
|
|
59
|
+
const overlay = document.createElement('div');
|
|
60
|
+
overlay.className = 'fa-modal-overlay';
|
|
61
|
+
overlay.innerHTML = `
|
|
62
|
+
<div class="fa-modal">
|
|
63
|
+
<h2>Confirm Identity</h2>
|
|
64
|
+
<p>This action requires recent authentication. Please enter your password to continue.</p>
|
|
65
|
+
<div class="fa-error" id="fa-error-msg"></div>
|
|
66
|
+
<input type="password" id="fa-password" placeholder="Password" autofocus>
|
|
67
|
+
<button id="fa-confirm">Confirm</button>
|
|
68
|
+
<button id="fa-cancel" class="cancel">Cancel</button>
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
document.body.appendChild(overlay);
|
|
73
|
+
|
|
74
|
+
const passwordInput = overlay.querySelector('#fa-password');
|
|
75
|
+
const confirmBtn = overlay.querySelector('#fa-confirm');
|
|
76
|
+
const cancelBtn = overlay.querySelector('#fa-cancel');
|
|
77
|
+
const errorMsg = overlay.querySelector('#fa-error-msg');
|
|
78
|
+
|
|
79
|
+
const handleConfirm = async () => {
|
|
80
|
+
confirmBtn.disabled = true;
|
|
81
|
+
confirmBtn.textContent = 'Verifying...';
|
|
82
|
+
errorMsg.style.display = 'none';
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const password = passwordInput.value;
|
|
86
|
+
if (!password) throw new Error('Password is required');
|
|
87
|
+
|
|
88
|
+
await this.auth.request('/fresh-auth', {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: { password }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
document.body.removeChild(overlay);
|
|
94
|
+
resolve(true);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
errorMsg.textContent = err.message || 'Verification failed';
|
|
97
|
+
errorMsg.style.display = 'block';
|
|
98
|
+
confirmBtn.disabled = false;
|
|
99
|
+
confirmBtn.textContent = 'Confirm';
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
confirmBtn.onclick = handleConfirm;
|
|
104
|
+
passwordInput.onkeypress = (e) => { if (e.key === 'Enter') handleConfirm(); };
|
|
105
|
+
cancelBtn.onclick = () => {
|
|
106
|
+
document.body.removeChild(overlay);
|
|
107
|
+
resolve(false);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -59,7 +59,23 @@ export class AuthClient {
|
|
|
59
59
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
let data = {};
|
|
63
|
+
const text = await res.text().catch(() => '');
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
data = JSON.parse(text);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// If it's not JSON, check if it's HTML
|
|
69
|
+
if (text.trim().toLowerCase().startsWith('<!doctype html') || text.trim().toLowerCase().startsWith('<html')) {
|
|
70
|
+
const titleMatch = text.match(/<title>(.*?)<\/title>/i);
|
|
71
|
+
const title = titleMatch ? titleMatch[1] : 'HTML Error Page';
|
|
72
|
+
const bodySnippet = text.replace(/<[^>]*>/g, '').substring(0, 100).trim();
|
|
73
|
+
data = { error: `Server returned HTML instead of JSON: "${title}". Snippet: ${bodySnippet}...` };
|
|
74
|
+
} else {
|
|
75
|
+
data = { error: text.substring(0, 100) || 'Unknown error' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
if (!res.ok) {
|
|
64
80
|
throw new AuthError(data.error || 'Request failed', res.status, data.code, data);
|
|
65
81
|
}
|
package/src/db/init.js
CHANGED
|
@@ -3,7 +3,6 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
let authDb;
|
|
6
|
-
let userDb;
|
|
7
6
|
|
|
8
7
|
export function initAuthDb(dataDir) {
|
|
9
8
|
if (!authDb) {
|
|
@@ -96,6 +95,7 @@ export function initAuthDb(dataDir) {
|
|
|
96
95
|
stack TEXT,
|
|
97
96
|
context TEXT,
|
|
98
97
|
user_id TEXT,
|
|
98
|
+
correlation_id TEXT,
|
|
99
99
|
timestamp INTEGER NOT NULL
|
|
100
100
|
);
|
|
101
101
|
|
|
@@ -143,6 +143,7 @@ export function initAuthDb(dataDir) {
|
|
|
143
143
|
CREATE INDEX IF NOT EXISTS idx_fresh_auth_expires ON fresh_auth_tokens(expires_at);
|
|
144
144
|
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON system_logs(timestamp);
|
|
145
145
|
CREATE INDEX IF NOT EXISTS idx_logs_level ON system_logs(level);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_logs_correlation ON system_logs(correlation_id);
|
|
146
147
|
`);
|
|
147
148
|
|
|
148
149
|
// Migration: Rename old keys if they exist
|
|
@@ -182,18 +183,7 @@ export function initAuthDb(dataDir) {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
export
|
|
186
|
-
if (!userDb) {
|
|
187
|
-
if (!fs.existsSync(dataDir)) {
|
|
188
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
189
|
-
}
|
|
190
|
-
userDb = new DatabaseSync(path.join(dataDir, 'users.db'));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Profile table removed from core library. Implement in demo if needed.
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export { authDb, userDb };
|
|
186
|
+
export { authDb };
|
|
197
187
|
export function getAppSettings() {
|
|
198
188
|
const rows = authDb.prepare('SELECT key, value FROM settings').all();
|
|
199
189
|
return rows.reduce((acc, row) => {
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { initAuthDb,
|
|
1
|
+
import { initAuthDb, authDb } from './db/init.js';
|
|
2
2
|
import SQLiteSessionStore from './db/sessionStore.js';
|
|
3
3
|
import authRouter from './routes/auth.js';
|
|
4
4
|
import { requireAuth, requireFreshAuth, requireApiKey, authErrorLogger } from './middleware/auth.js';
|
|
@@ -27,7 +27,6 @@ export function setupAuth(app, options = {}) {
|
|
|
27
27
|
const logger = options.logger || new DefaultLogger();
|
|
28
28
|
|
|
29
29
|
initAuthDb(dataDir);
|
|
30
|
-
initUserDb(dataDir);
|
|
31
30
|
|
|
32
31
|
// Store for middleware access
|
|
33
32
|
app.set('config', { ...config, exposeErrors });
|
|
@@ -48,7 +47,6 @@ export function setupAuth(app, options = {}) {
|
|
|
48
47
|
|
|
49
48
|
export {
|
|
50
49
|
authDb,
|
|
51
|
-
userDb,
|
|
52
50
|
SQLiteSessionStore,
|
|
53
51
|
authRouter,
|
|
54
52
|
authRouter as auth,
|
package/src/middleware/auth.js
CHANGED
|
@@ -90,12 +90,12 @@ export function authErrorLogger(err, req, res, next) {
|
|
|
90
90
|
err,
|
|
91
91
|
source: 'server',
|
|
92
92
|
userId: req.session?.userId || null,
|
|
93
|
+
correlationId: req.id,
|
|
93
94
|
context: {
|
|
94
95
|
url: req.url,
|
|
95
96
|
method: req.method,
|
|
96
97
|
ip: req.ip,
|
|
97
|
-
userAgent: req.get('user-agent')
|
|
98
|
-
requestId: req.get('x-request-id') // If available
|
|
98
|
+
userAgent: req.get('user-agent')
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
} else {
|
|
@@ -106,6 +106,7 @@ export function authErrorLogger(err, req, res, next) {
|
|
|
106
106
|
|
|
107
107
|
res.status(500).json({
|
|
108
108
|
error: exposeErrors ? (err.message || 'Internal server error') : 'Internal server error',
|
|
109
|
+
correlationId: req.id,
|
|
109
110
|
...(exposeErrors && { stack: err.stack })
|
|
110
111
|
});
|
|
111
112
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware to generate and attach a correlation ID to each request.
|
|
5
|
+
* It also attaches it to the response header.
|
|
6
|
+
*/
|
|
7
|
+
export function requestId(req, res, next) {
|
|
8
|
+
const headerName = 'X-Correlation-ID';
|
|
9
|
+
const correlationId = req.get(headerName) || randomUUID();
|
|
10
|
+
|
|
11
|
+
req.id = correlationId;
|
|
12
|
+
req.correlationId = correlationId; // Alias for clarity
|
|
13
|
+
|
|
14
|
+
res.set(headerName, correlationId);
|
|
15
|
+
next();
|
|
16
|
+
}
|
package/src/routes/auth.js
CHANGED
|
@@ -13,7 +13,10 @@ import { authDb, getAppSettings } from '../db/init.js';
|
|
|
13
13
|
import { requireAuth, requireFreshAuth } from '../middleware/auth.js';
|
|
14
14
|
import { generateRecoveryCodes, generateResetToken, getAuthResponse } from '../utils/authHelpers.js';
|
|
15
15
|
|
|
16
|
+
import { requestId } from '../middleware/requestId.js';
|
|
17
|
+
|
|
16
18
|
const router = Router();
|
|
19
|
+
router.use(requestId);
|
|
17
20
|
|
|
18
21
|
const SALT_ROUNDS = 12;
|
|
19
22
|
|
|
@@ -312,7 +315,7 @@ router.post('/2fa/verify-setup', requireAuth, (req, res) => {
|
|
|
312
315
|
|
|
313
316
|
authDb.prepare('UPDATE users SET totp_secret=?, totp_enabled=1 WHERE id=?').run(secret, req.session.userId);
|
|
314
317
|
|
|
315
|
-
generateRecoveryCodes(10).then(({ plain: codes, hashes }) => {
|
|
318
|
+
generateRecoveryCodes(10).then(({ plain: codes, hashed: hashes }) => {
|
|
316
319
|
const now = Date.now();
|
|
317
320
|
const stmt = authDb.prepare('INSERT INTO recovery_codes (id, user_id, code_hash, created_at) VALUES (?, ?, ?, ?)');
|
|
318
321
|
for (const hash of hashes) {
|
package/src/utils/logger.js
CHANGED
|
@@ -23,7 +23,7 @@ export class DefaultLogger extends Logger {
|
|
|
23
23
|
|
|
24
24
|
error(message, metadata = {}) {
|
|
25
25
|
if (this.console) {
|
|
26
|
-
console.error(`[error] ${message}`, metadata.err || '');
|
|
26
|
+
console.error(`[error]${metadata.correlationId ? ` [${metadata.correlationId}]` : ''} ${message}`, metadata.err || '');
|
|
27
27
|
}
|
|
28
28
|
if (this.db) {
|
|
29
29
|
this._logToDb('error', message, metadata);
|
|
@@ -31,28 +31,28 @@ export class DefaultLogger extends Logger {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
warn(message, metadata = {}) {
|
|
34
|
-
if (this.console) console.warn(`[warn] ${message}`, metadata);
|
|
34
|
+
if (this.console) console.warn(`[warn]${metadata.correlationId ? ` [${metadata.correlationId}]` : ''} ${message}`, metadata);
|
|
35
35
|
if (this.db) this._logToDb('warn', message, metadata);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
info(message, metadata = {}) {
|
|
39
|
-
if (this.console) console.info(`[info] ${message}`, metadata);
|
|
39
|
+
if (this.console) console.info(`[info]${metadata.correlationId ? ` [${metadata.correlationId}]` : ''} ${message}`, metadata);
|
|
40
40
|
if (this.db) this._logToDb('info', message, metadata);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
debug(message, metadata = {}) {
|
|
44
|
-
if (this.console) console.debug(`[debug] ${message}`, metadata);
|
|
44
|
+
if (this.console) console.debug(`[debug]${metadata.correlationId ? ` [${metadata.correlationId}]` : ''} ${message}`, metadata);
|
|
45
45
|
// Usually don't log debug to DB unless specified, to save space
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
_logToDb(level, message, metadata) {
|
|
49
49
|
try {
|
|
50
50
|
if (authDb) {
|
|
51
|
-
const { err, source = 'server', context = {}, userId = null } = metadata;
|
|
51
|
+
const { err, source = 'server', context = {}, userId = null, correlationId = null } = metadata;
|
|
52
52
|
|
|
53
53
|
authDb.prepare(`
|
|
54
|
-
INSERT INTO system_logs (id, level, source, message, stack, context, user_id, timestamp)
|
|
55
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
INSERT INTO system_logs (id, level, source, message, stack, context, user_id, correlation_id, timestamp)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
56
56
|
`).run(
|
|
57
57
|
randomUUID(),
|
|
58
58
|
level,
|
|
@@ -61,6 +61,7 @@ export class DefaultLogger extends Logger {
|
|
|
61
61
|
err?.stack || null,
|
|
62
62
|
JSON.stringify(context),
|
|
63
63
|
userId,
|
|
64
|
+
correlationId,
|
|
64
65
|
Date.now()
|
|
65
66
|
);
|
|
66
67
|
}
|
package/data-test-v2/users.db
DELETED
|
File without changes
|