@qwickapps/server 1.3.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 (132) hide show
  1. package/README.md +154 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +30 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  14. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  15. package/dist/plugins/auth/adapters/index.js +1 -0
  16. package/dist/plugins/auth/adapters/index.js.map +1 -1
  17. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  18. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  19. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  20. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  22. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  23. package/dist/plugins/auth/env-config.d.ts +88 -0
  24. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  25. package/dist/plugins/auth/env-config.js +489 -0
  26. package/dist/plugins/auth/env-config.js.map +1 -0
  27. package/dist/plugins/auth/index.d.ts +3 -1
  28. package/dist/plugins/auth/index.d.ts.map +1 -1
  29. package/dist/plugins/auth/index.js +3 -0
  30. package/dist/plugins/auth/index.js.map +1 -1
  31. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  32. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  33. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  34. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  35. package/dist/plugins/auth/types.d.ts +70 -0
  36. package/dist/plugins/auth/types.d.ts.map +1 -1
  37. package/dist/plugins/auth/types.js.map +1 -1
  38. package/dist/plugins/cache-plugin.test.js +3 -0
  39. package/dist/plugins/cache-plugin.test.js.map +1 -1
  40. package/dist/plugins/index.d.ts +4 -2
  41. package/dist/plugins/index.d.ts.map +1 -1
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/index.js.map +1 -1
  44. package/dist/plugins/postgres-plugin.test.js +3 -0
  45. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  46. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  47. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  48. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  49. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  50. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  51. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  52. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  53. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  54. package/dist/plugins/preferences/index.d.ts +12 -0
  55. package/dist/plugins/preferences/index.d.ts.map +1 -0
  56. package/dist/plugins/preferences/index.js +13 -0
  57. package/dist/plugins/preferences/index.js.map +1 -0
  58. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  59. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  60. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  61. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  62. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  63. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  64. package/dist/plugins/preferences/stores/index.js +9 -0
  65. package/dist/plugins/preferences/stores/index.js.map +1 -0
  66. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  67. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  68. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  69. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  70. package/dist/plugins/preferences/types.d.ts +91 -0
  71. package/dist/plugins/preferences/types.d.ts.map +1 -0
  72. package/dist/plugins/preferences/types.js +10 -0
  73. package/dist/plugins/preferences/types.js.map +1 -0
  74. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  75. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  76. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  77. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  78. package/dist/plugins/users/index.d.ts +2 -2
  79. package/dist/plugins/users/index.d.ts.map +1 -1
  80. package/dist/plugins/users/index.js +1 -1
  81. package/dist/plugins/users/index.js.map +1 -1
  82. package/dist/plugins/users/types.d.ts +36 -0
  83. package/dist/plugins/users/types.d.ts.map +1 -1
  84. package/dist/plugins/users/users-plugin.d.ts +8 -2
  85. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  86. package/dist/plugins/users/users-plugin.js +122 -5
  87. package/dist/plugins/users/users-plugin.js.map +1 -1
  88. package/dist-ui/assets/{index-Bsp2ntcw.js → index-BY8OxNgO.js} +112 -112
  89. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  90. package/dist-ui/index.html +1 -1
  91. package/dist-ui-lib/api/controlPanelApi.d.ts +53 -7
  92. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  93. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  94. package/dist-ui-lib/index.js +2382 -3651
  95. package/dist-ui-lib/index.js.map +1 -1
  96. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  97. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  98. package/package.json +7 -2
  99. package/src/core/control-panel.ts +33 -2
  100. package/src/core/plugin-registry.ts +63 -0
  101. package/src/index.ts +7 -0
  102. package/src/plugins/auth/adapters/index.ts +1 -0
  103. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  104. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  105. package/src/plugins/auth/env-config.ts +572 -0
  106. package/src/plugins/auth/index.ts +9 -0
  107. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  108. package/src/plugins/auth/types.ts +80 -0
  109. package/src/plugins/cache-plugin.test.ts +3 -0
  110. package/src/plugins/index.ts +26 -0
  111. package/src/plugins/postgres-plugin.test.ts +3 -0
  112. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  113. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  114. package/src/plugins/preferences/index.ts +30 -0
  115. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  116. package/src/plugins/preferences/stores/index.ts +9 -0
  117. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  118. package/src/plugins/preferences/types.ts +100 -0
  119. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  120. package/src/plugins/users/index.ts +3 -0
  121. package/src/plugins/users/types.ts +38 -0
  122. package/src/plugins/users/users-plugin.ts +142 -5
  123. package/ui/src/App.tsx +4 -1
  124. package/ui/src/api/controlPanelApi.ts +100 -1
  125. package/ui/src/components/ControlPanelApp.tsx +3 -0
  126. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  127. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  128. package/ui/src/dashboard/builtInWidgets.tsx +8 -2
  129. package/ui/src/pages/AuthPage.tsx +259 -0
  130. package/ui/src/pages/PluginsPage.tsx +394 -0
  131. package/ui/vite.lib.config.ts +5 -0
  132. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -0,0 +1 @@
