@qwickapps/server 1.2.0 → 1.3.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 +238 -0
- package/dist/core/control-panel.d.ts +7 -2
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +92 -54
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +159 -79
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +679 -319
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +271 -0
- package/dist/core/plugin-registry.d.ts.map +1 -0
- package/dist/core/plugin-registry.js +326 -0
- package/dist/core/plugin-registry.js.map +1 -0
- package/dist/core/types.d.ts +16 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
- package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +9 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/index.js +9 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts +40 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +255 -0
- package/dist/plugins/auth/auth-plugin.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.js +147 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +12 -0
- package/dist/plugins/auth/index.d.ts.map +1 -0
- package/dist/plugins/auth/index.js +13 -0
- package/dist/plugins/auth/index.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +148 -0
- package/dist/plugins/auth/types.d.ts.map +1 -0
- package/dist/plugins/auth/types.js +14 -0
- package/dist/plugins/auth/types.js.map +1 -0
- package/dist/plugins/bans/bans-plugin.d.ts +59 -0
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
- package/dist/plugins/bans/bans-plugin.js +428 -0
- package/dist/plugins/bans/bans-plugin.js.map +1 -0
- package/dist/plugins/bans/index.d.ts +9 -0
- package/dist/plugins/bans/index.d.ts.map +1 -0
- package/dist/plugins/bans/index.js +10 -0
- package/dist/plugins/bans/index.js.map +1 -0
- package/dist/plugins/bans/stores/index.d.ts +7 -0
- package/dist/plugins/bans/stores/index.d.ts.map +1 -0
- package/dist/plugins/bans/stores/index.js +7 -0
- package/dist/plugins/bans/stores/index.js.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.js +132 -0
- package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
- package/dist/plugins/bans/types.d.ts +128 -0
- package/dist/plugins/bans/types.d.ts.map +1 -0
- package/dist/plugins/bans/types.js +11 -0
- package/dist/plugins/bans/types.js.map +1 -0
- package/dist/plugins/cache-plugin.d.ts +14 -3
- package/dist/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/plugins/cache-plugin.js +27 -7
- package/dist/plugins/cache-plugin.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +96 -32
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +3 -2
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +17 -10
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.d.ts +2 -2
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +17 -10
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
- package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
- package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
- package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
- package/dist/plugins/entitlements/index.d.ts +12 -0
- package/dist/plugins/entitlements/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/index.js +16 -0
- package/dist/plugins/entitlements/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/index.d.ts +9 -0
- package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/index.js +9 -0
- package/dist/plugins/entitlements/sources/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
- package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
- package/dist/plugins/entitlements/types.d.ts +232 -0
- package/dist/plugins/entitlements/types.d.ts.map +1 -0
- package/dist/plugins/entitlements/types.js +11 -0
- package/dist/plugins/entitlements/types.js.map +1 -0
- package/dist/plugins/frontend-app-plugin.d.ts +9 -3
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +14 -9
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.d.ts +5 -2
- package/dist/plugins/health-plugin.d.ts.map +1 -1
- package/dist/plugins/health-plugin.js +20 -5
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +8 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts +3 -2
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +21 -12
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +3 -3
- package/dist/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/plugins/postgres-plugin.js +9 -7
- package/dist/plugins/postgres-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +47 -29
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +12 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +13 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/stores/index.d.ts +7 -0
- package/dist/plugins/users/stores/index.d.ts.map +1 -0
- package/dist/plugins/users/stores/index.js +7 -0
- package/dist/plugins/users/stores/index.js.map +1 -0
- package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/users/stores/postgres-store.js +157 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -0
- package/dist/plugins/users/types.d.ts +189 -0
- package/dist/plugins/users/types.d.ts.map +1 -0
- package/dist/plugins/users/types.js +12 -0
- package/dist/plugins/users/types.js.map +1 -0
- package/dist/plugins/users/users-plugin.d.ts +39 -0
- package/dist/plugins/users/users-plugin.d.ts.map +1 -0
- package/dist/plugins/users/users-plugin.js +242 -0
- package/dist/plugins/users/users-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
- package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
- package/dist-ui-lib/components/index.d.ts +18 -0
- package/dist-ui-lib/config/AppConfig.d.ts +7 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
- package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
- package/dist-ui-lib/dashboard/index.d.ts +13 -0
- package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
- package/dist-ui-lib/index.js +6441 -0
- package/dist-ui-lib/index.js.map +1 -0
- package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
- package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
- package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
- package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
- package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
- package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
- package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
- package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
- package/package.json +18 -6
- package/src/core/control-panel.ts +114 -61
- package/src/core/gateway.ts +863 -403
- package/src/core/index.ts +21 -2
- package/src/core/plugin-registry.ts +653 -0
- package/src/core/types.ts +31 -37
- package/src/index.ts +118 -19
- package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
- package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
- package/src/plugins/auth/adapters/index.ts +9 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
- package/src/plugins/auth/auth-plugin.test.ts +176 -0
- package/src/plugins/auth/auth-plugin.ts +303 -0
- package/src/plugins/auth/index.ts +33 -0
- package/src/plugins/auth/types.ts +165 -0
- package/src/plugins/bans/bans-plugin.ts +485 -0
- package/src/plugins/bans/index.ts +31 -0
- package/src/plugins/bans/stores/index.ts +7 -0
- package/src/plugins/bans/stores/postgres-store.ts +195 -0
- package/src/plugins/bans/types.ts +141 -0
- package/src/plugins/cache-plugin.test.ts +105 -32
- package/src/plugins/cache-plugin.ts +40 -9
- package/src/plugins/config-plugin.ts +23 -12
- package/src/plugins/diagnostics-plugin.ts +22 -12
- package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
- package/src/plugins/entitlements/index.ts +51 -0
- package/src/plugins/entitlements/sources/index.ts +9 -0
- package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
- package/src/plugins/entitlements/types.ts +256 -0
- package/src/plugins/frontend-app-plugin.ts +24 -12
- package/src/plugins/health-plugin.ts +27 -7
- package/src/plugins/index.ts +106 -4
- package/src/plugins/logs-plugin.ts +28 -14
- package/src/plugins/postgres-plugin.test.ts +49 -29
- package/src/plugins/postgres-plugin.ts +11 -9
- package/src/plugins/users/index.ts +35 -0
- package/src/plugins/users/stores/index.ts +7 -0
- package/src/plugins/users/stores/postgres-store.ts +225 -0
- package/src/plugins/users/types.ts +209 -0
- package/src/plugins/users/users-plugin.ts +281 -0
- package/ui/src/App.tsx +185 -31
- package/ui/src/api/controlPanelApi.ts +354 -1
- package/ui/src/components/ControlPanelApp.tsx +209 -0
- package/ui/src/components/index.ts +62 -0
- package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
- package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
- package/ui/src/dashboard/builtInWidgets.tsx +29 -0
- package/ui/src/dashboard/index.ts +35 -0
- package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
- package/ui/src/dashboard/widgets/index.ts +7 -0
- package/ui/src/pages/DashboardPage.tsx +28 -149
- package/ui/src/pages/EntitlementsPage.tsx +557 -0
- package/ui/src/pages/LogsPage.tsx +174 -8
- package/ui/src/pages/PluginPage.tsx +148 -0
- package/ui/src/pages/SystemPage.tsx +445 -0
- package/ui/src/pages/UsersPage.tsx +837 -0
- package/ui/tsconfig.lib.json +11 -0
- package/ui/vite.lib.config.ts +51 -0
- package/dist-ui/assets/index-CW1BviRn.js +0 -465
- package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
- package/ui/src/pages/HealthPage.tsx +0 -204
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Auth Adapter
|
|
3
|
+
*
|
|
4
|
+
* Provides Supabase authentication using JWT validation.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Request, Response, RequestHandler } from 'express';
|
|
10
|
+
import type { AuthAdapter, AuthenticatedUser, SupabaseAdapterConfig } from '../types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a Supabase authentication adapter
|
|
14
|
+
*/
|
|
15
|
+
export function supabaseAdapter(config: SupabaseAdapterConfig): AuthAdapter {
|
|
16
|
+
// Cache for validated users (short TTL to avoid stale data)
|
|
17
|
+
const userCache = new Map<string, { user: AuthenticatedUser; expires: number }>();
|
|
18
|
+
const CACHE_TTL = 60 * 1000; // 1 minute
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
name: 'supabase',
|
|
22
|
+
|
|
23
|
+
initialize(): RequestHandler {
|
|
24
|
+
// Supabase validation happens per-request, no initialization needed
|
|
25
|
+
return (_req, _res, next) => next();
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
isAuthenticated(req: Request): boolean {
|
|
29
|
+
// Check if we already validated this request
|
|
30
|
+
if ((req as any)._supabaseUser) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const authHeader = req.headers.authorization;
|
|
35
|
+
return !!authHeader && authHeader.startsWith('Bearer ');
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async getUser(req: Request): Promise<AuthenticatedUser | null> {
|
|
39
|
+
// Return cached user if available
|
|
40
|
+
if ((req as any)._supabaseUser) {
|
|
41
|
+
return (req as any)._supabaseUser;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const authHeader = req.headers.authorization;
|
|
45
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const token = authHeader.substring(7);
|
|
50
|
+
|
|
51
|
+
// Check token cache
|
|
52
|
+
const cached = userCache.get(token);
|
|
53
|
+
if (cached && cached.expires > Date.now()) {
|
|
54
|
+
(req as any)._supabaseUser = cached.user;
|
|
55
|
+
return cached.user;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Validate the JWT with Supabase
|
|
60
|
+
const response = await fetch(`${config.url}/auth/v1/user`, {
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
apikey: config.anonKey,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const supabaseUser = (await response.json()) as {
|
|
72
|
+
id: string;
|
|
73
|
+
email: string;
|
|
74
|
+
email_confirmed_at?: string;
|
|
75
|
+
user_metadata?: {
|
|
76
|
+
full_name?: string;
|
|
77
|
+
name?: string;
|
|
78
|
+
avatar_url?: string;
|
|
79
|
+
};
|
|
80
|
+
app_metadata?: {
|
|
81
|
+
roles?: string[];
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const user: AuthenticatedUser = {
|
|
86
|
+
id: supabaseUser.id,
|
|
87
|
+
email: supabaseUser.email,
|
|
88
|
+
name: supabaseUser.user_metadata?.full_name || supabaseUser.user_metadata?.name,
|
|
89
|
+
picture: supabaseUser.user_metadata?.avatar_url,
|
|
90
|
+
emailVerified: !!supabaseUser.email_confirmed_at,
|
|
91
|
+
roles: supabaseUser.app_metadata?.roles || [],
|
|
92
|
+
raw: supabaseUser,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Cache the validated user
|
|
96
|
+
userCache.set(token, { user, expires: Date.now() + CACHE_TTL });
|
|
97
|
+
(req as any)._supabaseUser = user;
|
|
98
|
+
|
|
99
|
+
// Cleanup old cache entries periodically
|
|
100
|
+
if (userCache.size > 1000) {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
for (const [key, value] of userCache) {
|
|
103
|
+
if (value.expires < now) {
|
|
104
|
+
userCache.delete(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return user;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('[SupabaseAdapter] Token validation error:', error);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
hasRoles(req: Request, roles: string[]): boolean {
|
|
117
|
+
const user = (req as any)._supabaseUser as AuthenticatedUser | undefined;
|
|
118
|
+
if (!user?.roles) return false;
|
|
119
|
+
return roles.every((role) => user.roles?.includes(role));
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
getAccessToken(req: Request): string | null {
|
|
123
|
+
const authHeader = req.headers.authorization;
|
|
124
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return authHeader.substring(7);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
onUnauthorized(_req: Request, res: Response): void {
|
|
131
|
+
res.status(401).json({
|
|
132
|
+
error: 'Unauthorized',
|
|
133
|
+
message: 'Missing or invalid authorization header. Expected: Bearer <token>',
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async shutdown(): Promise<void> {
|
|
138
|
+
userCache.clear();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Plugin Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the authentication plugin and adapters.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
10
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
11
|
+
import { basicAdapter } from './adapters/basic-adapter.js';
|
|
12
|
+
import type { AuthenticatedUser } from './types.js';
|
|
13
|
+
|
|
14
|
+
// Mock request/response helpers
|
|
15
|
+
function createMockRequest(overrides: Partial<Request> = {}): Request {
|
|
16
|
+
return {
|
|
17
|
+
headers: {},
|
|
18
|
+
path: '/',
|
|
19
|
+
originalUrl: '/',
|
|
20
|
+
...overrides,
|
|
21
|
+
} as unknown as Request;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createMockResponse(): Response {
|
|
25
|
+
const res = {
|
|
26
|
+
status: vi.fn().mockReturnThis(),
|
|
27
|
+
json: vi.fn().mockReturnThis(),
|
|
28
|
+
setHeader: vi.fn().mockReturnThis(),
|
|
29
|
+
redirect: vi.fn().mockReturnThis(),
|
|
30
|
+
};
|
|
31
|
+
return res as unknown as Response;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('basicAdapter', () => {
|
|
35
|
+
const config = {
|
|
36
|
+
username: 'admin',
|
|
37
|
+
password: 'secret123',
|
|
38
|
+
realm: 'Test Realm',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let adapter: ReturnType<typeof basicAdapter>;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
adapter = basicAdapter(config);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('name', () => {
|
|
48
|
+
it('should return "basic"', () => {
|
|
49
|
+
expect(adapter.name).toBe('basic');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('initialize', () => {
|
|
54
|
+
it('should return a pass-through middleware', () => {
|
|
55
|
+
const middleware = adapter.initialize();
|
|
56
|
+
const req = createMockRequest();
|
|
57
|
+
const res = createMockResponse();
|
|
58
|
+
const next = vi.fn();
|
|
59
|
+
|
|
60
|
+
// Handle both single middleware and array of middlewares
|
|
61
|
+
if (Array.isArray(middleware)) {
|
|
62
|
+
middleware[0](req, res, next);
|
|
63
|
+
} else {
|
|
64
|
+
middleware(req, res, next);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(next).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('isAuthenticated', () => {
|
|
72
|
+
it('should return true for valid basic auth credentials', () => {
|
|
73
|
+
const expectedAuth = `Basic ${Buffer.from('admin:secret123').toString('base64')}`;
|
|
74
|
+
const req = createMockRequest({
|
|
75
|
+
headers: { authorization: expectedAuth },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(adapter.isAuthenticated(req)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return false for invalid credentials', () => {
|
|
82
|
+
const wrongAuth = `Basic ${Buffer.from('admin:wrongpassword').toString('base64')}`;
|
|
83
|
+
const req = createMockRequest({
|
|
84
|
+
headers: { authorization: wrongAuth },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(adapter.isAuthenticated(req)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return false for missing authorization header', () => {
|
|
91
|
+
const req = createMockRequest();
|
|
92
|
+
expect(adapter.isAuthenticated(req)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return false for non-basic auth header', () => {
|
|
96
|
+
const req = createMockRequest({
|
|
97
|
+
headers: { authorization: 'Bearer some-token' },
|
|
98
|
+
});
|
|
99
|
+
expect(adapter.isAuthenticated(req)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('getUser', () => {
|
|
104
|
+
it('should return user for authenticated request', async () => {
|
|
105
|
+
const expectedAuth = `Basic ${Buffer.from('admin:secret123').toString('base64')}`;
|
|
106
|
+
const req = createMockRequest({
|
|
107
|
+
headers: { authorization: expectedAuth },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const user = await Promise.resolve(adapter.getUser(req));
|
|
111
|
+
expect(user).not.toBeNull();
|
|
112
|
+
expect(user?.id).toBe('basic-auth-user');
|
|
113
|
+
expect(user?.email).toBe('admin@localhost');
|
|
114
|
+
expect(user?.name).toBe('admin');
|
|
115
|
+
expect(user?.roles).toContain('admin');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should return null for unauthenticated request', async () => {
|
|
119
|
+
const req = createMockRequest();
|
|
120
|
+
expect(await Promise.resolve(adapter.getUser(req))).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('hasRoles', () => {
|
|
125
|
+
it('should return true if user has the role', () => {
|
|
126
|
+
const expectedAuth = `Basic ${Buffer.from('admin:secret123').toString('base64')}`;
|
|
127
|
+
const req = createMockRequest({
|
|
128
|
+
headers: { authorization: expectedAuth },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(adapter.hasRoles!(req, ['admin'])).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return false if user does not have the role', () => {
|
|
135
|
+
const expectedAuth = `Basic ${Buffer.from('admin:secret123').toString('base64')}`;
|
|
136
|
+
const req = createMockRequest({
|
|
137
|
+
headers: { authorization: expectedAuth },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(adapter.hasRoles!(req, ['superadmin'])).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('onUnauthorized', () => {
|
|
145
|
+
it('should set WWW-Authenticate header and return 401', () => {
|
|
146
|
+
const req = createMockRequest();
|
|
147
|
+
const res = createMockResponse();
|
|
148
|
+
|
|
149
|
+
adapter.onUnauthorized!(req, res);
|
|
150
|
+
|
|
151
|
+
expect(res.setHeader).toHaveBeenCalledWith('WWW-Authenticate', 'Basic realm="Test Realm"');
|
|
152
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
153
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
154
|
+
error: 'Unauthorized',
|
|
155
|
+
message: 'Authentication required.',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('Auth Plugin helpers', () => {
|
|
162
|
+
// These tests would require more complex setup with express app
|
|
163
|
+
// For now, we test the basic functionality
|
|
164
|
+
|
|
165
|
+
it('should export all required functions', async () => {
|
|
166
|
+
const authModule = await import('./auth-plugin.js');
|
|
167
|
+
|
|
168
|
+
expect(authModule.createAuthPlugin).toBeDefined();
|
|
169
|
+
expect(authModule.isAuthenticated).toBeDefined();
|
|
170
|
+
expect(authModule.getAuthenticatedUser).toBeDefined();
|
|
171
|
+
expect(authModule.getAccessToken).toBeDefined();
|
|
172
|
+
expect(authModule.requireAuth).toBeDefined();
|
|
173
|
+
expect(authModule.requireRoles).toBeDefined();
|
|
174
|
+
expect(authModule.requireAnyRole).toBeDefined();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Plugin
|
|
3
|
+
*
|
|
4
|
+
* Pluggable authentication plugin for @qwickapps/server.
|
|
5
|
+
* Supports multiple adapters (Auth0, Supabase, Basic) with fallback chain.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
11
|
+
import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
|
|
12
|
+
import type {
|
|
13
|
+
AuthPluginConfig,
|
|
14
|
+
AuthAdapter,
|
|
15
|
+
AuthenticatedUser,
|
|
16
|
+
AuthenticatedRequest,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
// Store the plugin instance for helper access
|
|
20
|
+
let currentAdapter: AuthAdapter | null = null;
|
|
21
|
+
let fallbackAdapters: AuthAdapter[] = [];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create the Auth plugin
|
|
25
|
+
*/
|
|
26
|
+
export function createAuthPlugin(config: AuthPluginConfig): Plugin {
|
|
27
|
+
const excludePaths = config.excludePaths || [];
|
|
28
|
+
const authRequired = config.authRequired !== false;
|
|
29
|
+
const debug = config.debug || false;
|
|
30
|
+
|
|
31
|
+
function log(message: string, data?: Record<string, unknown>) {
|
|
32
|
+
if (debug) {
|
|
33
|
+
console.log(`[AuthPlugin] ${message}`, data || '');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: 'auth',
|
|
39
|
+
name: 'Auth Plugin',
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
|
|
42
|
+
async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
|
|
43
|
+
const app = registry.getApp();
|
|
44
|
+
|
|
45
|
+
// Store adapters for helper access
|
|
46
|
+
currentAdapter = config.adapter;
|
|
47
|
+
fallbackAdapters = config.fallback || [];
|
|
48
|
+
|
|
49
|
+
log('Initializing auth plugin', {
|
|
50
|
+
adapter: config.adapter.name,
|
|
51
|
+
fallback: fallbackAdapters.map((a) => a.name),
|
|
52
|
+
excludePaths,
|
|
53
|
+
authRequired,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Initialize the primary adapter
|
|
57
|
+
const primaryMiddleware = config.adapter.initialize();
|
|
58
|
+
if (Array.isArray(primaryMiddleware)) {
|
|
59
|
+
primaryMiddleware.forEach((mw) => app.use(mw));
|
|
60
|
+
} else {
|
|
61
|
+
app.use(primaryMiddleware);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Initialize fallback adapters
|
|
65
|
+
for (const fallback of fallbackAdapters) {
|
|
66
|
+
const fallbackMiddleware = fallback.initialize();
|
|
67
|
+
if (Array.isArray(fallbackMiddleware)) {
|
|
68
|
+
fallbackMiddleware.forEach((mw) => app.use(mw));
|
|
69
|
+
} else {
|
|
70
|
+
app.use(fallbackMiddleware);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add the auth checking middleware
|
|
75
|
+
app.use(createAuthMiddleware());
|
|
76
|
+
|
|
77
|
+
// Register auth status route
|
|
78
|
+
registry.addRoute({
|
|
79
|
+
method: 'get',
|
|
80
|
+
path: '/api/auth/status',
|
|
81
|
+
handler: (_req: Request, res: Response) => {
|
|
82
|
+
const authReq = _req as AuthenticatedRequest;
|
|
83
|
+
res.json({
|
|
84
|
+
authenticated: authReq.auth?.isAuthenticated || false,
|
|
85
|
+
user: authReq.auth?.user
|
|
86
|
+
? {
|
|
87
|
+
id: authReq.auth.user.id,
|
|
88
|
+
email: authReq.auth.user.email,
|
|
89
|
+
name: authReq.auth.user.name,
|
|
90
|
+
picture: authReq.auth.user.picture,
|
|
91
|
+
roles: authReq.auth.user.roles,
|
|
92
|
+
}
|
|
93
|
+
: null,
|
|
94
|
+
adapter: authReq.auth?.adapter,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
pluginId: 'auth',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
log('Auth plugin initialized');
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async onStop(): Promise<void> {
|
|
104
|
+
log('Shutting down auth plugin');
|
|
105
|
+
|
|
106
|
+
// Cleanup adapters
|
|
107
|
+
if (currentAdapter?.shutdown) {
|
|
108
|
+
await currentAdapter.shutdown();
|
|
109
|
+
}
|
|
110
|
+
for (const fallback of fallbackAdapters) {
|
|
111
|
+
if (fallback.shutdown) {
|
|
112
|
+
await fallback.shutdown();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
currentAdapter = null;
|
|
117
|
+
fallbackAdapters = [];
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create the auth checking middleware
|
|
123
|
+
*/
|
|
124
|
+
function createAuthMiddleware(): RequestHandler {
|
|
125
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
126
|
+
const authReq = req as AuthenticatedRequest;
|
|
127
|
+
|
|
128
|
+
// Initialize auth object
|
|
129
|
+
authReq.auth = {
|
|
130
|
+
isAuthenticated: false,
|
|
131
|
+
user: null,
|
|
132
|
+
adapter: 'none',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Check if path is excluded
|
|
136
|
+
const isExcluded = excludePaths.some((path) => {
|
|
137
|
+
if (path.endsWith('*')) {
|
|
138
|
+
return req.path.startsWith(path.slice(0, -1));
|
|
139
|
+
}
|
|
140
|
+
return req.path === path || req.path.startsWith(path + '/');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (isExcluded) {
|
|
144
|
+
log('Path excluded from auth', { path: req.path });
|
|
145
|
+
return next();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Try primary adapter
|
|
149
|
+
let authenticated = false;
|
|
150
|
+
let user: AuthenticatedUser | null = null;
|
|
151
|
+
let activeAdapter = config.adapter;
|
|
152
|
+
|
|
153
|
+
if (config.adapter.isAuthenticated(req)) {
|
|
154
|
+
user = await Promise.resolve(config.adapter.getUser(req));
|
|
155
|
+
if (user) {
|
|
156
|
+
authenticated = true;
|
|
157
|
+
log('Authenticated via primary adapter', { adapter: config.adapter.name });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Try fallback adapters if primary didn't authenticate
|
|
162
|
+
if (!authenticated && fallbackAdapters.length > 0) {
|
|
163
|
+
for (const fallback of fallbackAdapters) {
|
|
164
|
+
if (fallback.isAuthenticated(req)) {
|
|
165
|
+
user = await Promise.resolve(fallback.getUser(req));
|
|
166
|
+
if (user) {
|
|
167
|
+
authenticated = true;
|
|
168
|
+
activeAdapter = fallback;
|
|
169
|
+
log('Authenticated via fallback adapter', { adapter: fallback.name });
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Set auth info on request
|
|
177
|
+
authReq.auth = {
|
|
178
|
+
isAuthenticated: authenticated,
|
|
179
|
+
user,
|
|
180
|
+
adapter: activeAdapter.name,
|
|
181
|
+
accessToken: activeAdapter.getAccessToken?.(req) || undefined,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Check if auth is required but user is not authenticated
|
|
185
|
+
if (authRequired && !authenticated) {
|
|
186
|
+
log('Auth required but not authenticated', { path: req.path });
|
|
187
|
+
|
|
188
|
+
// Use custom handler if provided
|
|
189
|
+
if (config.onUnauthorized) {
|
|
190
|
+
return config.onUnauthorized(req, res);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Use adapter's unauthorized handler
|
|
194
|
+
if (activeAdapter.onUnauthorized) {
|
|
195
|
+
return activeAdapter.onUnauthorized(req, res);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Default unauthorized response
|
|
199
|
+
return res.status(401).json({
|
|
200
|
+
error: 'Unauthorized',
|
|
201
|
+
message: 'Authentication required',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
next();
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if the current request is authenticated
|
|
212
|
+
*/
|
|
213
|
+
export function isAuthenticated(req: Request): boolean {
|
|
214
|
+
const authReq = req as AuthenticatedRequest;
|
|
215
|
+
return authReq.auth?.isAuthenticated || false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get the authenticated user from the request
|
|
220
|
+
*/
|
|
221
|
+
export function getAuthenticatedUser(req: Request): AuthenticatedUser | null {
|
|
222
|
+
const authReq = req as AuthenticatedRequest;
|
|
223
|
+
return authReq.auth?.user || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get the access token from the request
|
|
228
|
+
*/
|
|
229
|
+
export function getAccessToken(req: Request): string | null {
|
|
230
|
+
const authReq = req as AuthenticatedRequest;
|
|
231
|
+
return authReq.auth?.accessToken || null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Middleware to require authentication
|
|
236
|
+
*/
|
|
237
|
+
export function requireAuth(): RequestHandler {
|
|
238
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
239
|
+
if (!isAuthenticated(req)) {
|
|
240
|
+
return res.status(401).json({
|
|
241
|
+
error: 'Unauthorized',
|
|
242
|
+
message: 'Authentication required',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
next();
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Middleware to require specific roles
|
|
251
|
+
*/
|
|
252
|
+
export function requireRoles(...roles: string[]): RequestHandler {
|
|
253
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
254
|
+
const user = getAuthenticatedUser(req);
|
|
255
|
+
|
|
256
|
+
if (!user) {
|
|
257
|
+
return res.status(401).json({
|
|
258
|
+
error: 'Unauthorized',
|
|
259
|
+
message: 'Authentication required',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const userRoles = user.roles || [];
|
|
264
|
+
const hasAllRoles = roles.every((role) => userRoles.includes(role));
|
|
265
|
+
|
|
266
|
+
if (!hasAllRoles) {
|
|
267
|
+
return res.status(403).json({
|
|
268
|
+
error: 'Forbidden',
|
|
269
|
+
message: `Required roles: ${roles.join(', ')}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
next();
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Middleware to require any of the specified roles
|
|
279
|
+
*/
|
|
280
|
+
export function requireAnyRole(...roles: string[]): RequestHandler {
|
|
281
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
282
|
+
const user = getAuthenticatedUser(req);
|
|
283
|
+
|
|
284
|
+
if (!user) {
|
|
285
|
+
return res.status(401).json({
|
|
286
|
+
error: 'Unauthorized',
|
|
287
|
+
message: 'Authentication required',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const userRoles = user.roles || [];
|
|
292
|
+
const hasAnyRole = roles.some((role) => userRoles.includes(role));
|
|
293
|
+
|
|
294
|
+
if (!hasAnyRole) {
|
|
295
|
+
return res.status(403).json({
|
|
296
|
+
error: 'Forbidden',
|
|
297
|
+
message: `Required one of roles: ${roles.join(', ')}`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
next();
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Plugin Index
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Main plugin
|
|
8
|
+
export {
|
|
9
|
+
createAuthPlugin,
|
|
10
|
+
isAuthenticated,
|
|
11
|
+
getAuthenticatedUser,
|
|
12
|
+
getAccessToken,
|
|
13
|
+
requireAuth,
|
|
14
|
+
requireRoles,
|
|
15
|
+
requireAnyRole,
|
|
16
|
+
} from './auth-plugin.js';
|
|
17
|
+
|
|
18
|
+
// Types
|
|
19
|
+
export type {
|
|
20
|
+
AuthPluginConfig,
|
|
21
|
+
AuthAdapter,
|
|
22
|
+
AuthenticatedUser,
|
|
23
|
+
AuthenticatedRequest,
|
|
24
|
+
Auth0AdapterConfig,
|
|
25
|
+
SupabaseAdapterConfig,
|
|
26
|
+
BasicAdapterConfig,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
export { isAuthenticatedRequest } from './types.js';
|
|
29
|
+
|
|
30
|
+
// Adapters
|
|
31
|
+
export { auth0Adapter } from './adapters/auth0-adapter.js';
|
|
32
|
+
export { basicAdapter } from './adapters/basic-adapter.js';
|
|
33
|
+
export { supabaseAdapter } from './adapters/supabase-adapter.js';
|