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,248 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Flex,
|
|
7
|
+
Typography,
|
|
8
|
+
TextInput,
|
|
9
|
+
Textarea,
|
|
10
|
+
Checkbox,
|
|
11
|
+
Grid,
|
|
12
|
+
} from '@strapi/design-system';
|
|
13
|
+
import { ArrowLeft } from '@strapi/icons';
|
|
14
|
+
import { useMcpApi } from '../lib/api';
|
|
15
|
+
import { PageHeader } from '../components/PageHeader';
|
|
16
|
+
|
|
17
|
+
const ALL_SCOPES = [
|
|
18
|
+
{ id: 'strapi:content:read', label: 'Read content (list types, schemas, entries)' },
|
|
19
|
+
{ id: 'strapi:content:write', label: 'Create / update content entries (draft only)' },
|
|
20
|
+
{ id: 'strapi:media:read', label: 'List media files' },
|
|
21
|
+
{ id: 'strapi:media:write', label: 'Upload media files' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
interface Client {
|
|
25
|
+
clientId: string;
|
|
26
|
+
clientName: string;
|
|
27
|
+
isConfidential: boolean;
|
|
28
|
+
redirectUris: string[];
|
|
29
|
+
scopes: string[];
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function EditClient(): JSX.Element {
|
|
34
|
+
const api = useMcpApi();
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
const { clientId } = useParams<{ clientId: string }>();
|
|
37
|
+
|
|
38
|
+
const [original, setOriginal] = useState<Client | null>(null);
|
|
39
|
+
const [name, setName] = useState('');
|
|
40
|
+
const [redirects, setRedirects] = useState('');
|
|
41
|
+
const [scopes, setScopes] = useState<string[]>([]);
|
|
42
|
+
const [disabled, setDisabled] = useState(false);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
const [submitting, setSubmitting] = useState(false);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!clientId) return;
|
|
49
|
+
api
|
|
50
|
+
.getClient(clientId)
|
|
51
|
+
.then((c: Client) => {
|
|
52
|
+
setOriginal(c);
|
|
53
|
+
setName(c.clientName);
|
|
54
|
+
setRedirects((c.redirectUris ?? []).join('\n'));
|
|
55
|
+
setScopes(c.scopes ?? []);
|
|
56
|
+
setDisabled(c.disabled);
|
|
57
|
+
})
|
|
58
|
+
.catch((err: Error) => setError(err.message ?? String(err)))
|
|
59
|
+
.finally(() => setLoading(false));
|
|
60
|
+
}, [clientId]);
|
|
61
|
+
|
|
62
|
+
async function submit(): Promise<void> {
|
|
63
|
+
if (!clientId) return;
|
|
64
|
+
if (!name.trim() || !redirects.trim() || scopes.length === 0) {
|
|
65
|
+
setError('Name, at least one redirect URI, and at least one scope are required.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setSubmitting(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
try {
|
|
71
|
+
await api.updateClient(clientId, {
|
|
72
|
+
clientName: name.trim(),
|
|
73
|
+
redirectUris: redirects
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map((s) => s.trim())
|
|
76
|
+
.filter(Boolean),
|
|
77
|
+
scopes,
|
|
78
|
+
disabled,
|
|
79
|
+
});
|
|
80
|
+
navigate('/plugins/mcp-server/clients');
|
|
81
|
+
} catch (err) {
|
|
82
|
+
setError((err as Error).message);
|
|
83
|
+
} finally {
|
|
84
|
+
setSubmitting(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (loading && !original) {
|
|
89
|
+
return (
|
|
90
|
+
<Box>
|
|
91
|
+
<PageHeader title="Edit client" />
|
|
92
|
+
<Typography>Loading…</Typography>
|
|
93
|
+
</Box>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!original) {
|
|
98
|
+
return (
|
|
99
|
+
<Box>
|
|
100
|
+
<PageHeader title="Edit client" />
|
|
101
|
+
<Box background="danger100" padding={4} hasRadius>
|
|
102
|
+
<Typography textColor="danger700">
|
|
103
|
+
{error ?? 'Client not found'}
|
|
104
|
+
</Typography>
|
|
105
|
+
</Box>
|
|
106
|
+
</Box>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Box>
|
|
112
|
+
<PageHeader
|
|
113
|
+
title="Edit client"
|
|
114
|
+
subtitle={`Update settings for ${original.clientName}`}
|
|
115
|
+
actions={
|
|
116
|
+
<Button
|
|
117
|
+
variant="tertiary"
|
|
118
|
+
startIcon={<ArrowLeft />}
|
|
119
|
+
onClick={() => navigate('/plugins/mcp-server/clients')}
|
|
120
|
+
>
|
|
121
|
+
Back
|
|
122
|
+
</Button>
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
{error && (
|
|
127
|
+
<Box background="danger100" padding={4} hasRadius marginBottom={6}>
|
|
128
|
+
<Typography textColor="danger700">{error}</Typography>
|
|
129
|
+
</Box>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<Box background="neutral0" padding={6} hasRadius shadow="tableShadow">
|
|
133
|
+
<Flex direction="column" gap={6} alignItems="stretch">
|
|
134
|
+
<Grid.Root gap={6}>
|
|
135
|
+
<Grid.Item col={6} s={12} direction="column" alignItems="stretch">
|
|
136
|
+
<Box>
|
|
137
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
138
|
+
Client name
|
|
139
|
+
</Typography>
|
|
140
|
+
<Box paddingTop={1}>
|
|
141
|
+
<TextInput
|
|
142
|
+
name="clientName"
|
|
143
|
+
value={name}
|
|
144
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
145
|
+
setName(e.target.value)
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</Box>
|
|
149
|
+
</Box>
|
|
150
|
+
</Grid.Item>
|
|
151
|
+
<Grid.Item col={6} s={12} direction="column" alignItems="stretch">
|
|
152
|
+
<Box>
|
|
153
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
154
|
+
Client ID
|
|
155
|
+
</Typography>
|
|
156
|
+
<Box paddingTop={1}>
|
|
157
|
+
<TextInput name="clientId" value={original.clientId} disabled aria-readonly />
|
|
158
|
+
</Box>
|
|
159
|
+
</Box>
|
|
160
|
+
</Grid.Item>
|
|
161
|
+
<Grid.Item col={6} s={12} direction="column" alignItems="stretch">
|
|
162
|
+
<Box paddingTop={3}>
|
|
163
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
164
|
+
Type
|
|
165
|
+
</Typography>
|
|
166
|
+
<Box paddingTop={2}>
|
|
167
|
+
<Typography variant="omega" textColor="neutral700">
|
|
168
|
+
{original.isConfidential
|
|
169
|
+
? 'Confidential (cannot be changed after creation)'
|
|
170
|
+
: 'Public (cannot be changed after creation)'}
|
|
171
|
+
</Typography>
|
|
172
|
+
</Box>
|
|
173
|
+
</Box>
|
|
174
|
+
</Grid.Item>
|
|
175
|
+
<Grid.Item col={6} s={12} direction="column" alignItems="stretch">
|
|
176
|
+
<Box paddingTop={6}>
|
|
177
|
+
<Checkbox
|
|
178
|
+
checked={disabled}
|
|
179
|
+
onCheckedChange={(c: boolean | 'indeterminate') => setDisabled(c === true)}
|
|
180
|
+
>
|
|
181
|
+
Disabled — token issuance is blocked for this client
|
|
182
|
+
</Checkbox>
|
|
183
|
+
</Box>
|
|
184
|
+
</Grid.Item>
|
|
185
|
+
</Grid.Root>
|
|
186
|
+
|
|
187
|
+
<Box>
|
|
188
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
189
|
+
Redirect URIs
|
|
190
|
+
</Typography>
|
|
191
|
+
<Box paddingTop={2}>
|
|
192
|
+
<Textarea
|
|
193
|
+
name="redirectUris"
|
|
194
|
+
value={redirects}
|
|
195
|
+
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
196
|
+
setRedirects(e.target.value)
|
|
197
|
+
}
|
|
198
|
+
rows={5}
|
|
199
|
+
/>
|
|
200
|
+
</Box>
|
|
201
|
+
<Box paddingTop={1}>
|
|
202
|
+
<Typography variant="pi" textColor="neutral600">
|
|
203
|
+
One per line — exact match, no wildcards.
|
|
204
|
+
</Typography>
|
|
205
|
+
</Box>
|
|
206
|
+
</Box>
|
|
207
|
+
|
|
208
|
+
<Box>
|
|
209
|
+
<Typography variant="pi" fontWeight="bold" textColor="neutral800">
|
|
210
|
+
Scopes
|
|
211
|
+
</Typography>
|
|
212
|
+
<Box paddingTop={2}>
|
|
213
|
+
<Flex direction="column" gap={2} alignItems="stretch">
|
|
214
|
+
{ALL_SCOPES.map((s) => (
|
|
215
|
+
<Checkbox
|
|
216
|
+
key={s.id}
|
|
217
|
+
checked={scopes.includes(s.id)}
|
|
218
|
+
onCheckedChange={(c: boolean | 'indeterminate') => {
|
|
219
|
+
if (c === true) setScopes((prev) => [...prev, s.id]);
|
|
220
|
+
else setScopes((prev) => prev.filter((x) => x !== s.id));
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
<Typography variant="omega">
|
|
224
|
+
<code>{s.id}</code> — {s.label}
|
|
225
|
+
</Typography>
|
|
226
|
+
</Checkbox>
|
|
227
|
+
))}
|
|
228
|
+
</Flex>
|
|
229
|
+
</Box>
|
|
230
|
+
</Box>
|
|
231
|
+
|
|
232
|
+
<Flex justifyContent="flex-end" gap={2}>
|
|
233
|
+
<Button
|
|
234
|
+
variant="tertiary"
|
|
235
|
+
onClick={() => navigate('/plugins/mcp-server/clients')}
|
|
236
|
+
disabled={submitting}
|
|
237
|
+
>
|
|
238
|
+
Cancel
|
|
239
|
+
</Button>
|
|
240
|
+
<Button loading={submitting} onClick={submit}>
|
|
241
|
+
Save changes
|
|
242
|
+
</Button>
|
|
243
|
+
</Flex>
|
|
244
|
+
</Flex>
|
|
245
|
+
</Box>
|
|
246
|
+
</Box>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
Flex,
|
|
7
|
+
Grid,
|
|
8
|
+
IconButton,
|
|
9
|
+
Modal,
|
|
10
|
+
Table,
|
|
11
|
+
Tbody,
|
|
12
|
+
Td,
|
|
13
|
+
Th,
|
|
14
|
+
Thead,
|
|
15
|
+
Tr,
|
|
16
|
+
Typography,
|
|
17
|
+
} from '@strapi/design-system';
|
|
18
|
+
import { Eye } from '@strapi/icons';
|
|
19
|
+
import { useMcpApi } from '../lib/api';
|
|
20
|
+
import { PageHeader } from '../components/PageHeader';
|
|
21
|
+
|
|
22
|
+
interface PrincipalAdmin {
|
|
23
|
+
id: number;
|
|
24
|
+
email?: string;
|
|
25
|
+
firstname?: string;
|
|
26
|
+
lastname?: string;
|
|
27
|
+
username?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AuditClient {
|
|
31
|
+
clientId: string;
|
|
32
|
+
clientName: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RecentCall {
|
|
36
|
+
ts: string;
|
|
37
|
+
principalType?: string;
|
|
38
|
+
principalId: string;
|
|
39
|
+
principalAdmin?: PrincipalAdmin | null;
|
|
40
|
+
sessionId?: string | null;
|
|
41
|
+
clientId?: string | null;
|
|
42
|
+
client?: AuditClient | null;
|
|
43
|
+
tool: string;
|
|
44
|
+
params?: unknown;
|
|
45
|
+
resultStatus: 'ok' | 'error';
|
|
46
|
+
errorCode?: string | null;
|
|
47
|
+
durationMs?: number | null;
|
|
48
|
+
ip?: string | null;
|
|
49
|
+
userAgent?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Overview {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
resourceUrl: string;
|
|
55
|
+
allowedOrigins: string[];
|
|
56
|
+
sessions: { total: number; byPrincipal: Record<string, number> };
|
|
57
|
+
recentCalls: RecentCall[];
|
|
58
|
+
oauth: { mode: string; dcrEnabled: boolean };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatPrincipal(admin: PrincipalAdmin | null | undefined, fallbackId: string): string {
|
|
62
|
+
if (!admin) return fallbackId ? `#${fallbackId}` : '—';
|
|
63
|
+
const name = [admin.firstname, admin.lastname].filter(Boolean).join(' ').trim();
|
|
64
|
+
return name || admin.email || admin.username || `#${admin.id}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Card({ title, children }: { title: string; children: React.ReactNode }): JSX.Element {
|
|
68
|
+
return (
|
|
69
|
+
<Box background="neutral0" padding={6} hasRadius shadow="tableShadow">
|
|
70
|
+
<Box paddingBottom={4}>
|
|
71
|
+
<Typography variant="delta" tag="h2">
|
|
72
|
+
{title}
|
|
73
|
+
</Typography>
|
|
74
|
+
</Box>
|
|
75
|
+
{children}
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function StatRow({ label, value }: { label: string; value: React.ReactNode }): JSX.Element {
|
|
81
|
+
return (
|
|
82
|
+
<Box>
|
|
83
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
84
|
+
{label}
|
|
85
|
+
</Typography>
|
|
86
|
+
<Box paddingTop={1}>
|
|
87
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
88
|
+
{value}
|
|
89
|
+
</Typography>
|
|
90
|
+
</Box>
|
|
91
|
+
</Box>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function DetailRow({
|
|
96
|
+
label,
|
|
97
|
+
children,
|
|
98
|
+
}: {
|
|
99
|
+
label: string;
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
}): JSX.Element {
|
|
102
|
+
return (
|
|
103
|
+
<Box paddingBottom={4}>
|
|
104
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
105
|
+
{label}
|
|
106
|
+
</Typography>
|
|
107
|
+
<Box paddingTop={1}>{children}</Box>
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function HomePage(): JSX.Element {
|
|
113
|
+
const api = useMcpApi();
|
|
114
|
+
const [data, setData] = useState<Overview | null>(null);
|
|
115
|
+
const [error, setError] = useState<string | null>(null);
|
|
116
|
+
const [selected, setSelected] = useState<RecentCall | null>(null);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
api
|
|
120
|
+
.overview()
|
|
121
|
+
.then(setData)
|
|
122
|
+
.catch((err: Error) => setError(err.message ?? String(err)));
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Box>
|
|
127
|
+
<PageHeader title="Overview" subtitle="Server status, sessions, and recent activity" />
|
|
128
|
+
|
|
129
|
+
{error && (
|
|
130
|
+
<Box background="danger100" padding={4} hasRadius marginBottom={6}>
|
|
131
|
+
<Typography textColor="danger700">Failed to load overview: {error}</Typography>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{!data && !error && <Typography>Loading…</Typography>}
|
|
136
|
+
|
|
137
|
+
{data && (
|
|
138
|
+
<Flex direction="column" gap={6} alignItems="stretch">
|
|
139
|
+
<Card title="Status">
|
|
140
|
+
<Grid.Root gap={6}>
|
|
141
|
+
<Grid.Item col={3} s={6} xs={12} direction="column" alignItems="flex-start">
|
|
142
|
+
<StatRow
|
|
143
|
+
label="State"
|
|
144
|
+
value={
|
|
145
|
+
<Badge backgroundColor={data.enabled ? 'success100' : 'danger100'}>
|
|
146
|
+
{data.enabled ? 'Enabled' : 'Disabled'}
|
|
147
|
+
</Badge>
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
</Grid.Item>
|
|
151
|
+
<Grid.Item col={5} s={6} xs={12} direction="column" alignItems="flex-start">
|
|
152
|
+
<StatRow
|
|
153
|
+
label="Resource URL"
|
|
154
|
+
value={data.resourceUrl || '(not configured)'}
|
|
155
|
+
/>
|
|
156
|
+
</Grid.Item>
|
|
157
|
+
<Grid.Item col={2} s={6} xs={12} direction="column" alignItems="flex-start">
|
|
158
|
+
<StatRow label="OAuth mode" value={data.oauth.mode} />
|
|
159
|
+
</Grid.Item>
|
|
160
|
+
<Grid.Item col={2} s={6} xs={12} direction="column" alignItems="flex-start">
|
|
161
|
+
<StatRow label="DCR" value={data.oauth.dcrEnabled ? 'enabled' : 'disabled'} />
|
|
162
|
+
</Grid.Item>
|
|
163
|
+
</Grid.Root>
|
|
164
|
+
</Card>
|
|
165
|
+
|
|
166
|
+
<Card title="Sessions">
|
|
167
|
+
<Grid.Root gap={6}>
|
|
168
|
+
<Grid.Item col={3} s={6} xs={12} direction="column" alignItems="flex-start">
|
|
169
|
+
<StatRow label="Active total" value={String(data.sessions.total)} />
|
|
170
|
+
</Grid.Item>
|
|
171
|
+
</Grid.Root>
|
|
172
|
+
</Card>
|
|
173
|
+
|
|
174
|
+
<Card title="Recent tool calls">
|
|
175
|
+
<Table colCount={7} rowCount={data.recentCalls.length}>
|
|
176
|
+
<Thead>
|
|
177
|
+
<Tr>
|
|
178
|
+
<Th>
|
|
179
|
+
<Typography variant="sigma">Time</Typography>
|
|
180
|
+
</Th>
|
|
181
|
+
<Th>
|
|
182
|
+
<Typography variant="sigma">Principal</Typography>
|
|
183
|
+
</Th>
|
|
184
|
+
<Th>
|
|
185
|
+
<Typography variant="sigma">Client</Typography>
|
|
186
|
+
</Th>
|
|
187
|
+
<Th>
|
|
188
|
+
<Typography variant="sigma">Tool</Typography>
|
|
189
|
+
</Th>
|
|
190
|
+
<Th>
|
|
191
|
+
<Typography variant="sigma">Status</Typography>
|
|
192
|
+
</Th>
|
|
193
|
+
<Th>
|
|
194
|
+
<Typography variant="sigma">Duration</Typography>
|
|
195
|
+
</Th>
|
|
196
|
+
<Th>
|
|
197
|
+
<Typography variant="sigma"> </Typography>
|
|
198
|
+
</Th>
|
|
199
|
+
</Tr>
|
|
200
|
+
</Thead>
|
|
201
|
+
<Tbody>
|
|
202
|
+
{data.recentCalls.map((c, i) => (
|
|
203
|
+
<Tr key={i}>
|
|
204
|
+
<Td>
|
|
205
|
+
<Typography variant="omega" textColor="neutral700">
|
|
206
|
+
{new Date(c.ts).toLocaleString()}
|
|
207
|
+
</Typography>
|
|
208
|
+
</Td>
|
|
209
|
+
<Td>
|
|
210
|
+
<Typography variant="omega">
|
|
211
|
+
{formatPrincipal(c.principalAdmin, c.principalId)}
|
|
212
|
+
</Typography>
|
|
213
|
+
</Td>
|
|
214
|
+
<Td>
|
|
215
|
+
<Typography variant="omega" textColor="neutral700">
|
|
216
|
+
{c.client?.clientName ?? '—'}
|
|
217
|
+
</Typography>
|
|
218
|
+
</Td>
|
|
219
|
+
<Td>
|
|
220
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
221
|
+
{c.tool}
|
|
222
|
+
</Typography>
|
|
223
|
+
</Td>
|
|
224
|
+
<Td>
|
|
225
|
+
<Badge
|
|
226
|
+
backgroundColor={c.resultStatus === 'ok' ? 'success100' : 'danger100'}
|
|
227
|
+
>
|
|
228
|
+
{c.resultStatus}
|
|
229
|
+
</Badge>
|
|
230
|
+
</Td>
|
|
231
|
+
<Td>
|
|
232
|
+
<Typography variant="omega" textColor="neutral700">
|
|
233
|
+
{c.durationMs !== undefined && c.durationMs !== null
|
|
234
|
+
? `${c.durationMs}ms`
|
|
235
|
+
: ''}
|
|
236
|
+
</Typography>
|
|
237
|
+
</Td>
|
|
238
|
+
<Td>
|
|
239
|
+
<Flex justifyContent="flex-end">
|
|
240
|
+
<IconButton
|
|
241
|
+
label="View details"
|
|
242
|
+
variant="ghost"
|
|
243
|
+
onClick={() => setSelected(c)}
|
|
244
|
+
>
|
|
245
|
+
<Eye />
|
|
246
|
+
</IconButton>
|
|
247
|
+
</Flex>
|
|
248
|
+
</Td>
|
|
249
|
+
</Tr>
|
|
250
|
+
))}
|
|
251
|
+
{data.recentCalls.length === 0 && (
|
|
252
|
+
<Tr>
|
|
253
|
+
<Td colSpan={7}>
|
|
254
|
+
<Box paddingTop={6} paddingBottom={6}>
|
|
255
|
+
<Typography textColor="neutral600">No activity yet.</Typography>
|
|
256
|
+
</Box>
|
|
257
|
+
</Td>
|
|
258
|
+
</Tr>
|
|
259
|
+
)}
|
|
260
|
+
</Tbody>
|
|
261
|
+
</Table>
|
|
262
|
+
</Card>
|
|
263
|
+
</Flex>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
<Modal.Root
|
|
267
|
+
open={selected !== null}
|
|
268
|
+
onOpenChange={(open) => !open && setSelected(null)}
|
|
269
|
+
>
|
|
270
|
+
<Modal.Content>
|
|
271
|
+
<Modal.Header>
|
|
272
|
+
<Modal.Title>Audit entry</Modal.Title>
|
|
273
|
+
</Modal.Header>
|
|
274
|
+
<Modal.Body>
|
|
275
|
+
{selected && (
|
|
276
|
+
<Box paddingTop={2}>
|
|
277
|
+
<DetailRow label="Time">
|
|
278
|
+
<Typography variant="omega">
|
|
279
|
+
{new Date(selected.ts).toLocaleString()}
|
|
280
|
+
</Typography>
|
|
281
|
+
</DetailRow>
|
|
282
|
+
<DetailRow label="Tool">
|
|
283
|
+
<Typography variant="omega" fontWeight="semiBold">
|
|
284
|
+
{selected.tool}
|
|
285
|
+
</Typography>
|
|
286
|
+
</DetailRow>
|
|
287
|
+
<DetailRow label="Status">
|
|
288
|
+
<Badge
|
|
289
|
+
backgroundColor={
|
|
290
|
+
selected.resultStatus === 'ok' ? 'success100' : 'danger100'
|
|
291
|
+
}
|
|
292
|
+
>
|
|
293
|
+
{selected.resultStatus}
|
|
294
|
+
</Badge>
|
|
295
|
+
{selected.errorCode && (
|
|
296
|
+
<Box paddingTop={2}>
|
|
297
|
+
<Typography variant="omega" textColor="danger700">
|
|
298
|
+
Error: <code>{selected.errorCode}</code>
|
|
299
|
+
</Typography>
|
|
300
|
+
</Box>
|
|
301
|
+
)}
|
|
302
|
+
</DetailRow>
|
|
303
|
+
<DetailRow label="Duration">
|
|
304
|
+
<Typography variant="omega">
|
|
305
|
+
{selected.durationMs !== undefined && selected.durationMs !== null
|
|
306
|
+
? `${selected.durationMs}ms`
|
|
307
|
+
: '—'}
|
|
308
|
+
</Typography>
|
|
309
|
+
</DetailRow>
|
|
310
|
+
<DetailRow label="Principal">
|
|
311
|
+
<Typography variant="omega">
|
|
312
|
+
{formatPrincipal(selected.principalAdmin, selected.principalId)}
|
|
313
|
+
{selected.principalAdmin?.email && (
|
|
314
|
+
<Typography variant="pi" textColor="neutral600">
|
|
315
|
+
{' '}
|
|
316
|
+
({selected.principalAdmin.email})
|
|
317
|
+
</Typography>
|
|
318
|
+
)}
|
|
319
|
+
</Typography>
|
|
320
|
+
</DetailRow>
|
|
321
|
+
<DetailRow label="Client">
|
|
322
|
+
<Typography variant="omega">
|
|
323
|
+
{selected.client?.clientName ?? '—'}
|
|
324
|
+
{selected.clientId && (
|
|
325
|
+
<Typography variant="pi" textColor="neutral600">
|
|
326
|
+
{' '}
|
|
327
|
+
(<code>{selected.clientId}</code>)
|
|
328
|
+
</Typography>
|
|
329
|
+
)}
|
|
330
|
+
</Typography>
|
|
331
|
+
</DetailRow>
|
|
332
|
+
<DetailRow label="Session">
|
|
333
|
+
<Typography variant="omega" textColor="neutral700">
|
|
334
|
+
<code>{selected.sessionId ?? '—'}</code>
|
|
335
|
+
</Typography>
|
|
336
|
+
</DetailRow>
|
|
337
|
+
<DetailRow label="IP">
|
|
338
|
+
<Typography variant="omega" textColor="neutral700">
|
|
339
|
+
{selected.ip ?? '—'}
|
|
340
|
+
</Typography>
|
|
341
|
+
</DetailRow>
|
|
342
|
+
{selected.userAgent && (
|
|
343
|
+
<DetailRow label="User-Agent">
|
|
344
|
+
<Typography variant="omega" textColor="neutral700">
|
|
345
|
+
{selected.userAgent}
|
|
346
|
+
</Typography>
|
|
347
|
+
</DetailRow>
|
|
348
|
+
)}
|
|
349
|
+
<DetailRow label="Parameters">
|
|
350
|
+
<Box background="neutral100" padding={3} hasRadius>
|
|
351
|
+
<pre
|
|
352
|
+
style={{
|
|
353
|
+
margin: 0,
|
|
354
|
+
whiteSpace: 'pre-wrap',
|
|
355
|
+
wordBreak: 'break-all',
|
|
356
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
357
|
+
fontSize: 13,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
{selected.params === undefined || selected.params === null
|
|
361
|
+
? '(none)'
|
|
362
|
+
: JSON.stringify(selected.params, null, 2)}
|
|
363
|
+
</pre>
|
|
364
|
+
</Box>
|
|
365
|
+
</DetailRow>
|
|
366
|
+
</Box>
|
|
367
|
+
)}
|
|
368
|
+
</Modal.Body>
|
|
369
|
+
<Modal.Footer>
|
|
370
|
+
<Modal.Close>
|
|
371
|
+
<Button variant="tertiary">Close</Button>
|
|
372
|
+
</Modal.Close>
|
|
373
|
+
</Modal.Footer>
|
|
374
|
+
</Modal.Content>
|
|
375
|
+
</Modal.Root>
|
|
376
|
+
</Box>
|
|
377
|
+
);
|
|
378
|
+
}
|