strapi-mcp-server 0.1.1
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/LICENSE +21 -0
- package/README.md +415 -0
- package/admin/src/components/PageHeader.tsx +33 -0
- package/admin/src/components/Sidebar.tsx +138 -0
- package/admin/src/index.tsx +54 -0
- package/admin/src/lib/api.ts +27 -0
- package/admin/src/lib/applyQuery.ts +152 -0
- package/admin/src/pages/App.tsx +126 -0
- package/admin/src/pages/AuditLog.tsx +386 -0
- package/admin/src/pages/Clients.tsx +465 -0
- package/admin/src/pages/EditClient.tsx +248 -0
- package/admin/src/pages/HomePage.tsx +378 -0
- package/admin/src/pages/NewClient.tsx +244 -0
- package/admin/src/pages/Settings.tsx +514 -0
- package/admin/src/pages/SsoBridge.tsx +96 -0
- package/admin/src/pages/Tools.tsx +68 -0
- package/admin/src/pluginId.ts +1 -0
- package/admin/src/translations/en.json +8 -0
- package/package.json +105 -0
- package/server/src/bootstrap.ts +118 -0
- package/server/src/config/index.ts +290 -0
- package/server/src/content-types/audit-log/index.ts +3 -0
- package/server/src/content-types/audit-log/schema.json +32 -0
- package/server/src/content-types/index.ts +19 -0
- package/server/src/content-types/oauth-auth-code/index.ts +3 -0
- package/server/src/content-types/oauth-auth-code/schema.json +31 -0
- package/server/src/content-types/oauth-client/index.ts +3 -0
- package/server/src/content-types/oauth-client/schema.json +33 -0
- package/server/src/content-types/oauth-consent/index.ts +3 -0
- package/server/src/content-types/oauth-consent/schema.json +21 -0
- package/server/src/content-types/oauth-refresh-token/index.ts +3 -0
- package/server/src/content-types/oauth-refresh-token/schema.json +25 -0
- package/server/src/content-types/oauth-revocation/index.ts +3 -0
- package/server/src/content-types/oauth-revocation/schema.json +18 -0
- package/server/src/content-types/oauth-signing-key/index.ts +3 -0
- package/server/src/content-types/oauth-signing-key/schema.json +21 -0
- package/server/src/controllers/admin/audit.ts +30 -0
- package/server/src/controllers/admin/clients.ts +148 -0
- package/server/src/controllers/admin/dashboard.ts +28 -0
- package/server/src/controllers/admin/index.ts +15 -0
- package/server/src/controllers/admin/settings.ts +38 -0
- package/server/src/controllers/admin/tools.ts +23 -0
- package/server/src/controllers/index.ts +13 -0
- package/server/src/controllers/mcp.ts +168 -0
- package/server/src/controllers/oauth/authorize.ts +418 -0
- package/server/src/controllers/oauth/index.ts +15 -0
- package/server/src/controllers/oauth/introspect.ts +45 -0
- package/server/src/controllers/oauth/metadata.ts +86 -0
- package/server/src/controllers/oauth/mode-guard.ts +22 -0
- package/server/src/controllers/oauth/register.ts +109 -0
- package/server/src/controllers/oauth/token.ts +206 -0
- package/server/src/controllers/proxy.ts +81 -0
- package/server/src/destroy.ts +28 -0
- package/server/src/index.ts +23 -0
- package/server/src/policies/authenticate.ts +81 -0
- package/server/src/policies/index.ts +13 -0
- package/server/src/policies/origin.ts +50 -0
- package/server/src/policies/rateLimit.ts +27 -0
- package/server/src/policies/scope.ts +32 -0
- package/server/src/register.ts +48 -0
- package/server/src/routes/admin.ts +85 -0
- package/server/src/routes/index.ts +13 -0
- package/server/src/routes/mcp.ts +31 -0
- package/server/src/routes/oauth.ts +81 -0
- package/server/src/routes/proxy.ts +29 -0
- package/server/src/services/audit.ts +158 -0
- package/server/src/services/heartbeat.ts +76 -0
- package/server/src/services/index.ts +37 -0
- package/server/src/services/instance-id.ts +30 -0
- package/server/src/services/mcp-server.ts +100 -0
- package/server/src/services/oauth/audience.ts +26 -0
- package/server/src/services/oauth/auth-codes.ts +78 -0
- package/server/src/services/oauth/clients.ts +386 -0
- package/server/src/services/oauth/consent.ts +38 -0
- package/server/src/services/oauth/errors.ts +32 -0
- package/server/src/services/oauth/pkce.ts +34 -0
- package/server/src/services/oauth/scopes.ts +42 -0
- package/server/src/services/oauth/signing-keys.ts +166 -0
- package/server/src/services/oauth/tokens.ts +324 -0
- package/server/src/services/permissions.ts +87 -0
- package/server/src/services/proxy-client.ts +167 -0
- package/server/src/services/rate-limiter.ts +180 -0
- package/server/src/services/redis.ts +139 -0
- package/server/src/services/session-directory.ts +121 -0
- package/server/src/services/session-store.ts +216 -0
- package/server/src/services/sso-cookie.ts +146 -0
- package/server/src/services/tools/content.ts +284 -0
- package/server/src/services/tools/index.ts +23 -0
- package/server/src/services/tools/media.ts +170 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Dialog,
|
|
7
|
+
Flex,
|
|
8
|
+
IconButton,
|
|
9
|
+
Modal,
|
|
10
|
+
Typography,
|
|
11
|
+
Table,
|
|
12
|
+
Thead,
|
|
13
|
+
Tbody,
|
|
14
|
+
Tr,
|
|
15
|
+
Td,
|
|
16
|
+
Th,
|
|
17
|
+
} from '@strapi/design-system';
|
|
18
|
+
import { Pencil, Plus, Trash } from '@strapi/icons';
|
|
19
|
+
import { Filters, Pagination, SearchInput, useQueryParams } from '@strapi/strapi/admin';
|
|
20
|
+
import styled from 'styled-components';
|
|
21
|
+
import { useMcpApi } from '../lib/api';
|
|
22
|
+
import { PageHeader } from '../components/PageHeader';
|
|
23
|
+
import { applyMcpQuery, hasActiveQuery, paginate, type McpListQuery } from '../lib/applyQuery';
|
|
24
|
+
|
|
25
|
+
const CLIENT_FILTERS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'type',
|
|
28
|
+
label: 'Type',
|
|
29
|
+
type: 'enumeration' as const,
|
|
30
|
+
options: [
|
|
31
|
+
{ label: 'Confidential', value: 'confidential' },
|
|
32
|
+
{ label: 'Public', value: 'public' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'createdAt',
|
|
37
|
+
label: 'Created',
|
|
38
|
+
type: 'date' as const,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
interface AdminUser {
|
|
43
|
+
id: number;
|
|
44
|
+
email?: string;
|
|
45
|
+
firstname?: string;
|
|
46
|
+
lastname?: string;
|
|
47
|
+
username?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ClientRow {
|
|
51
|
+
clientId: string;
|
|
52
|
+
clientName: string;
|
|
53
|
+
isConfidential: boolean;
|
|
54
|
+
redirectUris: string[];
|
|
55
|
+
scopes: string[];
|
|
56
|
+
disabled: boolean;
|
|
57
|
+
lastUsedAt?: string | null;
|
|
58
|
+
createdAt?: string | null;
|
|
59
|
+
ownerAdmin?: AdminUser | null;
|
|
60
|
+
createdByAdmin?: AdminUser | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatAdmin(user: AdminUser | null | undefined): string {
|
|
64
|
+
if (!user) return '—';
|
|
65
|
+
const name = [user.firstname, user.lastname].filter(Boolean).join(' ').trim();
|
|
66
|
+
return name || user.email || user.username || `#${user.id}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ClickableTr = styled(Tr)`
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: background-color 100ms ease;
|
|
72
|
+
&:hover td {
|
|
73
|
+
background: ${({ theme }) => theme.colors.neutral100};
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
function formatLastUsed(value: string | null | undefined): string {
|
|
78
|
+
if (!value) return 'Never';
|
|
79
|
+
const d = new Date(value);
|
|
80
|
+
if (Number.isNaN(d.getTime())) return 'Never';
|
|
81
|
+
return d.toLocaleString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatCreated(value: string | null | undefined): string {
|
|
85
|
+
if (!value) return '—';
|
|
86
|
+
const d = new Date(value);
|
|
87
|
+
if (Number.isNaN(d.getTime())) return '—';
|
|
88
|
+
return d.toLocaleString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function DetailRow({
|
|
92
|
+
label,
|
|
93
|
+
children,
|
|
94
|
+
}: {
|
|
95
|
+
label: string;
|
|
96
|
+
children: React.ReactNode;
|
|
97
|
+
}): JSX.Element {
|
|
98
|
+
return (
|
|
99
|
+
<Box paddingBottom={4}>
|
|
100
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
101
|
+
{label}
|
|
102
|
+
</Typography>
|
|
103
|
+
<Box paddingTop={1}>{children}</Box>
|
|
104
|
+
</Box>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function Clients(): JSX.Element {
|
|
109
|
+
const api = useMcpApi();
|
|
110
|
+
const navigate = useNavigate();
|
|
111
|
+
const [clients, setClients] = useState<ClientRow[]>([]);
|
|
112
|
+
const [error, setError] = useState<string | null>(null);
|
|
113
|
+
const [loading, setLoading] = useState(true);
|
|
114
|
+
const [selected, setSelected] = useState<ClientRow | null>(null);
|
|
115
|
+
const [pendingDelete, setPendingDelete] = useState<ClientRow | null>(null);
|
|
116
|
+
const [deleting, setDeleting] = useState(false);
|
|
117
|
+
const [{ query }] = useQueryParams<McpListQuery>();
|
|
118
|
+
|
|
119
|
+
const filtered = useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
applyMcpQuery(clients, query, {
|
|
122
|
+
searchText: (c) =>
|
|
123
|
+
[
|
|
124
|
+
c.clientName,
|
|
125
|
+
c.clientId,
|
|
126
|
+
formatAdmin(c.ownerAdmin),
|
|
127
|
+
formatAdmin(c.createdByAdmin),
|
|
128
|
+
].join(' '),
|
|
129
|
+
field: (c, name) => {
|
|
130
|
+
if (name === 'type') return c.isConfidential ? 'confidential' : 'public';
|
|
131
|
+
if (name === 'createdAt') return c.createdAt ?? '';
|
|
132
|
+
return '';
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
[clients, query]
|
|
136
|
+
);
|
|
137
|
+
const paged = useMemo(() => paginate(filtered, query), [filtered, query]);
|
|
138
|
+
|
|
139
|
+
const hasFilters = hasActiveQuery(query);
|
|
140
|
+
|
|
141
|
+
function refresh(): Promise<void> {
|
|
142
|
+
return api
|
|
143
|
+
.listClients()
|
|
144
|
+
.then((d: { clients: ClientRow[] }) => setClients(d.clients ?? []))
|
|
145
|
+
.catch((err: Error) => setError(err.message ?? String(err)))
|
|
146
|
+
.finally(() => setLoading(false));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
refresh();
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
async function confirmDelete(): Promise<void> {
|
|
154
|
+
if (!pendingDelete) return;
|
|
155
|
+
setDeleting(true);
|
|
156
|
+
try {
|
|
157
|
+
await api.deleteClient(pendingDelete.clientId);
|
|
158
|
+
setPendingDelete(null);
|
|
159
|
+
setSelected(null);
|
|
160
|
+
await refresh();
|
|
161
|
+
} catch (err) {
|
|
162
|
+
setError((err as Error).message);
|
|
163
|
+
} finally {
|
|
164
|
+
setDeleting(false);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Box>
|
|
170
|
+
<PageHeader
|
|
171
|
+
title="Clients"
|
|
172
|
+
subtitle="OAuth 2.1 clients allowed to obtain MCP access tokens"
|
|
173
|
+
actions={
|
|
174
|
+
<Button startIcon={<Plus />} onClick={() => navigate('new')}>
|
|
175
|
+
New client
|
|
176
|
+
</Button>
|
|
177
|
+
}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
{error && (
|
|
181
|
+
<Box background="danger100" padding={4} hasRadius marginBottom={6}>
|
|
182
|
+
<Typography textColor="danger700">Failed: {error}</Typography>
|
|
183
|
+
</Box>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
<Flex gap={1} paddingBottom={4} alignItems="flex-start">
|
|
187
|
+
<SearchInput
|
|
188
|
+
label="Search clients"
|
|
189
|
+
placeholder="Search by name, client ID, owner, or creator"
|
|
190
|
+
/>
|
|
191
|
+
<Filters.Root options={CLIENT_FILTERS}>
|
|
192
|
+
<Filters.Trigger />
|
|
193
|
+
<Filters.Popover />
|
|
194
|
+
<Filters.List />
|
|
195
|
+
</Filters.Root>
|
|
196
|
+
</Flex>
|
|
197
|
+
|
|
198
|
+
<Box background="neutral0" hasRadius shadow="tableShadow">
|
|
199
|
+
<Table colCount={6} rowCount={paged.rows.length}>
|
|
200
|
+
<Thead>
|
|
201
|
+
<Tr>
|
|
202
|
+
<Th>
|
|
203
|
+
<Typography variant="sigma">Name</Typography>
|
|
204
|
+
</Th>
|
|
205
|
+
<Th>
|
|
206
|
+
<Typography variant="sigma">Type</Typography>
|
|
207
|
+
</Th>
|
|
208
|
+
<Th>
|
|
209
|
+
<Typography variant="sigma">Created by</Typography>
|
|
210
|
+
</Th>
|
|
211
|
+
<Th>
|
|
212
|
+
<Typography variant="sigma">Owner</Typography>
|
|
213
|
+
</Th>
|
|
214
|
+
<Th>
|
|
215
|
+
<Typography variant="sigma">Created</Typography>
|
|
216
|
+
</Th>
|
|
217
|
+
<Th>
|
|
218
|
+
<Typography variant="sigma"> </Typography>
|
|
219
|
+
</Th>
|
|
220
|
+
</Tr>
|
|
221
|
+
</Thead>
|
|
222
|
+
<Tbody>
|
|
223
|
+
{paged.rows.map((c) => (
|
|
224
|
+
<ClickableTr
|
|
225
|
+
key={c.clientId}
|
|
226
|
+
onClick={() => setSelected(c)}
|
|
227
|
+
role="button"
|
|
228
|
+
tabIndex={0}
|
|
229
|
+
onKeyDown={(e: React.KeyboardEvent) => {
|
|
230
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
setSelected(c);
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<Td>
|
|
237
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
238
|
+
{c.clientName}
|
|
239
|
+
</Typography>
|
|
240
|
+
</Td>
|
|
241
|
+
<Td>
|
|
242
|
+
<Typography variant="omega">
|
|
243
|
+
{c.isConfidential ? 'Confidential' : 'Public'}
|
|
244
|
+
</Typography>
|
|
245
|
+
</Td>
|
|
246
|
+
<Td>
|
|
247
|
+
<Typography variant="omega" textColor="neutral700">
|
|
248
|
+
{formatAdmin(c.createdByAdmin)}
|
|
249
|
+
</Typography>
|
|
250
|
+
</Td>
|
|
251
|
+
<Td>
|
|
252
|
+
<Typography variant="omega" textColor="neutral700">
|
|
253
|
+
{formatAdmin(c.ownerAdmin)}
|
|
254
|
+
</Typography>
|
|
255
|
+
</Td>
|
|
256
|
+
<Td>
|
|
257
|
+
<Typography variant="omega" textColor="neutral700">
|
|
258
|
+
{formatCreated(c.createdAt)}
|
|
259
|
+
</Typography>
|
|
260
|
+
</Td>
|
|
261
|
+
<Td onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
|
262
|
+
<Flex gap={2} justifyContent="flex-end">
|
|
263
|
+
<IconButton
|
|
264
|
+
label="Edit"
|
|
265
|
+
variant="ghost"
|
|
266
|
+
onClick={(e: React.MouseEvent) => {
|
|
267
|
+
e.stopPropagation();
|
|
268
|
+
navigate(`${c.clientId}/edit`);
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
<Pencil />
|
|
272
|
+
</IconButton>
|
|
273
|
+
<IconButton
|
|
274
|
+
label="Delete"
|
|
275
|
+
variant="ghost"
|
|
276
|
+
onClick={(e: React.MouseEvent) => {
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
setPendingDelete(c);
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
<Trash />
|
|
282
|
+
</IconButton>
|
|
283
|
+
</Flex>
|
|
284
|
+
</Td>
|
|
285
|
+
</ClickableTr>
|
|
286
|
+
))}
|
|
287
|
+
{!loading && paged.rows.length === 0 && (
|
|
288
|
+
<Tr>
|
|
289
|
+
<Td colSpan={6}>
|
|
290
|
+
<Box paddingTop={6} paddingBottom={6}>
|
|
291
|
+
<Typography textColor="neutral600">
|
|
292
|
+
{clients.length === 0
|
|
293
|
+
? 'No OAuth clients yet. Create one, or wait for an MCP client to register via DCR.'
|
|
294
|
+
: hasFilters
|
|
295
|
+
? 'No clients match the current search or filters.'
|
|
296
|
+
: 'No clients to display.'}
|
|
297
|
+
</Typography>
|
|
298
|
+
</Box>
|
|
299
|
+
</Td>
|
|
300
|
+
</Tr>
|
|
301
|
+
)}
|
|
302
|
+
</Tbody>
|
|
303
|
+
</Table>
|
|
304
|
+
</Box>
|
|
305
|
+
|
|
306
|
+
{paged.total > 0 && (
|
|
307
|
+
<Box paddingTop={4}>
|
|
308
|
+
<Pagination.Root pageCount={paged.pageCount} total={paged.total}>
|
|
309
|
+
<Pagination.PageSize />
|
|
310
|
+
<Pagination.Links />
|
|
311
|
+
</Pagination.Root>
|
|
312
|
+
</Box>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{/* Detail modal */}
|
|
316
|
+
<Modal.Root
|
|
317
|
+
open={selected !== null}
|
|
318
|
+
onOpenChange={(open) => !open && setSelected(null)}
|
|
319
|
+
>
|
|
320
|
+
<Modal.Content>
|
|
321
|
+
<Modal.Header>
|
|
322
|
+
<Modal.Title>{selected?.clientName ?? 'Client'}</Modal.Title>
|
|
323
|
+
</Modal.Header>
|
|
324
|
+
<Modal.Body>
|
|
325
|
+
{selected && (
|
|
326
|
+
<Box paddingTop={2}>
|
|
327
|
+
<DetailRow label="Client ID">
|
|
328
|
+
<Typography variant="omega" textColor="neutral800">
|
|
329
|
+
<code>{selected.clientId}</code>
|
|
330
|
+
</Typography>
|
|
331
|
+
</DetailRow>
|
|
332
|
+
<DetailRow label="Type">
|
|
333
|
+
<Typography variant="omega">
|
|
334
|
+
{selected.isConfidential ? 'Confidential' : 'Public'}
|
|
335
|
+
</Typography>
|
|
336
|
+
</DetailRow>
|
|
337
|
+
<DetailRow label="Created by">
|
|
338
|
+
<Typography variant="omega" textColor="neutral800">
|
|
339
|
+
{formatAdmin(selected.createdByAdmin)}
|
|
340
|
+
{selected.createdByAdmin?.email && (
|
|
341
|
+
<Typography variant="pi" textColor="neutral600">
|
|
342
|
+
{' '}
|
|
343
|
+
({selected.createdByAdmin.email})
|
|
344
|
+
</Typography>
|
|
345
|
+
)}
|
|
346
|
+
</Typography>
|
|
347
|
+
</DetailRow>
|
|
348
|
+
<DetailRow label="Owner">
|
|
349
|
+
<Typography variant="omega" textColor="neutral800">
|
|
350
|
+
{formatAdmin(selected.ownerAdmin)}
|
|
351
|
+
{selected.ownerAdmin?.email && (
|
|
352
|
+
<Typography variant="pi" textColor="neutral600">
|
|
353
|
+
{' '}
|
|
354
|
+
({selected.ownerAdmin.email})
|
|
355
|
+
</Typography>
|
|
356
|
+
)}
|
|
357
|
+
</Typography>
|
|
358
|
+
</DetailRow>
|
|
359
|
+
<DetailRow label="Created">
|
|
360
|
+
<Typography variant="omega" textColor="neutral800">
|
|
361
|
+
{formatCreated(selected.createdAt)}
|
|
362
|
+
</Typography>
|
|
363
|
+
</DetailRow>
|
|
364
|
+
<DetailRow label="Last used">
|
|
365
|
+
<Typography variant="omega" textColor="neutral800">
|
|
366
|
+
{formatLastUsed(selected.lastUsedAt)}
|
|
367
|
+
</Typography>
|
|
368
|
+
</DetailRow>
|
|
369
|
+
<DetailRow label="Scopes">
|
|
370
|
+
{selected.scopes.length === 0 ? (
|
|
371
|
+
<Typography variant="omega" textColor="neutral600">
|
|
372
|
+
(none)
|
|
373
|
+
</Typography>
|
|
374
|
+
) : (
|
|
375
|
+
<Flex direction="column" gap={1} alignItems="flex-start">
|
|
376
|
+
{selected.scopes.map((s) => (
|
|
377
|
+
<Typography key={s} variant="omega">
|
|
378
|
+
<code>{s}</code>
|
|
379
|
+
</Typography>
|
|
380
|
+
))}
|
|
381
|
+
</Flex>
|
|
382
|
+
)}
|
|
383
|
+
</DetailRow>
|
|
384
|
+
<DetailRow label="Redirect URIs">
|
|
385
|
+
{selected.redirectUris.length === 0 ? (
|
|
386
|
+
<Typography variant="omega" textColor="neutral600">
|
|
387
|
+
(none)
|
|
388
|
+
</Typography>
|
|
389
|
+
) : (
|
|
390
|
+
<Flex direction="column" gap={1} alignItems="flex-start">
|
|
391
|
+
{selected.redirectUris.map((u) => (
|
|
392
|
+
<Typography key={u} variant="omega" textColor="neutral800">
|
|
393
|
+
<code>{u}</code>
|
|
394
|
+
</Typography>
|
|
395
|
+
))}
|
|
396
|
+
</Flex>
|
|
397
|
+
)}
|
|
398
|
+
</DetailRow>
|
|
399
|
+
</Box>
|
|
400
|
+
)}
|
|
401
|
+
</Modal.Body>
|
|
402
|
+
<Modal.Footer justifyContent="space-between">
|
|
403
|
+
<Modal.Close>
|
|
404
|
+
<Button variant="tertiary">Cancel</Button>
|
|
405
|
+
</Modal.Close>
|
|
406
|
+
<Flex gap={2}>
|
|
407
|
+
<Button
|
|
408
|
+
variant="danger-light"
|
|
409
|
+
startIcon={<Trash />}
|
|
410
|
+
onClick={() => {
|
|
411
|
+
if (selected) {
|
|
412
|
+
setPendingDelete(selected);
|
|
413
|
+
setSelected(null);
|
|
414
|
+
}
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
Delete
|
|
418
|
+
</Button>
|
|
419
|
+
<Button
|
|
420
|
+
variant="default"
|
|
421
|
+
startIcon={<Pencil />}
|
|
422
|
+
onClick={() => {
|
|
423
|
+
if (selected) {
|
|
424
|
+
const id = selected.clientId;
|
|
425
|
+
setSelected(null);
|
|
426
|
+
navigate(`${id}/edit`);
|
|
427
|
+
}
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
Edit
|
|
431
|
+
</Button>
|
|
432
|
+
</Flex>
|
|
433
|
+
</Modal.Footer>
|
|
434
|
+
</Modal.Content>
|
|
435
|
+
</Modal.Root>
|
|
436
|
+
|
|
437
|
+
{/* Delete confirmation */}
|
|
438
|
+
<Dialog.Root
|
|
439
|
+
open={pendingDelete !== null}
|
|
440
|
+
onOpenChange={(open) => !open && setPendingDelete(null)}
|
|
441
|
+
>
|
|
442
|
+
<Dialog.Content>
|
|
443
|
+
<Dialog.Header>Delete client</Dialog.Header>
|
|
444
|
+
<Dialog.Body>
|
|
445
|
+
<Typography>
|
|
446
|
+
Delete <strong>{pendingDelete?.clientName}</strong>? This revokes the client_id,
|
|
447
|
+
invalidates any active sessions tied to it, and removes its stored refresh
|
|
448
|
+
tokens. This cannot be undone.
|
|
449
|
+
</Typography>
|
|
450
|
+
</Dialog.Body>
|
|
451
|
+
<Dialog.Footer>
|
|
452
|
+
<Dialog.Cancel>
|
|
453
|
+
<Button variant="tertiary">Cancel</Button>
|
|
454
|
+
</Dialog.Cancel>
|
|
455
|
+
<Dialog.Action>
|
|
456
|
+
<Button variant="danger-light" loading={deleting} onClick={confirmDelete}>
|
|
457
|
+
Delete client
|
|
458
|
+
</Button>
|
|
459
|
+
</Dialog.Action>
|
|
460
|
+
</Dialog.Footer>
|
|
461
|
+
</Dialog.Content>
|
|
462
|
+
</Dialog.Root>
|
|
463
|
+
</Box>
|
|
464
|
+
);
|
|
465
|
+
}
|