@qwickapps/server 1.1.9 → 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.
Files changed (240) hide show
  1. package/README.md +318 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +99 -60
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +683 -315
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +271 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +326 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +9 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +9 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  41. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  42. package/dist/plugins/auth/auth-plugin.js +255 -0
  43. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  45. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  47. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  48. package/dist/plugins/auth/index.d.ts +12 -0
  49. package/dist/plugins/auth/index.d.ts.map +1 -0
  50. package/dist/plugins/auth/index.js +13 -0
  51. package/dist/plugins/auth/index.js.map +1 -0
  52. package/dist/plugins/auth/types.d.ts +148 -0
  53. package/dist/plugins/auth/types.d.ts.map +1 -0
  54. package/dist/plugins/auth/types.js +14 -0
  55. package/dist/plugins/auth/types.js.map +1 -0
  56. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  57. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  58. package/dist/plugins/bans/bans-plugin.js +428 -0
  59. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  60. package/dist/plugins/bans/index.d.ts +9 -0
  61. package/dist/plugins/bans/index.d.ts.map +1 -0
  62. package/dist/plugins/bans/index.js +10 -0
  63. package/dist/plugins/bans/index.js.map +1 -0
  64. package/dist/plugins/bans/stores/index.d.ts +7 -0
  65. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  66. package/dist/plugins/bans/stores/index.js +7 -0
  67. package/dist/plugins/bans/stores/index.js.map +1 -0
  68. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  69. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  70. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  71. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  72. package/dist/plugins/bans/types.d.ts +128 -0
  73. package/dist/plugins/bans/types.d.ts.map +1 -0
  74. package/dist/plugins/bans/types.js +11 -0
  75. package/dist/plugins/bans/types.js.map +1 -0
  76. package/dist/plugins/cache-plugin.d.ts +14 -3
  77. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  78. package/dist/plugins/cache-plugin.js +27 -7
  79. package/dist/plugins/cache-plugin.js.map +1 -1
  80. package/dist/plugins/cache-plugin.test.js +96 -32
  81. package/dist/plugins/cache-plugin.test.js.map +1 -1
  82. package/dist/plugins/config-plugin.d.ts +3 -2
  83. package/dist/plugins/config-plugin.d.ts.map +1 -1
  84. package/dist/plugins/config-plugin.js +17 -10
  85. package/dist/plugins/config-plugin.js.map +1 -1
  86. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  87. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  88. package/dist/plugins/diagnostics-plugin.js +17 -10
  89. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  90. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  91. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  92. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  93. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  94. package/dist/plugins/entitlements/index.d.ts +12 -0
  95. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  96. package/dist/plugins/entitlements/index.js +16 -0
  97. package/dist/plugins/entitlements/index.js.map +1 -0
  98. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  99. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  100. package/dist/plugins/entitlements/sources/index.js +9 -0
  101. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  102. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  103. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  105. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  106. package/dist/plugins/entitlements/types.d.ts +232 -0
  107. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/types.js +11 -0
  109. package/dist/plugins/entitlements/types.js.map +1 -0
  110. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  111. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  112. package/dist/plugins/frontend-app-plugin.js +14 -9
  113. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  114. package/dist/plugins/health-plugin.d.ts +5 -2
  115. package/dist/plugins/health-plugin.d.ts.map +1 -1
  116. package/dist/plugins/health-plugin.js +20 -5
  117. package/dist/plugins/health-plugin.js.map +1 -1
  118. package/dist/plugins/index.d.ts +8 -2
  119. package/dist/plugins/index.d.ts.map +1 -1
  120. package/dist/plugins/index.js +8 -2
  121. package/dist/plugins/index.js.map +1 -1
  122. package/dist/plugins/logs-plugin.d.ts +3 -2
  123. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  124. package/dist/plugins/logs-plugin.js +21 -12
  125. package/dist/plugins/logs-plugin.js.map +1 -1
  126. package/dist/plugins/postgres-plugin.d.ts +3 -3
  127. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  128. package/dist/plugins/postgres-plugin.js +9 -7
  129. package/dist/plugins/postgres-plugin.js.map +1 -1
  130. package/dist/plugins/postgres-plugin.test.js +47 -29
  131. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  132. package/dist/plugins/users/index.d.ts +12 -0
  133. package/dist/plugins/users/index.d.ts.map +1 -0
  134. package/dist/plugins/users/index.js +13 -0
  135. package/dist/plugins/users/index.js.map +1 -0
  136. package/dist/plugins/users/stores/index.d.ts +7 -0
  137. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  138. package/dist/plugins/users/stores/index.js +7 -0
  139. package/dist/plugins/users/stores/index.js.map +1 -0
  140. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  141. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  142. package/dist/plugins/users/stores/postgres-store.js +157 -0
  143. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  144. package/dist/plugins/users/types.d.ts +189 -0
  145. package/dist/plugins/users/types.d.ts.map +1 -0
  146. package/dist/plugins/users/types.js +12 -0
  147. package/dist/plugins/users/types.js.map +1 -0
  148. package/dist/plugins/users/users-plugin.d.ts +39 -0
  149. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  150. package/dist/plugins/users/users-plugin.js +242 -0
  151. package/dist/plugins/users/users-plugin.js.map +1 -0
  152. package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
  153. package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
  154. package/dist-ui/index.html +1 -1
  155. package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
  156. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  157. package/dist-ui-lib/components/index.d.ts +18 -0
  158. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  159. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  160. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  161. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  162. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
  163. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
  164. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  165. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  166. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  167. package/dist-ui-lib/index.js +6441 -0
  168. package/dist-ui-lib/index.js.map +1 -0
  169. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  170. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  171. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  172. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  173. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  174. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  175. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  176. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  177. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  178. package/package.json +18 -6
  179. package/src/core/control-panel.ts +122 -68
  180. package/src/core/gateway.ts +870 -399
  181. package/src/core/index.ts +21 -2
  182. package/src/core/plugin-registry.ts +653 -0
  183. package/src/core/types.ts +31 -37
  184. package/src/index.ts +118 -19
  185. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  186. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  187. package/src/plugins/auth/adapters/index.ts +9 -0
  188. package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
  189. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  190. package/src/plugins/auth/auth-plugin.ts +303 -0
  191. package/src/plugins/auth/index.ts +33 -0
  192. package/src/plugins/auth/types.ts +165 -0
  193. package/src/plugins/bans/bans-plugin.ts +485 -0
  194. package/src/plugins/bans/index.ts +31 -0
  195. package/src/plugins/bans/stores/index.ts +7 -0
  196. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  197. package/src/plugins/bans/types.ts +141 -0
  198. package/src/plugins/cache-plugin.test.ts +105 -32
  199. package/src/plugins/cache-plugin.ts +40 -9
  200. package/src/plugins/config-plugin.ts +23 -12
  201. package/src/plugins/diagnostics-plugin.ts +22 -12
  202. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  203. package/src/plugins/entitlements/index.ts +51 -0
  204. package/src/plugins/entitlements/sources/index.ts +9 -0
  205. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  206. package/src/plugins/entitlements/types.ts +256 -0
  207. package/src/plugins/frontend-app-plugin.ts +24 -12
  208. package/src/plugins/health-plugin.ts +27 -7
  209. package/src/plugins/index.ts +106 -4
  210. package/src/plugins/logs-plugin.ts +28 -14
  211. package/src/plugins/postgres-plugin.test.ts +49 -29
  212. package/src/plugins/postgres-plugin.ts +11 -9
  213. package/src/plugins/users/index.ts +35 -0
  214. package/src/plugins/users/stores/index.ts +7 -0
  215. package/src/plugins/users/stores/postgres-store.ts +225 -0
  216. package/src/plugins/users/types.ts +209 -0
  217. package/src/plugins/users/users-plugin.ts +281 -0
  218. package/ui/src/App.tsx +185 -31
  219. package/ui/src/api/controlPanelApi.ts +354 -1
  220. package/ui/src/components/ControlPanelApp.tsx +209 -0
  221. package/ui/src/components/index.ts +62 -0
  222. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  223. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  224. package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
  225. package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
  226. package/ui/src/dashboard/builtInWidgets.tsx +29 -0
  227. package/ui/src/dashboard/index.ts +35 -0
  228. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  229. package/ui/src/dashboard/widgets/index.ts +7 -0
  230. package/ui/src/pages/DashboardPage.tsx +28 -149
  231. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  232. package/ui/src/pages/LogsPage.tsx +174 -8
  233. package/ui/src/pages/PluginPage.tsx +148 -0
  234. package/ui/src/pages/SystemPage.tsx +445 -0
  235. package/ui/src/pages/UsersPage.tsx +837 -0
  236. package/ui/tsconfig.lib.json +11 -0
  237. package/ui/vite.lib.config.ts +51 -0
  238. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  239. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  240. 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';