@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.
Files changed (241) hide show
  1. package/README.md +311 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +144 -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/core/types.d.ts +19 -0
  10. package/dist/core/types.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  16. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  18. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  19. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  20. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  22. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  23. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  24. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  25. package/dist/plugins/auth/adapters/index.js +1 -0
  26. package/dist/plugins/auth/adapters/index.js.map +1 -1
  27. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  28. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  29. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  30. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  31. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  32. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  33. package/dist/plugins/auth/config-store.d.ts +11 -0
  34. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  35. package/dist/plugins/auth/config-store.js +232 -0
  36. package/dist/plugins/auth/config-store.js.map +1 -0
  37. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  38. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  39. package/dist/plugins/auth/config-store.test.js +299 -0
  40. package/dist/plugins/auth/config-store.test.js.map +1 -0
  41. package/dist/plugins/auth/env-config.d.ts +138 -0
  42. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  43. package/dist/plugins/auth/env-config.js +1122 -0
  44. package/dist/plugins/auth/env-config.js.map +1 -0
  45. package/dist/plugins/auth/index.d.ts +7 -1
  46. package/dist/plugins/auth/index.d.ts.map +1 -1
  47. package/dist/plugins/auth/index.js +7 -0
  48. package/dist/plugins/auth/index.js.map +1 -1
  49. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  50. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  51. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  52. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  53. package/dist/plugins/auth/types.d.ts +176 -0
  54. package/dist/plugins/auth/types.d.ts.map +1 -1
  55. package/dist/plugins/auth/types.js.map +1 -1
  56. package/dist/plugins/cache-plugin.test.js +3 -0
  57. package/dist/plugins/cache-plugin.test.js.map +1 -1
  58. package/dist/plugins/index.d.ts +6 -2
  59. package/dist/plugins/index.d.ts.map +1 -1
  60. package/dist/plugins/index.js +5 -1
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/plugins/postgres-plugin.test.js +3 -0
  63. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  64. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  65. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  66. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  67. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  68. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  69. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  70. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  71. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  72. package/dist/plugins/preferences/index.d.ts +12 -0
  73. package/dist/plugins/preferences/index.d.ts.map +1 -0
  74. package/dist/plugins/preferences/index.js +13 -0
  75. package/dist/plugins/preferences/index.js.map +1 -0
  76. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  77. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  78. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  79. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  80. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  81. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  82. package/dist/plugins/preferences/stores/index.js +9 -0
  83. package/dist/plugins/preferences/stores/index.js.map +1 -0
  84. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  85. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  86. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  87. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  88. package/dist/plugins/preferences/types.d.ts +91 -0
  89. package/dist/plugins/preferences/types.d.ts.map +1 -0
  90. package/dist/plugins/preferences/types.js +10 -0
  91. package/dist/plugins/preferences/types.js.map +1 -0
  92. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  93. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  94. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  95. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  96. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  97. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  98. package/dist/plugins/rate-limit/cleanup.js +72 -0
  99. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  100. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  101. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  102. package/dist/plugins/rate-limit/env-config.js +318 -0
  103. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  104. package/dist/plugins/rate-limit/index.d.ts +76 -0
  105. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  106. package/dist/plugins/rate-limit/index.js +79 -0
  107. package/dist/plugins/rate-limit/index.js.map +1 -0
  108. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  109. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  110. package/dist/plugins/rate-limit/middleware.js +169 -0
  111. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  112. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  113. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  114. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  115. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  116. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  117. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  118. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  119. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  120. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  121. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  122. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  123. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  124. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  125. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  126. package/dist/plugins/rate-limit/stores/index.js +8 -0
  127. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  128. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  129. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  130. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  131. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  132. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  133. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  134. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  135. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  136. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  137. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  138. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  139. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  140. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  141. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  142. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  143. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  144. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  145. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  146. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  147. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  148. package/dist/plugins/rate-limit/types.d.ts +265 -0
  149. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  150. package/dist/plugins/rate-limit/types.js +9 -0
  151. package/dist/plugins/rate-limit/types.js.map +1 -0
  152. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  153. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  154. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  155. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  156. package/dist/plugins/users/index.d.ts +2 -2
  157. package/dist/plugins/users/index.d.ts.map +1 -1
  158. package/dist/plugins/users/index.js +1 -1
  159. package/dist/plugins/users/index.js.map +1 -1
  160. package/dist/plugins/users/types.d.ts +36 -0
  161. package/dist/plugins/users/types.d.ts.map +1 -1
  162. package/dist/plugins/users/users-plugin.d.ts +8 -2
  163. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  164. package/dist/plugins/users/users-plugin.js +122 -5
  165. package/dist/plugins/users/users-plugin.js.map +1 -1
  166. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  167. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  168. package/dist-ui/index.html +1 -1
  169. package/dist-ui-lib/api/controlPanelApi.d.ts +194 -7
  170. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  171. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  172. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  173. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  174. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  175. package/dist-ui-lib/index.js +3665 -3945
  176. package/dist-ui-lib/index.js.map +1 -1
  177. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  178. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  179. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  180. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  181. package/package.json +7 -2
  182. package/src/core/control-panel.ts +161 -2
  183. package/src/core/plugin-registry.ts +63 -0
  184. package/src/core/types.ts +17 -0
  185. package/src/index.ts +45 -0
  186. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  187. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  188. package/src/plugins/auth/adapters/index.ts +1 -0
  189. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  190. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  191. package/src/plugins/auth/config-store.test.ts +417 -0
  192. package/src/plugins/auth/config-store.ts +305 -0
  193. package/src/plugins/auth/env-config.ts +1279 -0
  194. package/src/plugins/auth/index.ts +30 -0
  195. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  196. package/src/plugins/auth/types.ts +218 -0
  197. package/src/plugins/cache-plugin.test.ts +3 -0
  198. package/src/plugins/index.ts +75 -0
  199. package/src/plugins/postgres-plugin.test.ts +3 -0
  200. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  201. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  202. package/src/plugins/preferences/index.ts +30 -0
  203. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  204. package/src/plugins/preferences/stores/index.ts +9 -0
  205. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  206. package/src/plugins/preferences/types.ts +100 -0
  207. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  208. package/src/plugins/rate-limit/cleanup.ts +117 -0
  209. package/src/plugins/rate-limit/env-config.ts +400 -0
  210. package/src/plugins/rate-limit/index.ts +128 -0
  211. package/src/plugins/rate-limit/middleware.ts +212 -0
  212. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  213. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  214. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  215. package/src/plugins/rate-limit/stores/index.ts +8 -0
  216. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  217. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  218. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  219. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  220. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  221. package/src/plugins/rate-limit/types.ts +338 -0
  222. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  223. package/src/plugins/users/index.ts +3 -0
  224. package/src/plugins/users/types.ts +38 -0
  225. package/src/plugins/users/users-plugin.ts +142 -5
  226. package/ui/src/App.tsx +35 -14
  227. package/ui/src/api/controlPanelApi.ts +326 -1
  228. package/ui/src/components/ControlPanelApp.tsx +3 -0
  229. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  230. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  231. package/ui/src/dashboard/builtInWidgets.tsx +13 -3
  232. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  233. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  234. package/ui/src/dashboard/widgets/index.ts +2 -0
  235. package/ui/src/pages/AuthPage.tsx +1103 -0
  236. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  237. package/ui/src/pages/PluginsPage.tsx +394 -0
  238. package/ui/src/pages/RateLimitPage.tsx +292 -0
  239. package/ui/vite.lib.config.ts +5 -0
  240. package/dist-ui/assets/index-Bsp2ntcw.js +0 -465
  241. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -0,0 +1,1103 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableContainer,