1
+ export declare function AuthPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function PluginsPage(): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/server",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -76,6 +76,7 @@
76
76
  "react": "^18.2.0",
77
77
  "react-dom": "^18.2.0",
78
78
  "react-router-dom": "^6.30.1",
79
+ "supertokens-node": "^20.1.7",
79
80
  "tsx": "^4.20.6",
80
81
  "typescript": "^5.3.3",
81
82
  "vite": "^6.0.0",
@@ -85,7 +86,8 @@
85
86
  "@qwickapps/react-framework": ">=1.0.0",
86
87
  "express-openid-connect": ">=2.0.0",
87
88
  "ioredis": ">=5.0.0",
88
- "pg": ">=8.0.0"
89
+ "pg": ">=8.0.0",
90
+ "supertokens-node": ">=20.0.0"
89
91
  },
90
92
  "peerDependenciesMeta": {
91
93
  "@qwickapps/react-framework": {
@@ -99,6 +101,9 @@
99
101
  },
100
102
  "pg": {
101
103
  "optional": true
104
+ },
105
+ "supertokens-node": {
106
+ "optional": true
102
107
  }
103
108
  },
104
109
  "keywords": [
@@ -197,11 +197,42 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
197
197
  });
198
198
 
199
199
  /**
200
- * GET /api/plugins - List all registered plugins
200
+ * GET /api/plugins - List all registered plugins with contribution counts
201
201
  */
202
202
  router.get('/plugins', (_req: Request, res: Response) => {
203
+ const plugins = pluginRegistry.listPlugins().map((plugin) => {
204
+ const contributions = pluginRegistry.getPluginContributions(plugin.id);
205
+ return {
206
+ ...plugin,
207
+ contributionCounts: {
208
+ routes: contributions.routes.length,
209
+ menuItems: contributions.menuItems.length,
210
+ pages: contributions.pages.length,
211
+ widgets: contributions.widgets.length,
212
+ hasConfig: !!contributions.config,
213
+ },
214
+ };
215
+ });
216
+ res.json({ plugins });
217
+ });
218
+
219
+ /**
220
+ * GET /api/plugins/:id - Get detailed plugin info with contributions
221
+ */
222
+ router.get('/plugins/:id', (req: Request, res: Response) => {
223
+ const { id } = req.params;
224
+ const plugins = pluginRegistry.listPlugins();
225
+ const plugin = plugins.find((p) => p.id === id);
226
+
227
+ if (!plugin) {
228
+ res.status(404).json({ error: `Plugin not found: ${id}` });
229
+ return;
230
+ }
231
+
232
+ const contributions = pluginRegistry.getPluginContributions(id);
203
233
  res.json({
204
- plugins: pluginRegistry.listPlugins(),
234
+ ...plugin,
235
+ contributions,
205
236
  });
206
237
  });
