@qwickapps/server 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +311 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +144 -2
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +36 -0
- package/dist/core/plugin-registry.d.ts.map +1 -1
- package/dist/core/plugin-registry.js +26 -0
- package/dist/core/plugin-registry.js.map +1 -1
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
- package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.js +166 -0
- package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
- package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +1 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/index.js +1 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
- package/dist/plugins/auth/config-store.d.ts +11 -0
- package/dist/plugins/auth/config-store.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.js +232 -0
- package/dist/plugins/auth/config-store.js.map +1 -0
- package/dist/plugins/auth/config-store.test.d.ts +7 -0
- package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.test.js +299 -0
- package/dist/plugins/auth/config-store.test.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +138 -0
- package/dist/plugins/auth/env-config.d.ts.map +1 -0
- package/dist/plugins/auth/env-config.js +1122 -0
- package/dist/plugins/auth/env-config.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +7 -1
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +7 -0
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
- package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
- package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
- package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +176 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/auth/types.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +3 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/index.d.ts +6 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +5 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +3 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
- package/dist/plugins/preferences/index.d.ts +12 -0
- package/dist/plugins/preferences/index.d.ts.map +1 -0
- package/dist/plugins/preferences/index.js +13 -0
- package/dist/plugins/preferences/index.js.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.js +226 -0
- package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
- package/dist/plugins/preferences/stores/index.d.ts +9 -0
- package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/index.js +9 -0
- package/dist/plugins/preferences/stores/index.js.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.js +181 -0
- package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
- package/dist/plugins/preferences/types.d.ts +91 -0
- package/dist/plugins/preferences/types.d.ts.map +1 -0
- package/dist/plugins/preferences/types.js +10 -0
- package/dist/plugins/preferences/types.js.map +1 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
- package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
- package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
- package/dist/plugins/rate-limit/cleanup.js +72 -0
- package/dist/plugins/rate-limit/cleanup.js.map +1 -0
- package/dist/plugins/rate-limit/env-config.d.ts +91 -0
- package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
- package/dist/plugins/rate-limit/env-config.js +318 -0
- package/dist/plugins/rate-limit/env-config.js.map +1 -0
- package/dist/plugins/rate-limit/index.d.ts +76 -0
- package/dist/plugins/rate-limit/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/index.js +79 -0
- package/dist/plugins/rate-limit/index.js.map +1 -0
- package/dist/plugins/rate-limit/middleware.d.ts +40 -0
- package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
- package/dist/plugins/rate-limit/middleware.js +169 -0
- package/dist/plugins/rate-limit/middleware.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
- package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
- package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
- package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
- package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/index.js +8 -0
- package/dist/plugins/rate-limit/stores/index.js.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.js +27 -0
- package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
- package/dist/plugins/rate-limit/types.d.ts +265 -0
- package/dist/plugins/rate-limit/types.d.ts.map +1 -0
- package/dist/plugins/rate-limit/types.js +9 -0
- package/dist/plugins/rate-limit/types.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
- package/dist/plugins/users/index.d.ts +2 -2
- package/dist/plugins/users/index.d.ts.map +1 -1
- package/dist/plugins/users/index.js +1 -1
- package/dist/plugins/users/index.js.map +1 -1
- package/dist/plugins/users/types.d.ts +36 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +8 -2
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +122 -5
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
- package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +194 -7
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
- package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
- package/dist-ui-lib/index.js +3665 -3945
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
- package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
- package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
- package/package.json +7 -2
- package/src/core/control-panel.ts +161 -2
- package/src/core/plugin-registry.ts +63 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +45 -0
- package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
- package/src/plugins/auth/adapter-wrapper.ts +205 -0
- package/src/plugins/auth/adapters/index.ts +1 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
- package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
- package/src/plugins/auth/config-store.test.ts +417 -0
- package/src/plugins/auth/config-store.ts +305 -0
- package/src/plugins/auth/env-config.ts +1279 -0
- package/src/plugins/auth/index.ts +30 -0
- package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
- package/src/plugins/auth/types.ts +218 -0
- package/src/plugins/cache-plugin.test.ts +3 -0
- package/src/plugins/index.ts +75 -0
- package/src/plugins/postgres-plugin.test.ts +3 -0
- package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
- package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
- package/src/plugins/preferences/index.ts +30 -0
- package/src/plugins/preferences/preferences-plugin.ts +270 -0
- package/src/plugins/preferences/stores/index.ts +9 -0
- package/src/plugins/preferences/stores/postgres-store.ts +252 -0
- package/src/plugins/preferences/types.ts +100 -0
- package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
- package/src/plugins/rate-limit/cleanup.ts +117 -0
- package/src/plugins/rate-limit/env-config.ts +400 -0
- package/src/plugins/rate-limit/index.ts +128 -0
- package/src/plugins/rate-limit/middleware.ts +212 -0
- package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
- package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
- package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
- package/src/plugins/rate-limit/stores/index.ts +8 -0
- package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
- package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
- package/src/plugins/rate-limit/strategies/index.ts +30 -0
- package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
- package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
- package/src/plugins/rate-limit/types.ts +338 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
- package/src/plugins/users/index.ts +3 -0
- package/src/plugins/users/types.ts +38 -0
- package/src/plugins/users/users-plugin.ts +142 -5
- package/ui/src/App.tsx +35 -14
- package/ui/src/api/controlPanelApi.ts +326 -1
- package/ui/src/components/ControlPanelApp.tsx +3 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
- package/ui/src/dashboard/builtInWidgets.tsx +13 -3
- package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
- package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
- package/ui/src/dashboard/widgets/index.ts +2 -0
- package/ui/src/pages/AuthPage.tsx +1103 -0
- package/ui/src/pages/IntegrationsPage.tsx +288 -0
- package/ui/src/pages/PluginsPage.tsx +394 -0
- package/ui/src/pages/RateLimitPage.tsx +292 -0
- package/ui/vite.lib.config.ts +5 -0
- package/dist-ui/assets/index-Bsp2ntcw.js +0 -465
- package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Plugin Environment Configuration
|
|
3
|
+
*
|
|
4
|
+
* Factory function and utilities for configuring auth adapters via environment variables.
|
|
5
|
+
* Supports all adapters: Auth0, Supabase, Supertokens, Basic.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Request, Response } from 'express';
|
|
11
|
+
import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
|
|
12
|
+
import type {
|
|
13
|
+
AuthPluginConfig,
|
|
14
|
+
AuthAdapter,
|
|
15
|
+
Auth0AdapterConfig,
|
|
16
|
+
SupabaseAdapterConfig,
|
|
17
|
+
SupertokensAdapterConfig,
|
|
18
|
+
BasicAdapterConfig,
|
|
19
|
+
AuthEnvPluginOptions,
|
|
20
|
+
AuthConfigStatus,
|
|
21
|
+
AuthPluginState,
|
|
22
|
+
AuthAdapterType,
|
|
23
|
+
RuntimeAuthConfig,
|
|
24
|
+
UpdateAuthConfigRequest,
|
|
25
|
+
TestProviderRequest,
|
|
26
|
+
TestProviderResponse,
|
|
27
|
+
AuthConfigStore,
|
|
28
|
+
} from './types.js';
|
|
29
|
+
import { createAuthPlugin } from './auth-plugin.js';
|
|
30
|
+
import { auth0Adapter } from './adapters/auth0-adapter.js';
|
|
31
|
+
import { supabaseAdapter } from './adapters/supabase-adapter.js';
|
|
32
|
+
import { supertokensAdapter } from './adapters/supertokens-adapter.js';
|
|
33
|
+
import { basicAdapter } from './adapters/basic-adapter.js';
|
|
34
|
+
import { createAdapterWrapper, type AdapterWrapper } from './adapter-wrapper.js';
|
|
35
|
+
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// Module State
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
let currentStatus: AuthConfigStatus = {
|
|
41
|
+
state: 'disabled',
|
|
42
|
+
adapter: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Runtime configuration store (PostgreSQL-backed)
|
|
46
|
+
let configStore: AuthConfigStore | null = null;
|
|
47
|
+
|
|
48
|
+
// Adapter wrapper for hot-reload support
|
|
49
|
+
let adapterWrapper: AdapterWrapper | null = null;
|
|
50
|
+
|
|
51
|
+
// Unsubscribe function for config change listener
|
|
52
|
+
let configUnsubscribe: (() => void) | null = null;
|
|
53
|
+
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
55
|
+
// Environment Variable Helpers
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get an environment variable, treating empty strings as undefined
|
|
60
|
+
*/
|
|
61
|
+
function getEnv(key: string): string | undefined {
|
|
62
|
+
const value = process.env[key];
|
|
63
|
+
if (value === undefined || value === null || value.trim() === '') {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return value.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse a boolean environment variable
|
|
71
|
+
* Supports: true/false, 1/0, yes/no (case-insensitive)
|
|
72
|
+
*/
|
|
73
|
+
function getEnvBool(key: string, defaultValue: boolean): boolean {
|
|
74
|
+
const value = getEnv(key);
|
|
75
|
+
if (value === undefined) {
|
|
76
|
+
return defaultValue;
|
|
77
|
+
}
|
|
78
|
+
const lower = value.toLowerCase();
|
|
79
|
+
if (['true', '1', 'yes'].includes(lower)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (['false', '0', 'no'].includes(lower)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return defaultValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse a comma-separated list environment variable
|
|
90
|
+
*/
|
|
91
|
+
function getEnvList(key: string): string[] | undefined {
|
|
92
|
+
const value = getEnv(key);
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return value
|
|
97
|
+
.split(',')
|
|
98
|
+
.map((s) => s.trim())
|
|
99
|
+
.filter((s) => s.length > 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mask a sensitive value for display
|
|
104
|
+
*/
|
|
105
|
+
function maskValue(value: string): string {
|
|
106
|
+
if (value.length <= 4) {
|
|
107
|
+
return '****';
|
|
108
|
+
}
|
|
109
|
+
return value.substring(0, 2) + '*'.repeat(Math.min(value.length - 4, 20)) + value.substring(value.length - 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
+
// Environment Parsers
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
interface EnvParseResult<T> {
|
|
117
|
+
config: T | null;
|
|
118
|
+
errors: string[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse Supertokens configuration from environment variables
|
|
123
|
+
*/
|
|
124
|
+
function parseSupertokensEnv(): EnvParseResult<SupertokensAdapterConfig> {
|
|
125
|
+
const errors: string[] = [];
|
|
126
|
+
|
|
127
|
+
const connectionUri = getEnv('SUPERTOKENS_CONNECTION_URI');
|
|
128
|
+
const appName = getEnv('SUPERTOKENS_APP_NAME');
|
|
129
|
+
const apiDomain = getEnv('SUPERTOKENS_API_DOMAIN');
|
|
130
|
+
const websiteDomain = getEnv('SUPERTOKENS_WEBSITE_DOMAIN');
|
|
131
|
+
|
|
132
|
+
// Check required vars
|
|
133
|
+
if (!connectionUri) errors.push('SUPERTOKENS_CONNECTION_URI');
|
|
134
|
+
if (!appName) errors.push('SUPERTOKENS_APP_NAME');
|
|
135
|
+
if (!apiDomain) errors.push('SUPERTOKENS_API_DOMAIN');
|
|
136
|
+
if (!websiteDomain) errors.push('SUPERTOKENS_WEBSITE_DOMAIN');
|
|
137
|
+
|
|
138
|
+
if (errors.length > 0) {
|
|
139
|
+
return { config: null, errors };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Build config
|
|
143
|
+
const config: SupertokensAdapterConfig = {
|
|
144
|
+
connectionUri: connectionUri!,
|
|
145
|
+
appName: appName!,
|
|
146
|
+
apiDomain: apiDomain!,
|
|
147
|
+
websiteDomain: websiteDomain!,
|
|
148
|
+
apiKey: getEnv('SUPERTOKENS_API_KEY'),
|
|
149
|
+
apiBasePath: getEnv('SUPERTOKENS_API_BASE_PATH') ?? '/auth',
|
|
150
|
+
websiteBasePath: getEnv('SUPERTOKENS_WEBSITE_BASE_PATH') ?? '/auth',
|
|
151
|
+
enableEmailPassword: getEnvBool('SUPERTOKENS_ENABLE_EMAIL_PASSWORD', true),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Parse social providers
|
|
155
|
+
const googleClientId = getEnv('SUPERTOKENS_GOOGLE_CLIENT_ID');
|
|
156
|
+
const googleClientSecret = getEnv('SUPERTOKENS_GOOGLE_CLIENT_SECRET');
|
|
157
|
+
const githubClientId = getEnv('SUPERTOKENS_GITHUB_CLIENT_ID');
|
|
158
|
+
const githubClientSecret = getEnv('SUPERTOKENS_GITHUB_CLIENT_SECRET');
|
|
159
|
+
const appleClientId = getEnv('SUPERTOKENS_APPLE_CLIENT_ID');
|
|
160
|
+
const appleClientSecret = getEnv('SUPERTOKENS_APPLE_CLIENT_SECRET');
|
|
161
|
+
const appleKeyId = getEnv('SUPERTOKENS_APPLE_KEY_ID');
|
|
162
|
+
const appleTeamId = getEnv('SUPERTOKENS_APPLE_TEAM_ID');
|
|
163
|
+
|
|
164
|
+
// Only add social providers if both client ID and secret are provided
|
|
165
|
+
if (googleClientId && googleClientSecret) {
|
|
166
|
+
config.socialProviders = config.socialProviders || {};
|
|
167
|
+
config.socialProviders.google = {
|
|
168
|
+
clientId: googleClientId,
|
|
169
|
+
clientSecret: googleClientSecret,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (githubClientId && githubClientSecret) {
|
|
174
|
+
config.socialProviders = config.socialProviders || {};
|
|
175
|
+
config.socialProviders.github = {
|
|
176
|
+
clientId: githubClientId,
|
|
177
|
+
clientSecret: githubClientSecret,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (appleClientId && appleClientSecret && appleKeyId && appleTeamId) {
|
|
182
|
+
config.socialProviders = config.socialProviders || {};
|
|
183
|
+
config.socialProviders.apple = {
|
|
184
|
+
clientId: appleClientId,
|
|
185
|
+
clientSecret: appleClientSecret,
|
|
186
|
+
keyId: appleKeyId,
|
|
187
|
+
teamId: appleTeamId,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { config, errors: [] };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parse Auth0 configuration from environment variables
|
|
196
|
+
*/
|
|
197
|
+
function parseAuth0Env(): EnvParseResult<Auth0AdapterConfig> {
|
|
198
|
+
const errors: string[] = [];
|
|
199
|
+
|
|
200
|
+
const domain = getEnv('AUTH0_DOMAIN');
|
|
201
|
+
const clientId = getEnv('AUTH0_CLIENT_ID');
|
|
202
|
+
const clientSecret = getEnv('AUTH0_CLIENT_SECRET');
|
|
203
|
+
const baseUrl = getEnv('AUTH0_BASE_URL');
|
|
204
|
+
const secret = getEnv('AUTH0_SECRET');
|
|
205
|
+
|
|
206
|
+
// Check required vars
|
|
207
|
+
if (!domain) errors.push('AUTH0_DOMAIN');
|
|
208
|
+
if (!clientId) errors.push('AUTH0_CLIENT_ID');
|
|
209
|
+
if (!clientSecret) errors.push('AUTH0_CLIENT_SECRET');
|
|
210
|
+
if (!baseUrl) errors.push('AUTH0_BASE_URL');
|
|
211
|
+
if (!secret) errors.push('AUTH0_SECRET');
|
|
212
|
+
|
|
213
|
+
if (errors.length > 0) {
|
|
214
|
+
return { config: null, errors };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Build config
|
|
218
|
+
const config: Auth0AdapterConfig = {
|
|
219
|
+
domain: domain!,
|
|
220
|
+
clientId: clientId!,
|
|
221
|
+
clientSecret: clientSecret!,
|
|
222
|
+
baseUrl: baseUrl!,
|
|
223
|
+
secret: secret!,
|
|
224
|
+
audience: getEnv('AUTH0_AUDIENCE'),
|
|
225
|
+
scopes: getEnvList('AUTH0_SCOPES') ?? ['openid', 'profile', 'email'],
|
|
226
|
+
allowedRoles: getEnvList('AUTH0_ALLOWED_ROLES'),
|
|
227
|
+
allowedDomains: getEnvList('AUTH0_ALLOWED_DOMAINS'),
|
|
228
|
+
exposeAccessToken: getEnvBool('AUTH0_EXPOSE_ACCESS_TOKEN', false),
|
|
229
|
+
routes: {
|
|
230
|
+
login: getEnv('AUTH0_LOGIN_PATH') ?? '/login',
|
|
231
|
+
logout: getEnv('AUTH0_LOGOUT_PATH') ?? '/logout',
|
|
232
|
+
callback: getEnv('AUTH0_CALLBACK_PATH') ?? '/callback',
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return { config, errors: [] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse Supabase configuration from environment variables
|
|
241
|
+
*/
|
|
242
|
+
function parseSupabaseEnv(): EnvParseResult<SupabaseAdapterConfig> {
|
|
243
|
+
const errors: string[] = [];
|
|
244
|
+
|
|
245
|
+
const url = getEnv('SUPABASE_URL');
|
|
246
|
+
const anonKey = getEnv('SUPABASE_ANON_KEY');
|
|
247
|
+
|
|
248
|
+
// Check required vars
|
|
249
|
+
if (!url) errors.push('SUPABASE_URL');
|
|
250
|
+
if (!anonKey) errors.push('SUPABASE_ANON_KEY');
|
|
251
|
+
|
|
252
|
+
if (errors.length > 0) {
|
|
253
|
+
return { config: null, errors };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const config: SupabaseAdapterConfig = {
|
|
257
|
+
url: url!,
|
|
258
|
+
anonKey: anonKey!,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return { config, errors: [] };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Parse Basic Auth configuration from environment variables
|
|
266
|
+
*/
|
|
267
|
+
function parseBasicAuthEnv(): EnvParseResult<BasicAdapterConfig> {
|
|
268
|
+
const errors: string[] = [];
|
|
269
|
+
|
|
270
|
+
const username = getEnv('BASIC_AUTH_USERNAME');
|
|
271
|
+
const password = getEnv('BASIC_AUTH_PASSWORD');
|
|
272
|
+
|
|
273
|
+
// Check required vars
|
|
274
|
+
if (!username) errors.push('BASIC_AUTH_USERNAME');
|
|
275
|
+
if (!password) errors.push('BASIC_AUTH_PASSWORD');
|
|
276
|
+
|
|
277
|
+
if (errors.length > 0) {
|
|
278
|
+
return { config: null, errors };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const config: BasicAdapterConfig = {
|
|
282
|
+
username: username!,
|
|
283
|
+
password: password!,
|
|
284
|
+
realm: getEnv('BASIC_AUTH_REALM') ?? 'Protected',
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return { config, errors: [] };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
291
|
+
// Factory Function
|
|
292
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Valid adapter names
|
|
296
|
+
*/
|
|
297
|
+
const VALID_ADAPTERS = ['supertokens', 'auth0', 'supabase', 'basic'] as const;
|
|
298
|
+
type AdapterName = (typeof VALID_ADAPTERS)[number];
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create an auth plugin configured from environment variables.
|
|
302
|
+
*
|
|
303
|
+
* The plugin state depends on environment configuration:
|
|
304
|
+
* - **disabled**: AUTH_ADAPTER not set - no authentication middleware is applied
|
|
305
|
+
* - **enabled**: Valid configuration - adapter is active and working
|
|
306
|
+
* - **error**: Invalid configuration - plugin is disabled with error details
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* // Zero-config setup - reads everything from env vars
|
|
311
|
+
* const authPlugin = createAuthPluginFromEnv();
|
|
312
|
+
*
|
|
313
|
+
* // With overrides
|
|
314
|
+
* const authPlugin = createAuthPluginFromEnv({
|
|
315
|
+
* excludePaths: ['/health', '/metrics'],
|
|
316
|
+
* authRequired: true,
|
|
317
|
+
* });
|
|
318
|
+
* ```
|
|
319
|
+
*
|
|
320
|
+
* @param options - Optional overrides (env vars take precedence for adapter config)
|
|
321
|
+
* @returns A Plugin instance
|
|
322
|
+
*/
|
|
323
|
+
export function createAuthPluginFromEnv(options?: AuthEnvPluginOptions): Plugin {
|
|
324
|
+
const adapterName = getEnv('AUTH_ADAPTER')?.toLowerCase();
|
|
325
|
+
|
|
326
|
+
// No adapter specified - return disabled plugin
|
|
327
|
+
if (!adapterName) {
|
|
328
|
+
currentStatus = {
|
|
329
|
+
state: 'disabled',
|
|
330
|
+
adapter: null,
|
|
331
|
+
};
|
|
332
|
+
return createDisabledPlugin();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Validate adapter name
|
|
336
|
+
if (!VALID_ADAPTERS.includes(adapterName as AdapterName)) {
|
|
337
|
+
const error = `Invalid AUTH_ADAPTER value: "${adapterName}". Valid options: ${VALID_ADAPTERS.join(', ')}`;
|
|
338
|
+
currentStatus = {
|
|
339
|
+
state: 'error',
|
|
340
|
+
adapter: null,
|
|
341
|
+
error,
|
|
342
|
+
};
|
|
343
|
+
return createErrorPlugin(error);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Parse adapter-specific configuration
|
|
347
|
+
let parseResult: EnvParseResult<
|
|
348
|
+
Auth0AdapterConfig | SupabaseAdapterConfig | SupertokensAdapterConfig | BasicAdapterConfig
|
|
349
|
+
>;
|
|
350
|
+
|
|
351
|
+
switch (adapterName as AdapterName) {
|
|
352
|
+
case 'supertokens':
|
|
353
|
+
parseResult = parseSupertokensEnv();
|
|
354
|
+
break;
|
|
355
|
+
case 'auth0':
|
|
356
|
+
parseResult = parseAuth0Env();
|
|
357
|
+
break;
|
|
358
|
+
case 'supabase':
|
|
359
|
+
parseResult = parseSupabaseEnv();
|
|
360
|
+
break;
|
|
361
|
+
case 'basic':
|
|
362
|
+
parseResult = parseBasicAuthEnv();
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check for parsing errors
|
|
367
|
+
if (parseResult.errors.length > 0) {
|
|
368
|
+
const error = `Missing required environment variables for ${adapterName} adapter: ${parseResult.errors.join(', ')}`;
|
|
369
|
+
currentStatus = {
|
|
370
|
+
state: 'error',
|
|
371
|
+
adapter: adapterName,
|
|
372
|
+
error,
|
|
373
|
+
missingVars: parseResult.errors,
|
|
374
|
+
};
|
|
375
|
+
return createErrorPlugin(error);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Create the adapter
|
|
379
|
+
let adapter;
|
|
380
|
+
switch (adapterName as AdapterName) {
|
|
381
|
+
case 'supertokens':
|
|
382
|
+
adapter = supertokensAdapter(parseResult.config as SupertokensAdapterConfig);
|
|
383
|
+
break;
|
|
384
|
+
case 'auth0':
|
|
385
|
+
adapter = auth0Adapter(parseResult.config as Auth0AdapterConfig);
|
|
386
|
+
break;
|
|
387
|
+
case 'supabase':
|
|
388
|
+
adapter = supabaseAdapter(parseResult.config as SupabaseAdapterConfig);
|
|
389
|
+
break;
|
|
390
|
+
case 'basic':
|
|
391
|
+
adapter = basicAdapter(parseResult.config as BasicAdapterConfig);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Build plugin configuration
|
|
396
|
+
const excludePaths = options?.excludePaths ?? getEnvList('AUTH_EXCLUDE_PATHS') ?? [];
|
|
397
|
+
const authRequired = options?.authRequired ?? getEnvBool('AUTH_REQUIRED', true);
|
|
398
|
+
const debug = options?.debug ?? getEnvBool('AUTH_DEBUG', false);
|
|
399
|
+
|
|
400
|
+
const pluginConfig: AuthPluginConfig = {
|
|
401
|
+
adapter,
|
|
402
|
+
excludePaths,
|
|
403
|
+
authRequired,
|
|
404
|
+
debug,
|
|
405
|
+
onUnauthorized: options?.onUnauthorized,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Update status
|
|
409
|
+
currentStatus = {
|
|
410
|
+
state: 'enabled',
|
|
411
|
+
adapter: adapterName,
|
|
412
|
+
config: getMaskedConfig(adapterName as AdapterName),
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Create the plugin
|
|
416
|
+
const basePlugin = createAuthPlugin(pluginConfig);
|
|
417
|
+
|
|
418
|
+
// Wrap to add config status routes
|
|
419
|
+
return {
|
|
420
|
+
...basePlugin,
|
|
421
|
+
async onStart(pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
|
|
422
|
+
// Call base plugin onStart
|
|
423
|
+
await basePlugin.onStart?.(pluginConfig, registry);
|
|
424
|
+
|
|
425
|
+
// Add config status routes
|
|
426
|
+
registerConfigRoutes(registry);
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
432
|
+
// Status & Config API
|
|
433
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get current auth plugin status
|
|
437
|
+
*/
|
|
438
|
+
export function getAuthStatus(): AuthConfigStatus {
|
|
439
|
+
return { ...currentStatus };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Get masked configuration for the current adapter
|
|
444
|
+
*/
|
|
445
|
+
function getMaskedConfig(adapter: AdapterName): Record<string, string> {
|
|
446
|
+
const config: Record<string, string> = {};
|
|
447
|
+
|
|
448
|
+
// Sensitive keys that should be masked
|
|
449
|
+
const sensitiveKeys = [
|
|
450
|
+
'SECRET',
|
|
451
|
+
'PASSWORD',
|
|
452
|
+
'KEY',
|
|
453
|
+
'TOKEN',
|
|
454
|
+
'CREDENTIAL',
|
|
455
|
+
'ANON_KEY',
|
|
456
|
+
'API_KEY',
|
|
457
|
+
'CLIENT_SECRET',
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
const isSensitive = (key: string): boolean => {
|
|
461
|
+
const upper = key.toUpperCase();
|
|
462
|
+
return sensitiveKeys.some((s) => upper.includes(s));
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Get all env vars for the adapter
|
|
466
|
+
const prefixes: Record<AdapterName, string[]> = {
|
|
467
|
+
supertokens: ['SUPERTOKENS_'],
|
|
468
|
+
auth0: ['AUTH0_'],
|
|
469
|
+
supabase: ['SUPABASE_'],
|
|
470
|
+
basic: ['BASIC_AUTH_'],
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
474
|
+
const matchesPrefix = prefixes[adapter].some((p) => key.startsWith(p));
|
|
475
|
+
if (matchesPrefix && value) {
|
|
476
|
+
config[key] = isSensitive(key) ? maskValue(value) : value;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Add general auth vars
|
|
481
|
+
const generalVars = ['AUTH_ADAPTER', 'AUTH_REQUIRED', 'AUTH_EXCLUDE_PATHS', 'AUTH_DEBUG'];
|
|
482
|
+
for (const key of generalVars) {
|
|
483
|
+
const value = process.env[key];
|
|
484
|
+
if (value) {
|
|
485
|
+
config[key] = value;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return config;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Register config API routes
|
|
494
|
+
*/
|
|
495
|
+
function registerConfigRoutes(registry: PluginRegistry): void {
|
|
496
|
+
// GET /auth/config/status - Get current auth status
|
|
497
|
+
registry.addRoute({
|
|
498
|
+
method: 'get',
|
|
499
|
+
path: '/auth/config/status',
|
|
500
|
+
pluginId: 'auth',
|
|
501
|
+
handler: (_req: Request, res: Response) => {
|
|
502
|
+
res.json(getAuthStatus());
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// GET /auth/config - Get current configuration (masked)
|
|
507
|
+
registry.addRoute({
|
|
508
|
+
method: 'get',
|
|
509
|
+
path: '/auth/config',
|
|
510
|
+
pluginId: 'auth',
|
|
511
|
+
handler: async (_req: Request, res: Response) => {
|
|
512
|
+
const status = getAuthStatus();
|
|
513
|
+
const response: Record<string, unknown> = {
|
|
514
|
+
state: status.state,
|
|
515
|
+
adapter: status.adapter,
|
|
516
|
+
config: status.config || {},
|
|
517
|
+
error: status.error,
|
|
518
|
+
missingVars: status.missingVars,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Include runtime config if available from store
|
|
522
|
+
if (configStore) {
|
|
523
|
+
try {
|
|
524
|
+
const runtimeConfig = await configStore.load();
|
|
525
|
+
if (runtimeConfig) {
|
|
526
|
+
response.runtimeConfig = runtimeConfig;
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.error('[AuthPlugin] Failed to load runtime config:', err);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
res.json(response);
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// PUT /auth/config - Save new configuration
|
|
538
|
+
registry.addRoute({
|
|
539
|
+
method: 'put',
|
|
540
|
+
path: '/auth/config',
|
|
541
|
+
pluginId: 'auth',
|
|
542
|
+
handler: async (req: Request, res: Response) => {
|
|
543
|
+
try {
|
|
544
|
+
// Check if config store is available
|
|
545
|
+
if (!configStore) {
|
|
546
|
+
return res.status(503).json({
|
|
547
|
+
error: 'Configuration store not available',
|
|
548
|
+
message: 'Runtime configuration requires a config store (PostgreSQL)',
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Parse and validate request body
|
|
553
|
+
const body = req.body as UpdateAuthConfigRequest;
|
|
554
|
+
if (!body || !body.adapter) {
|
|
555
|
+
return res.status(400).json({
|
|
556
|
+
error: 'Invalid request',
|
|
557
|
+
message: 'Request body must include adapter type',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Validate adapter type
|
|
562
|
+
if (!VALID_ADAPTERS.includes(body.adapter as AdapterName)) {
|
|
563
|
+
return res.status(400).json({
|
|
564
|
+
error: 'Invalid adapter',
|
|
565
|
+
message: `Valid adapters: ${VALID_ADAPTERS.join(', ')}`,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Validate adapter config
|
|
570
|
+
const validation = validateAdapterConfig(body.adapter, body.config || {});
|
|
571
|
+
if (!validation.valid) {
|
|
572
|
+
return res.status(400).json({
|
|
573
|
+
error: 'Invalid configuration',
|
|
574
|
+
message: validation.errors.join(', '),
|
|
575
|
+
errors: validation.errors,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Build runtime config
|
|
580
|
+
const runtimeConfig: RuntimeAuthConfig = {
|
|
581
|
+
adapter: body.adapter,
|
|
582
|
+
config: {
|
|
583
|
+
[body.adapter]: body.config,
|
|
584
|
+
} as RuntimeAuthConfig['config'],
|
|
585
|
+
settings: body.settings || {},
|
|
586
|
+
updatedAt: new Date().toISOString(),
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Save to store
|
|
590
|
+
await configStore.save(runtimeConfig);
|
|
591
|
+
|
|
592
|
+
// Apply config immediately (hot-reload)
|
|
593
|
+
if (adapterWrapper) {
|
|
594
|
+
const adapter = createAdapterFromConfig(body.adapter, runtimeConfig.config);
|
|
595
|
+
if (adapter) {
|
|
596
|
+
await adapterWrapper.setAdapter(adapter);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Update status
|
|
601
|
+
currentStatus = {
|
|
602
|
+
state: 'enabled',
|
|
603
|
+
adapter: body.adapter,
|
|
604
|
+
config: getMaskedRuntimeConfig(runtimeConfig),
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
res.json({
|
|
608
|
+
success: true,
|
|
609
|
+
status: getAuthStatus(),
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
console.error('[AuthPlugin] Failed to save config:', error);
|
|
613
|
+
res.status(500).json({
|
|
614
|
+
error: 'Failed to save configuration',
|
|
615
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// DELETE /auth/config - Revert to environment variables
|
|
622
|
+
registry.addRoute({
|
|
623
|
+
method: 'delete',
|
|
624
|
+
path: '/auth/config',
|
|
625
|
+
pluginId: 'auth',
|
|
626
|
+
handler: async (_req: Request, res: Response) => {
|
|
627
|
+
try {
|
|
628
|
+
// Check if config store is available
|
|
629
|
+
if (!configStore) {
|
|
630
|
+
return res.status(503).json({
|
|
631
|
+
error: 'Configuration store not available',
|
|
632
|
+
message: 'Runtime configuration requires a config store (PostgreSQL)',
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Delete from store
|
|
637
|
+
await configStore.delete();
|
|
638
|
+
|
|
639
|
+
// Revert to env vars
|
|
640
|
+
const adapterName = getEnv('AUTH_ADAPTER')?.toLowerCase() as AdapterName | undefined;
|
|
641
|
+
if (adapterName && VALID_ADAPTERS.includes(adapterName)) {
|
|
642
|
+
const parseResult = getParseResultForAdapter(adapterName);
|
|
643
|
+
if (parseResult.config && adapterWrapper) {
|
|
644
|
+
const adapter = createAdapterForName(adapterName, parseResult.config);
|
|
645
|
+
await adapterWrapper.setAdapter(adapter);
|
|
646
|
+
currentStatus = {
|
|
647
|
+
state: 'enabled',
|
|
648
|
+
adapter: adapterName,
|
|
649
|
+
config: getMaskedConfig(adapterName),
|
|
650
|
+
};
|
|
651
|
+
} else if (parseResult.errors.length > 0) {
|
|
652
|
+
currentStatus = {
|
|
653
|
+
state: 'error',
|
|
654
|
+
adapter: adapterName,
|
|
655
|
+
error: `Missing env vars: ${parseResult.errors.join(', ')}`,
|
|
656
|
+
missingVars: parseResult.errors,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
currentStatus = {
|
|
661
|
+
state: 'disabled',
|
|
662
|
+
adapter: null,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
res.json({
|
|
667
|
+
success: true,
|
|
668
|
+
status: getAuthStatus(),
|
|
669
|
+
});
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error('[AuthPlugin] Failed to delete config:', error);
|
|
672
|
+
res.status(500).json({
|
|
673
|
+
error: 'Failed to delete configuration',
|
|
674
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// POST /auth/test-provider - Test provider connection
|
|
681
|
+
registry.addRoute({
|
|
682
|
+
method: 'post',
|
|
683
|
+
path: '/auth/test-provider',
|
|
684
|
+
pluginId: 'auth',
|
|
685
|
+
handler: async (req: Request, res: Response) => {
|
|
686
|
+
try {
|
|
687
|
+
const body = req.body as TestProviderRequest;
|
|
688
|
+
if (!body || !body.adapter) {
|
|
689
|
+
return res.status(400).json({
|
|
690
|
+
error: 'Invalid request',
|
|
691
|
+
message: 'Request body must include adapter type',
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Validate adapter type
|
|
696
|
+
if (!VALID_ADAPTERS.includes(body.adapter as AdapterName)) {
|
|
697
|
+
return res.status(400).json({
|
|
698
|
+
error: 'Invalid adapter',
|
|
699
|
+
message: `Valid adapters: ${VALID_ADAPTERS.join(', ')}`,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Test the connection
|
|
704
|
+
const result = await testProviderConnection(body.adapter, body.config || {});
|
|
705
|
+
res.json(result);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error('[AuthPlugin] Test provider error:', error);
|
|
708
|
+
res.status(500).json({
|
|
709
|
+
success: false,
|
|
710
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// POST /auth/test-current - Test current auth configuration (env-based or runtime)
|
|
717
|
+
registry.addRoute({
|
|
718
|
+
method: 'post',
|
|
719
|
+
path: '/auth/test-current',
|
|
720
|
+
pluginId: 'auth',
|
|
721
|
+
handler: async (_req: Request, res: Response) => {
|
|
722
|
+
try {
|
|
723
|
+
const status = getAuthStatus();
|
|
724
|
+
|
|
725
|
+
if (status.state !== 'enabled' || !status.adapter) {
|
|
726
|
+
return res.status(400).json({
|
|
727
|
+
success: false,
|
|
728
|
+
message: 'No active auth configuration to test',
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Get the current configuration from env vars
|
|
733
|
+
const adapterName = status.adapter as AdapterName;
|
|
734
|
+
const parseResult = getParseResultForAdapter(adapterName);
|
|
735
|
+
|
|
736
|
+
if (parseResult.errors.length > 0) {
|
|
737
|
+
return res.json({
|
|
738
|
+
success: false,
|
|
739
|
+
message: `Missing configuration: ${parseResult.errors.join(', ')}`,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Test the connection using the current env-based config
|
|
744
|
+
// Use JSON round-trip for safe conversion to plain object
|
|
745
|
+
const configObj = JSON.parse(JSON.stringify(parseResult.config)) as Record<string, unknown>;
|
|
746
|
+
const result = await testProviderConnection(adapterName, configObj);
|
|
747
|
+
res.json(result);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
console.error('[AuthPlugin] Test current provider error:', error);
|
|
750
|
+
res.status(500).json({
|
|
751
|
+
success: false,
|
|
752
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// Register UI menu item for auth configuration
|
|
759
|
+
registry.addMenuItem({
|
|
760
|
+
pluginId: 'auth',
|
|
761
|
+
id: 'auth:sidebar',
|
|
762
|
+
label: 'Authentication',
|
|
763
|
+
icon: 'security',
|
|
764
|
+
route: '/auth',
|
|
765
|
+
order: 50, // After Entitlements (35)
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Register page contribution
|
|
769
|
+
registry.addPage({
|
|
770
|
+
pluginId: 'auth',
|
|
771
|
+
id: 'auth:config-page',
|
|
772
|
+
route: '/auth',
|
|
773
|
+
component: 'AuthPage',
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Register dashboard widget
|
|
777
|
+
registry.addWidget({
|
|
778
|
+
id: 'auth-status',
|
|
779
|
+
title: 'Authentication',
|
|
780
|
+
component: 'AuthStatusWidget',
|
|
781
|
+
priority: 40, // Show before integrations
|
|
782
|
+
showByDefault: true,
|
|
783
|
+
pluginId: 'auth',
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Register health check for auth provider connection
|
|
787
|
+
const status = getAuthStatus();
|
|
788
|
+
if (status.state === 'enabled' && status.adapter) {
|
|
789
|
+
registry.registerHealthCheck({
|
|
790
|
+
name: `auth-${status.adapter}`,
|
|
791
|
+
type: 'custom',
|
|
792
|
+
check: async () => {
|
|
793
|
+
const currentStatus = getAuthStatus();
|
|
794
|
+
if (currentStatus.state !== 'enabled' || !currentStatus.adapter) {
|
|
795
|
+
return { healthy: false, message: 'Auth not configured' };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const adapterName = currentStatus.adapter as AdapterName;
|
|
799
|
+
const parseResult = getParseResultForAdapter(adapterName);
|
|
800
|
+
if (parseResult.errors.length > 0) {
|
|
801
|
+
return { healthy: false, message: `Missing config: ${parseResult.errors.join(', ')}` };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const configObj = JSON.parse(JSON.stringify(parseResult.config)) as Record<string, unknown>;
|
|
805
|
+
const result = await testProviderConnection(adapterName, configObj);
|
|
806
|
+
return {
|
|
807
|
+
healthy: result.success,
|
|
808
|
+
message: result.message,
|
|
809
|
+
details: result.details,
|
|
810
|
+
};
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
817
|
+
// Disabled & Error Plugins
|
|
818
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Create a disabled plugin (no auth middleware)
|
|
822
|
+
*/
|
|
823
|
+
function createDisabledPlugin(): Plugin {
|
|
824
|
+
return {
|
|
825
|
+
id: 'auth',
|
|
826
|
+
name: 'Auth Plugin (Disabled)',
|
|
827
|
+
version: '1.0.0',
|
|
828
|
+
|
|
829
|
+
async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
|
|
830
|
+
const logger = registry.getLogger('auth');
|
|
831
|
+
logger.info('Auth plugin disabled - AUTH_ADAPTER not set');
|
|
832
|
+
|
|
833
|
+
// Register status routes even when disabled
|
|
834
|
+
registerConfigRoutes(registry);
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
async onStop(): Promise<void> {
|
|
838
|
+
// Nothing to cleanup
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Create an error plugin (auth disabled due to configuration error)
|
|
845
|
+
*/
|
|
846
|
+
function createErrorPlugin(error: string): Plugin {
|
|
847
|
+
return {
|
|
848
|
+
id: 'auth',
|
|
849
|
+
name: 'Auth Plugin (Error)',
|
|
850
|
+
version: '1.0.0',
|
|
851
|
+
|
|
852
|
+
async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
|
|
853
|
+
const logger = registry.getLogger('auth');
|
|
854
|
+
logger.error(`Auth plugin error: ${error}`);
|
|
855
|
+
|
|
856
|
+
// Register status routes so admin can see the error
|
|
857
|
+
registerConfigRoutes(registry);
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
async onStop(): Promise<void> {
|
|
861
|
+
// Nothing to cleanup
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
867
|
+
// Runtime Configuration Support
|
|
868
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Extended options for createAuthPluginFromEnv with runtime config support
|
|
872
|
+
*/
|
|
873
|
+
export interface AuthEnvPluginOptionsExtended extends AuthEnvPluginOptions {
|
|
874
|
+
/** Configuration store for runtime config persistence */
|
|
875
|
+
configStore?: AuthConfigStore;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Create an adapter from runtime configuration
|
|
880
|
+
*/
|
|
881
|
+
function createAdapterFromConfig(
|
|
882
|
+
adapterType: AuthAdapterType,
|
|
883
|
+
config: RuntimeAuthConfig['config']
|
|
884
|
+
): AuthAdapter | null {
|
|
885
|
+
switch (adapterType) {
|
|
886
|
+
case 'supertokens':
|
|
887
|
+
if (config.supertokens) {
|
|
888
|
+
return supertokensAdapter(config.supertokens);
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
case 'auth0':
|
|
892
|
+
if (config.auth0) {
|
|
893
|
+
return auth0Adapter(config.auth0);
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
case 'supabase':
|
|
897
|
+
if (config.supabase) {
|
|
898
|
+
return supabaseAdapter(config.supabase);
|
|
899
|
+
}
|
|
900
|
+
break;
|
|
901
|
+
case 'basic':
|
|
902
|
+
if (config.basic) {
|
|
903
|
+
return basicAdapter(config.basic);
|
|
904
|
+
}
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Validate adapter configuration
|
|
912
|
+
*/
|
|
913
|
+
function validateAdapterConfig(
|
|
914
|
+
adapterType: AuthAdapterType,
|
|
915
|
+
config: Record<string, unknown>
|
|
916
|
+
): { valid: boolean; errors: string[] } {
|
|
917
|
+
const errors: string[] = [];
|
|
918
|
+
|
|
919
|
+
switch (adapterType) {
|
|
920
|
+
case 'supertokens':
|
|
921
|
+
if (!config.connectionUri) errors.push('connectionUri is required');
|
|
922
|
+
if (!config.appName) errors.push('appName is required');
|
|
923
|
+
if (!config.apiDomain) errors.push('apiDomain is required');
|
|
924
|
+
if (!config.websiteDomain) errors.push('websiteDomain is required');
|
|
925
|
+
break;
|
|
926
|
+
case 'auth0':
|
|
927
|
+
if (!config.domain) errors.push('domain is required');
|
|
928
|
+
if (!config.clientId) errors.push('clientId is required');
|
|
929
|
+
if (!config.clientSecret) errors.push('clientSecret is required');
|
|
930
|
+
if (!config.baseUrl) errors.push('baseUrl is required');
|
|
931
|
+
if (!config.secret) errors.push('secret is required');
|
|
932
|
+
break;
|
|
933
|
+
case 'supabase':
|
|
934
|
+
if (!config.url) errors.push('url is required');
|
|
935
|
+
if (!config.anonKey) errors.push('anonKey is required');
|
|
936
|
+
break;
|
|
937
|
+
case 'basic':
|
|
938
|
+
if (!config.username) errors.push('username is required');
|
|
939
|
+
if (!config.password) errors.push('password is required');
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return { valid: errors.length === 0, errors };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Validate a URL for security (prevent SSRF attacks)
|
|
948
|
+
*/
|
|
949
|
+
function validateUrl(urlString: string): URL {
|
|
950
|
+
const url = new URL(urlString);
|
|
951
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
952
|
+
throw new Error('URL must use http or https protocol');
|
|
953
|
+
}
|
|
954
|
+
// Block private/internal IPs (basic SSRF protection)
|
|
955
|
+
const hostname = url.hostname.toLowerCase();
|
|
956
|
+
if (
|
|
957
|
+
hostname === 'localhost' ||
|
|
958
|
+
hostname === '127.0.0.1' ||
|
|
959
|
+
hostname === '::1' ||
|
|
960
|
+
hostname.startsWith('192.168.') ||
|
|
961
|
+
hostname.startsWith('10.') ||
|
|
962
|
+
hostname.startsWith('172.') ||
|
|
963
|
+
hostname.endsWith('.internal') ||
|
|
964
|
+
hostname.endsWith('.local')
|
|
965
|
+
) {
|
|
966
|
+
// Allow localhost for dev/testing, but log it
|
|
967
|
+
console.warn(`[AuthPlugin] Testing connection to internal address: ${hostname}`);
|
|
968
|
+
}
|
|
969
|
+
return url;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Test a provider connection
|
|
974
|
+
*/
|
|
975
|
+
async function testProviderConnection(
|
|
976
|
+
adapterType: AuthAdapterType,
|
|
977
|
+
config: Record<string, unknown>
|
|
978
|
+
): Promise<TestProviderResponse> {
|
|
979
|
+
const startTime = Date.now();
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
// Validate config first
|
|
983
|
+
const validation = validateAdapterConfig(adapterType, config);
|
|
984
|
+
if (!validation.valid) {
|
|
985
|
+
return {
|
|
986
|
+
success: false,
|
|
987
|
+
message: `Invalid configuration: ${validation.errors.join(', ')}`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Test connection based on adapter type
|
|
992
|
+
switch (adapterType) {
|
|
993
|
+
case 'supertokens': {
|
|
994
|
+
// Try to connect to Supertokens core
|
|
995
|
+
const connectionUri = config.connectionUri as string;
|
|
996
|
+
validateUrl(connectionUri); // Validate URL before making request
|
|
997
|
+
const apiKey = config.apiKey as string | undefined;
|
|
998
|
+
|
|
999
|
+
const headers: Record<string, string> = {
|
|
1000
|
+
'Content-Type': 'application/json',
|
|
1001
|
+
};
|
|
1002
|
+
if (apiKey) {
|
|
1003
|
+
headers['api-key'] = apiKey;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const response = await fetch(`${connectionUri}/apiversion`, { headers });
|
|
1007
|
+
if (!response.ok) {
|
|
1008
|
+
return {
|
|
1009
|
+
success: false,
|
|
1010
|
+
message: `Failed to connect to Supertokens core: ${response.status} ${response.statusText}`,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const data = (await response.json()) as { versions?: string[] };
|
|
1015
|
+
return {
|
|
1016
|
+
success: true,
|
|
1017
|
+
message: 'Successfully connected to Supertokens core',
|
|
1018
|
+
details: {
|
|
1019
|
+
latency: Date.now() - startTime,
|
|
1020
|
+
version: data.versions?.[0],
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
case 'auth0': {
|
|
1026
|
+
// Test Auth0 domain is reachable
|
|
1027
|
+
const domain = config.domain as string;
|
|
1028
|
+
const response = await fetch(`https://${domain}/.well-known/openid-configuration`);
|
|
1029
|
+
if (!response.ok) {
|
|
1030
|
+
return {
|
|
1031
|
+
success: false,
|
|
1032
|
+
message: `Failed to connect to Auth0: ${response.status} ${response.statusText}`,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
success: true,
|
|
1038
|
+
message: 'Successfully connected to Auth0',
|
|
1039
|
+
details: {
|
|
1040
|
+
latency: Date.now() - startTime,
|
|
1041
|
+
},
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
case 'supabase': {
|
|
1046
|
+
// Test Supabase endpoint
|
|
1047
|
+
const url = config.url as string;
|
|
1048
|
+
validateUrl(url); // Validate URL before making request
|
|
1049
|
+
const anonKey = config.anonKey as string;
|
|
1050
|
+
|
|
1051
|
+
const response = await fetch(`${url}/rest/v1/`, {
|
|
1052
|
+
headers: {
|
|
1053
|
+
apikey: anonKey,
|
|
1054
|
+
Authorization: `Bearer ${anonKey}`,
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// Supabase returns 200 even without tables
|
|
1059
|
+
if (!response.ok && response.status !== 404) {
|
|
1060
|
+
return {
|
|
1061
|
+
success: false,
|
|
1062
|
+
message: `Failed to connect to Supabase: ${response.status} ${response.statusText}`,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
success: true,
|
|
1068
|
+
message: 'Successfully connected to Supabase',
|
|
1069
|
+
details: {
|
|
1070
|
+
latency: Date.now() - startTime,
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
case 'basic': {
|
|
1076
|
+
// Basic auth just validates credentials are present
|
|
1077
|
+
return {
|
|
1078
|
+
success: true,
|
|
1079
|
+
message: 'Basic auth configuration is valid',
|
|
1080
|
+
details: {
|
|
1081
|
+
latency: Date.now() - startTime,
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
default:
|
|
1087
|
+
return {
|
|
1088
|
+
success: false,
|
|
1089
|
+
message: `Unknown adapter type: ${adapterType}`,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
// Provide more helpful error messages for common network errors
|
|
1094
|
+
let message = 'Connection test failed';
|
|
1095
|
+
if (error instanceof Error) {
|
|
1096
|
+
const errorWithCause = error as Error & { cause?: Error };
|
|
1097
|
+
if (error.message === 'fetch failed' || errorWithCause.cause) {
|
|
1098
|
+
// Network error - server not reachable
|
|
1099
|
+
const cause = errorWithCause.cause;
|
|
1100
|
+
if (cause?.message?.includes('ECONNREFUSED')) {
|
|
1101
|
+
const uri = adapterType === 'supertokens' ? config.connectionUri :
|
|
1102
|
+
adapterType === 'supabase' ? config.url :
|
|
1103
|
+
adapterType === 'auth0' ? `https://${config.domain}` : 'server';
|
|
1104
|
+
message = `Cannot connect to ${adapterType} at ${uri}. Is the server running?`;
|
|
1105
|
+
} else {
|
|
1106
|
+
message = `Network error: Unable to reach the ${adapterType} server. Check if it's running and accessible.`;
|
|
1107
|
+
}
|
|
1108
|
+
} else {
|
|
1109
|
+
message = `Connection test failed: ${error.message}`;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
success: false,
|
|
1114
|
+
message,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Handle configuration change (from database)
|
|
1121
|
+
* Called when pg_notify fires on another instance
|
|
1122
|
+
*/
|
|
1123
|
+
async function handleConfigChange(newConfig: RuntimeAuthConfig | null): Promise<void> {
|
|
1124
|
+
if (!adapterWrapper) return;
|
|
1125
|
+
|
|
1126
|
+
if (!newConfig || !newConfig.adapter) {
|
|
1127
|
+
// No config in database - revert to env vars
|
|
1128
|
+
const adapterName = getEnv('AUTH_ADAPTER')?.toLowerCase() as AdapterName | undefined;
|
|
1129
|
+
if (adapterName && VALID_ADAPTERS.includes(adapterName)) {
|
|
1130
|
+
// Re-create adapter from env vars
|
|
1131
|
+
const parseResult = getParseResultForAdapter(adapterName);
|
|
1132
|
+
if (parseResult.config) {
|
|
1133
|
+
const adapter = createAdapterForName(adapterName, parseResult.config);
|
|
1134
|
+
if (adapter) {
|
|
1135
|
+
await adapterWrapper.setAdapter(adapter);
|
|
1136
|
+
currentStatus = {
|
|
1137
|
+
state: 'enabled',
|
|
1138
|
+
adapter: adapterName,
|
|
1139
|
+
config: getMaskedConfig(adapterName),
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
} else {
|
|
1144
|
+
// No env config either - disable
|
|
1145
|
+
currentStatus = {
|
|
1146
|
+
state: 'disabled',
|
|
1147
|
+
adapter: null,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Apply new config
|
|
1154
|
+
const adapter = createAdapterFromConfig(newConfig.adapter, newConfig.config);
|
|
1155
|
+
if (adapter) {
|
|
1156
|
+
await adapterWrapper.setAdapter(adapter);
|
|
1157
|
+
currentStatus = {
|
|
1158
|
+
state: 'enabled',
|
|
1159
|
+
adapter: newConfig.adapter,
|
|
1160
|
+
config: getMaskedRuntimeConfig(newConfig),
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Get masked runtime configuration
|
|
1167
|
+
*/
|
|
1168
|
+
function getMaskedRuntimeConfig(config: RuntimeAuthConfig): Record<string, string> {
|
|
1169
|
+
const result: Record<string, string> = {};
|
|
1170
|
+
|
|
1171
|
+
if (!config.adapter) return result;
|
|
1172
|
+
|
|
1173
|
+
const sensitiveKeys = ['secret', 'password', 'key', 'token', 'anonKey', 'clientSecret', 'apiKey'];
|
|
1174
|
+
const isSensitive = (key: string): boolean =>
|
|
1175
|
+
sensitiveKeys.some((s) => key.toLowerCase().includes(s.toLowerCase()));
|
|
1176
|
+
|
|
1177
|
+
// Get adapter-specific config
|
|
1178
|
+
const adapterConfig = config.config[config.adapter];
|
|
1179
|
+
if (adapterConfig) {
|
|
1180
|
+
for (const [key, value] of Object.entries(adapterConfig)) {
|
|
1181
|
+
if (typeof value === 'string') {
|
|
1182
|
+
result[key] = isSensitive(key) ? maskValue(value) : value;
|
|
1183
|
+
} else if (typeof value === 'boolean' || typeof value === 'number') {
|
|
1184
|
+
result[key] = String(value);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
result['AUTH_ADAPTER'] = config.adapter;
|
|
1190
|
+
|
|
1191
|
+
return result;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Helper to get parse result for a given adapter
|
|
1196
|
+
*/
|
|
1197
|
+
function getParseResultForAdapter(adapterName: AdapterName): EnvParseResult<
|
|
1198
|
+
Auth0AdapterConfig | SupabaseAdapterConfig | SupertokensAdapterConfig | BasicAdapterConfig
|
|
1199
|
+
> {
|
|
1200
|
+
switch (adapterName) {
|
|
1201
|
+
case 'supertokens':
|
|
1202
|
+
return parseSupertokensEnv();
|
|
1203
|
+
case 'auth0':
|
|
1204
|
+
return parseAuth0Env();
|
|
1205
|
+
case 'supabase':
|
|
1206
|
+
return parseSupabaseEnv();
|
|
1207
|
+
case 'basic':
|
|
1208
|
+
return parseBasicAuthEnv();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Helper to create adapter for a given name and config
|
|
1214
|
+
*/
|
|
1215
|
+
function createAdapterForName(
|
|
1216
|
+
adapterName: AdapterName,
|
|
1217
|
+
config: Auth0AdapterConfig | SupabaseAdapterConfig | SupertokensAdapterConfig | BasicAdapterConfig
|
|
1218
|
+
): AuthAdapter {
|
|
1219
|
+
switch (adapterName) {
|
|
1220
|
+
case 'supertokens':
|
|
1221
|
+
return supertokensAdapter(config as SupertokensAdapterConfig);
|
|
1222
|
+
case 'auth0':
|
|
1223
|
+
return auth0Adapter(config as Auth0AdapterConfig);
|
|
1224
|
+
case 'supabase':
|
|
1225
|
+
return supabaseAdapter(config as SupabaseAdapterConfig);
|
|
1226
|
+
case 'basic':
|
|
1227
|
+
return basicAdapter(config as BasicAdapterConfig);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Set the configuration store for runtime auth config persistence.
|
|
1233
|
+
*
|
|
1234
|
+
* This must be called during application startup to enable runtime configuration
|
|
1235
|
+
* features. Without a config store, the PUT/DELETE endpoints will return 503.
|
|
1236
|
+
*
|
|
1237
|
+
* @param store - PostgreSQL-backed config store from `postgresAuthConfigStore()`
|
|
1238
|
+
*
|
|
1239
|
+
* @example
|
|
1240
|
+
* ```typescript
|
|
1241
|
+
* import { Pool } from 'pg';
|
|
1242
|
+
* import { setAuthConfigStore, postgresAuthConfigStore } from '@qwickapps/server';
|
|
1243
|
+
*
|
|
1244
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
1245
|
+
* const configStore = postgresAuthConfigStore({ pool });
|
|
1246
|
+
* await configStore.initialize();
|
|
1247
|
+
* setAuthConfigStore(configStore);
|
|
1248
|
+
* ```
|
|
1249
|
+
*/
|
|
1250
|
+
export function setAuthConfigStore(store: AuthConfigStore): void {
|
|
1251
|
+
configStore = store;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Get the current adapter wrapper
|
|
1256
|
+
*/
|
|
1257
|
+
export function getAdapterWrapper(): AdapterWrapper | null {
|
|
1258
|
+
return adapterWrapper;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1262
|
+
// Exports for Testing
|
|
1263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1264
|
+
|
|
1265
|
+
// Export internal functions for testing
|
|
1266
|
+
export const __testing = {
|
|
1267
|
+
parseSupertokensEnv,
|
|
1268
|
+
parseAuth0Env,
|
|
1269
|
+
parseSupabaseEnv,
|
|
1270
|
+
parseBasicAuthEnv,
|
|
1271
|
+
getEnv,
|
|
1272
|
+
getEnvBool,
|
|
1273
|
+
getEnvList,
|
|
1274
|
+
maskValue,
|
|
1275
|
+
VALID_ADAPTERS,
|
|
1276
|
+
validateAdapterConfig,
|
|
1277
|
+
testProviderConnection,
|
|
1278
|
+
createAdapterFromConfig,
|
|
1279
|
+
};
|