@qwickapps/server 1.0.0 → 1.1.6
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 +178 -79
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +37 -42
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +32 -13
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +144 -81
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +167 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/types.d.ts +104 -11
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/frontend-app-plugin.d.ts +39 -0
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -0
- package/dist/plugins/frontend-app-plugin.js +176 -0
- package/dist/plugins/frontend-app-plugin.js.map +1 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/index.js.map +1 -1
- package/package.json +7 -2
- package/src/core/control-panel.ts +41 -50
- package/src/core/gateway.ts +186 -105
- package/src/core/guards.ts +190 -0
- package/src/core/types.ts +115 -9
- package/src/index.ts +18 -0
- package/src/plugins/frontend-app-plugin.ts +211 -0
- package/src/plugins/index.ts +3 -0
package/src/core/gateway.ts
CHANGED
|
@@ -23,7 +23,6 @@ import type { ControlPanelConfig, ControlPanelPlugin, Logger } from './types.js'
|
|
|
23
23
|
import { createControlPanel } from './control-panel.js';
|
|
24
24
|
import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './logging.js';
|
|
25
25
|
import { createProxyMiddleware, type Options } from 'http-proxy-middleware';
|
|
26
|
-
import { randomBytes } from 'crypto';
|
|
27
26
|
import express from 'express';
|
|
28
27
|
import { existsSync } from 'fs';
|
|
29
28
|
import { resolve } from 'path';
|
|
@@ -56,31 +55,47 @@ export interface GatewayConfig {
|
|
|
56
55
|
/** Quick links for the control panel */
|
|
57
56
|
links?: ControlPanelConfig['links'];
|
|
58
57
|
|
|
59
|
-
/** Path to custom React UI dist folder */
|
|
58
|
+
/** Path to custom React UI dist folder for the control panel */
|
|
60
59
|
customUiPath?: string;
|
|
61
60
|
|
|
62
61
|
/**
|
|
63
|
-
*
|
|
64
|
-
* Defaults to
|
|
65
|
-
* The gateway always proxies /health to the internal service.
|
|
62
|
+
* Mount path for the control panel.
|
|
63
|
+
* Defaults to '/cpanel'.
|
|
66
64
|
*/
|
|
67
|
-
|
|
65
|
+
controlPanelPath?: string;
|
|
68
66
|
|
|
69
67
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* - 'basic': HTTP Basic Auth with username/password
|
|
73
|
-
* - 'auto': Auto-generate password on startup (default)
|
|
68
|
+
* Route guard for the control panel.
|
|
69
|
+
* Defaults to auto-generated basic auth.
|
|
74
70
|
*/
|
|
75
|
-
|
|
71
|
+
controlPanelGuard?: ControlPanelConfig['guard'];
|
|
76
72
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Frontend app configuration for the root path (/).
|
|
75
|
+
* If not provided, root path is not handled by the gateway.
|
|
76
|
+
*/
|
|
77
|
+
frontendApp?: {
|
|
78
|
+
/** Redirect to another URL */
|
|
79
|
+
redirectUrl?: string;
|
|
80
|
+
/** Path to static files to serve */
|
|
81
|
+
staticPath?: string;
|
|
82
|
+
/** Landing page configuration */
|
|
83
|
+
landingPage?: {
|
|
84
|
+
title: string;
|
|
85
|
+
heading?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
links?: Array<{ label: string; url: string }>;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
79
90
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
91
|
+
/**
|
|
92
|
+
* API paths to proxy to the internal service.
|
|
93
|
+
* Defaults to ['/api/v1'] if not specified.
|
|
94
|
+
* The gateway always proxies /health to the internal service.
|
|
95
|
+
*/
|
|
96
|
+
proxyPaths?: string[];
|
|
82
97
|
|
|
83
|
-
/** Logger instance
|
|
98
|
+
/** Logger instance */
|
|
84
99
|
logger?: Logger;
|
|
85
100
|
|
|
86
101
|
/** Logging configuration */
|
|
@@ -131,63 +146,106 @@ export interface GatewayInstance {
|
|
|
131
146
|
|
|
132
147
|
|
|
133
148
|
/**
|
|
134
|
-
*
|
|
135
|
-
* - Skips localhost requests
|
|
136
|
-
* - Skips API routes (/api/v1/*) - they have their own service auth
|
|
137
|
-
* - Skips health endpoints - these should be public
|
|
138
|
-
* - Requires valid credentials for non-localhost control panel access
|
|
149
|
+
* Generate landing page HTML for the frontend app
|
|
139
150
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
function generateLandingPageHtml(
|
|
152
|
+
config: NonNullable<GatewayConfig['frontendApp']>['landingPage'],
|
|
153
|
+
controlPanelPath: string
|
|
154
|
+
): string {
|
|
155
|
+
if (!config) return '';
|
|
156
|
+
|
|
157
|
+
const primaryColor = '#6366f1';
|
|
158
|
+
|
|
159
|
+
const links = config.links || [
|
|
160
|
+
{ label: 'Control Panel', url: controlPanelPath },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const linksHtml = links
|
|
164
|
+
.map(
|
|
165
|
+
(link) =>
|
|
166
|
+
`<a href="${link.url}" class="link">${link.label}</a>`
|
|
167
|
+
)
|
|
168
|
+
.join('');
|
|
169
|
+
|
|
170
|
+
return `<!DOCTYPE html>
|
|
171
|
+
<html lang="en">
|
|
172
|
+
<head>
|
|
173
|
+
<meta charset="UTF-8">
|
|
174
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
175
|
+
<title>${config.title}</title>
|
|
176
|
+
<style>
|
|
177
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
178
|
+
body {
|
|
179
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
180
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
181
|
+
color: #e2e8f0;
|
|
182
|
+
min-height: 100vh;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
155
186
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
.container {
|
|
188
|
+
text-align: center;
|
|
189
|
+
max-width: 600px;
|
|
190
|
+
padding: 2rem;
|
|
160
191
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const isLocalhost =
|
|
166
|
-
host === 'localhost' ||
|
|
167
|
-
host === '127.0.0.1' ||
|
|
168
|
-
host.startsWith('localhost:') ||
|
|
169
|
-
host.startsWith('127.0.0.1:') ||
|
|
170
|
-
remoteAddress === '127.0.0.1' ||
|
|
171
|
-
remoteAddress === '::1' ||
|
|
172
|
-
remoteAddress === '::ffff:127.0.0.1';
|
|
173
|
-
|
|
174
|
-
if (isLocalhost) {
|
|
175
|
-
return next();
|
|
192
|
+
h1 {
|
|
193
|
+
font-size: 2.5rem;
|
|
194
|
+
color: ${primaryColor};
|
|
195
|
+
margin-bottom: 1rem;
|
|
176
196
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
p {
|
|
198
|
+
font-size: 1.125rem;
|
|
199
|
+
color: #94a3b8;
|
|
200
|
+
margin-bottom: 2rem;
|
|
201
|
+
line-height: 1.6;
|
|
182
202
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
.links {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
gap: 1rem;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
}
|
|
209
|
+
.link {
|
|
210
|
+
display: inline-block;
|
|
211
|
+
padding: 0.875rem 2rem;
|
|
212
|
+
background: ${primaryColor};
|
|
213
|
+
color: white;
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
border-radius: 0.5rem;
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
transition: all 0.2s;
|
|
218
|
+
}
|
|
219
|
+
.link:hover {
|
|
220
|
+
transform: translateY(-2px);
|
|
221
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.3);
|
|
222
|
+
}
|
|
223
|
+
.footer {
|
|
224
|
+
position: fixed;
|
|
225
|
+
bottom: 1rem;
|
|
226
|
+
left: 0;
|
|
227
|
+
right: 0;
|
|
228
|
+
text-align: center;
|
|
229
|
+
color: #64748b;
|
|
230
|
+
font-size: 0.875rem;
|
|
231
|
+
}
|
|
232
|
+
.footer a {
|
|
233
|
+
color: ${primaryColor};
|
|
234
|
+
text-decoration: none;
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
237
|
+
</head>
|
|
238
|
+
<body>
|
|
239
|
+
<div class="container">
|
|
240
|
+
<h1>${config.heading || config.title}</h1>
|
|
241
|
+
${config.description ? `<p>${config.description}</p>` : ''}
|
|
242
|
+
${linksHtml ? `<div class="links">${linksHtml}</div>` : ''}
|
|
243
|
+
</div>
|
|
244
|
+
<div class="footer">
|
|
245
|
+
Powered by <a href="https://qwickapps.com" target="_blank">QwickApps</a>
|
|
246
|
+
</div>
|
|
247
|
+
</body>
|
|
248
|
+
</html>`;
|
|
191
249
|
}
|
|
192
250
|
|
|
193
251
|
/**
|
|
@@ -238,12 +296,11 @@ export function createGateway(
|
|
|
238
296
|
const servicePort = config.servicePort || parseInt(process.env.SERVICE_PORT || '3100', 10);
|
|
239
297
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
240
298
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const isPasswordAutoGenerated = !providedPassword && authMode === 'auto';
|
|
299
|
+
// Control panel mount path (defaults to /cpanel)
|
|
300
|
+
const controlPanelPath = config.controlPanelPath || '/cpanel';
|
|
301
|
+
|
|
302
|
+
// Guard configuration for control panel
|
|
303
|
+
const guardConfig = config.controlPanelGuard;
|
|
247
304
|
|
|
248
305
|
// API paths to proxy
|
|
249
306
|
const proxyPaths = config.proxyPaths || ['/api/v1'];
|
|
@@ -260,19 +317,18 @@ export function createGateway(
|
|
|
260
317
|
cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
|
|
261
318
|
// Skip body parsing for proxied paths
|
|
262
319
|
skipBodyParserPaths: [...proxyPaths, '/health'],
|
|
263
|
-
//
|
|
264
|
-
|
|
320
|
+
// Mount path for control panel
|
|
321
|
+
mountPath: controlPanelPath,
|
|
322
|
+
// Route guard
|
|
323
|
+
guard: guardConfig,
|
|
324
|
+
// Custom UI path
|
|
325
|
+
customUiPath: config.customUiPath,
|
|
265
326
|
links: config.links,
|
|
266
327
|
},
|
|
267
328
|
plugins: config.plugins || [],
|
|
268
329
|
logger,
|
|
269
330
|
});
|
|
270
331
|
|
|
271
|
-
// Add basic auth middleware if enabled
|
|
272
|
-
if (authMode === 'basic' || authMode === 'auto') {
|
|
273
|
-
controlPanel.app.use(createBasicAuthMiddleware(basicAuthUser, basicAuthPassword, proxyPaths));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
332
|
// Setup proxy middleware for API paths
|
|
277
333
|
const setupProxyMiddleware = () => {
|
|
278
334
|
const target = `http://localhost:${servicePort}`;
|
|
@@ -325,18 +381,41 @@ export function createGateway(
|
|
|
325
381
|
controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
|
|
326
382
|
};
|
|
327
383
|
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
if (config.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
384
|
+
// Setup frontend app at root path
|
|
385
|
+
const setupFrontendApp = () => {
|
|
386
|
+
if (!config.frontendApp) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { redirectUrl, staticPath, landingPage } = config.frontendApp;
|
|
391
|
+
|
|
392
|
+
// Priority 1: Redirect
|
|
393
|
+
if (redirectUrl) {
|
|
394
|
+
logger.info(`Frontend app: Redirecting / to ${redirectUrl}`);
|
|
395
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
396
|
+
res.redirect(redirectUrl);
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Priority 2: Serve static files
|
|
402
|
+
if (staticPath && existsSync(staticPath)) {
|
|
403
|
+
logger.info(`Frontend app: Serving static files from ${staticPath}`);
|
|
404
|
+
controlPanel.app.use('/', express.static(staticPath));
|
|
405
|
+
|
|
406
|
+
// SPA fallback for root
|
|
407
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
408
|
+
res.sendFile(resolve(staticPath, 'index.html'));
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Priority 3: Landing page
|
|
414
|
+
if (landingPage) {
|
|
415
|
+
logger.info(`Frontend app: Serving landing page`);
|
|
416
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
417
|
+
const html = generateLandingPageHtml(landingPage, controlPanelPath);
|
|
418
|
+
res.type('html').send(html);
|
|
340
419
|
});
|
|
341
420
|
}
|
|
342
421
|
};
|
|
@@ -352,12 +431,15 @@ export function createGateway(
|
|
|
352
431
|
// 2. Setup proxy middleware (after service is started)
|
|
353
432
|
setupProxyMiddleware();
|
|
354
433
|
|
|
355
|
-
// 3. Setup
|
|
356
|
-
|
|
434
|
+
// 3. Setup frontend app at root path
|
|
435
|
+
setupFrontendApp();
|
|
357
436
|
|
|
358
437
|
// 4. Start control panel gateway
|
|
359
438
|
await controlPanel.start();
|
|
360
439
|
|
|
440
|
+
// Calculate API base path
|
|
441
|
+
const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
|
|
442
|
+
|
|
361
443
|
// Log startup info
|
|
362
444
|
logger.info('');
|
|
363
445
|
logger.info('========================================');
|
|
@@ -368,25 +450,24 @@ export function createGateway(
|
|
|
368
450
|
logger.info(` Service Port: ${servicePort} (internal)`);
|
|
369
451
|
logger.info('');
|
|
370
452
|
|
|
371
|
-
if (
|
|
453
|
+
if (guardConfig && guardConfig.type === 'basic') {
|
|
372
454
|
logger.info(' Control Panel Auth: HTTP Basic Auth');
|
|
373
455
|
logger.info(' ----------------------------------------');
|
|
374
|
-
logger.info(` Username: ${
|
|
375
|
-
if (isPasswordAutoGenerated) {
|
|
376
|
-
logger.info(` Password: ${basicAuthPassword}`);
|
|
377
|
-
logger.info(' (auto-generated, set BASIC_AUTH_PASSWORD to use a fixed password)');
|
|
378
|
-
} else {
|
|
379
|
-
logger.info(' Password: ********** (from environment)');
|
|
380
|
-
}
|
|
456
|
+
logger.info(` Username: ${guardConfig.username}`);
|
|
381
457
|
logger.info(' ----------------------------------------');
|
|
458
|
+
} else if (guardConfig && guardConfig.type !== 'none') {
|
|
459
|
+
logger.info(` Control Panel Auth: ${guardConfig.type}`);
|
|
382
460
|
} else {
|
|
383
461
|
logger.info(' Control Panel Auth: None (not recommended)');
|
|
384
462
|
}
|
|
385
463
|
|
|
386
464
|
logger.info('');
|
|
387
465
|
logger.info(' Endpoints:');
|
|
388
|
-
|
|
389
|
-
|
|
466
|
+
if (config.frontendApp) {
|
|
467
|
+
logger.info(` GET / - Frontend App`);
|
|
468
|
+
}
|
|
469
|
+
logger.info(` GET ${controlPanelPath.padEnd(20)} - Control Panel UI`);
|
|
470
|
+
logger.info(` GET ${apiBasePath}/health - Gateway health`);
|
|
390
471
|
logger.info(` GET /health - Service health (proxied)`);
|
|
391
472
|
for (const apiPath of proxyPaths) {
|
|
392
473
|
logger.info(` * ${apiPath}/* - Service API (proxied)`);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Guards for @qwickapps/server
|
|
3
|
+
*
|
|
4
|
+
* Provides authentication middleware for protecting routes.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
10
|
+
import type {
|
|
11
|
+
RouteGuardConfig,
|
|
12
|
+
BasicAuthGuardConfig,
|
|
13
|
+
SupabaseAuthGuardConfig,
|
|
14
|
+
Auth0GuardConfig,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a route guard middleware from configuration
|
|
19
|
+
*/
|
|
20
|
+
export function createRouteGuard(config: RouteGuardConfig): RequestHandler {
|
|
21
|
+
switch (config.type) {
|
|
22
|
+
case 'none':
|
|
23
|
+
return (_req, _res, next) => next();
|
|
24
|
+
case 'basic':
|
|
25
|
+
return createBasicAuthGuard(config);
|
|
26
|
+
case 'supabase':
|
|
27
|
+
return createSupabaseGuard(config);
|
|
28
|
+
case 'auth0':
|
|
29
|
+
return createAuth0Guard(config);
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unknown guard type: ${(config as any).type}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create basic auth guard middleware
|
|
37
|
+
*/
|
|
38
|
+
function createBasicAuthGuard(config: BasicAuthGuardConfig): RequestHandler {
|
|
39
|
+
const expectedAuth = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`;
|
|
40
|
+
const realm = config.realm || 'Protected';
|
|
41
|
+
const excludePaths = config.excludePaths || [];
|
|
42
|
+
|
|
43
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
44
|
+
// Check if path is excluded
|
|
45
|
+
if (excludePaths.some(path => req.path.startsWith(path))) {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const authHeader = req.headers.authorization;
|
|
50
|
+
if (authHeader === expectedAuth) {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
55
|
+
res.status(401).json({
|
|
56
|
+
error: 'Unauthorized',
|
|
57
|
+
message: 'Authentication required.',
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create Supabase auth guard middleware
|
|
64
|
+
*
|
|
65
|
+
* Validates JWT tokens from Supabase Auth
|
|
66
|
+
*/
|
|
67
|
+
function createSupabaseGuard(config: SupabaseAuthGuardConfig): RequestHandler {
|
|
68
|
+
const excludePaths = config.excludePaths || [];
|
|
69
|
+
|
|
70
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
71
|
+
// Check if path is excluded
|
|
72
|
+
if (excludePaths.some(path => req.path.startsWith(path))) {
|
|
73
|
+
return next();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const authHeader = req.headers.authorization;
|
|
77
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
78
|
+
return res.status(401).json({
|
|
79
|
+
error: 'Unauthorized',
|
|
80
|
+
message: 'Missing or invalid authorization header. Expected: Bearer <token>',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const token = authHeader.substring(7);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Validate the JWT with Supabase
|
|
88
|
+
const response = await fetch(`${config.supabaseUrl}/auth/v1/user`, {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${token}`,
|
|
91
|
+
apikey: config.supabaseAnonKey,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
return res.status(401).json({
|
|
97
|
+
error: 'Unauthorized',
|
|
98
|
+
message: 'Invalid or expired token.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const user = await response.json();
|
|
103
|
+
(req as any).user = user;
|
|
104
|
+
next();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return res.status(401).json({
|
|
107
|
+
error: 'Unauthorized',
|
|
108
|
+
message: 'Failed to validate token.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create Auth0 guard middleware
|
|
116
|
+
*
|
|
117
|
+
* Uses express-openid-connect for Auth0 authentication
|
|
118
|
+
*/
|
|
119
|
+
function createAuth0Guard(config: Auth0GuardConfig): RequestHandler {
|
|
120
|
+
// Lazy-load express-openid-connect to avoid requiring it when not used
|
|
121
|
+
let authMiddleware: RequestHandler | null = null;
|
|
122
|
+
|
|
123
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
124
|
+
// Lazy initialize the middleware
|
|
125
|
+
if (!authMiddleware) {
|
|
126
|
+
try {
|
|
127
|
+
const { auth } = await import('express-openid-connect');
|
|
128
|
+
authMiddleware = auth({
|
|
129
|
+
authRequired: true,
|
|
130
|
+
auth0Logout: true,
|
|
131
|
+
secret: config.secret,
|
|
132
|
+
baseURL: config.baseUrl,
|
|
133
|
+
clientID: config.clientId,
|
|
134
|
+
issuerBaseURL: `https://${config.domain}`,
|
|
135
|
+
clientSecret: config.clientSecret,
|
|
136
|
+
idpLogout: true,
|
|
137
|
+
routes: {
|
|
138
|
+
login: config.routes?.login || '/login',
|
|
139
|
+
logout: config.routes?.logout || '/logout',
|
|
140
|
+
callback: config.routes?.callback || '/callback',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return res.status(500).json({
|
|
145
|
+
error: 'Configuration Error',
|
|
146
|
+
message: 'Auth0 is not properly configured. Install express-openid-connect package.',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if path is excluded
|
|
152
|
+
const excludePaths = config.excludePaths || [];
|
|
153
|
+
if (excludePaths.some(path => req.path.startsWith(path))) {
|
|
154
|
+
return next();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply Auth0 middleware
|
|
158
|
+
authMiddleware!(req, res, next);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Helper to check if a request is authenticated (for use in handlers)
|
|
164
|
+
*/
|
|
165
|
+
export function isAuthenticated(req: Request): boolean {
|
|
166
|
+
// Check for Auth0 session
|
|
167
|
+
if ((req as any).oidc?.isAuthenticated?.()) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// Check for Supabase user
|
|
171
|
+
if ((req as any).user) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the authenticated user from the request
|
|
179
|
+
*/
|
|
180
|
+
export function getAuthenticatedUser(req: Request): any | null {
|
|
181
|
+
// Check for Auth0 user
|
|
182
|
+
if ((req as any).oidc?.user) {
|
|
183
|
+
return (req as any).oidc.user;
|
|
184
|
+
}
|
|
185
|
+
// Check for Supabase user
|
|
186
|
+
if ((req as any).user) {
|
|
187
|
+
return (req as any).user;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|