207
238
 
@@ -164,6 +164,31 @@ export interface RouteDefinition {
164
164
  pluginId: string;
165
165
  }
166
166
 
167
+ /**
168
+ * Configuration UI contribution for plugin settings
169
+ */
170
+ export interface ConfigContribution {
171
+ /** Unique ID for this config contribution */
172
+ id: string;
173
+ /** React component name to render (matched by frontend registry) */
174
+ component: string;
175
+ /** Display title for the config section */
176
+ title?: string;
177
+ /** Plugin ID that contributed this */
178
+ pluginId: string;
179
+ }
180
+
181
+ /**
182
+ * Aggregated contributions for a specific plugin
183
+ */
184
+ export interface PluginContributions {
185
+ routes: Array<{ method: string; path: string }>;
186
+ menuItems: MenuContribution[];
187
+ pages: PageContribution[];
188
+ widgets: WidgetContribution[];
189
+ config?: ConfigContribution;
190
+ }
191
+
167
192
  // =============================================================================
168
193
  // Plugin Registry Interface
169
194
  // =============================================================================
@@ -204,6 +229,9 @@ export interface PluginRegistry {
204
229
  /** Register a widget */
205
230
  addWidget(widget: WidgetContribution): void;
206
231
 
232
+ /** Register a config component for plugin settings UI */
233
+ addConfigComponent(config: ConfigContribution): void;
234
+
207
235
  // ---------------------------------------------------------------------------
208
236
  // Contribution queries
209
237
  // ---------------------------------------------------------------------------
@@ -220,6 +248,12 @@ export interface PluginRegistry {
220
248
  /** Get all widgets */
221
249
  getWidgets(): WidgetContribution[];
222
250
 
251
+ /** Get all config components */
252
+ getConfigComponents(): ConfigContribution[];
253
+
254
+ /** Get all contributions for a specific plugin */
255
+ getPluginContributions(pluginId: string): PluginContributions;
256
+
223
257
  // ---------------------------------------------------------------------------
224
258
  // Configuration
225
259
  // ---------------------------------------------------------------------------
@@ -292,6 +326,7 @@ export class PluginRegistryImpl implements PluginRegistry {
292
326
  private menuItems: MenuContribution[] = [];
293
327
  private pages: PageContribution[] = [];
294
328
  private widgets: WidgetContribution[] = [];
329
+ private configComponents: ConfigContribution[] = [];
295
330
 
296
331
  private eventHandlers = new Set<PluginEventHandler>();
297
332
 
@@ -385,6 +420,17 @@ export class PluginRegistryImpl implements PluginRegistry {
385
420
  this.logger.debug(`Widget registered: ${widget.title} by ${widget.pluginId}`);
386
421
  }
387
422
 
423
+ addConfigComponent(config: ConfigContribution): void {
424
+ // Only one config component per plugin - warn if replacing
425
+ const existing = this.configComponents.find((c) => c.pluginId === config.pluginId);
426
+ if (existing) {
427
+ this.logger.warn(`Replacing config component for plugin ${config.pluginId}: ${existing.component} → ${config.component}`);
428
+ }
429
+ this.configComponents = this.configComponents.filter((c) => c.pluginId !== config.pluginId);
430
+ this.configComponents.push(config);
431
+ this.logger.debug(`Config component registered: ${config.component} by ${config.pluginId}`);
432
+ }
433
+
388
434
  // ---------------------------------------------------------------------------
389
435
  // Contribution queries
390
436
  // ---------------------------------------------------------------------------
@@ -405,6 +451,22 @@ export class PluginRegistryImpl implements PluginRegistry {
405
451
  return [...this.widgets];
406
452
  }
407
453
 
454
+ getConfigComponents(): ConfigContribution[] {
455
+ return [...this.configComponents];
456
+ }
457
+
458
+ getPluginContributions(pluginId: string): PluginContributions {
459
+ return {
460
+ routes: this.routes
461
+ .filter((r) => r.pluginId === pluginId)
462
+ .map((r) => ({ method: r.method, path: r.path })),
463
+ menuItems: this.menuItems.filter((m) => m.pluginId === pluginId),
464
+ pages: this.pages.filter((p) => p.pluginId === pluginId),
465
+ widgets: this.widgets.filter((w) => w.pluginId === pluginId),
466
+ config: this.configComponents.find((c) => c.pluginId === pluginId),
467
+ };
468
+ }
469
+
408
470
  // ---------------------------------------------------------------------------
409
471
  // Configuration
410
472
  // ---------------------------------------------------------------------------
@@ -577,6 +639,7 @@ export class PluginRegistryImpl implements PluginRegistry {
577
639
  this.menuItems = this.menuItems.filter((m) => m.pluginId !== pluginId);
578
640
  this.pages = this.pages.filter((p) => p.pluginId !== pluginId);
579
641
  this.widgets = this.widgets.filter((w) => w.pluginId !== pluginId);
642
+ this.configComponents = this.configComponents.filter((c) => c.pluginId !== pluginId);
580
643
 
581
644
  this.emit({
582
645
  type: 'plugin:stopped',
package/src/index.ts CHANGED
@@ -89,6 +89,8 @@ export {
89
89
  hasCache,
90
90
  // Auth plugin
91
91
  createAuthPlugin,
92
+ createAuthPluginFromEnv,
93
+ getAuthStatus,
92
94
  isAuthenticated,
93
95
  getAuthenticatedUser,
94
96
  getAccessToken,
@@ -98,6 +100,7 @@ export {
98
100
  auth0Adapter,
99
101
  basicAdapter,
100
102
  supabaseAdapter,
103
+ supertokensAdapter,
101
104
  isAuthenticatedRequest,
102
105
  // Users plugin
103
106
  createUsersPlugin,
@@ -159,6 +162,10 @@ export type {
159
162
  Auth0AdapterConfig,
160
163
  SupabaseAdapterConfig,
161
164
  BasicAdapterConfig,
165
+ SupertokensAdapterConfig,
166
+ AuthPluginState,
167
+ AuthEnvPluginOptions,
168
+ AuthConfigStatus,
162
169
  // Users plugin types
163
170
  UsersPluginConfig,
164
171
  UserStore,
@@ -7,3 +7,4 @@
7
7
  export { auth0Adapter } from './auth0-adapter.js';
8
8
  export { basicAdapter } from './basic-adapter.js';
9
9
  export { supabaseAdapter } from './supabase-adapter.js';
10
+ export { supertokensAdapter } from './supertokens-adapter.js';
@@ -9,6 +9,26 @@
9
9
  import type { Request, Response, RequestHandler } from 'express';
10
10
  import type { AuthAdapter, AuthenticatedUser, SupabaseAdapterConfig } from '../types.js';
11
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
+
12
32
  /**
13
33
  * Create a Supabase authentication adapter
14
34
  */
@@ -68,19 +88,7 @@ export function supabaseAdapter(config: SupabaseAdapterConfig): AuthAdapter {
68
88
  return null;
69
89
  }
70
90
 
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
- };
91
+ const supabaseUser = (await response.json()) as SupabaseUserResponse;
84
92
 
85
93
  const user: AuthenticatedUser = {
86
94
  id: supabaseUser.id,
@@ -89,7 +97,7 @@ export function supabaseAdapter(config: SupabaseAdapterConfig): AuthAdapter {
89
97
  picture: supabaseUser.user_metadata?.avatar_url,
90
98
  emailVerified: !!supabaseUser.email_confirmed_at,
91
99
  roles: supabaseUser.app_metadata?.roles || [],
92
- raw: supabaseUser,
100
+ raw: supabaseUser as unknown as Record<string, unknown>,
93
101
  };
94
102
 
95
103
  // Cache the validated user
@@ -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
+ }