@javagt/express-easy-auth 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/.env.example +13 -0
- package/demo/profileRouter.js +64 -0
- package/demo/public/css/style.css +1293 -0
- package/demo/public/index.html +272 -0
- package/demo/public/js/app.js +540 -0
- package/demo/server.js +195 -0
- package/examples/01-basic-setup.js +118 -0
- package/examples/02-passkeys.js +106 -0
- package/examples/03-api-keys.js +108 -0
- package/examples/04-totp-setup.js +125 -0
- package/examples/05-custom-logger.js +105 -0
- package/examples/06-password-reset.js +104 -0
- package/examples/08-external-db-linking.js +158 -0
- package/examples/README.md +32 -0
- package/openapi.yaml +263 -0
- package/package.json +35 -0
- package/readme.md +165 -0
- package/scratch/debug_bindings.js +29 -0
- package/scratch/test_sqlite.js +7 -0
- package/scratch/test_sqlite_multargs.js +17 -0
- package/scratch/test_sqlite_undefined.js +9 -0
- package/scratch/verify_sqlite_fix.js +14 -0
- package/src/client.js +295 -0
- package/src/db/init.js +203 -0
- package/src/db/sessionStore.js +67 -0
- package/src/index.js +61 -0
- package/src/middleware/auth.js +111 -0
- package/src/routes/auth.js +569 -0
- package/src/utils/authHelpers.js +48 -0
- package/src/utils/logger.js +71 -0
- package/test/auth.test.js +32 -0
- package/test/passkeys.test.js +19 -0
- package/test/user.test.js +29 -0
package/src/db/init.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
let authDb;
|
|
6
|
+
let userDb;
|
|
7
|
+
|
|
8
|
+
export function initAuthDb(dataDir) {
|
|
9
|
+
if (!authDb) {
|
|
10
|
+
if (!fs.existsSync(dataDir)) {
|
|
11
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
authDb = new DatabaseSync(path.join(dataDir, 'auth.db'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
authDb.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
username TEXT UNIQUE NOT NULL,
|
|
20
|
+
email TEXT UNIQUE NOT NULL,
|
|
21
|
+
password_hash TEXT NOT NULL,
|
|
22
|
+
totp_enabled INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
totp_secret TEXT,
|
|
24
|
+
mfa_required INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
locked_until INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
created_at INTEGER NOT NULL,
|
|
28
|
+
updated_at INTEGER NOT NULL
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
user_id TEXT, -- Nullable for anonymous/pending sessions
|
|
34
|
+
data TEXT NOT NULL,
|
|
35
|
+
created_at INTEGER NOT NULL,
|
|
36
|
+
expires_at INTEGER NOT NULL,
|
|
37
|
+
last_activity INTEGER NOT NULL,
|
|
38
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS totp_secrets (
|
|
42
|
+
user_id TEXT PRIMARY KEY,
|
|
43
|
+
secret TEXT NOT NULL,
|
|
44
|
+
verified INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
created_at INTEGER NOT NULL,
|
|
46
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS passkeys (
|
|
50
|
+
id TEXT PRIMARY KEY,
|
|
51
|
+
user_id TEXT NOT NULL,
|
|
52
|
+
credential_id TEXT UNIQUE NOT NULL,
|
|
53
|
+
public_key TEXT NOT NULL,
|
|
54
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
device_type TEXT,
|
|
56
|
+
backed_up INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
transports TEXT,
|
|
58
|
+
friendly_name TEXT,
|
|
59
|
+
created_at INTEGER NOT NULL,
|
|
60
|
+
last_used INTEGER,
|
|
61
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS webauthn_challenges (
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
user_id TEXT,
|
|
67
|
+
challenge TEXT NOT NULL,
|
|
68
|
+
type TEXT NOT NULL,
|
|
69
|
+
created_at INTEGER NOT NULL,
|
|
70
|
+
expires_at INTEGER NOT NULL
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS fresh_auth_tokens (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
user_id TEXT NOT NULL,
|
|
76
|
+
session_id TEXT NOT NULL,
|
|
77
|
+
verified_at INTEGER NOT NULL,
|
|
78
|
+
expires_at INTEGER NOT NULL,
|
|
79
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS recovery_codes (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
user_id TEXT NOT NULL,
|
|
85
|
+
code_hash TEXT NOT NULL,
|
|
86
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
created_at INTEGER NOT NULL,
|
|
88
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS system_logs (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
level TEXT NOT NULL,
|
|
94
|
+
source TEXT NOT NULL,
|
|
95
|
+
message TEXT NOT NULL,
|
|
96
|
+
stack TEXT,
|
|
97
|
+
context TEXT,
|
|
98
|
+
user_id TEXT,
|
|
99
|
+
timestamp INTEGER NOT NULL
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
|
103
|
+
token_hash TEXT PRIMARY KEY,
|
|
104
|
+
user_id TEXT NOT NULL,
|
|
105
|
+
expires_at INTEGER NOT NULL,
|
|
106
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
user_id TEXT NOT NULL,
|
|
113
|
+
key_hash TEXT UNIQUE NOT NULL,
|
|
114
|
+
name TEXT,
|
|
115
|
+
permissions TEXT, -- JSON string
|
|
116
|
+
created_at INTEGER NOT NULL,
|
|
117
|
+
last_used INTEGER,
|
|
118
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
122
|
+
key TEXT PRIMARY KEY,
|
|
123
|
+
value TEXT
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
-- Seed settings with new grouped keys
|
|
127
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('auth_mfa_force_all', 'false');
|
|
128
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('auth_mfa_force_new_users', 'false');
|
|
129
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('auth_registration_enabled', 'true');
|
|
130
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('lockout_duration_mins', '15');
|
|
131
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('lockout_max_attempts', '5');
|
|
132
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('password_min_length', '8');
|
|
133
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('password_reset_expiry_mins', '30');
|
|
134
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('session_duration_days', '7');
|
|
135
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('session_fresh_auth_mins', '5');
|
|
136
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('site_admin_emails', 'admin@example.com');
|
|
137
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('site_name', 'Authentication Server');
|
|
138
|
+
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_passkeys_user ON passkeys(user_id);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_challenges_expires ON webauthn_challenges(expires_at);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_fresh_auth_expires ON fresh_auth_tokens(expires_at);
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON system_logs(timestamp);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_logs_level ON system_logs(level);
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
// Migration: Rename old keys if they exist
|
|
149
|
+
const migrateMap = {
|
|
150
|
+
'force_2fa': 'auth_mfa_force_all',
|
|
151
|
+
'enforce_mfa_new_users': 'auth_mfa_force_new_users',
|
|
152
|
+
'registration_enabled': 'auth_registration_enabled',
|
|
153
|
+
'max_login_attempts': 'lockout_max_attempts',
|
|
154
|
+
'min_password_length': 'password_min_length',
|
|
155
|
+
'reset_token_expiry_mins': 'password_reset_expiry_mins',
|
|
156
|
+
'fresh_auth_duration': 'session_fresh_auth_mins',
|
|
157
|
+
'admin_email': 'site_admin_emails'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (const [oldKey, newKey] of Object.entries(migrateMap)) {
|
|
161
|
+
try {
|
|
162
|
+
authDb.prepare(`
|
|
163
|
+
UPDATE settings SET key = ?
|
|
164
|
+
WHERE key = ? AND NOT EXISTS (SELECT 1 FROM settings WHERE key = ?)
|
|
165
|
+
`).run(newKey, oldKey, newKey);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// Ignore migration errors (e.g. key already migrated)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Migration: Rename 'name' to 'friendly_name' in passkeys table if it exists
|
|
172
|
+
try {
|
|
173
|
+
const columns = authDb.prepare("PRAGMA table_info(passkeys)").all();
|
|
174
|
+
const hasName = columns.some(c => c.name === 'name');
|
|
175
|
+
const hasFriendlyName = columns.some(c => c.name === 'friendly_name');
|
|
176
|
+
|
|
177
|
+
if (hasName && !hasFriendlyName) {
|
|
178
|
+
authDb.exec("ALTER TABLE passkeys RENAME COLUMN name TO friendly_name");
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// Ignore table_info errors or if table doesn't exist yet
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
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 };
|
|
197
|
+
export function getAppSettings() {
|
|
198
|
+
const rows = authDb.prepare('SELECT key, value FROM settings').all();
|
|
199
|
+
return rows.reduce((acc, row) => {
|
|
200
|
+
acc[row.key] = row.value;
|
|
201
|
+
return acc;
|
|
202
|
+
}, {});
|
|
203
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import session from 'express-session';
|
|
2
|
+
import { authDb } from './init.js';
|
|
3
|
+
|
|
4
|
+
const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
5
|
+
|
|
6
|
+
export default class SQLiteSessionStore extends session.Store {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
setInterval(() => this._cleanup(), 15 * 60 * 1000).unref();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get db() { return authDb; }
|
|
13
|
+
|
|
14
|
+
get(sid, callback) {
|
|
15
|
+
try {
|
|
16
|
+
const row = this.db
|
|
17
|
+
.prepare('SELECT data FROM sessions WHERE id=? AND expires_at > unixepoch()')
|
|
18
|
+
.get(sid);
|
|
19
|
+
if (!row) {
|
|
20
|
+
console.log(`[session] GET ${sid} -> NOT FOUND`);
|
|
21
|
+
return callback(null, null);
|
|
22
|
+
}
|
|
23
|
+
console.log(`[session] GET ${sid} -> FOUND`);
|
|
24
|
+
callback(null, JSON.parse(row.data));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(`[session] GET ${sid} -> ERROR`, e);
|
|
27
|
+
callback(e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(sid, session, callback) {
|
|
32
|
+
try {
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
const expires = Math.floor((Date.now() + SESSION_TTL_MS) / 1000);
|
|
35
|
+
const data = JSON.stringify(session);
|
|
36
|
+
const userId = session.userId || null;
|
|
37
|
+
|
|
38
|
+
this.db.prepare(`
|
|
39
|
+
INSERT INTO sessions (id, user_id, data, created_at, expires_at, last_activity)
|
|
40
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
41
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
42
|
+
data=excluded.data,
|
|
43
|
+
user_id=excluded.user_id,
|
|
44
|
+
expires_at=excluded.expires_at,
|
|
45
|
+
last_activity=excluded.last_activity
|
|
46
|
+
`).run(sid, userId, data, now, expires, now);
|
|
47
|
+
console.log(`[session] SET ${sid} (user: ${userId})`);
|
|
48
|
+
callback(null);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(`[session] SET ${sid} -> ERROR`, e);
|
|
51
|
+
callback(e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
destroy(sid, callback) {
|
|
56
|
+
try {
|
|
57
|
+
this.db.prepare('DELETE FROM sessions WHERE id=?').run(sid);
|
|
58
|
+
callback(null);
|
|
59
|
+
} catch (e) { callback(e); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
touch(sid, session, callback) { this.set(sid, session, callback); }
|
|
63
|
+
|
|
64
|
+
_cleanup() {
|
|
65
|
+
try { this.db.prepare('DELETE FROM sessions WHERE expires_at <= unixepoch()').run(); } catch (_) { }
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { initAuthDb, initUserDb, authDb, userDb } from './db/init.js';
|
|
2
|
+
import SQLiteSessionStore from './db/sessionStore.js';
|
|
3
|
+
import authRouter from './routes/auth.js';
|
|
4
|
+
import { requireAuth, requireFreshAuth, requireApiKey, authErrorLogger } from './middleware/auth.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
import { DefaultLogger } from './utils/logger.js';
|
|
10
|
+
import { AuthClient } from './client.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initializes the authentication databases and configuration.
|
|
14
|
+
* @param {import('express').Application} app - The Express application.
|
|
15
|
+
* @param {Object} options - Configuration options.
|
|
16
|
+
* @param {string} [options.dataDir] - Directory to store SQLite databases. Defaults to './data'.
|
|
17
|
+
* @param {Object} [options.config] - Authentication configuration (domain, rpID, etc.).
|
|
18
|
+
* @param {string|boolean} [options.sdkRoute='/auth-sdk.js'] - Route to serve the frontend SDK. Set to false to disable.
|
|
19
|
+
* @param {boolean} [options.exposeErrors=false] - If true, detailed error messages are sent to the client.
|
|
20
|
+
* @param {Object} [options.logger] - Custom logger object. Should implement error, warn, info, debug.
|
|
21
|
+
*/
|
|
22
|
+
export function setupAuth(app, options = {}) {
|
|
23
|
+
const dataDir = options.dataDir || path.join(process.cwd(), 'data');
|
|
24
|
+
const config = options.config || {};
|
|
25
|
+
const sdkRoute = options.sdkRoute !== undefined ? options.sdkRoute : '/auth-sdk.js';
|
|
26
|
+
const exposeErrors = options.exposeErrors !== undefined ? options.exposeErrors : false;
|
|
27
|
+
const logger = options.logger || new DefaultLogger();
|
|
28
|
+
|
|
29
|
+
initAuthDb(dataDir);
|
|
30
|
+
initUserDb(dataDir);
|
|
31
|
+
|
|
32
|
+
// Store for middleware access
|
|
33
|
+
app.set('config', { ...config, exposeErrors });
|
|
34
|
+
app.set('logger', logger);
|
|
35
|
+
|
|
36
|
+
// Serve the frontend SDK if enabled
|
|
37
|
+
if (sdkRoute) {
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = dirname(__filename);
|
|
40
|
+
const sdkPath = path.join(__dirname, 'client.js');
|
|
41
|
+
|
|
42
|
+
app.get(sdkRoute, (req, res) => {
|
|
43
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
44
|
+
res.sendFile(sdkPath);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
authDb,
|
|
51
|
+
userDb,
|
|
52
|
+
SQLiteSessionStore,
|
|
53
|
+
authRouter,
|
|
54
|
+
authRouter as auth,
|
|
55
|
+
requireAuth,
|
|
56
|
+
requireFreshAuth,
|
|
57
|
+
requireApiKey,
|
|
58
|
+
authErrorLogger,
|
|
59
|
+
DefaultLogger,
|
|
60
|
+
AuthClient
|
|
61
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { randomUUID, randomBytes } from 'crypto';
|
|
2
|
+
import { authDb } from '../db/init.js';
|
|
3
|
+
import bcrypt from 'bcrypt';
|
|
4
|
+
|
|
5
|
+
const FRESH_AUTH_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
|
|
7
|
+
export function requireAuth(req, res, next) {
|
|
8
|
+
if (!req.session?.userId) {
|
|
9
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
10
|
+
}
|
|
11
|
+
req.userId = req.session.userId; // Convenience attachment
|
|
12
|
+
next();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function requireFreshAuth(req, res, next) {
|
|
16
|
+
if (!req.session?.userId) {
|
|
17
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
18
|
+
}
|
|
19
|
+
req.userId = req.session.userId; // Ensure it's there too
|
|
20
|
+
const lastAuthed = req.session.lastAuthedAt;
|
|
21
|
+
|
|
22
|
+
// Pull dynamic duration from settings
|
|
23
|
+
const rows = authDb.prepare("SELECT value FROM settings WHERE key='session_fresh_auth_mins'").get();
|
|
24
|
+
const durationMins = parseInt(rows?.value || '5', 10);
|
|
25
|
+
const windowMs = durationMins * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
if (!lastAuthed || Date.now() - lastAuthed > windowMs) {
|
|
28
|
+
return res.status(403).json({
|
|
29
|
+
error: 'Fresh authentication required',
|
|
30
|
+
code: 'FRESH_AUTH_REQUIRED',
|
|
31
|
+
freshAuthWindowMs: windowMs
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
next();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Middleware to authenticate requests via API Key.
|
|
39
|
+
* Supports X-API-Key header or Authorization: Bearer <key>.
|
|
40
|
+
*/
|
|
41
|
+
export async function requireApiKey(req, res, next) {
|
|
42
|
+
let key = req.get('X-API-Key');
|
|
43
|
+
if (!key) {
|
|
44
|
+
const authHeader = req.get('Authorization');
|
|
45
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
46
|
+
key = authHeader.substring(7);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!key) {
|
|
51
|
+
return res.status(401).json({ error: 'API Key required' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parts = key.split('_');
|
|
55
|
+
if (parts.length < 4) {
|
|
56
|
+
return res.status(401).json({ error: 'Invalid API Key format' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const keyId = parts[2];
|
|
60
|
+
const secret = parts[3];
|
|
61
|
+
|
|
62
|
+
const apiKey = authDb.prepare('SELECT * FROM api_keys WHERE id = ?').get(keyId);
|
|
63
|
+
if (!apiKey) {
|
|
64
|
+
return res.status(401).json({ error: 'Invalid API Key' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const valid = await bcrypt.compare(secret, apiKey.key_hash);
|
|
68
|
+
if (!valid) {
|
|
69
|
+
return res.status(401).json({ error: 'Invalid API Key' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Update last used
|
|
73
|
+
authDb.prepare('UPDATE api_keys SET last_used = ? WHERE id = ?').run(Date.now(), keyId);
|
|
74
|
+
|
|
75
|
+
req.userId = apiKey.user_id;
|
|
76
|
+
req.permissions = JSON.parse(apiKey.permissions || '[]');
|
|
77
|
+
next();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Global error handler that logs via the configured logger
|
|
82
|
+
*/
|
|
83
|
+
export function authErrorLogger(err, req, res, next) {
|
|
84
|
+
const logger = req.app.get('logger');
|
|
85
|
+
const config = req.app.get('config') || {};
|
|
86
|
+
const exposeErrors = config.exposeErrors;
|
|
87
|
+
|
|
88
|
+
if (logger) {
|
|
89
|
+
logger.error(err.message || String(err), {
|
|
90
|
+
err,
|
|
91
|
+
source: 'server',
|
|
92
|
+
userId: req.session?.userId || null,
|
|
93
|
+
context: {
|
|
94
|
+
url: req.url,
|
|
95
|
+
method: req.method,
|
|
96
|
+
ip: req.ip,
|
|
97
|
+
userAgent: req.get('user-agent'),
|
|
98
|
+
requestId: req.get('x-request-id') // If available
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
console.error('[auth-server] Logger not found, falling back to console:', err);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (res.headersSent) return next(err);
|
|
106
|
+
|
|
107
|
+
res.status(500).json({
|
|
108
|
+
error: exposeErrors ? (err.message || 'Internal server error') : 'Internal server error',
|
|
109
|
+
...(exposeErrors && { stack: err.stack })
|
|
110
|
+
});
|
|
111
|
+
}
|