@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@javagt/express-easy-auth",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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
- const data = await res.json().catch(() => ({}));
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 function initUserDb(dataDir) {
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, initUserDb, authDb, userDb } from './db/init.js';
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,
@@ -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
+ }
@@ -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) {
@@ -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
  }
File without changes