@qwickapps/server 1.3.1 → 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 +157 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +114 -0
- package/dist/core/control-panel.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/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 +51 -1
- package/dist/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/plugins/auth/env-config.js +640 -7
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/auth/index.d.ts +6 -2
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +5 -1
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/types.d.ts +106 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/index.js.map +1 -1
- 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-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 +141 -0
- 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 +3332 -2343
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
- package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/control-panel.ts +128 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +38 -0
- package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
- package/src/plugins/auth/adapter-wrapper.ts +205 -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 +714 -7
- package/src/plugins/auth/index.ts +22 -1
- package/src/plugins/auth/types.ts +138 -0
- package/src/plugins/index.ts +49 -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/ui/src/App.tsx +32 -14
- package/ui/src/api/controlPanelApi.ts +226 -0
- package/ui/src/dashboard/builtInWidgets.tsx +5 -1
- 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 +986 -142
- package/ui/src/pages/IntegrationsPage.tsx +288 -0
- package/ui/src/pages/RateLimitPage.tsx +292 -0
- package/dist-ui/assets/index-BY8OxNgO.js +0 -465
- package/dist-ui/assets/index-BY8OxNgO.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
4
|
Card,
|
|
@@ -15,13 +15,44 @@ import {
|
|
|
15
15
|
Alert,
|
|
16
16
|
IconButton,
|
|
17
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,
|
|
18
32
|
} from '@mui/material';
|
|
19
33
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
20
34
|
import ErrorIcon from '@mui/icons-material/Error';
|
|
21
35
|
import BlockIcon from '@mui/icons-material/Block';
|
|
22
36
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
23
37
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
24
|
-
import
|
|
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';
|
|
25
56
|
|
|
26
57
|
/**
|
|
27
58
|
* Get the status color for the auth state
|
|
@@ -53,28 +84,152 @@ function getStateIcon(state: string) {
|
|
|
53
84
|
}
|
|
54
85
|
}
|
|
55
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
|
+
|
|
56
131
|
export function AuthPage() {
|
|
57
132
|
const [status, setStatus] = useState<AuthConfigStatus | null>(null);
|
|
58
133
|
const [loading, setLoading] = useState(true);
|
|
59
134
|
const [error, setError] = useState<string | null>(null);
|
|
60
135
|
const [copied, setCopied] = useState<string | null>(null);
|
|
61
136
|
|
|
62
|
-
|
|
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 () => {
|
|
63
176
|
setLoading(true);
|
|
64
177
|
setError(null);
|
|
65
178
|
try {
|
|
66
179
|
const data = await api.getAuthConfig();
|
|
67
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
|
+
}
|
|
68
223
|
} catch (err) {
|
|
69
224
|
setError(err instanceof Error ? err.message : 'Failed to fetch auth status');
|
|
70
225
|
} finally {
|
|
71
226
|
setLoading(false);
|
|
72
227
|
}
|
|
73
|
-
};
|
|
228
|
+
}, []);
|
|
74
229
|
|
|
75
230
|
useEffect(() => {
|
|
76
231
|
fetchStatus();
|
|
77
|
-
}, []);
|
|
232
|
+
}, [fetchStatus]);
|
|
78
233
|
|
|
79
234
|
const handleCopy = (key: string, value: string) => {
|
|
80
235
|
navigator.clipboard.writeText(value);
|
|
@@ -82,6 +237,148 @@ export function AuthPage() {
|
|
|
82
237
|
setTimeout(() => setCopied(null), 2000);
|
|
83
238
|
};
|
|
84
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
|
+
|
|
85
382
|
if (loading) {
|
|
86
383
|
return (
|
|
87
384
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
|
|
@@ -90,16 +387,6 @@ export function AuthPage() {
|
|
|
90
387
|
);
|
|
91
388
|
}
|
|
92
389
|
|
|
93
|
-
if (error) {
|
|
94
|
-
return (
|
|
95
|
-
<Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
|
|
96
|
-
<CardContent>
|
|
97
|
-
<Typography color="error">{error}</Typography>
|
|
98
|
-
</CardContent>
|
|
99
|
-
</Card>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
390
|
const configEntries = status?.config ? Object.entries(status.config) : [];
|
|
104
391
|
|
|
105
392
|
return (
|
|
@@ -108,152 +395,709 @@ export function AuthPage() {
|
|
|
108
395
|
<Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
109
396
|
Authentication
|
|
110
397
|
</Typography>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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>
|
|
116
414
|
</Box>
|
|
117
415
|
<Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
|
|
118
|
-
Auth plugin configuration status
|
|
416
|
+
{editMode ? 'Configure authentication provider' : 'Auth plugin configuration status'}
|
|
119
417
|
</Typography>
|
|
120
418
|
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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)}
|
|
131
468
|
size="small"
|
|
132
|
-
sx={{
|
|
133
|
-
|
|
134
|
-
color: getStateColor(status?.state || 'disabled'),
|
|
135
|
-
fontWeight: 600,
|
|
136
|
-
}}
|
|
469
|
+
sx={{ flex: 1, '& .MuiInputBase-input': { color: 'var(--theme-text-primary)' } }}
|
|
470
|
+
placeholder="/api/health, /api/public/*"
|
|
137
471
|
/>
|
|
138
|
-
</
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
482
|
</Typography>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
)}
|
|
146
533
|
|
|
147
|
-
{/*
|
|
148
|
-
{
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
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>
|
|
152
561
|
)}
|
|
153
562
|
|
|
154
|
-
{/*
|
|
155
|
-
{
|
|
156
|
-
<
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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>
|
|
168
596
|
)}
|
|
169
597
|
|
|
170
|
-
{/*
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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'}
|
|
178
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
|
+
)}
|
|
179
825
|
</Alert>
|
|
180
826
|
)}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}}
|
|
204
921
|
>
|
|
205
|
-
|
|
206
|
-
</
|
|
207
|
-
|
|
208
|
-
</
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 }}
|
|
224
1027
|
>
|
|
225
|
-
|
|
226
|
-
</
|
|
227
|
-
</
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
)}
|
|
246
1071
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
</>
|
|
256
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>
|
|
257
1101
|
</Box>
|
|
258
1102
|
);
|
|
259
1103
|
}
|