@qwickapps/server 1.5.1 → 1.6.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/CHANGELOG.md +43 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +41 -0
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/guards.d.ts.map +1 -1
- package/dist/core/guards.js +77 -0
- package/dist/core/guards.js.map +1 -1
- package/dist/core/health-manager.d.ts +4 -0
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +6 -1
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +55 -5
- package/dist/core/plugin-registry.d.ts.map +1 -1
- package/dist/core/plugin-registry.js +57 -19
- package/dist/core/plugin-registry.js.map +1 -1
- package/dist/core/types.d.ts +2 -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 +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/api-keys/api-keys-plugin.d.ts +46 -0
- package/dist/plugins/api-keys/api-keys-plugin.d.ts.map +1 -0
- package/dist/plugins/api-keys/api-keys-plugin.js +329 -0
- package/dist/plugins/api-keys/api-keys-plugin.js.map +1 -0
- package/dist/plugins/api-keys/index.d.ts +14 -0
- package/dist/plugins/api-keys/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/index.js +17 -0
- package/dist/plugins/api-keys/index.js.map +1 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts +74 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts.map +1 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.js +201 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.js.map +1 -0
- package/dist/plugins/api-keys/middleware/index.d.ts +7 -0
- package/dist/plugins/api-keys/middleware/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/middleware/index.js +7 -0
- package/dist/plugins/api-keys/middleware/index.js.map +1 -0
- package/dist/plugins/api-keys/stores/index.d.ts +7 -0
- package/dist/plugins/api-keys/stores/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/stores/index.js +7 -0
- package/dist/plugins/api-keys/stores/index.js.map +1 -0
- package/dist/plugins/api-keys/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/api-keys/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/api-keys/stores/postgres-store.js +360 -0
- package/dist/plugins/api-keys/stores/postgres-store.js.map +1 -0
- package/dist/plugins/api-keys/types.d.ts +268 -0
- package/dist/plugins/api-keys/types.d.ts.map +1 -0
- package/dist/plugins/api-keys/types.js +56 -0
- package/dist/plugins/api-keys/types.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -1
- package/dist/plugins/auth/auth-plugin.js +17 -1
- package/dist/plugins/auth/auth-plugin.js.map +1 -1
- package/dist/plugins/auth/auth-plugin.test.js +133 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -1
- package/dist/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/plugins/auth/env-config.js +6 -2
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/auth/types.d.ts +10 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/auth/types.js.map +1 -1
- package/dist/plugins/devices/__tests__/token-utils.test.js +4 -2
- package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -1
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +21 -4
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/qwickbrain/index.d.ts +25 -0
- package/dist/plugins/qwickbrain/index.d.ts.map +1 -0
- package/dist/plugins/qwickbrain/index.js +24 -0
- package/dist/plugins/qwickbrain/index.js.map +1 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts +23 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.js +528 -0
- package/dist/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -0
- package/dist/plugins/qwickbrain/types.d.ts +131 -0
- package/dist/plugins/qwickbrain/types.d.ts.map +1 -0
- package/dist/plugins/qwickbrain/types.js +9 -0
- package/dist/plugins/qwickbrain/types.js.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -1
- package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
- package/dist/plugins/users/stores/postgres-store.js +59 -1
- package/dist/plugins/users/stores/postgres-store.js.map +1 -1
- package/dist/plugins/users/types.d.ts +22 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist-ui/assets/index-5nX8fM1a.js +469 -0
- package/dist-ui/assets/index-5nX8fM1a.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +68 -0
- package/dist-ui-lib/components/index.d.ts +2 -1
- package/dist-ui-lib/index.js +2642 -2281
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/APIKeysPage.d.ts +13 -0
- package/dist-ui-lib/pages/AcceptInvitationPage.d.ts +28 -0
- package/package.json +3 -2
- package/src/core/control-panel.ts +47 -0
- package/src/core/guards.ts +89 -0
- package/src/core/health-manager.ts +6 -1
- package/src/core/plugin-registry.ts +123 -25
- package/src/core/types.ts +2 -0
- package/src/index.ts +11 -0
- package/src/plugins/api-keys/api-keys-plugin.ts +397 -0
- package/src/plugins/api-keys/index.ts +49 -0
- package/src/plugins/api-keys/middleware/bearer-token-auth.ts +250 -0
- package/src/plugins/api-keys/middleware/index.ts +12 -0
- package/src/plugins/api-keys/stores/index.ts +7 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +487 -0
- package/src/plugins/api-keys/types.ts +243 -0
- package/src/plugins/auth/auth-plugin.test.ts +167 -0
- package/src/plugins/auth/auth-plugin.ts +17 -1
- package/src/plugins/auth/env-config.ts +6 -2
- package/src/plugins/auth/types.ts +10 -0
- package/src/plugins/devices/__tests__/token-utils.test.ts +4 -2
- package/src/plugins/frontend-app-plugin.ts +24 -4
- package/src/plugins/index.ts +15 -0
- package/src/plugins/qwickbrain/index.ts +33 -0
- package/src/plugins/qwickbrain/qwickbrain-plugin.ts +642 -0
- package/src/plugins/qwickbrain/types.ts +146 -0
- package/src/plugins/users/__tests__/postgres-store.test.ts +1 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
- package/src/plugins/users/stores/postgres-store.ts +69 -0
- package/src/plugins/users/types.ts +25 -0
- package/ui/src/App.tsx +6 -1
- package/ui/src/api/controlPanelApi.ts +206 -37
- package/ui/src/components/index.ts +6 -0
- package/ui/src/pages/APIKeysPage.tsx +661 -0
- package/ui/src/pages/AcceptInvitationPage.tsx +169 -0
- package/ui/src/pages/UsersPage.tsx +225 -2
- package/dist-ui/assets/index-CynOqPkb.js +0 -469
- package/dist-ui/assets/index-CynOqPkb.js.map +0 -1
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APIKeysPage Component
|
|
3
|
+
*
|
|
4
|
+
* API key management page for authentication and authorization.
|
|
5
|
+
* Allows users to create, view, and manage API keys with scopes.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
11
|
+
import {
|
|
12
|
+
Box,
|
|
13
|
+
Card,
|
|
14
|
+
CardContent,
|
|
15
|
+
Table,
|
|
16
|
+
TableBody,
|
|
17
|
+
TableCell,
|
|
18
|
+
TableContainer,
|
|
19
|
+
TableHead,
|
|
20
|
+
TableRow,
|
|
21
|
+
Chip,
|
|
22
|
+
Alert,
|
|
23
|
+
LinearProgress,
|
|
24
|
+
IconButton,
|
|
25
|
+
Tooltip,
|
|
26
|
+
TextField,
|
|
27
|
+
FormControl,
|
|
28
|
+
InputLabel,
|
|
29
|
+
Select,
|
|
30
|
+
MenuItem,
|
|
31
|
+
FormHelperText,
|
|
32
|
+
Checkbox,
|
|
33
|
+
FormControlLabel,
|
|
34
|
+
FormGroup,
|
|
35
|
+
} from '@mui/material';
|
|
36
|
+
import {
|
|
37
|
+
Text,
|
|
38
|
+
Button,
|
|
39
|
+
Dialog,
|
|
40
|
+
DialogTitle,
|
|
41
|
+
DialogContent,
|
|
42
|
+
DialogActions,
|
|
43
|
+
GridLayout,
|
|
44
|
+
} from '@qwickapps/react-framework';
|
|
45
|
+
import KeyIcon from '@mui/icons-material/Key';
|
|
46
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
47
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
48
|
+
import EditIcon from '@mui/icons-material/Edit';
|
|
49
|
+
import VisibilityIcon from '@mui/icons-material/Visibility';
|
|
50
|
+
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
51
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
52
|
+
import CancelIcon from '@mui/icons-material/Cancel';
|
|
53
|
+
import {
|
|
54
|
+
api,
|
|
55
|
+
type ApiKey,
|
|
56
|
+
type ApiKeyWithPlaintext,
|
|
57
|
+
type CreateApiKeyRequest,
|
|
58
|
+
} from '../api/controlPanelApi';
|
|
59
|
+
|
|
60
|
+
export interface APIKeysPageProps {
|
|
61
|
+
title?: string;
|
|
62
|
+
subtitle?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function APIKeysPage({
|
|
66
|
+
title = 'API Keys',
|
|
67
|
+
subtitle = 'Manage API keys for programmatic access',
|
|
68
|
+
}: APIKeysPageProps) {
|
|
69
|
+
// State
|
|
70
|
+
const [keys, setKeys] = useState<ApiKey[]>([]);
|
|
71
|
+
const [loading, setLoading] = useState(true);
|
|
72
|
+
const [error, setError] = useState<string | null>(null);
|
|
73
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
74
|
+
|
|
75
|
+
// Create key dialog state
|
|
76
|
+
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
77
|
+
const [creating, setCreating] = useState(false);
|
|
78
|
+
const [newKey, setNewKey] = useState<CreateApiKeyRequest>({
|
|
79
|
+
name: '',
|
|
80
|
+
key_type: 'pat',
|
|
81
|
+
scopes: ['read'],
|
|
82
|
+
expires_at: '',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Created key display state
|
|
86
|
+
const [createdKey, setCreatedKey] = useState<ApiKeyWithPlaintext | null>(null);
|
|
87
|
+
const [showCreatedKey, setShowCreatedKey] = useState(true);
|
|
88
|
+
const [copiedKey, setCopiedKey] = useState(false);
|
|
89
|
+
|
|
90
|
+
// Edit key dialog state
|
|
91
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
92
|
+
const [editingKey, setEditingKey] = useState<ApiKey | null>(null);
|
|
93
|
+
const [editForm, setEditForm] = useState({
|
|
94
|
+
name: '',
|
|
95
|
+
scopes: [] as Array<'read' | 'write' | 'admin'>,
|
|
96
|
+
is_active: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Delete confirmation dialog state
|
|
100
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
101
|
+
const [keyToDelete, setKeyToDelete] = useState<{ id: string; name: string } | null>(null);
|
|
102
|
+
|
|
103
|
+
// Fetch API keys
|
|
104
|
+
const fetchKeys = useCallback(async () => {
|
|
105
|
+
setLoading(true);
|
|
106
|
+
try {
|
|
107
|
+
const data = await api.getApiKeys();
|
|
108
|
+
setKeys(data.keys || []);
|
|
109
|
+
setError(null);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch API keys');
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
}
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
fetchKeys();
|
|
119
|
+
}, [fetchKeys]);
|
|
120
|
+
|
|
121
|
+
// Create key handler
|
|
122
|
+
const handleCreateKey = async () => {
|
|
123
|
+
setCreating(true);
|
|
124
|
+
try {
|
|
125
|
+
const created = await api.createApiKey(newKey);
|
|
126
|
+
setCreatedKey(created);
|
|
127
|
+
setCreateDialogOpen(false);
|
|
128
|
+
setNewKey({
|
|
129
|
+
name: '',
|
|
130
|
+
key_type: 'pat',
|
|
131
|
+
scopes: ['read'],
|
|
132
|
+
expires_at: '',
|
|
133
|
+
});
|
|
134
|
+
fetchKeys();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
setError(err instanceof Error ? err.message : 'Failed to create API key');
|
|
137
|
+
} finally {
|
|
138
|
+
setCreating(false);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Delete key handlers
|
|
143
|
+
const openDeleteDialog = (keyId: string, keyName: string) => {
|
|
144
|
+
setKeyToDelete({ id: keyId, name: keyName });
|
|
145
|
+
setDeleteDialogOpen(true);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const confirmDelete = async () => {
|
|
149
|
+
if (!keyToDelete) return;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await api.deleteApiKey(keyToDelete.id);
|
|
153
|
+
setSuccess(`API key "${keyToDelete.name}" deleted`);
|
|
154
|
+
setDeleteDialogOpen(false);
|
|
155
|
+
setKeyToDelete(null);
|
|
156
|
+
fetchKeys();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
setError(err instanceof Error ? err.message : 'Failed to delete API key');
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const cancelDelete = () => {
|
|
163
|
+
setDeleteDialogOpen(false);
|
|
164
|
+
setKeyToDelete(null);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Edit key handlers
|
|
168
|
+
const openEditDialog = (key: ApiKey) => {
|
|
169
|
+
setEditingKey(key);
|
|
170
|
+
setEditForm({
|
|
171
|
+
name: key.name,
|
|
172
|
+
scopes: [...key.scopes],
|
|
173
|
+
is_active: key.is_active,
|
|
174
|
+
});
|
|
175
|
+
setEditDialogOpen(true);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleUpdateKey = async () => {
|
|
179
|
+
if (!editingKey) return;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await api.updateApiKey(editingKey.id, editForm);
|
|
183
|
+
setSuccess(`API key "${editingKey.name}" updated`);
|
|
184
|
+
setEditDialogOpen(false);
|
|
185
|
+
setEditingKey(null);
|
|
186
|
+
fetchKeys();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
setError(err instanceof Error ? err.message : 'Failed to update API key');
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Copy key to clipboard
|
|
193
|
+
const copyToClipboard = (text: string) => {
|
|
194
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
195
|
+
setCopiedKey(true);
|
|
196
|
+
setTimeout(() => setCopiedKey(false), 2000);
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Close created key dialog
|
|
201
|
+
const closeCreatedKeyDialog = () => {
|
|
202
|
+
setCreatedKey(null);
|
|
203
|
+
setShowCreatedKey(true);
|
|
204
|
+
setCopiedKey(false);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Toggle scope
|
|
208
|
+
const toggleScope = (scope: 'read' | 'write' | 'admin') => {
|
|
209
|
+
setNewKey(prev => ({
|
|
210
|
+
...prev,
|
|
211
|
+
scopes: prev.scopes.includes(scope)
|
|
212
|
+
? prev.scopes.filter(s => s !== scope)
|
|
213
|
+
: [...prev.scopes, scope],
|
|
214
|
+
}));
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const toggleEditScope = (scope: 'read' | 'write' | 'admin') => {
|
|
218
|
+
setEditForm(prev => ({
|
|
219
|
+
...prev,
|
|
220
|
+
scopes: prev.scopes.includes(scope)
|
|
221
|
+
? prev.scopes.filter(s => s !== scope)
|
|
222
|
+
: [...prev.scopes, scope],
|
|
223
|
+
}));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Format date
|
|
227
|
+
const formatDate = (date: string | null) => {
|
|
228
|
+
if (!date) return 'Never';
|
|
229
|
+
return new Date(date).toLocaleDateString('en-US', {
|
|
230
|
+
year: 'numeric',
|
|
231
|
+
month: 'short',
|
|
232
|
+
day: 'numeric',
|
|
233
|
+
hour: '2-digit',
|
|
234
|
+
minute: '2-digit',
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Get scope color
|
|
239
|
+
const getScopeColor = (scope: string) => {
|
|
240
|
+
switch (scope) {
|
|
241
|
+
case 'read':
|
|
242
|
+
return 'var(--theme-info)';
|
|
243
|
+
case 'write':
|
|
244
|
+
return 'var(--theme-warning)';
|
|
245
|
+
case 'admin':
|
|
246
|
+
return 'var(--theme-error)';
|
|
247
|
+
default:
|
|
248
|
+
return 'var(--theme-text-secondary)';
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<Box>
|
|
254
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
|
255
|
+
<Box>
|
|
256
|
+
<Text variant="h4" content={title} customColor="var(--theme-text-primary)" />
|
|
257
|
+
<Text variant="body2" content={subtitle} customColor="var(--theme-text-secondary)" />
|
|
258
|
+
</Box>
|
|
259
|
+
<Button
|
|
260
|
+
variant="primary"
|
|
261
|
+
icon="add"
|
|
262
|
+
label="Create API Key"
|
|
263
|
+
onClick={() => setCreateDialogOpen(true)}
|
|
264
|
+
/>
|
|
265
|
+
</Box>
|
|
266
|
+
|
|
267
|
+
{loading && <LinearProgress sx={{ mb: 2 }} />}
|
|
268
|
+
|
|
269
|
+
{error && (
|
|
270
|
+
<Alert severity="error" onClose={() => setError(null)} sx={{ mb: 2 }}>
|
|
271
|
+
{error}
|
|
272
|
+
</Alert>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{success && (
|
|
276
|
+
<Alert severity="success" onClose={() => setSuccess(null)} sx={{ mb: 2 }}>
|
|
277
|
+
{success}
|
|
278
|
+
</Alert>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{/* Stats Card */}
|
|
282
|
+
<GridLayout columns={3} spacing="medium" sx={{ mb: 3 }} equalHeight>
|
|
283
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)' }}>
|
|
284
|
+
<CardContent>
|
|
285
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
286
|
+
<KeyIcon sx={{ fontSize: 40, color: 'var(--theme-primary)' }} />
|
|
287
|
+
<Box>
|
|
288
|
+
<Text variant="h4" content={keys.length.toString()} customColor="var(--theme-text-primary)" />
|
|
289
|
+
<Text variant="body2" content="Total Keys" customColor="var(--theme-text-secondary)" />
|
|
290
|
+
</Box>
|
|
291
|
+
</Box>
|
|
292
|
+
</CardContent>
|
|
293
|
+
</Card>
|
|
294
|
+
|
|
295
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)' }}>
|
|
296
|
+
<CardContent>
|
|
297
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
298
|
+
<CheckCircleIcon sx={{ fontSize: 40, color: 'var(--theme-success)' }} />
|
|
299
|
+
<Box>
|
|
300
|
+
<Text variant="h4" content={keys.filter(k => k.is_active).length.toString()} customColor="var(--theme-text-primary)" />
|
|
301
|
+
<Text variant="body2" content="Active Keys" customColor="var(--theme-text-secondary)" />
|
|
302
|
+
</Box>
|
|
303
|
+
</Box>
|
|
304
|
+
</CardContent>
|
|
305
|
+
</Card>
|
|
306
|
+
|
|
307
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)' }}>
|
|
308
|
+
<CardContent>
|
|
309
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
310
|
+
<CancelIcon sx={{ fontSize: 40, color: keys.filter(k => !k.is_active).length > 0 ? 'var(--theme-error)' : 'var(--theme-text-secondary)' }} />
|
|
311
|
+
<Box>
|
|
312
|
+
<Text variant="h4" content={keys.filter(k => !k.is_active).length.toString()} customColor="var(--theme-text-primary)" />
|
|
313
|
+
<Text variant="body2" content="Inactive Keys" customColor="var(--theme-text-secondary)" />
|
|
314
|
+
</Box>
|
|
315
|
+
</Box>
|
|
316
|
+
</CardContent>
|
|
317
|
+
</Card>
|
|
318
|
+
</GridLayout>
|
|
319
|
+
|
|
320
|
+
{/* API Keys Table */}
|
|
321
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)' }}>
|
|
322
|
+
<CardContent sx={{ p: 0 }}>
|
|
323
|
+
<TableContainer>
|
|
324
|
+
<Table>
|
|
325
|
+
<TableHead>
|
|
326
|
+
<TableRow>
|
|
327
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Name</TableCell>
|
|
328
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Prefix</TableCell>
|
|
329
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Type</TableCell>
|
|
330
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Scopes</TableCell>
|
|
331
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Status</TableCell>
|
|
332
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Last Used</TableCell>
|
|
333
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Expires</TableCell>
|
|
334
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }} align="right">Actions</TableCell>
|
|
335
|
+
</TableRow>
|
|
336
|
+
</TableHead>
|
|
337
|
+
<TableBody>
|
|
338
|
+
{keys.map((key) => (
|
|
339
|
+
<TableRow key={key.id} hover>
|
|
340
|
+
<TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
|
|
341
|
+
<Text variant="body1" content={key.name} fontWeight="500" />
|
|
342
|
+
</TableCell>
|
|
343
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
|
344
|
+
{key.key_prefix}...
|
|
345
|
+
</TableCell>
|
|
346
|
+
<TableCell sx={{ borderColor: 'var(--theme-border)' }}>
|
|
347
|
+
<Chip
|
|
348
|
+
size="small"
|
|
349
|
+
label={key.key_type.toUpperCase()}
|
|
350
|
+
sx={{
|
|
351
|
+
bgcolor: key.key_type === 'm2m' ? 'var(--theme-info)20' : 'var(--theme-success)20',
|
|
352
|
+
color: key.key_type === 'm2m' ? 'var(--theme-info)' : 'var(--theme-success)',
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
</TableCell>
|
|
356
|
+
<TableCell sx={{ borderColor: 'var(--theme-border)' }}>
|
|
357
|
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
|
358
|
+
{key.scopes.map((scope) => (
|
|
359
|
+
<Chip
|
|
360
|
+
key={scope}
|
|
361
|
+
size="small"
|
|
362
|
+
label={scope}
|
|
363
|
+
sx={{
|
|
364
|
+
bgcolor: `${getScopeColor(scope)}20`,
|
|
365
|
+
color: getScopeColor(scope),
|
|
366
|
+
fontSize: '0.7rem',
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
))}
|
|
370
|
+
</Box>
|
|
371
|
+
</TableCell>
|
|
372
|
+
<TableCell sx={{ borderColor: 'var(--theme-border)' }}>
|
|
373
|
+
<Chip
|
|
374
|
+
size="small"
|
|
375
|
+
icon={key.is_active ? <CheckCircleIcon sx={{ fontSize: 14 }} /> : <CancelIcon sx={{ fontSize: 14 }} />}
|
|
376
|
+
label={key.is_active ? 'Active' : 'Inactive'}
|
|
377
|
+
sx={{
|
|
378
|
+
bgcolor: key.is_active ? 'var(--theme-success)20' : 'var(--theme-error)20',
|
|
379
|
+
color: key.is_active ? 'var(--theme-success)' : 'var(--theme-error)',
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
</TableCell>
|
|
383
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
384
|
+
{formatDate(key.last_used_at)}
|
|
385
|
+
</TableCell>
|
|
386
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
387
|
+
{formatDate(key.expires_at)}
|
|
388
|
+
</TableCell>
|
|
389
|
+
<TableCell sx={{ borderColor: 'var(--theme-border)' }} align="right">
|
|
390
|
+
<Tooltip title="Edit">
|
|
391
|
+
<IconButton size="small" onClick={() => openEditDialog(key)}>
|
|
392
|
+
<EditIcon fontSize="small" />
|
|
393
|
+
</IconButton>
|
|
394
|
+
</Tooltip>
|
|
395
|
+
<Tooltip title="Delete">
|
|
396
|
+
<IconButton
|
|
397
|
+
size="small"
|
|
398
|
+
onClick={() => openDeleteDialog(key.id, key.name)}
|
|
399
|
+
sx={{ color: 'var(--theme-error)' }}
|
|
400
|
+
>
|
|
401
|
+
<DeleteIcon fontSize="small" />
|
|
402
|
+
</IconButton>
|
|
403
|
+
</Tooltip>
|
|
404
|
+
</TableCell>
|
|
405
|
+
</TableRow>
|
|
406
|
+
))}
|
|
407
|
+
{keys.length === 0 && !loading && (
|
|
408
|
+
<TableRow>
|
|
409
|
+
<TableCell colSpan={8} align="center" sx={{ py: 4, color: 'var(--theme-text-secondary)' }}>
|
|
410
|
+
No API keys found. Create one to get started.
|
|
411
|
+
</TableCell>
|
|
412
|
+
</TableRow>
|
|
413
|
+
)}
|
|
414
|
+
</TableBody>
|
|
415
|
+
</Table>
|
|
416
|
+
</TableContainer>
|
|
417
|
+
</CardContent>
|
|
418
|
+
</Card>
|
|
419
|
+
|
|
420
|
+
{/* Create Key Dialog */}
|
|
421
|
+
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth>
|
|
422
|
+
<DialogTitle>Create API Key</DialogTitle>
|
|
423
|
+
<DialogContent>
|
|
424
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
425
|
+
<TextField
|
|
426
|
+
label="Key Name"
|
|
427
|
+
fullWidth
|
|
428
|
+
value={newKey.name}
|
|
429
|
+
onChange={(e) => setNewKey({ ...newKey, name: e.target.value })}
|
|
430
|
+
placeholder="Enter a descriptive name"
|
|
431
|
+
helperText="Choose a name that helps you identify this key"
|
|
432
|
+
/>
|
|
433
|
+
|
|
434
|
+
<FormControl fullWidth>
|
|
435
|
+
<InputLabel>Key Type</InputLabel>
|
|
436
|
+
<Select
|
|
437
|
+
value={newKey.key_type}
|
|
438
|
+
label="Key Type"
|
|
439
|
+
onChange={(e) => setNewKey({ ...newKey, key_type: e.target.value as 'm2m' | 'pat' })}
|
|
440
|
+
>
|
|
441
|
+
<MenuItem value="pat">PAT (Personal Access Token)</MenuItem>
|
|
442
|
+
<MenuItem value="m2m">M2M (Machine-to-Machine)</MenuItem>
|
|
443
|
+
</Select>
|
|
444
|
+
<FormHelperText>
|
|
445
|
+
PAT for personal use, M2M for service-to-service communication
|
|
446
|
+
</FormHelperText>
|
|
447
|
+
</FormControl>
|
|
448
|
+
|
|
449
|
+
<FormControl component="fieldset">
|
|
450
|
+
<Text variant="subtitle2" content="Scopes" customColor="var(--theme-text-primary)" />
|
|
451
|
+
<FormGroup>
|
|
452
|
+
<FormControlLabel
|
|
453
|
+
control={
|
|
454
|
+
<Checkbox
|
|
455
|
+
checked={newKey.scopes.includes('read')}
|
|
456
|
+
onChange={() => toggleScope('read')}
|
|
457
|
+
/>
|
|
458
|
+
}
|
|
459
|
+
label="Read - View data and resources"
|
|
460
|
+
/>
|
|
461
|
+
<FormControlLabel
|
|
462
|
+
control={
|
|
463
|
+
<Checkbox
|
|
464
|
+
checked={newKey.scopes.includes('write')}
|
|
465
|
+
onChange={() => toggleScope('write')}
|
|
466
|
+
/>
|
|
467
|
+
}
|
|
468
|
+
label="Write - Create and update resources"
|
|
469
|
+
/>
|
|
470
|
+
<FormControlLabel
|
|
471
|
+
control={
|
|
472
|
+
<Checkbox
|
|
473
|
+
checked={newKey.scopes.includes('admin')}
|
|
474
|
+
onChange={() => toggleScope('admin')}
|
|
475
|
+
/>
|
|
476
|
+
}
|
|
477
|
+
label="Admin - Full administrative access"
|
|
478
|
+
/>
|
|
479
|
+
</FormGroup>
|
|
480
|
+
</FormControl>
|
|
481
|
+
|
|
482
|
+
<TextField
|
|
483
|
+
label="Expiration (Optional)"
|
|
484
|
+
type="datetime-local"
|
|
485
|
+
fullWidth
|
|
486
|
+
value={newKey.expires_at}
|
|
487
|
+
onChange={(e) => setNewKey({ ...newKey, expires_at: e.target.value })}
|
|
488
|
+
InputLabelProps={{ shrink: true }}
|
|
489
|
+
helperText="Leave empty for no expiration (90 days default)"
|
|
490
|
+
/>
|
|
491
|
+
</Box>
|
|
492
|
+
</DialogContent>
|
|
493
|
+
<DialogActions>
|
|
494
|
+
<Button variant="text" label="Cancel" onClick={() => setCreateDialogOpen(false)} />
|
|
495
|
+
<Button
|
|
496
|
+
variant="primary"
|
|
497
|
+
label="Create Key"
|
|
498
|
+
onClick={handleCreateKey}
|
|
499
|
+
disabled={creating || !newKey.name || newKey.scopes.length === 0}
|
|
500
|
+
/>
|
|
501
|
+
</DialogActions>
|
|
502
|
+
</Dialog>
|
|
503
|
+
|
|
504
|
+
{/* Created Key Display Dialog */}
|
|
505
|
+
<Dialog open={!!createdKey} onClose={closeCreatedKeyDialog} maxWidth="md" fullWidth>
|
|
506
|
+
<DialogTitle>API Key Created</DialogTitle>
|
|
507
|
+
<DialogContent>
|
|
508
|
+
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
509
|
+
<Text variant="body2" content="Save this key now. You won't be able to see it again!" fontWeight="500" />
|
|
510
|
+
</Alert>
|
|
511
|
+
|
|
512
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
513
|
+
<Box>
|
|
514
|
+
<Text variant="subtitle2" content="Key Name" customColor="var(--theme-text-secondary)" />
|
|
515
|
+
<Text variant="body1" content={createdKey?.name || ''} fontWeight="500" />
|
|
516
|
+
</Box>
|
|
517
|
+
|
|
518
|
+
<Box>
|
|
519
|
+
<Text variant="subtitle2" content="API Key" customColor="var(--theme-text-secondary)" />
|
|
520
|
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 1 }}>
|
|
521
|
+
<TextField
|
|
522
|
+
fullWidth
|
|
523
|
+
value={createdKey?.key || ''}
|
|
524
|
+
type={showCreatedKey ? 'text' : 'password'}
|
|
525
|
+
InputProps={{
|
|
526
|
+
readOnly: true,
|
|
527
|
+
sx: { fontFamily: 'monospace', fontSize: '0.9rem' },
|
|
528
|
+
}}
|
|
529
|
+
/>
|
|
530
|
+
<Tooltip title={showCreatedKey ? 'Hide' : 'Show'}>
|
|
531
|
+
<IconButton onClick={() => setShowCreatedKey(!showCreatedKey)}>
|
|
532
|
+
{showCreatedKey ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
|
533
|
+
</IconButton>
|
|
534
|
+
</Tooltip>
|
|
535
|
+
<Tooltip title={copiedKey ? 'Copied!' : 'Copy'}>
|
|
536
|
+
<IconButton onClick={() => copyToClipboard(createdKey?.key || '')}>
|
|
537
|
+
<ContentCopyIcon />
|
|
538
|
+
</IconButton>
|
|
539
|
+
</Tooltip>
|
|
540
|
+
</Box>
|
|
541
|
+
</Box>
|
|
542
|
+
|
|
543
|
+
<Box>
|
|
544
|
+
<Text variant="subtitle2" content="Scopes" customColor="var(--theme-text-secondary)" />
|
|
545
|
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 1 }}>
|
|
546
|
+
{createdKey?.scopes.map((scope) => (
|
|
547
|
+
<Chip
|
|
548
|
+
key={scope}
|
|
549
|
+
size="small"
|
|
550
|
+
label={scope}
|
|
551
|
+
sx={{
|
|
552
|
+
bgcolor: `${getScopeColor(scope)}20`,
|
|
553
|
+
color: getScopeColor(scope),
|
|
554
|
+
}}
|
|
555
|
+
/>
|
|
556
|
+
))}
|
|
557
|
+
</Box>
|
|
558
|
+
</Box>
|
|
559
|
+
</Box>
|
|
560
|
+
</DialogContent>
|
|
561
|
+
<DialogActions>
|
|
562
|
+
<Button variant="primary" label="I've Saved the Key" onClick={closeCreatedKeyDialog} />
|
|
563
|
+
</DialogActions>
|
|
564
|
+
</Dialog>
|
|
565
|
+
|
|
566
|
+
{/* Edit Key Dialog */}
|
|
567
|
+
<Dialog open={editDialogOpen} onClose={() => setEditDialogOpen(false)} maxWidth="sm" fullWidth>
|
|
568
|
+
<DialogTitle>Edit API Key</DialogTitle>
|
|
569
|
+
<DialogContent>
|
|
570
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
571
|
+
<TextField
|
|
572
|
+
label="Key Name"
|
|
573
|
+
fullWidth
|
|
574
|
+
value={editForm.name}
|
|
575
|
+
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
|
576
|
+
/>
|
|
577
|
+
|
|
578
|
+
<FormControl component="fieldset">
|
|
579
|
+
<Text variant="subtitle2" content="Scopes" customColor="var(--theme-text-primary)" />
|
|
580
|
+
<FormGroup>
|
|
581
|
+
<FormControlLabel
|
|
582
|
+
control={
|
|
583
|
+
<Checkbox
|
|
584
|
+
checked={editForm.scopes.includes('read')}
|
|
585
|
+
onChange={() => toggleEditScope('read')}
|
|
586
|
+
/>
|
|
587
|
+
}
|
|
588
|
+
label="Read - View data and resources"
|
|
589
|
+
/>
|
|
590
|
+
<FormControlLabel
|
|
591
|
+
control={
|
|
592
|
+
<Checkbox
|
|
593
|
+
checked={editForm.scopes.includes('write')}
|
|
594
|
+
onChange={() => toggleEditScope('write')}
|
|
595
|
+
/>
|
|
596
|
+
}
|
|
597
|
+
label="Write - Create and update resources"
|
|
598
|
+
/>
|
|
599
|
+
<FormControlLabel
|
|
600
|
+
control={
|
|
601
|
+
<Checkbox
|
|
602
|
+
checked={editForm.scopes.includes('admin')}
|
|
603
|
+
onChange={() => toggleEditScope('admin')}
|
|
604
|
+
/>
|
|
605
|
+
}
|
|
606
|
+
label="Admin - Full administrative access"
|
|
607
|
+
/>
|
|
608
|
+
</FormGroup>
|
|
609
|
+
</FormControl>
|
|
610
|
+
|
|
611
|
+
<FormControlLabel
|
|
612
|
+
control={
|
|
613
|
+
<Checkbox
|
|
614
|
+
checked={editForm.is_active}
|
|
615
|
+
onChange={(e) => setEditForm({ ...editForm, is_active: e.target.checked })}
|
|
616
|
+
/>
|
|
617
|
+
}
|
|
618
|
+
label="Active (key can be used for authentication)"
|
|
619
|
+
/>
|
|
620
|
+
</Box>
|
|
621
|
+
</DialogContent>
|
|
622
|
+
<DialogActions>
|
|
623
|
+
<Button variant="text" label="Cancel" onClick={() => setEditDialogOpen(false)} />
|
|
624
|
+
<Button
|
|
625
|
+
variant="primary"
|
|
626
|
+
label="Update Key"
|
|
627
|
+
onClick={handleUpdateKey}
|
|
628
|
+
disabled={!editForm.name || editForm.scopes.length === 0}
|
|
629
|
+
/>
|
|
630
|
+
</DialogActions>
|
|
631
|
+
</Dialog>
|
|
632
|
+
|
|
633
|
+
{/* Delete Confirmation Dialog */}
|
|
634
|
+
<Dialog open={deleteDialogOpen} onClose={cancelDelete} maxWidth="sm" fullWidth>
|
|
635
|
+
<DialogTitle>Delete API Key</DialogTitle>
|
|
636
|
+
<DialogContent>
|
|
637
|
+
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
638
|
+
This action cannot be undone. The API key will be permanently deleted.
|
|
639
|
+
</Alert>
|
|
640
|
+
<Text
|
|
641
|
+
variant="body1"
|
|
642
|
+
content={`Are you sure you want to delete the API key "${keyToDelete?.name}"?`}
|
|
643
|
+
customColor="var(--theme-text-primary)"
|
|
644
|
+
/>
|
|
645
|
+
</DialogContent>
|
|
646
|
+
<DialogActions>
|
|
647
|
+
<Button variant="text" label="Cancel" onClick={cancelDelete} />
|
|
648
|
+
<Button
|
|
649
|
+
variant="primary"
|
|
650
|
+
label="Delete"
|
|
651
|
+
onClick={confirmDelete}
|
|
652
|
+
sx={{
|
|
653
|
+
bgcolor: 'var(--theme-error)',
|
|
654
|
+
'&:hover': { bgcolor: 'var(--theme-error)' },
|
|
655
|
+
}}
|
|
656
|
+
/>
|
|
657
|
+
</DialogActions>
|
|
658
|
+
</Dialog>
|
|
659
|
+
</Box>
|
|
660
|
+
);
|
|
661
|
+
}
|