@qwickapps/server 1.2.0 → 1.3.1

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 (299) hide show
  1. package/README.md +392 -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 +120 -54
  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 +679 -319
  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 +307 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +352 -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 +10 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +10 -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/adapters/supertokens-adapter.d.ts +18 -0
  41. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  42. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  43. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  45. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.js +255 -0
  47. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  48. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  49. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  51. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  52. package/dist/plugins/auth/env-config.d.ts +88 -0
  53. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  54. package/dist/plugins/auth/env-config.js +489 -0
  55. package/dist/plugins/auth/env-config.js.map +1 -0
  56. package/dist/plugins/auth/index.d.ts +14 -0
  57. package/dist/plugins/auth/index.d.ts.map +1 -0
  58. package/dist/plugins/auth/index.js +16 -0
  59. package/dist/plugins/auth/index.js.map +1 -0
  60. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  61. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  62. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  63. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  64. package/dist/plugins/auth/types.d.ts +218 -0
  65. package/dist/plugins/auth/types.d.ts.map +1 -0
  66. package/dist/plugins/auth/types.js +14 -0
  67. package/dist/plugins/auth/types.js.map +1 -0
  68. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  69. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  70. package/dist/plugins/bans/bans-plugin.js +428 -0
  71. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  72. package/dist/plugins/bans/index.d.ts +9 -0
  73. package/dist/plugins/bans/index.d.ts.map +1 -0
  74. package/dist/plugins/bans/index.js +10 -0
  75. package/dist/plugins/bans/index.js.map +1 -0
  76. package/dist/plugins/bans/stores/index.d.ts +7 -0
  77. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  78. package/dist/plugins/bans/stores/index.js +7 -0
  79. package/dist/plugins/bans/stores/index.js.map +1 -0
  80. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  81. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  82. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  83. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  84. package/dist/plugins/bans/types.d.ts +128 -0
  85. package/dist/plugins/bans/types.d.ts.map +1 -0
  86. package/dist/plugins/bans/types.js +11 -0
  87. package/dist/plugins/bans/types.js.map +1 -0
  88. package/dist/plugins/cache-plugin.d.ts +14 -3
  89. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  90. package/dist/plugins/cache-plugin.js +27 -7
  91. package/dist/plugins/cache-plugin.js.map +1 -1
  92. package/dist/plugins/cache-plugin.test.js +99 -32
  93. package/dist/plugins/cache-plugin.test.js.map +1 -1
  94. package/dist/plugins/config-plugin.d.ts +3 -2
  95. package/dist/plugins/config-plugin.d.ts.map +1 -1
  96. package/dist/plugins/config-plugin.js +17 -10
  97. package/dist/plugins/config-plugin.js.map +1 -1
  98. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  99. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  100. package/dist/plugins/diagnostics-plugin.js +17 -10
  101. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  102. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  103. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  105. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  106. package/dist/plugins/entitlements/index.d.ts +12 -0
  107. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/index.js +16 -0
  109. package/dist/plugins/entitlements/index.js.map +1 -0
  110. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  111. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  112. package/dist/plugins/entitlements/sources/index.js +9 -0
  113. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  114. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  115. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  116. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  117. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  118. package/dist/plugins/entitlements/types.d.ts +232 -0
  119. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  120. package/dist/plugins/entitlements/types.js +11 -0
  121. package/dist/plugins/entitlements/types.js.map +1 -0
  122. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  123. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  124. package/dist/plugins/frontend-app-plugin.js +14 -9
  125. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  126. package/dist/plugins/health-plugin.d.ts +5 -2
  127. package/dist/plugins/health-plugin.d.ts.map +1 -1
  128. package/dist/plugins/health-plugin.js +20 -5
  129. package/dist/plugins/health-plugin.js.map +1 -1
  130. package/dist/plugins/index.d.ts +10 -2
  131. package/dist/plugins/index.d.ts.map +1 -1
  132. package/dist/plugins/index.js +10 -2
  133. package/dist/plugins/index.js.map +1 -1
  134. package/dist/plugins/logs-plugin.d.ts +3 -2
  135. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  136. package/dist/plugins/logs-plugin.js +21 -12
  137. package/dist/plugins/logs-plugin.js.map +1 -1
  138. package/dist/plugins/postgres-plugin.d.ts +3 -3
  139. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  140. package/dist/plugins/postgres-plugin.js +9 -7
  141. package/dist/plugins/postgres-plugin.js.map +1 -1
  142. package/dist/plugins/postgres-plugin.test.js +50 -29
  143. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  144. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  145. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  146. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  147. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  148. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  149. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  150. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  151. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  152. package/dist/plugins/preferences/index.d.ts +12 -0
  153. package/dist/plugins/preferences/index.d.ts.map +1 -0
  154. package/dist/plugins/preferences/index.js +13 -0
  155. package/dist/plugins/preferences/index.js.map +1 -0
  156. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  157. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  158. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  159. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  160. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  161. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  162. package/dist/plugins/preferences/stores/index.js +9 -0
  163. package/dist/plugins/preferences/stores/index.js.map +1 -0
  164. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  165. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  166. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  167. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  168. package/dist/plugins/preferences/types.d.ts +91 -0
  169. package/dist/plugins/preferences/types.d.ts.map +1 -0
  170. package/dist/plugins/preferences/types.js +10 -0
  171. package/dist/plugins/preferences/types.js.map +1 -0
  172. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  173. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  174. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  175. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  176. package/dist/plugins/users/index.d.ts +12 -0
  177. package/dist/plugins/users/index.d.ts.map +1 -0
  178. package/dist/plugins/users/index.js +13 -0
  179. package/dist/plugins/users/index.js.map +1 -0
  180. package/dist/plugins/users/stores/index.d.ts +7 -0
  181. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  182. package/dist/plugins/users/stores/index.js +7 -0
  183. package/dist/plugins/users/stores/index.js.map +1 -0
  184. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  185. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  186. package/dist/plugins/users/stores/postgres-store.js +157 -0
  187. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  188. package/dist/plugins/users/types.d.ts +225 -0
  189. package/dist/plugins/users/types.d.ts.map +1 -0
  190. package/dist/plugins/users/types.js +12 -0
  191. package/dist/plugins/users/types.js.map +1 -0
  192. package/dist/plugins/users/users-plugin.d.ts +45 -0
  193. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  194. package/dist/plugins/users/users-plugin.js +359 -0
  195. package/dist/plugins/users/users-plugin.js.map +1 -0
  196. package/dist-ui/assets/index-BY8OxNgO.js +465 -0
  197. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  198. package/dist-ui/index.html +1 -1
  199. package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
  200. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  201. package/dist-ui-lib/components/index.d.ts +18 -0
  202. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  203. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  204. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  205. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  206. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
  207. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
  208. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  209. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  210. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  211. package/dist-ui-lib/index.js +5172 -0
  212. package/dist-ui-lib/index.js.map +1 -0
  213. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  214. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  215. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  216. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  217. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  218. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  219. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  220. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  221. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  222. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  223. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  224. package/package.json +24 -7
  225. package/src/core/control-panel.ts +145 -61
  226. package/src/core/gateway.ts +863 -403
  227. package/src/core/index.ts +21 -2
  228. package/src/core/plugin-registry.ts +716 -0
  229. package/src/core/types.ts +31 -37
  230. package/src/index.ts +125 -19
  231. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  232. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  233. package/src/plugins/auth/adapters/index.ts +10 -0
  234. package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
  235. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  236. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  237. package/src/plugins/auth/auth-plugin.ts +303 -0
  238. package/src/plugins/auth/env-config.ts +572 -0
  239. package/src/plugins/auth/index.ts +42 -0
  240. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  241. package/src/plugins/auth/types.ts +245 -0
  242. package/src/plugins/bans/bans-plugin.ts +485 -0
  243. package/src/plugins/bans/index.ts +31 -0
  244. package/src/plugins/bans/stores/index.ts +7 -0
  245. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  246. package/src/plugins/bans/types.ts +141 -0
  247. package/src/plugins/cache-plugin.test.ts +108 -32
  248. package/src/plugins/cache-plugin.ts +40 -9
  249. package/src/plugins/config-plugin.ts +23 -12
  250. package/src/plugins/diagnostics-plugin.ts +22 -12
  251. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  252. package/src/plugins/entitlements/index.ts +51 -0
  253. package/src/plugins/entitlements/sources/index.ts +9 -0
  254. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  255. package/src/plugins/entitlements/types.ts +256 -0
  256. package/src/plugins/frontend-app-plugin.ts +24 -12
  257. package/src/plugins/health-plugin.ts +27 -7
  258. package/src/plugins/index.ts +132 -4
  259. package/src/plugins/logs-plugin.ts +28 -14
  260. package/src/plugins/postgres-plugin.test.ts +52 -29
  261. package/src/plugins/postgres-plugin.ts +11 -9
  262. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  263. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  264. package/src/plugins/preferences/index.ts +30 -0
  265. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  266. package/src/plugins/preferences/stores/index.ts +9 -0
  267. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  268. package/src/plugins/preferences/types.ts +100 -0
  269. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  270. package/src/plugins/users/index.ts +38 -0
  271. package/src/plugins/users/stores/index.ts +7 -0
  272. package/src/plugins/users/stores/postgres-store.ts +225 -0
  273. package/src/plugins/users/types.ts +247 -0
  274. package/src/plugins/users/users-plugin.ts +418 -0
  275. package/ui/src/App.tsx +188 -31
  276. package/ui/src/api/controlPanelApi.ts +453 -1
  277. package/ui/src/components/ControlPanelApp.tsx +212 -0
  278. package/ui/src/components/index.ts +62 -0
  279. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  280. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  281. package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
  282. package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
  283. package/ui/src/dashboard/builtInWidgets.tsx +35 -0
  284. package/ui/src/dashboard/index.ts +35 -0
  285. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  286. package/ui/src/dashboard/widgets/index.ts +7 -0
  287. package/ui/src/pages/AuthPage.tsx +259 -0
  288. package/ui/src/pages/DashboardPage.tsx +28 -149
  289. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  290. package/ui/src/pages/LogsPage.tsx +174 -8
  291. package/ui/src/pages/PluginPage.tsx +148 -0
  292. package/ui/src/pages/PluginsPage.tsx +394 -0
  293. package/ui/src/pages/SystemPage.tsx +445 -0
  294. package/ui/src/pages/UsersPage.tsx +837 -0
  295. package/ui/tsconfig.lib.json +11 -0
  296. package/ui/vite.lib.config.ts +56 -0
  297. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  298. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  299. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,149 @@
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
+ * Supabase user response from /auth/v1/user endpoint
14
+ * @see https://supabase.com/docs/reference/javascript/auth-getuser
15
+ */
16
+ interface SupabaseUserResponse {
17
+ id: string;
18
+ email: string;
19
+ email_confirmed_at?: string;
20
+ user_metadata?: {
21
+ full_name?: string;
22
+ name?: string;
23
+ avatar_url?: string;
24
+ [key: string]: unknown;
25
+ };
26
+ app_metadata?: {
27
+ roles?: string[];
28
+ [key: string]: unknown;
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Create a Supabase authentication adapter
34
+ */
35
+ export function supabaseAdapter(config: SupabaseAdapterConfig): AuthAdapter {
36
+ // Cache for validated users (short TTL to avoid stale data)
37
+ const userCache = new Map<string, { user: AuthenticatedUser; expires: number }>();
38
+ const CACHE_TTL = 60 * 1000; // 1 minute
39
+
40
+ return {
41
+ name: 'supabase',
42
+
43
+ initialize(): RequestHandler {
44
+ // Supabase validation happens per-request, no initialization needed
45
+ return (_req, _res, next) => next();
46
+ },
47
+
48
+ isAuthenticated(req: Request): boolean {
49
+ // Check if we already validated this request
50
+ if ((req as any)._supabaseUser) {
51
+ return true;
52
+ }
53
+
54
+ const authHeader = req.headers.authorization;
55
+ return !!authHeader && authHeader.startsWith('Bearer ');
56
+ },
57
+
58
+ async getUser(req: Request): Promise<AuthenticatedUser | null> {
59
+ // Return cached user if available
60
+ if ((req as any)._supabaseUser) {
61
+ return (req as any)._supabaseUser;
62
+ }
63
+
64
+ const authHeader = req.headers.authorization;
65
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
66
+ return null;
67
+ }
68
+
69
+ const token = authHeader.substring(7);
70
+
71
+ // Check token cache
72
+ const cached = userCache.get(token);
73
+ if (cached && cached.expires > Date.now()) {
74
+ (req as any)._supabaseUser = cached.user;
75
+ return cached.user;
76
+ }
77
+
78
+ try {
79
+ // Validate the JWT with Supabase
80
+ const response = await fetch(`${config.url}/auth/v1/user`, {
81
+ headers: {
82
+ Authorization: `Bearer ${token}`,
83
+ apikey: config.anonKey,
84
+ },
85
+ });
86
+
87
+ if (!response.ok) {
88
+ return null;
89
+ }
90
+
91
+ const supabaseUser = (await response.json()) as SupabaseUserResponse;
92
+
93
+ const user: AuthenticatedUser = {
94
+ id: supabaseUser.id,
95
+ email: supabaseUser.email,
96
+ name: supabaseUser.user_metadata?.full_name || supabaseUser.user_metadata?.name,
97
+ picture: supabaseUser.user_metadata?.avatar_url,
98
+ emailVerified: !!supabaseUser.email_confirmed_at,
99
+ roles: supabaseUser.app_metadata?.roles || [],
100
+ raw: supabaseUser as unknown as Record<string, unknown>,
101
+ };
102
+
103
+ // Cache the validated user
104
+ userCache.set(token, { user, expires: Date.now() + CACHE_TTL });
105
+ (req as any)._supabaseUser = user;
106
+
107
+ // Cleanup old cache entries periodically
108
+ if (userCache.size > 1000) {
109
+ const now = Date.now();
110
+ for (const [key, value] of userCache) {
111
+ if (value.expires < now) {
112
+ userCache.delete(key);
113
+ }
114
+ }
115
+ }
116
+
117
+ return user;
118
+ } catch (error) {
119
+ console.error('[SupabaseAdapter] Token validation error:', error);
120
+ return null;
121
+ }
122
+ },
123
+
124
+ hasRoles(req: Request, roles: string[]): boolean {
125
+ const user = (req as any)._supabaseUser as AuthenticatedUser | undefined;
126
+ if (!user?.roles) return false;
127
+ return roles.every((role) => user.roles?.includes(role));
128
+ },
129
+
130
+ getAccessToken(req: Request): string | null {
131
+ const authHeader = req.headers.authorization;
132
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
133
+ return null;
134
+ }
135
+ return authHeader.substring(7);
136
+ },
137
+
138
+ onUnauthorized(_req: Request, res: Response): void {
139
+ res.status(401).json({
140
+ error: 'Unauthorized',
141
+ message: 'Missing or invalid authorization header. Expected: Bearer <token>',
142
+ });
143
+ },
144
+
145
+ async shutdown(): Promise<void> {
146
+ userCache.clear();
147
+ },
148
+ };
149
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Supertokens Auth Adapter
3
+ *
4
+ * Provides Supertokens authentication using EmailPassword and ThirdParty recipes.
5
+ * Supports email/password and social logins (Google, Apple, GitHub).
6
+ *
7
+ * Note: Requires supertokens-node v20+
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ import type { Request, Response, RequestHandler } from 'express';
13
+ import type { AuthAdapter, AuthenticatedUser, SupertokensAdapterConfig } from '../types.js';
14
+
15
+ // Keys for storing data on the request object
16
+ const REQUEST_USER_KEY = '_supertokensUser';
17
+ const REQUEST_RES_KEY = '_supertokensRes';
18
+ const REQUEST_SESSION_KEY = '_supertokensSession';
19
+
20
+ // Type for extended request with our custom properties
21
+ interface SupertokensExtendedRequest extends Request {
22
+ [REQUEST_USER_KEY]?: AuthenticatedUser;
23
+ [REQUEST_RES_KEY]?: Response;
24
+ [REQUEST_SESSION_KEY]?: unknown;
25
+ }
26
+
27
+ /**
28
+ * Create a Supertokens authentication adapter
29
+ *
30
+ * Uses EmailPassword and ThirdParty recipes (Supertokens v20+)
31
+ */
32
+ export function supertokensAdapter(config: SupertokensAdapterConfig): AuthAdapter {
33
+ // Track initialization state
34
+ let initialized = false;
35
+ let initializationError: Error | null = null;
36
+
37
+ return {
38
+ name: 'supertokens',
39
+
40
+ initialize(): RequestHandler[] {
41
+ // Return middleware that lazily initializes Supertokens
42
+ const initMiddleware: RequestHandler = async (
43
+ req: Request,
44
+ res: Response,
45
+ next: (err?: unknown) => void
46
+ ) => {
47
+ // Store response on request for later use in getUser()
48
+ (req as SupertokensExtendedRequest)[REQUEST_RES_KEY] = res;
49
+
50
+ // Skip if already initialized with error
51
+ if (initializationError) {
52
+ return res.status(500).json({
53
+ error: 'Auth Configuration Error',
54
+ message:
55
+ 'Supertokens is not properly configured. Install supertokens-node package: npm install supertokens-node',
56
+ details: initializationError.message,
57
+ });
58
+ }
59
+
60
+ // Lazy initialize Supertokens
61
+ if (!initialized) {
62
+ try {
63
+ const supertokens = await import('supertokens-node');
64
+ const Session = await import('supertokens-node/recipe/session');
65
+ const EmailPassword = await import('supertokens-node/recipe/emailpassword');
66
+ const ThirdParty = await import('supertokens-node/recipe/thirdparty');
67
+
68
+ // Build recipe list - using any[] for Supertokens internal types
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const recipeList: any[] = [];
71
+
72
+ // Add EmailPassword recipe if enabled (default: true)
73
+ if (config.enableEmailPassword !== false) {
74
+ recipeList.push(EmailPassword.default.init());
75
+ }
76
+
77
+ // Add ThirdParty recipe if any social providers configured
78
+ if (config.socialProviders) {
79
+ // Build provider configurations using Supertokens ProviderInput type
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const providers: any[] = [];
82
+
83
+ if (config.socialProviders.google) {
84
+ providers.push({
85
+ config: {
86
+ thirdPartyId: 'google',
87
+ clients: [
88
+ {
89
+ clientId: config.socialProviders.google.clientId,
90
+ clientSecret: config.socialProviders.google.clientSecret,
91
+ },
92
+ ],
93
+ },
94
+ });
95
+ }
96
+
97
+ if (config.socialProviders.apple) {
98
+ // Apple requires keyId, teamId, and privateKey in additionalConfig
99
+ providers.push({
100
+ config: {
101
+ thirdPartyId: 'apple',
102
+ clients: [
103
+ {
104
+ clientId: config.socialProviders.apple.clientId,
105
+ clientSecret: config.socialProviders.apple.clientSecret,
106
+ additionalConfig: {
107
+ keyId: config.socialProviders.apple.keyId,
108
+ teamId: config.socialProviders.apple.teamId,
109
+ },
110
+ },
111
+ ],
112
+ },
113
+ });
114
+ }
115
+
116
+ if (config.socialProviders.github) {
117
+ providers.push({
118
+ config: {
119
+ thirdPartyId: 'github',
120
+ clients: [
121
+ {
122
+ clientId: config.socialProviders.github.clientId,
123
+ clientSecret: config.socialProviders.github.clientSecret,
124
+ },
125
+ ],
126
+ },
127
+ });
128
+ }
129
+
130
+ if (providers.length > 0) {
131
+ recipeList.push(
132
+ ThirdParty.default.init({
133
+ signInAndUpFeature: {
134
+ providers,
135
+ },
136
+ })
137
+ );
138
+ }
139
+ }
140
+
141
+ // Always add Session recipe
142
+ recipeList.push(Session.default.init());
143
+
144
+ // Initialize Supertokens
145
+ supertokens.default.init({
146
+ framework: 'express',
147
+ supertokens: {
148
+ connectionURI: config.connectionUri,
149
+ apiKey: config.apiKey,
150
+ },
151
+ appInfo: {
152
+ appName: config.appName,
153
+ apiDomain: config.apiDomain,
154
+ websiteDomain: config.websiteDomain,
155
+ apiBasePath: config.apiBasePath ?? '/auth',
156
+ websiteBasePath: config.websiteBasePath ?? '/auth',
157
+ },
158
+ recipeList,
159
+ });
160
+
161
+ initialized = true;
162
+ } catch (error) {
163
+ initializationError =
164
+ error instanceof Error ? error : new Error('Failed to initialize Supertokens');
165
+ console.error('[SupertokensAdapter] Initialization error:', error);
166
+ return res.status(500).json({
167
+ error: 'Auth Configuration Error',
168
+ message:
169
+ 'Supertokens is not properly configured. Install supertokens-node package: npm install supertokens-node',
170
+ details: initializationError.message,
171
+ });
172
+ }
173
+ }
174
+
175
+ next();
176
+ };
177
+
178
+ // Supertokens middleware for handling auth routes
179
+ const supertokensMiddleware: RequestHandler = async (req, res, next) => {
180
+ if (!initialized) {
181
+ return next();
182
+ }
183
+
184
+ try {
185
+ const { middleware } = await import('supertokens-node/framework/express');
186
+ middleware()(req, res, next);
187
+ } catch {
188
+ next();
189
+ }
190
+ };
191
+
192
+ return [initMiddleware, supertokensMiddleware];
193
+ },
194
+
195
+ isAuthenticated(req: Request): boolean {
196
+ const extReq = req as SupertokensExtendedRequest;
197
+
198
+ // Check if we already validated this request
199
+ if (extReq[REQUEST_USER_KEY]) {
200
+ return true;
201
+ }
202
+
203
+ // Check if session was already retrieved
204
+ if (extReq[REQUEST_SESSION_KEY]) {
205
+ return true;
206
+ }
207
+
208
+ // For synchronous check, we can only check if session cookies exist
209
+ // Full validation happens in getUser()
210
+ // Supertokens uses cookies, so we check for session tokens
211
+ const cookies = req.cookies || {};
212
+ const accessToken = cookies.sAccessToken;
213
+ const refreshToken = cookies.sRefreshToken;
214
+
215
+ // Also check for Authorization header (for API clients)
216
+ const authHeader = req.headers.authorization;
217
+ const hasBearerToken = authHeader?.startsWith('Bearer ');
218
+
219
+ return !!(accessToken || refreshToken || hasBearerToken);
220
+ },
221
+
222
+ async getUser(req: Request): Promise<AuthenticatedUser | null> {
223
+ const extReq = req as SupertokensExtendedRequest;
224
+
225
+ // Return cached user if available
226
+ const cachedUser = extReq[REQUEST_USER_KEY];
227
+ if (cachedUser) {
228
+ return cachedUser;
229
+ }
230
+
231
+ if (!initialized) {
232
+ return null;
233
+ }
234
+
235
+ // Get response object stored during middleware
236
+ const res = extReq[REQUEST_RES_KEY];
237
+ if (!res) {
238
+ console.error('[SupertokensAdapter] Response object not found on request');
239
+ return null;
240
+ }
241
+
242
+ try {
243
+ const Session = await import('supertokens-node/recipe/session');
244
+ const supertokens = await import('supertokens-node');
245
+
246
+ // Get session - sessionRequired: false means it won't throw if no session
247
+ const session = await Session.default.getSession(req, res, {
248
+ sessionRequired: false,
249
+ });
250
+
251
+ if (!session) {
252
+ return null;
253
+ }
254
+
255
+ // Cache session for isAuthenticated check
256
+ extReq[REQUEST_SESSION_KEY] = session;
257
+
258
+ const userId = session.getUserId();
259
+
260
+ // Get user info from Supertokens
261
+ const userInfo = await supertokens.default.getUser(userId);
262
+
263
+ if (!userInfo) {
264
+ return null;
265
+ }
266
+
267
+ // Get roles from session access token payload if available
268
+ const accessTokenPayload = session.getAccessTokenPayload();
269
+ const roles: string[] = accessTokenPayload?.roles || [];
270
+
271
+ // Map Supertokens user to AuthenticatedUser
272
+ const user: AuthenticatedUser = {
273
+ id: userId,
274
+ email: userInfo.emails?.[0] ?? '',
275
+ name:
276
+ accessTokenPayload?.name ||
277
+ userInfo.thirdParty?.[0]?.userId ||
278
+ userInfo.emails?.[0]?.split('@')[0],
279
+ picture: accessTokenPayload?.picture,
280
+ emailVerified: userInfo.emails?.[0] ? true : false,
281
+ roles,
282
+ raw: {
283
+ ...userInfo,
284
+ sessionHandle: session.getHandle(),
285
+ accessTokenPayload,
286
+ } as Record<string, unknown>,
287
+ };
288
+
289
+ // Cache on request object
290
+ extReq[REQUEST_USER_KEY] = user;
291
+
292
+ return user;
293
+ } catch (error) {
294
+ console.error('[SupertokensAdapter] Error getting user:', error);
295
+ return null;
296
+ }
297
+ },
298
+
299
+ hasRoles(req: Request, roles: string[]): boolean {
300
+ const extReq = req as SupertokensExtendedRequest;
301
+ const user = extReq[REQUEST_USER_KEY];
302
+ if (!user?.roles) return false;
303
+ return roles.every((role) => user.roles?.includes(role));
304
+ },
305
+
306
+ getAccessToken(_req: Request): string | null {
307
+ // Supertokens uses session cookies, not access tokens
308
+ // Return null as per the design decision
309
+ return null;
310
+ },
311
+
312
+ onUnauthorized(_req: Request, res: Response): void {
313
+ res.status(401).json({
314
+ error: 'Unauthorized',
315
+ message: 'Authentication required. Please sign in.',
316
+ hint: 'Use the /auth endpoints to authenticate',
317
+ });
318
+ },
319
+
320
+ async shutdown(): Promise<void> {
321
+ // Supertokens doesn't require explicit cleanup
322
+ initialized = false;
323
+ initializationError = null;
324
+ },
325
+ };
326
+ }
@@ -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
+ });