@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +41 -0
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/guards.d.ts.map +1 -1
  6. package/dist/core/guards.js +77 -0
  7. package/dist/core/guards.js.map +1 -1
  8. package/dist/core/health-manager.d.ts +4 -0
  9. package/dist/core/health-manager.d.ts.map +1 -1
  10. package/dist/core/health-manager.js +6 -1
  11. package/dist/core/health-manager.js.map +1 -1
  12. package/dist/core/plugin-registry.d.ts +55 -5
  13. package/dist/core/plugin-registry.d.ts.map +1 -1
  14. package/dist/core/plugin-registry.js +57 -19
  15. package/dist/core/plugin-registry.js.map +1 -1
  16. package/dist/core/types.d.ts +2 -0
  17. package/dist/core/types.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/plugins/api-keys/api-keys-plugin.d.ts +46 -0
  23. package/dist/plugins/api-keys/api-keys-plugin.d.ts.map +1 -0
  24. package/dist/plugins/api-keys/api-keys-plugin.js +329 -0
  25. package/dist/plugins/api-keys/api-keys-plugin.js.map +1 -0
  26. package/dist/plugins/api-keys/index.d.ts +14 -0
  27. package/dist/plugins/api-keys/index.d.ts.map +1 -0
  28. package/dist/plugins/api-keys/index.js +17 -0
  29. package/dist/plugins/api-keys/index.js.map +1 -0
  30. package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts +74 -0
  31. package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts.map +1 -0
  32. package/dist/plugins/api-keys/middleware/bearer-token-auth.js +201 -0
  33. package/dist/plugins/api-keys/middleware/bearer-token-auth.js.map +1 -0
  34. package/dist/plugins/api-keys/middleware/index.d.ts +7 -0
  35. package/dist/plugins/api-keys/middleware/index.d.ts.map +1 -0
  36. package/dist/plugins/api-keys/middleware/index.js +7 -0
  37. package/dist/plugins/api-keys/middleware/index.js.map +1 -0
  38. package/dist/plugins/api-keys/stores/index.d.ts +7 -0
  39. package/dist/plugins/api-keys/stores/index.d.ts.map +1 -0
  40. package/dist/plugins/api-keys/stores/index.js +7 -0
  41. package/dist/plugins/api-keys/stores/index.js.map +1 -0
  42. package/dist/plugins/api-keys/stores/postgres-store.d.ts +34 -0
  43. package/dist/plugins/api-keys/stores/postgres-store.d.ts.map +1 -0
  44. package/dist/plugins/api-keys/stores/postgres-store.js +360 -0
  45. package/dist/plugins/api-keys/stores/postgres-store.js.map +1 -0
  46. package/dist/plugins/api-keys/types.d.ts +268 -0
  47. package/dist/plugins/api-keys/types.d.ts.map +1 -0
  48. package/dist/plugins/api-keys/types.js +56 -0
  49. package/dist/plugins/api-keys/types.js.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -1
  51. package/dist/plugins/auth/auth-plugin.js +17 -1
  52. package/dist/plugins/auth/auth-plugin.js.map +1 -1
  53. package/dist/plugins/auth/auth-plugin.test.js +133 -0
  54. package/dist/plugins/auth/auth-plugin.test.js.map +1 -1
  55. package/dist/plugins/auth/env-config.d.ts.map +1 -1
  56. package/dist/plugins/auth/env-config.js +6 -2
  57. package/dist/plugins/auth/env-config.js.map +1 -1
  58. package/dist/plugins/auth/types.d.ts +10 -0
  59. package/dist/plugins/auth/types.d.ts.map +1 -1
  60. package/dist/plugins/auth/types.js.map +1 -1
  61. package/dist/plugins/devices/__tests__/token-utils.test.js +4 -2
  62. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -1
  63. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  64. package/dist/plugins/frontend-app-plugin.js +21 -4
  65. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  66. package/dist/plugins/index.d.ts +2 -0
  67. package/dist/plugins/index.d.ts.map +1 -1
  68. package/dist/plugins/index.js +2 -0
  69. package/dist/plugins/index.js.map +1 -1
  70. package/dist/plugins/qwickbrain/index.d.ts +25 -0
  71. package/dist/plugins/qwickbrain/index.d.ts.map +1 -0
  72. package/dist/plugins/qwickbrain/index.js +24 -0
  73. package/dist/plugins/qwickbrain/index.js.map +1 -0
  74. package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts +23 -0
  75. package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -0
  76. package/dist/plugins/qwickbrain/qwickbrain-plugin.js +528 -0
  77. package/dist/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -0
  78. package/dist/plugins/qwickbrain/types.d.ts +131 -0
  79. package/dist/plugins/qwickbrain/types.d.ts.map +1 -0
  80. package/dist/plugins/qwickbrain/types.js +9 -0
  81. package/dist/plugins/qwickbrain/types.js.map +1 -0
  82. package/dist/plugins/users/__tests__/postgres-store.test.js +1 -0
  83. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -1
  84. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  85. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  86. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  87. package/dist/plugins/users/stores/postgres-store.js +59 -1
  88. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  89. package/dist/plugins/users/types.d.ts +22 -0
  90. package/dist/plugins/users/types.d.ts.map +1 -1
  91. package/dist-ui/assets/index-5nX8fM1a.js +469 -0
  92. package/dist-ui/assets/index-5nX8fM1a.js.map +1 -0
  93. package/dist-ui/index.html +1 -1
  94. package/dist-ui-lib/api/controlPanelApi.d.ts +68 -0
  95. package/dist-ui-lib/components/index.d.ts +2 -1
  96. package/dist-ui-lib/index.js +2642 -2281
  97. package/dist-ui-lib/index.js.map +1 -1
  98. package/dist-ui-lib/pages/APIKeysPage.d.ts +13 -0
  99. package/dist-ui-lib/pages/AcceptInvitationPage.d.ts +28 -0
  100. package/package.json +3 -2
  101. package/src/core/control-panel.ts +47 -0
  102. package/src/core/guards.ts +89 -0
  103. package/src/core/health-manager.ts +6 -1
  104. package/src/core/plugin-registry.ts +123 -25
  105. package/src/core/types.ts +2 -0
  106. package/src/index.ts +11 -0
  107. package/src/plugins/api-keys/api-keys-plugin.ts +397 -0
  108. package/src/plugins/api-keys/index.ts +49 -0
  109. package/src/plugins/api-keys/middleware/bearer-token-auth.ts +250 -0
  110. package/src/plugins/api-keys/middleware/index.ts +12 -0
  111. package/src/plugins/api-keys/stores/index.ts +7 -0
  112. package/src/plugins/api-keys/stores/postgres-store.ts +487 -0
  113. package/src/plugins/api-keys/types.ts +243 -0
  114. package/src/plugins/auth/auth-plugin.test.ts +167 -0
  115. package/src/plugins/auth/auth-plugin.ts +17 -1
  116. package/src/plugins/auth/env-config.ts +6 -2
  117. package/src/plugins/auth/types.ts +10 -0
  118. package/src/plugins/devices/__tests__/token-utils.test.ts +4 -2
  119. package/src/plugins/frontend-app-plugin.ts +24 -4
  120. package/src/plugins/index.ts +15 -0
  121. package/src/plugins/qwickbrain/index.ts +33 -0
  122. package/src/plugins/qwickbrain/qwickbrain-plugin.ts +642 -0
  123. package/src/plugins/qwickbrain/types.ts +146 -0
  124. package/src/plugins/users/__tests__/postgres-store.test.ts +1 -0
  125. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  126. package/src/plugins/users/stores/postgres-store.ts +69 -0
  127. package/src/plugins/users/types.ts +25 -0
  128. package/ui/src/App.tsx +6 -1
  129. package/ui/src/api/controlPanelApi.ts +206 -37
  130. package/ui/src/components/index.ts +6 -0
  131. package/ui/src/pages/APIKeysPage.tsx +661 -0
  132. package/ui/src/pages/AcceptInvitationPage.tsx +169 -0
  133. package/ui/src/pages/UsersPage.tsx +225 -2
  134. package/dist-ui/assets/index-CynOqPkb.js +0 -469
  135. 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
+ }