11
+ TableHead,
12
+ TableRow,
13
+ Chip,
14
+ CircularProgress,
15
+ Alert,
16
+ IconButton,
17
+ Tooltip,
18
+ Button,
19
+ TextField,
20
+ Select,
21
+ MenuItem,
22
+ FormControl,
23
+ InputLabel,
24
+ FormControlLabel,
25
+ Switch,
26
+ Divider,
27
+ Collapse,
28
+ Dialog,
29
+ DialogTitle,
30
+ DialogContent,
31
+ DialogActions,
32
+ } from '@mui/material';
33
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
34
+ import ErrorIcon from '@mui/icons-material/Error';
35
+ import BlockIcon from '@mui/icons-material/Block';
36
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
37
+ import RefreshIcon from '@mui/icons-material/Refresh';
38
+ import EditIcon from '@mui/icons-material/Edit';
39
+ import SaveIcon from '@mui/icons-material/Save';
40
+ import CancelIcon from '@mui/icons-material/Cancel';
41
+ import DeleteIcon from '@mui/icons-material/Delete';
42
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
43
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
44
+ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
45
+ import {
46
+ api,
47
+ AuthConfigStatus,
48
+ AuthAdapterType,
49
+ Auth0AdapterConfig,
50
+ SupabaseAdapterConfig,
51
+ SupertokensAdapterConfig,
52
+ BasicAdapterConfig,
53
+ UpdateAuthConfigRequest,
54
+ TestProviderResponse,
55
+ } from '../api/controlPanelApi';
56
+
57
+ /**
58
+ * Get the status color for the auth state
59
+ */
60
+ function getStateColor(state: string): string {
61
+ switch (state) {
62
+ case 'enabled':
63
+ return 'var(--theme-success)';
64
+ case 'error':
65
+ return 'var(--theme-error)';
66
+ case 'disabled':
67
+ default:
68
+ return 'var(--theme-text-secondary)';
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get the status icon for the auth state
74
+ */
75
+ function getStateIcon(state: string) {
76
+ switch (state) {
77
+ case 'enabled':
78
+ return <CheckCircleIcon sx={{ color: 'var(--theme-success)' }} />;
79
+ case 'error':
80
+ return <ErrorIcon sx={{ color: 'var(--theme-error)' }} />;
81
+ case 'disabled':
82
+ default:
83
+ return <BlockIcon sx={{ color: 'var(--theme-text-secondary)' }} />;
84
+ }
85
+ }
86
+
87
+ // Default empty configs for each adapter type
88
+ const defaultAuth0Config: Auth0AdapterConfig = {
89
+ domain: '',
90
+ clientId: '',
91
+ clientSecret: '',
92
+ baseUrl: '',
93
+ secret: '',
94
+ audience: '',
95
+ scopes: ['openid', 'profile', 'email'],
96
+ allowedRoles: [],
97
+ allowedDomains: [],
98
+ };
99
+
100
+ const defaultSupabaseConfig: SupabaseAdapterConfig = {
101
+ url: '',
102
+ anonKey: '',
103
+ };
104
+
105
+ const defaultBasicConfig: BasicAdapterConfig = {
106
+ username: '',
107
+ password: '',
108
+ realm: 'Protected Area',
109
+ };
110
+
111
+ const defaultSupertokensConfig: SupertokensAdapterConfig = {
112
+ connectionUri: '',
113
+ apiKey: '',
114
+ appName: '',
115
+ apiDomain: '',
116
+ websiteDomain: '',
117
+ apiBasePath: '/auth',
118
+ websiteBasePath: '/auth',
119
+ enableEmailPassword: true,
120
+ socialProviders: {},
121
+ };
122
+
123
+ interface SocialProvider {
124
+ enabled: boolean;
125
+ clientId: string;
126
+ clientSecret: string;
127
+ keyId?: string;
128
+ teamId?: string;
129
+ }
130
+
131
+ export function AuthPage() {
132
+ const [status, setStatus] = useState<AuthConfigStatus | null>(null);
133
+ const [loading, setLoading] = useState(true);
134
+ const [error, setError] = useState<string | null>(null);
135
+ const [copied, setCopied] = useState<string | null>(null);
136
+
137
+ // Edit mode state
138
+ const [editMode, setEditMode] = useState(false);
139
+ const [saving, setSaving] = useState(false);
140
+ const [testing, setTesting] = useState(false);
141
+ const [testResult, setTestResult] = useState<TestProviderResponse | null>(null);
142
+
143
+ // Form state
144
+ const [selectedAdapter, setSelectedAdapter] = useState<AuthAdapterType | ''>('');
145
+ const [auth0Config, setAuth0Config] = useState<Auth0AdapterConfig>(defaultAuth0Config);
146
+ const [supabaseConfig, setSupabaseConfig] = useState<SupabaseAdapterConfig>(defaultSupabaseConfig);
147
+ const [basicConfig, setBasicConfig] = useState<BasicAdapterConfig>(defaultBasicConfig);
148
+ const [supertokensConfig, setSupertokensConfig] = useState<SupertokensAdapterConfig>(defaultSupertokensConfig);
149
+ const [authRequired, setAuthRequired] = useState(true);
150
+ const [excludePaths, setExcludePaths] = useState('');
151
+
152
+ // Social providers state (for SuperTokens)
153
+ const [googleProvider, setGoogleProvider] = useState<SocialProvider>({
154
+ enabled: false,
155
+ clientId: '',
156
+ clientSecret: '',
157
+ });
158
+ const [githubProvider, setGithubProvider] = useState<SocialProvider>({
159
+ enabled: false,
160
+ clientId: '',
161
+ clientSecret: '',
162
+ });
163
+ const [appleProvider, setAppleProvider] = useState<SocialProvider>({
164
+ enabled: false,
165
+ clientId: '',
166
+ clientSecret: '',
167
+ keyId: '',
168
+ teamId: '',
169
+ });
170
+
171
+ // UI state
172
+ const [showSocialProviders, setShowSocialProviders] = useState(false);
173
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
174
+
175
+ const fetchStatus = useCallback(async () => {
176
+ setLoading(true);
177
+ setError(null);
178
+ try {
179
+ const data = await api.getAuthConfig();
180
+ setStatus(data);
181
+
182
+ // Initialize form from runtime config if available
183
+ if (data.runtimeConfig) {
184
+ const rc = data.runtimeConfig;
185
+ setSelectedAdapter(rc.adapter || '');
186
+ setAuthRequired(rc.settings.authRequired ?? true);
187
+ setExcludePaths(rc.settings.excludePaths?.join(', ') || '');
188
+
189
+ if (rc.config.auth0) setAuth0Config({ ...defaultAuth0Config, ...rc.config.auth0 });
190
+ if (rc.config.supabase) setSupabaseConfig({ ...defaultSupabaseConfig, ...rc.config.supabase });
191
+ if (rc.config.basic) setBasicConfig({ ...defaultBasicConfig, ...rc.config.basic });
192
+ if (rc.config.supertokens) {
193
+ const st = rc.config.supertokens;
194
+ setSupertokensConfig({ ...defaultSupertokensConfig, ...st });
195
+ if (st.socialProviders?.google) {
196
+ setGoogleProvider({
197
+ enabled: true,
198
+ clientId: st.socialProviders.google.clientId,
199
+ clientSecret: st.socialProviders.google.clientSecret,
200
+ });
201
+ }
202
+ if (st.socialProviders?.github) {
203
+ setGithubProvider({
204
+ enabled: true,
205
+ clientId: st.socialProviders.github.clientId,
206
+ clientSecret: st.socialProviders.github.clientSecret,
207
+ });
208
+ }
209
+ if (st.socialProviders?.apple) {
210
+ setAppleProvider({
211
+ enabled: true,
212
+ clientId: st.socialProviders.apple.clientId,
213
+ clientSecret: st.socialProviders.apple.clientSecret,
214
+ keyId: st.socialProviders.apple.keyId,
215
+ teamId: st.socialProviders.apple.teamId,
216
+ });
217
+ }
218
+ }
219
+ } else if (data.adapter) {
220
+ // Initialize from current adapter if no runtime config
221
+ setSelectedAdapter(data.adapter as AuthAdapterType);
222
+ }
223
+ } catch (err) {
224
+ setError(err instanceof Error ? err.message : 'Failed to fetch auth status');
225
+ } finally {
226
+ setLoading(false);
227
+ }
228
+ }, []);
229
+
230
+ useEffect(() => {
231
+ fetchStatus();
232
+ }, [fetchStatus]);
233
+
234
+ const handleCopy = (key: string, value: string) => {
235
+ navigator.clipboard.writeText(value);
236
+ setCopied(key);
237
+ setTimeout(() => setCopied(null), 2000);
238
+ };
239
+
240
+ const handleEnterEditMode = () => {
241
+ setEditMode(true);
242
+ setTestResult(null);
243
+ };
244
+
245
+ const handleCancelEdit = () => {
246
+ setEditMode(false);
247
+ setTestResult(null);
248
+ // Reset form to current status
249
+ fetchStatus();
250
+ };
251
+
252
+ // Helper to convert typed config to plain object for API calls
253
+ // Uses JSON round-trip to ensure clean serialization
254
+ const toPlainObject = <T extends object>(obj: T): Record<string, unknown> =>
255
+ JSON.parse(JSON.stringify(obj));
256
+
257
+ const getCurrentConfig = (): Record<string, unknown> => {
258
+ switch (selectedAdapter) {
259
+ case 'auth0':
260
+ return toPlainObject(auth0Config);
261
+ case 'supabase':
262
+ return toPlainObject(supabaseConfig);
263
+ case 'basic':
264
+ return toPlainObject(basicConfig);
265
+ case 'supertokens': {
266
+ const config: SupertokensAdapterConfig = { ...supertokensConfig };
267
+ const socialProviders: SupertokensAdapterConfig['socialProviders'] = {};
268
+ if (googleProvider.enabled) {
269
+ socialProviders.google = {
270
+ clientId: googleProvider.clientId,
271
+ clientSecret: googleProvider.clientSecret,
272
+ };
273
+ }
274
+ if (githubProvider.enabled) {
275
+ socialProviders.github = {
276
+ clientId: githubProvider.clientId,
277
+ clientSecret: githubProvider.clientSecret,
278
+ };
279
+ }
280
+ if (appleProvider.enabled) {
281
+ socialProviders.apple = {
282
+ clientId: appleProvider.clientId,
283
+ clientSecret: appleProvider.clientSecret,
284
+ keyId: appleProvider.keyId || '',
285
+ teamId: appleProvider.teamId || '',
286
+ };
287
+ }
288
+ if (Object.keys(socialProviders).length > 0) {
289
+ config.socialProviders = socialProviders;
290
+ }
291
+ return toPlainObject(config);
292
+ }
293
+ default:
294
+ return {};
295
+ }
296
+ };
297
+
298
+ const handleTestConnection = async () => {
299
+ if (!selectedAdapter) return;
300
+
301
+ setTesting(true);
302
+ setTestResult(null);
303
+ try {
304
+ const result = await api.testAuthProvider({
305
+ adapter: selectedAdapter,
306
+ config: getCurrentConfig(),
307
+ });
308
+ setTestResult(result);
309
+ } catch (err) {
310
+ setTestResult({
311
+ success: false,
312
+ message: err instanceof Error ? err.message : 'Test failed',
313
+ });
314
+ } finally {
315
+ setTesting(false);
316
+ }
317
+ };
318
+
319
+ // Test the current connection (env-based or runtime config)
320
+ const handleTestCurrentConnection = async () => {
321
+ if (!status?.adapter) return;
322
+
323
+ setTesting(true);
324
+ setTestResult(null);
325
+ try {
326
+ // Call the test endpoint with "current" flag to test existing config
327
+ const result = await api.testCurrentAuthProvider();
328
+ setTestResult(result);
329
+ } catch (err) {
330
+ setTestResult({
331
+ success: false,
332
+ message: err instanceof Error ? err.message : 'Test failed',
333
+ });
334
+ } finally {
335
+ setTesting(false);
336
+ }
337
+ };
338
+
339
+ const handleSave = async () => {
340
+ if (!selectedAdapter) return;
341
+
342
+ setSaving(true);
343
+ setError(null);
344
+ try {
345
+ const request: UpdateAuthConfigRequest = {
346
+ adapter: selectedAdapter,
347
+ config: getCurrentConfig(),
348
+ settings: {
349
+ authRequired,
350
+ excludePaths: excludePaths
351
+ .split(',')
352
+ .map((p) => p.trim())
353
+ .filter(Boolean),
354
+ },
355
+ };
356
+
357
+ await api.updateAuthConfig(request);
358
+ setEditMode(false);
359
+ await fetchStatus();
360
+ } catch (err) {
361
+ setError(err instanceof Error ? err.message : 'Failed to save configuration');
362
+ } finally {
363
+ setSaving(false);
364
+ }
365
+ };
366
+
367
+ const handleDelete = async () => {
368
+ setSaving(true);
369
+ setError(null);
370
+ try {
371
+ await api.deleteAuthConfig();
372
+ setDeleteDialogOpen(false);
373
+ setEditMode(false);
374
+ await fetchStatus();
375
+ } catch (err) {
376
+ setError(err instanceof Error ? err.message : 'Failed to delete configuration');
377
+ } finally {
378
+ setSaving(false);
379
+ }
380
+ };
381
+
382
+ if (loading) {
383
+ return (
384
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
385
+ <CircularProgress />
386
+ </Box>
387
+ );
388
+ }
389
+
390
+ const configEntries = status?.config ? Object.entries(status.config) : [];
391
+
392
+ return (
393
+ <Box>
394
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
395
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
396
+ Authentication
397
+ </Typography>
398
+ <Box sx={{ display: 'flex', gap: 1 }}>
399
+ {!editMode && (
400
+ <>
401
+ <Tooltip title="Edit Configuration">
402
+ <IconButton onClick={handleEnterEditMode} sx={{ color: 'var(--theme-primary)' }}>
403
+ <EditIcon />
404
+ </IconButton>
405
+ </Tooltip>
406
+ <Tooltip title="Refresh">
407
+ <IconButton onClick={fetchStatus} sx={{ color: 'var(--theme-text-secondary)' }}>
408
+ <RefreshIcon />
409
+ </IconButton>
410
+ </Tooltip>
411
+ </>
412
+ )}
413
+ </Box>
414
+ </Box>
415
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
416
+ {editMode ? 'Configure authentication provider' : 'Auth plugin configuration status'}
417
+ </Typography>
418
+
419
+ {error && (
420
+ <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
421
+ {error}
422
+ </Alert>
423
+ )}
424
+
425
+ {/* Edit Mode */}
426
+ {editMode ? (
427
+ <Box>
428
+ {/* Adapter Selection */}
429
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
430
+ <CardContent>
431
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
432
+ Provider Selection
433
+ </Typography>
434
+ <FormControl fullWidth sx={{ mb: 2 }}>
435
+ <InputLabel sx={{ color: 'var(--theme-text-secondary)' }}>Auth Provider</InputLabel>
436
+ <Select
437
+ value={selectedAdapter}
438
+ onChange={(e) => setSelectedAdapter(e.target.value as AuthAdapterType | '')}
439
+ label="Auth Provider"
440
+ sx={{ color: 'var(--theme-text-primary)' }}
441
+ >
442
+ <MenuItem value="">
443
+ <em>None (Disabled)</em>
444
+ </MenuItem>
445
+ <MenuItem value="supertokens">SuperTokens</MenuItem>
446
+ <MenuItem value="auth0">Auth0</MenuItem>
447
+ <MenuItem value="supabase">Supabase</MenuItem>
448
+ <MenuItem value="basic">Basic Auth</MenuItem>
449
+ </Select>
450
+ </FormControl>
451
+
452
+ <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
453
+ <FormControlLabel
454
+ control={
455
+ <Switch
456
+ checked={authRequired}
457
+ onChange={(e) => setAuthRequired(e.target.checked)}
458
+ sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: 'var(--theme-primary)' } }}
459
+ />
460
+ }
461
+ label="Auth Required"
462
+ sx={{ color: 'var(--theme-text-primary)' }}
463
+ />
464
+ <TextField
465
+ label="Exclude Paths (comma-separated)"
466
+ value={excludePaths}
467
+ onChange={(e) => setExcludePaths(e.target.value)}
468
+ size="small"
469
+ sx={{ flex: 1, '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
470
+ placeholder="/api/health, /api/public/*"
471
+ />
472
+ </Box>
473
+ </CardContent>
474
+ </Card>
475
+
476
+ {/* Auth0 Config */}
477
+ {selectedAdapter === 'auth0' && (
478
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
479
+ <CardContent>
480
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
481
+ Auth0 Configuration
482
+ </Typography>
483
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
484
+ <TextField
485
+ label="Domain"
486
+ value={auth0Config.domain}
487
+ onChange={(e) => setAuth0Config({ ...auth0Config, domain: e.target.value })}
488
+ required
489
+ placeholder="your-tenant.auth0.com"
490
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
491
+ />
492
+ <TextField
493
+ label="Client ID"
494
+ value={auth0Config.clientId}
495
+ onChange={(e) => setAuth0Config({ ...auth0Config, clientId: e.target.value })}
496
+ required
497
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
498
+ />
499
+ <TextField
500
+ label="Client Secret"
501
+ type="password"
502
+ value={auth0Config.clientSecret}
503
+ onChange={(e) => setAuth0Config({ ...auth0Config, clientSecret: e.target.value })}
504
+ required
505
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
506
+ />
507
+ <TextField
508
+ label="Base URL"
509
+ value={auth0Config.baseUrl}
510
+ onChange={(e) => setAuth0Config({ ...auth0Config, baseUrl: e.target.value })}
511
+ required
512
+ placeholder="https://your-app.com"
513
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
514
+ />
515
+ <TextField
516
+ label="Session Secret"
517
+ type="password"
518
+ value={auth0Config.secret}
519
+ onChange={(e) => setAuth0Config({ ...auth0Config, secret: e.target.value })}
520
+ required
521
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
522
+ />
523
+ <TextField
524
+ label="API Audience (optional)"
525
+ value={auth0Config.audience || ''}
526
+ onChange={(e) => setAuth0Config({ ...auth0Config, audience: e.target.value })}
527
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
528
+ />
529
+ </Box>
530
+ </CardContent>
531
+ </Card>
532
+ )}
533
+
534
+ {/* Supabase Config */}
535
+ {selectedAdapter === 'supabase' && (
536
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
537
+ <CardContent>
538
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
539
+ Supabase Configuration
540
+ </Typography>
541
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
542
+ <TextField
543
+ label="Project URL"
544
+ value={supabaseConfig.url}
545
+ onChange={(e) => setSupabaseConfig({ ...supabaseConfig, url: e.target.value })}
546
+ required
547
+ placeholder="https://your-project.supabase.co"
548
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
549
+ />
550
+ <TextField
551
+ label="Anon Key"
552
+ type="password"
553
+ value={supabaseConfig.anonKey}
554
+ onChange={(e) => setSupabaseConfig({ ...supabaseConfig, anonKey: e.target.value })}
555
+ required
556
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
557
+ />
558
+ </Box>
559
+ </CardContent>
560
+ </Card>
561
+ )}
562
+
563
+ {/* Basic Auth Config */}
564
+ {selectedAdapter === 'basic' && (
565
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
566
+ <CardContent>
567
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
568
+ Basic Auth Configuration
569
+ </Typography>
570
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr 1fr' }, gap: 2 }}>
571
+ <TextField
572
+ label="Username"
573
+ value={basicConfig.username}
574
+ onChange={(e) => setBasicConfig({ ...basicConfig, username: e.target.value })}
575
+ required
576
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
577
+ />
578
+ <TextField
579
+ label="Password"
580
+ type="password"
581
+ value={basicConfig.password}
582
+ onChange={(e) => setBasicConfig({ ...basicConfig, password: e.target.value })}
583
+ required
584
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
585
+ />
586
+ <TextField
587
+ label="Realm (optional)"
588
+ value={basicConfig.realm || ''}
589
+ onChange={(e) => setBasicConfig({ ...basicConfig, realm: e.target.value })}
590
+ placeholder="Protected Area"
591
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
592
+ />
593
+ </Box>
594
+ </CardContent>
595
+ </Card>
596
+ )}
597
+
598
+ {/* SuperTokens Config */}
599
+ {selectedAdapter === 'supertokens' && (
600
+ <>
601
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
602
+ <CardContent>
603
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
604
+ SuperTokens Configuration
605
+ </Typography>
606
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}>
607
+ <TextField
608
+ label="Connection URI"
609
+ value={supertokensConfig.connectionUri}
610
+ onChange={(e) => setSupertokensConfig({ ...supertokensConfig, connectionUri: e.target.value })}
611
+ required
612
+ placeholder="http://localhost:3567"
613
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
614
+ />
615
+ <TextField
616
+ label="API Key (optional)"
617
+ type="password"
618
+ value={supertokensConfig.apiKey || ''}
619
+ onChange={(e) => setSupertokensConfig({ ...supertokensConfig, apiKey: e.target.value })}
620
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
621
+ />
622
+ <TextField
623
+ label="App Name"
624
+ value={supertokensConfig.appName}
625
+ onChange={(e) => setSupertokensConfig({ ...supertokensConfig, appName: e.target.value })}
626
+ required
627
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
628
+ />
629
+ <TextField
630
+ label="API Domain"
631
+ value={supertokensConfig.apiDomain}
632
+ onChange={(e) => setSupertokensConfig({ ...supertokensConfig, apiDomain: e.target.value })}
633
+ required
634
+ placeholder="http://localhost:3000"
635
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
636
+ />
637
+ <TextField
638
+ label="Website Domain"
639
+ value={supertokensConfig.websiteDomain}
640
+ onChange={(e) => setSupertokensConfig({ ...supertokensConfig, websiteDomain: e.target.value })}
641
+ required
642
+ placeholder="http://localhost:3000"
643
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
644
+ />
645
+ <TextField
646
+ label="API Base Path"
647
+ value={supertokensConfig.apiBasePath || '/auth'}
648
+ onChange={(e) => setSupertokensConfig({ ...supertokensConfig, apiBasePath: e.target.value })}
649
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
650
+ />
651
+ </Box>
652
+ <Box sx={{ mt: 2 }}>
653
+ <FormControlLabel
654
+ control={
655
+ <Switch
656
+ checked={supertokensConfig.enableEmailPassword ?? true}
657
+ onChange={(e) =>
658
+ setSupertokensConfig({ ...supertokensConfig, enableEmailPassword: e.target.checked })
659
+ }
660
+ sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: 'var(--theme-primary)' } }}
661
+ />
662
+ }
663
+ label="Enable Email/Password Auth"
664
+ sx={{ color: 'var(--theme-text-primary)' }}
665
+ />
666
+ </Box>
667
+ </CardContent>
668
+ </Card>
669
+
670
+ {/* Social Providers */}
671
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
672
+ <CardContent sx={{ pb: showSocialProviders ? 2 : 0 }}>
673
+ <Box
674
+ sx={{
675
+ display: 'flex',
676
+ justifyContent: 'space-between',
677
+ alignItems: 'center',
678
+ cursor: 'pointer',
679
+ }}
680
+ onClick={() => setShowSocialProviders(!showSocialProviders)}
681
+ >
682
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
683
+ Social Login Providers
684
+ </Typography>
685
+ {showSocialProviders ? <ExpandLessIcon /> : <ExpandMoreIcon />}
686
+ </Box>
687
+ </CardContent>
688
+ <Collapse in={showSocialProviders}>
689
+ <CardContent sx={{ pt: 0 }}>
690
+ <Divider sx={{ mb: 2 }} />
691
+
692
+ {/* Google */}
693
+ <Box sx={{ mb: 3 }}>
694
+ <FormControlLabel
695
+ control={
696
+ <Switch
697
+ checked={googleProvider.enabled}
698
+ onChange={(e) => setGoogleProvider({ ...googleProvider, enabled: e.target.checked })}
699
+ sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: 'var(--theme-primary)' } }}
700
+ />
701
+ }
702
+ label="Google"
703
+ sx={{ color: 'var(--theme-text-primary)', mb: 1 }}
704
+ />
705
+ {googleProvider.enabled && (
706
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, ml: 4 }}>
707
+ <TextField
708
+ label="Client ID"
709
+ size="small"
710
+ value={googleProvider.clientId}
711
+ onChange={(e) => setGoogleProvider({ ...googleProvider, clientId: e.target.value })}
712
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
713
+ />
714
+ <TextField
715
+ label="Client Secret"
716
+ size="small"
717
+ type="password"
718
+ value={googleProvider.clientSecret}
719
+ onChange={(e) => setGoogleProvider({ ...googleProvider, clientSecret: e.target.value })}
720
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
721
+ />
722
+ </Box>
723
+ )}
724
+ </Box>
725
+
726
+ {/* GitHub */}
727
+ <Box sx={{ mb: 3 }}>
728
+ <FormControlLabel
729
+ control={
730
+ <Switch
731
+ checked={githubProvider.enabled}
732
+ onChange={(e) => setGithubProvider({ ...githubProvider, enabled: e.target.checked })}
733
+ sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: 'var(--theme-primary)' } }}
734
+ />
735
+ }
736
+ label="GitHub"
737
+ sx={{ color: 'var(--theme-text-primary)', mb: 1 }}
738
+ />
739
+ {githubProvider.enabled && (
740
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, ml: 4 }}>
741
+ <TextField
742
+ label="Client ID"
743
+ size="small"
744
+ value={githubProvider.clientId}
745
+ onChange={(e) => setGithubProvider({ ...githubProvider, clientId: e.target.value })}
746
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
747
+ />
748
+ <TextField
749
+ label="Client Secret"
750
+ size="small"
751
+ type="password"
752
+ value={githubProvider.clientSecret}
753
+ onChange={(e) => setGithubProvider({ ...githubProvider, clientSecret: e.target.value })}
754
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
755
+ />
756
+ </Box>
757
+ )}
758
+ </Box>
759
+
760
+ {/* Apple */}
761
+ <Box>
762
+ <FormControlLabel
763
+ control={
764
+ <Switch
765
+ checked={appleProvider.enabled}
766
+ onChange={(e) => setAppleProvider({ ...appleProvider, enabled: e.target.checked })}
767
+ sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: 'var(--theme-primary)' } }}
768
+ />
769
+ }
770
+ label="Apple"
771
+ sx={{ color: 'var(--theme-text-primary)', mb: 1 }}
772
+ />
773
+ {appleProvider.enabled && (
774
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2, ml: 4 }}>
775
+ <TextField
776
+ label="Client ID"
777
+ size="small"
778
+ value={appleProvider.clientId}
779
+ onChange={(e) => setAppleProvider({ ...appleProvider, clientId: e.target.value })}
780
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
781
+ />
782
+ <TextField
783
+ label="Client Secret"
784
+ size="small"
785
+ type="password"
786
+ value={appleProvider.clientSecret}
787
+ onChange={(e) => setAppleProvider({ ...appleProvider, clientSecret: e.target.value })}
788
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
789
+ />
790
+ <TextField
791
+ label="Key ID"
792
+ size="small"
793
+ value={appleProvider.keyId || ''}
794
+ onChange={(e) => setAppleProvider({ ...appleProvider, keyId: e.target.value })}
795
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
796
+ />
797
+ <TextField
798
+ label="Team ID"
799
+ size="small"
800
+ value={appleProvider.teamId || ''}
801
+ onChange={(e) => setAppleProvider({ ...appleProvider, teamId: e.target.value })}
802
+ sx={{ '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
803
+ />
804
+ </Box>
805
+ )}
806
+ </Box>
807
+ </CardContent>
808
+ </Collapse>
809
+ </Card>
810
+ </>
811
+ )}
812
+
813
+ {/* Test Result */}
814
+ {testResult && (
815
+ <Alert severity={testResult.success ? 'success' : 'error'} sx={{ mb: 3 }}>
816
+ <Typography variant="body2" sx={{ fontWeight: 600 }}>
817
+ {testResult.success ? 'Connection Successful' : 'Connection Failed'}
818
+ </Typography>
819
+ <Typography variant="body2">{testResult.message}</Typography>
820
+ {testResult.details?.latency && (
821
+ <Typography variant="caption" sx={{ display: 'block', mt: 0.5 }}>
822
+ Latency: {testResult.details.latency}ms
823
+ </Typography>
824
+ )}
825
+ </Alert>
826
+ )}
827
+
828
+ {/* Action Buttons */}
829
+ <Box sx={{ display: 'flex', gap: 2, justifyContent: 'space-between' }}>
830
+ <Box sx={{ display: 'flex', gap: 2 }}>
831
+ <Button
832
+ variant="outlined"
833
+ startIcon={<CancelIcon />}
834
+ onClick={handleCancelEdit}
835
+ disabled={saving}
836
+ sx={{
837
+ color: 'var(--theme-text-secondary)',
838
+ borderColor: 'var(--theme-border)',
839
+ }}
840
+ >
841
+ Cancel
842
+ </Button>
843
+ {status?.runtimeConfig && (
844
+ <Button
845
+ variant="outlined"
846
+ color="error"
847
+ startIcon={<DeleteIcon />}
848
+ onClick={() => setDeleteDialogOpen(true)}
849
+ disabled={saving}
850
+ >
851
+ Reset to Env Vars
852
+ </Button>
853
+ )}
854
+ </Box>
855
+ <Box sx={{ display: 'flex', gap: 2 }}>
856
+ <Button
857
+ variant="outlined"
858
+ startIcon={testing ? <CircularProgress size={16} /> : <PlayArrowIcon />}
859
+ onClick={handleTestConnection}
860
+ disabled={!selectedAdapter || testing || saving}
861
+ sx={{
862
+ color: 'var(--theme-text-primary)',
863
+ borderColor: 'var(--theme-border)',
864
+ }}
865
+ >
866
+ Test Connection
867
+ </Button>
868
+ <Button
869
+ variant="contained"
870
+ startIcon={saving ? <CircularProgress size={16} sx={{ color: 'white' }} /> : <SaveIcon />}
871
+ onClick={handleSave}
872
+ disabled={saving}
873
+ sx={{
874
+ bgcolor: 'var(--theme-primary)',
875
+ '&:hover': { bgcolor: 'var(--theme-primary-dark)' },
876
+ }}
877
+ >
878
+ Save Configuration
879
+ </Button>
880
+ </Box>
881
+ </Box>
882
+ </Box>
883
+ ) : (
884
+ <>
885
+ {/* Status Card (Read-only view) */}
886
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
887
+ <CardContent>
888
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
889
+ {getStateIcon(status?.state || 'disabled')}
890
+ <Box sx={{ flex: 1 }}>
891
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
892
+ Status:{' '}
893
+ <Chip
894
+ label={status?.state?.toUpperCase() || 'UNKNOWN'}
895
+ size="small"
896
+ sx={{
897
+ bgcolor: `${getStateColor(status?.state || 'disabled')}20`,
898
+ color: getStateColor(status?.state || 'disabled'),
899
+ fontWeight: 600,
900
+ }}
901
+ />
902
+ </Typography>
903
+ {status?.adapter && (
904
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)', mt: 0.5 }}>
905
+ Adapter: <strong>{status.adapter}</strong>
906
+ </Typography>
907
+ )}
908
+ </Box>
909
+ {/* Test Current Connection Button */}
910
+ {status?.state === 'enabled' && status?.adapter && (
911
+ <Button
912
+ variant="outlined"
913
+ size="small"
914
+ startIcon={testing ? <CircularProgress size={14} /> : <PlayArrowIcon />}
915
+ onClick={handleTestCurrentConnection}
916
+ disabled={testing}
917
+ sx={{
918
+ color: 'var(--theme-text-primary)',
919
+ borderColor: 'var(--theme-border)',
920
+ }}
921
+ >
922
+ Test Connection
923
+ </Button>
924
+ )}
925
+ </Box>
926
+
927
+ {/* Test Result for current connection */}
928
+ {testResult && !editMode && (
929
+ <Alert severity={testResult.success ? 'success' : 'error'} sx={{ mb: 2 }}>
930
+ <Typography variant="body2" sx={{ fontWeight: 600 }}>
931
+ {testResult.success ? 'Connection Successful' : 'Connection Failed'}
932
+ </Typography>
933
+ <Typography variant="body2">{testResult.message}</Typography>
934
+ {testResult.details?.latency && (
935
+ <Typography variant="caption" sx={{ display: 'block', mt: 0.5 }}>
936
+ Latency: {testResult.details.latency}ms
937
+ </Typography>
938
+ )}
939
+ </Alert>
940
+ )}
941
+
942
+ {/* Environment Variables Config Badge */}
943
+ {status?.state === 'enabled' && !status?.runtimeConfig && (
944
+ <Alert severity="success" sx={{ mb: 2 }}>
945
+ <Typography variant="body2" sx={{ fontWeight: 600 }}>
946
+ Configured via Environment Variables
947
+ </Typography>
948
+ <Typography variant="body2">
949
+ Authentication is configured using environment variables. Click "Edit" to override with runtime
950
+ configuration (requires PostgreSQL).
951
+ </Typography>
952
+ </Alert>
953
+ )}
954
+
955
+ {/* Runtime Config Badge */}
956
+ {status?.runtimeConfig && (
957
+ <Chip
958
+ label="Runtime Configuration Active"
959
+ size="small"
960
+ sx={{
961
+ bgcolor: 'var(--theme-primary)',
962
+ color: 'white',
963
+ mb: 2,
964
+ }}
965
+ />
966
+ )}
967
+
968
+ {/* Error Message */}
969
+ {status?.state === 'error' && status.error && (
970
+ <Alert severity="error" sx={{ mb: 2 }}>
971
+ {status.error}
972
+ </Alert>
973
+ )}
974
+
975
+ {/* Missing Variables */}
976
+ {status?.missingVars && status.missingVars.length > 0 && (
977
+ <Alert severity="warning" sx={{ mb: 2 }}>
978
+ <Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
979
+ Missing environment variables:
980
+ </Typography>
981
+ <Box component="ul" sx={{ m: 0, pl: 2 }}>
982
+ {status.missingVars.map((v) => (
983
+ <li key={v}>
984
+ <code>{v}</code>
985
+ </li>
986
+ ))}
987
+ </Box>
988
+ </Alert>
989
+ )}
990
+
991
+ {/* Disabled State Info */}
992
+ {status?.state === 'disabled' && (
993
+ <Alert severity="info">
994
+ <Typography variant="body2">
995
+ Authentication is disabled. Click the edit button to configure a provider, or set the{' '}
996
+ <code>AUTH_ADAPTER</code> environment variable.
997
+ </Typography>
998
+ <Typography variant="body2" sx={{ mt: 1 }}>
999
+ Valid options: <code>supertokens</code>, <code>auth0</code>, <code>supabase</code>,{' '}
1000
+ <code>basic</code>
1001
+ </Typography>
1002
+ </Alert>
1003
+ )}
1004
+ </CardContent>
1005
+ </Card>
1006
+
1007
+ {/* Configuration Table */}
1008
+ {configEntries.length > 0 && (
1009
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
1010
+ <CardContent sx={{ pb: 0 }}>
1011
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
1012
+ Current Configuration
1013
+ </Typography>
1014
+ </CardContent>
1015
+ <TableContainer>
1016
+ <Table size="small">
1017
+ <TableHead>
1018
+ <TableRow>
1019
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
1020
+ Variable
1021
+ </TableCell>
1022
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
1023
+ Value
1024
+ </TableCell>
1025
+ <TableCell
1026
+ sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 60 }}
1027
+ >
1028
+ Actions
1029
+ </TableCell>
1030
+ </TableRow>
1031
+ </TableHead>
1032
+ <TableBody>
1033
+ {configEntries.map(([key, value]) => (
1034
+ <TableRow key={key}>
1035
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
1036
+ <Typography
1037
+ sx={{ color: 'var(--theme-text-primary)', fontFamily: 'monospace', fontSize: 13 }}
1038
+ >
1039
+ {key}
1040
+ </Typography>
1041
+ </TableCell>
1042
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
1043
+ <Typography
1044
+ sx={{
1045
+ color: value.includes('*') ? 'var(--theme-text-secondary)' : 'var(--theme-text-primary)',
1046
+ fontFamily: 'monospace',
1047
+ fontSize: 13,
1048
+ }}
1049
+ >
1050
+ {value}
1051
+ </Typography>
1052
+ </TableCell>
1053
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
1054
+ <Tooltip title={copied === key ? 'Copied!' : 'Copy value'}>
1055
+ <IconButton
1056
+ size="small"
1057
+ onClick={() => handleCopy(key, value)}
1058
+ sx={{ color: copied === key ? 'var(--theme-success)' : 'var(--theme-text-secondary)' }}
1059
+ >
1060
+ <ContentCopyIcon fontSize="small" />
1061
+ </IconButton>
1062
+ </Tooltip>
1063
+ </TableCell>
1064
+ </TableRow>
1065
+ ))}
1066
+ </TableBody>
1067
+ </Table>
1068
+ </TableContainer>
1069
+ </Card>
1070
+ )}
1071
+
1072
+ {/* No Configuration */}
1073
+ {status?.state === 'enabled' && configEntries.length === 0 && (
1074
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
1075
+ <CardContent>
1076
+ <Typography sx={{ color: 'var(--theme-text-secondary)', textAlign: 'center' }}>
1077
+ No configuration details available
1078
+ </Typography>
1079
+ </CardContent>
1080
+ </Card>
1081
+ )}
1082
+ </>
1083
+ )}
1084
+
1085
+ {/* Delete Confirmation Dialog */}
1086
+ <Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
1087
+ <DialogTitle>Reset to Environment Variables?</DialogTitle>
1088
+ <DialogContent>
1089
+ <Typography>
1090
+ This will delete the runtime configuration from the database. The auth plugin will fall back to environment
1091
+ variables on the next request.
1092
+ </Typography>
1093
+ </DialogContent>
1094
+ <DialogActions>
1095
+ <Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
1096
+ <Button onClick={handleDelete} color="error" disabled={saving}>
1097
+ {saving ? <CircularProgress size={20} /> : 'Reset'}
1098
+ </Button>
1099
+ </DialogActions>
1100
+ </Dialog>
1101
+ </Box>
1102
+ );
1103
+ }