@rebasepro/studio 0.17.2 → 0.17.3-canary.g785baa2
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/dist/ApiKeysView-DiCurTEU.js +1224 -0
- package/dist/ApiKeysView-DiCurTEU.js.map +1 -0
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +6 -0
- package/dist/components/ApiKeys/permissions.d.ts +66 -0
- package/dist/index.es.js +1 -1
- package/package.json +9 -9
- package/src/components/ApiKeys/ApiKeysView.tsx +69 -211
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +711 -0
- package/src/components/ApiKeys/permissions.ts +145 -0
- package/dist/ApiKeysView-C_UoUPuR.js +0 -728
- package/dist/ApiKeysView-C_UoUPuR.js.map +0 -1
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
import { useApiBase, useApiConfig, useRebaseClient, useSnackbarController, useStudioCollectionRegistry } from "@rebasepro/app";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { AlertCircleIcon, AlertTriangleIcon, BooleanSwitchWithLabel, Button, CheckCircleIcon, ChevronsUpDownIcon, Chip, CircularProgress, CopyIcon, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, FolderIcon, FunctionSquareIcon, GlobeIcon, IconButton, KeyRoundIcon, PlusIcon, RefreshCwIcon, Select, SelectGroup, SelectItem, ShieldIcon, TextField, Tooltip, Trash2Icon, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
/** Grants the storage routes. */
|
|
6
|
+
var RESOURCE_STORAGE = "storage";
|
|
7
|
+
/** Grants every custom function. */
|
|
8
|
+
var RESOURCE_ALL_FUNCTIONS = "functions";
|
|
9
|
+
/** Prefix addressing a single custom function. */
|
|
10
|
+
var FUNCTION_PREFIX = "functions/";
|
|
11
|
+
/** Classify a raw `collection` field into the namespace it addresses. */
|
|
12
|
+
function parseResource(collection) {
|
|
13
|
+
const value = collection.trim();
|
|
14
|
+
if (value === "*") return {
|
|
15
|
+
kind: "everything",
|
|
16
|
+
name: ""
|
|
17
|
+
};
|
|
18
|
+
if (value === "storage") return {
|
|
19
|
+
kind: "storage",
|
|
20
|
+
name: ""
|
|
21
|
+
};
|
|
22
|
+
if (value === "functions") return {
|
|
23
|
+
kind: "all-functions",
|
|
24
|
+
name: ""
|
|
25
|
+
};
|
|
26
|
+
if (value.startsWith("functions/")) return {
|
|
27
|
+
kind: "function",
|
|
28
|
+
name: value.slice(10)
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
kind: "collection",
|
|
32
|
+
name: value
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Short label for a resource — what a picker or a chip shows.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately not the raw value: `"*"` alone is the thing nobody could read.
|
|
39
|
+
*/
|
|
40
|
+
function resourceLabel(collection) {
|
|
41
|
+
const { kind, name } = parseResource(collection);
|
|
42
|
+
switch (kind) {
|
|
43
|
+
case "everything": return "Everything";
|
|
44
|
+
case "storage": return "Storage";
|
|
45
|
+
case "all-functions": return "All functions";
|
|
46
|
+
case "function": return name ? `${name}()` : "Function";
|
|
47
|
+
case "collection": return name || "—";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The resource as a sentence fragment, for "this key can read <fragment>".
|
|
52
|
+
*
|
|
53
|
+
* `"everything"` spells out all three namespaces, because that is exactly the
|
|
54
|
+
* fact the old `*` input hid.
|
|
55
|
+
*/
|
|
56
|
+
function resourcePhrase(collection) {
|
|
57
|
+
const { kind, name } = parseResource(collection);
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case "everything": return "every collection, every custom function and storage";
|
|
60
|
+
case "storage": return "storage";
|
|
61
|
+
case "all-functions": return "every custom function";
|
|
62
|
+
case "function": return name ? `the ${name} function` : "one function";
|
|
63
|
+
case "collection": return name ? `the ${name} collection` : "an unnamed collection";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** How each operation reads for a given namespace, so the sentence stays true. */
|
|
67
|
+
function operationPhrase(kind, operation) {
|
|
68
|
+
if (kind === "all-functions" || kind === "function") return operation === "read" ? "call (GET)" : operation === "write" ? "call (POST, PUT, PATCH)" : "call (DELETE)";
|
|
69
|
+
if (kind === "storage") return operation === "read" ? "download from" : operation === "write" ? "upload to" : "delete from";
|
|
70
|
+
return operation;
|
|
71
|
+
}
|
|
72
|
+
/** Join with an Oxford-less "and", the way the rest of the panel reads. */
|
|
73
|
+
function joinPhrases(parts) {
|
|
74
|
+
if (parts.length <= 1) return parts[0] ?? "";
|
|
75
|
+
return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* One plain sentence per permission entry: what the key will actually be able
|
|
79
|
+
* to do. An entry with no operations selected grants nothing and says so,
|
|
80
|
+
* rather than being silently dropped at submit time.
|
|
81
|
+
*/
|
|
82
|
+
function grantSentence(perm) {
|
|
83
|
+
const { kind } = parseResource(perm.collection);
|
|
84
|
+
const target = resourcePhrase(perm.collection);
|
|
85
|
+
if (perm.operations.length === 0) return `No access to ${target}`;
|
|
86
|
+
const verbs = joinPhrases(perm.operations.map((op) => operationPhrase(kind, op)));
|
|
87
|
+
return `${verbs.charAt(0).toUpperCase() + verbs.slice(1)} ${target}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Dense one-line summary for list rows and the created-key confirmation.
|
|
91
|
+
*
|
|
92
|
+
* The wildcard wins over everything else in the array, because the guard
|
|
93
|
+
* returns on the first match — a key holding `*` is a full-access key no
|
|
94
|
+
* matter what else is listed beside it.
|
|
95
|
+
*/
|
|
96
|
+
function permissionSummary(perms) {
|
|
97
|
+
if (perms.length === 0) return "No permissions";
|
|
98
|
+
const wildcard = perms.find((p) => p.collection === "*");
|
|
99
|
+
if (wildcard) return `Everything (${wildcard.operations.join(", ")})`;
|
|
100
|
+
if (perms.length === 1) return `${resourceLabel(perms[0].collection)} (${perms[0].operations.join(", ")})`;
|
|
101
|
+
return `${perms.length} resources`;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/components/ApiKeys/CreateApiKeyDialog.tsx
|
|
105
|
+
var OPERATIONS = [
|
|
106
|
+
"read",
|
|
107
|
+
"write",
|
|
108
|
+
"delete"
|
|
109
|
+
];
|
|
110
|
+
/** What each operation actually permits, per namespace, shown on the toggle. */
|
|
111
|
+
var OPERATION_HINT = {
|
|
112
|
+
everything: {
|
|
113
|
+
read: "GET on every resource",
|
|
114
|
+
write: "POST, PUT and PATCH on every resource",
|
|
115
|
+
delete: "DELETE on every resource"
|
|
116
|
+
},
|
|
117
|
+
collection: {
|
|
118
|
+
read: "List and get snapshots",
|
|
119
|
+
write: "Create and update snapshots",
|
|
120
|
+
delete: "Delete snapshots"
|
|
121
|
+
},
|
|
122
|
+
storage: {
|
|
123
|
+
read: "List and download files",
|
|
124
|
+
write: "Upload files and create folders",
|
|
125
|
+
delete: "Delete files"
|
|
126
|
+
},
|
|
127
|
+
"all-functions": {
|
|
128
|
+
read: "Call any function over GET",
|
|
129
|
+
write: "Call any function over POST, PUT or PATCH",
|
|
130
|
+
delete: "Call any function over DELETE"
|
|
131
|
+
},
|
|
132
|
+
function: {
|
|
133
|
+
read: "Call it over GET",
|
|
134
|
+
write: "Call it over POST, PUT or PATCH",
|
|
135
|
+
delete: "Call it over DELETE"
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Picker value that switches a row to free text.
|
|
140
|
+
*
|
|
141
|
+
* Never reaches the wire — choosing it sets `freeText` and clears the resource
|
|
142
|
+
* — so it only has to be a value no collection slug would take.
|
|
143
|
+
*/
|
|
144
|
+
var SENTINEL_FREE_TEXT = "__custom__";
|
|
145
|
+
var rowToPermission = (row) => ({
|
|
146
|
+
collection: row.resource.trim(),
|
|
147
|
+
operations: OPERATIONS.filter((op) => row[op])
|
|
148
|
+
});
|
|
149
|
+
var rowGrantsNothing = (row) => {
|
|
150
|
+
const perm = rowToPermission(row);
|
|
151
|
+
return !perm.collection || perm.operations.length === 0;
|
|
152
|
+
};
|
|
153
|
+
function ResourceIcon({ kind, className }) {
|
|
154
|
+
return /* @__PURE__ */ jsx(kind === "everything" ? GlobeIcon : kind === "storage" ? FolderIcon : kind === "all-functions" || kind === "function" ? FunctionSquareIcon : DatabaseIcon, {
|
|
155
|
+
size: iconSize.smallest,
|
|
156
|
+
className
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
var OPERATION_STYLES = {
|
|
160
|
+
read: {
|
|
161
|
+
on: "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300 ring-emerald-500/40",
|
|
162
|
+
dot: "bg-emerald-500"
|
|
163
|
+
},
|
|
164
|
+
write: {
|
|
165
|
+
on: "bg-blue-500/12 text-blue-700 dark:text-blue-300 ring-blue-500/40",
|
|
166
|
+
dot: "bg-blue-500"
|
|
167
|
+
},
|
|
168
|
+
delete: {
|
|
169
|
+
on: "bg-rose-500/12 text-rose-700 dark:text-rose-300 ring-rose-500/40",
|
|
170
|
+
dot: "bg-rose-500"
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function OperationToggles({ row, onToggle }) {
|
|
174
|
+
const kind = parseResource(row.resource).kind;
|
|
175
|
+
return /* @__PURE__ */ jsx("div", {
|
|
176
|
+
className: "flex items-center gap-1",
|
|
177
|
+
role: "group",
|
|
178
|
+
"aria-label": "Allowed operations",
|
|
179
|
+
children: OPERATIONS.map((op) => {
|
|
180
|
+
const active = row[op];
|
|
181
|
+
const styles = OPERATION_STYLES[op];
|
|
182
|
+
return /* @__PURE__ */ jsx(Tooltip, {
|
|
183
|
+
title: OPERATION_HINT[kind][op],
|
|
184
|
+
delayDuration: 400,
|
|
185
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
186
|
+
type: "button",
|
|
187
|
+
"aria-pressed": active,
|
|
188
|
+
onClick: () => onToggle(op, !active),
|
|
189
|
+
className: cls("flex items-center gap-1.5 h-7 pl-2 pr-2.5 rounded-md text-2xs font-medium", "ring-1 transition-colors duration-150 cursor-pointer", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", active ? styles.on : "bg-transparent ring-transparent text-surface-500 dark:text-surface-400 hover:bg-surface-accent-100 dark:hover:bg-surface-800"),
|
|
190
|
+
children: [/* @__PURE__ */ jsx("span", { className: cls("w-1.5 h-1.5 rounded-full transition-colors duration-150", active ? styles.dot : "bg-surface-300 dark:bg-surface-600") }), op]
|
|
191
|
+
})
|
|
192
|
+
}, op);
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function SectionLabel({ children, hint }) {
|
|
197
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
198
|
+
className: "flex items-baseline gap-2 mb-2",
|
|
199
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
200
|
+
variant: "label",
|
|
201
|
+
className: "text-2xs uppercase tracking-wider font-semibold text-surface-600 dark:text-surface-300",
|
|
202
|
+
gutterBottom: false,
|
|
203
|
+
children
|
|
204
|
+
}), hint && /* @__PURE__ */ jsx(Typography, {
|
|
205
|
+
variant: "caption",
|
|
206
|
+
color: "secondary",
|
|
207
|
+
className: "text-2xs",
|
|
208
|
+
gutterBottom: false,
|
|
209
|
+
children: hint
|
|
210
|
+
})]
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function CreateApiKeyDialog({ onClose, onCreated }) {
|
|
214
|
+
const client = useRebaseClient();
|
|
215
|
+
const snackbar = useSnackbarController();
|
|
216
|
+
const collectionRegistry = useStudioCollectionRegistry();
|
|
217
|
+
const apiConfig = useApiConfig();
|
|
218
|
+
const apiBase = useApiBase();
|
|
219
|
+
const [name, setName] = useState("");
|
|
220
|
+
const [rows, setRows] = useState([{
|
|
221
|
+
resource: "*",
|
|
222
|
+
read: true,
|
|
223
|
+
write: false,
|
|
224
|
+
delete: false,
|
|
225
|
+
freeText: false
|
|
226
|
+
}]);
|
|
227
|
+
const [admin, setAdmin] = useState(false);
|
|
228
|
+
const [rateLimit, setRateLimit] = useState("");
|
|
229
|
+
const [expiresIn, setExpiresIn] = useState("never");
|
|
230
|
+
const [creating, setCreating] = useState(false);
|
|
231
|
+
const collections = useMemo(() => (collectionRegistry?.collections ?? []).map((col) => ({
|
|
232
|
+
slug: col.slug,
|
|
233
|
+
name: col.name
|
|
234
|
+
})).filter((col) => !!col.slug).sort((a, b) => a.slug.localeCompare(b.slug)), [collectionRegistry?.collections]);
|
|
235
|
+
/**
|
|
236
|
+
* Deployed functions, so a single-function grant can be picked instead of
|
|
237
|
+
* spelled `functions/<name>` from memory. Best-effort: this is an ordinary
|
|
238
|
+
* API route, and a backend that serves no functions — or refuses the
|
|
239
|
+
* request — just leaves the free-text escape to cover it.
|
|
240
|
+
*/
|
|
241
|
+
const [functionNames, setFunctionNames] = useState([]);
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (!apiBase) return;
|
|
244
|
+
let cancelled = false;
|
|
245
|
+
(async () => {
|
|
246
|
+
try {
|
|
247
|
+
const token = await apiConfig?.getAuthToken?.();
|
|
248
|
+
const res = await fetch(`${apiBase}/functions`, { headers: token ? { Authorization: `Bearer ${token}` } : void 0 });
|
|
249
|
+
if (!res.ok) return;
|
|
250
|
+
const body = await res.json();
|
|
251
|
+
if (cancelled) return;
|
|
252
|
+
setFunctionNames((body.functions ?? []).map((fn) => fn.name).filter((fnName) => !!fnName));
|
|
253
|
+
} catch {}
|
|
254
|
+
})();
|
|
255
|
+
return () => {
|
|
256
|
+
cancelled = true;
|
|
257
|
+
};
|
|
258
|
+
}, [apiBase, apiConfig]);
|
|
259
|
+
const updateRow = useCallback((idx, patch) => {
|
|
260
|
+
setRows((current) => current.map((row, i) => i === idx ? {
|
|
261
|
+
...row,
|
|
262
|
+
...patch
|
|
263
|
+
} : row));
|
|
264
|
+
}, []);
|
|
265
|
+
const addRow = () => setRows((current) => [...current, {
|
|
266
|
+
resource: collections[0]?.slug ?? "",
|
|
267
|
+
read: true,
|
|
268
|
+
write: false,
|
|
269
|
+
delete: false,
|
|
270
|
+
freeText: collections.length === 0
|
|
271
|
+
}]);
|
|
272
|
+
const removeRow = (idx) => setRows((current) => current.filter((_, i) => i !== idx));
|
|
273
|
+
const effectivePermissions = useMemo(() => rows.map(rowToPermission).filter((perm) => perm.collection && perm.operations.length > 0), [rows]);
|
|
274
|
+
const droppedRows = rows.filter(rowGrantsNothing).length;
|
|
275
|
+
const hasWildcard = rows.some((row) => row.resource === "*" && OPERATIONS.some((op) => row[op]));
|
|
276
|
+
const canSubmit = !!name.trim() && effectivePermissions.length > 0 && !creating;
|
|
277
|
+
const handleCreate = async () => {
|
|
278
|
+
if (!client?.apiKeys || !canSubmit) return;
|
|
279
|
+
let expires_at = null;
|
|
280
|
+
if (expiresIn === "7d") expires_at = new Date(Date.now() + 7 * 864e5).toISOString();
|
|
281
|
+
else if (expiresIn === "30d") expires_at = new Date(Date.now() + 30 * 864e5).toISOString();
|
|
282
|
+
else if (expiresIn === "90d") expires_at = new Date(Date.now() + 90 * 864e5).toISOString();
|
|
283
|
+
else if (expiresIn === "1y") expires_at = new Date(Date.now() + 365 * 864e5).toISOString();
|
|
284
|
+
setCreating(true);
|
|
285
|
+
try {
|
|
286
|
+
onCreated((await client.apiKeys.createKey({
|
|
287
|
+
name: name.trim(),
|
|
288
|
+
permissions: effectivePermissions,
|
|
289
|
+
admin,
|
|
290
|
+
rate_limit: rateLimit ? parseInt(rateLimit, 10) : null,
|
|
291
|
+
expires_at
|
|
292
|
+
})).key);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
snackbar.open({
|
|
295
|
+
type: "error",
|
|
296
|
+
message: e instanceof Error ? e.message : String(e)
|
|
297
|
+
});
|
|
298
|
+
} finally {
|
|
299
|
+
setCreating(false);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const submitBlockedReason = !name.trim() ? "Name the key first" : effectivePermissions.length === 0 ? "Grant at least one operation on one resource" : "";
|
|
303
|
+
return /* @__PURE__ */ jsxs(Dialog, {
|
|
304
|
+
open: true,
|
|
305
|
+
onOpenChange: (open) => {
|
|
306
|
+
if (!open && !creating) onClose();
|
|
307
|
+
},
|
|
308
|
+
maxWidth: "2xl",
|
|
309
|
+
children: [
|
|
310
|
+
/* @__PURE__ */ jsx(DialogTitle, {
|
|
311
|
+
variant: "subtitle1",
|
|
312
|
+
gutterBottom: false,
|
|
313
|
+
className: "font-semibold",
|
|
314
|
+
children: "Create API key"
|
|
315
|
+
}),
|
|
316
|
+
/* @__PURE__ */ jsxs(DialogContent, {
|
|
317
|
+
includeMargin: false,
|
|
318
|
+
className: "px-8 pt-2 pb-4 flex flex-col gap-6",
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
321
|
+
variant: "body2",
|
|
322
|
+
color: "secondary",
|
|
323
|
+
gutterBottom: false,
|
|
324
|
+
className: "text-[13px] max-w-[62ch]",
|
|
325
|
+
children: "A credential for scripts, cron jobs and agents. It authenticates as itself rather than as a user, and reaches only what you grant here."
|
|
326
|
+
}),
|
|
327
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
328
|
+
label: "Name",
|
|
329
|
+
value: name,
|
|
330
|
+
onChange: (e) => setName(e.target.value),
|
|
331
|
+
placeholder: "e.g. Analytics pipeline",
|
|
332
|
+
size: "small",
|
|
333
|
+
autoFocus: true
|
|
334
|
+
}),
|
|
335
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
336
|
+
/* @__PURE__ */ jsx(SectionLabel, {
|
|
337
|
+
hint: "What the key may call",
|
|
338
|
+
children: "Access"
|
|
339
|
+
}),
|
|
340
|
+
/* @__PURE__ */ jsx("div", {
|
|
341
|
+
className: cls("rounded-lg border overflow-hidden", defaultBorderMixin),
|
|
342
|
+
children: rows.map((row, idx) => {
|
|
343
|
+
const parsed = parseResource(row.resource);
|
|
344
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
345
|
+
role: "group",
|
|
346
|
+
"aria-label": `Resource ${idx + 1}`,
|
|
347
|
+
className: cls("flex flex-wrap items-center gap-x-3 gap-y-2 px-3 py-2.5", idx > 0 && cls("border-t", defaultBorderMixin)),
|
|
348
|
+
children: [
|
|
349
|
+
/* @__PURE__ */ jsxs("div", {
|
|
350
|
+
className: "flex items-center gap-2 flex-1 min-w-[15rem]",
|
|
351
|
+
children: [/* @__PURE__ */ jsx(ResourceIcon, {
|
|
352
|
+
kind: parsed.kind,
|
|
353
|
+
className: cls("shrink-0", parsed.kind === "everything" ? "text-amber-600 dark:text-amber-400" : "text-surface-500 dark:text-surface-400")
|
|
354
|
+
}), row.freeText ? /* @__PURE__ */ jsx(TextField, {
|
|
355
|
+
size: "small",
|
|
356
|
+
"aria-label": `Resource ${idx + 1} name`,
|
|
357
|
+
value: row.resource,
|
|
358
|
+
onChange: (e) => updateRow(idx, { resource: e.target.value }),
|
|
359
|
+
placeholder: "collection slug, or functions/<name>",
|
|
360
|
+
className: "flex-1",
|
|
361
|
+
endAdornment: /* @__PURE__ */ jsx(Tooltip, {
|
|
362
|
+
title: "Back to the list",
|
|
363
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
364
|
+
size: "smallest",
|
|
365
|
+
"aria-label": "Pick from the list instead",
|
|
366
|
+
onClick: () => updateRow(idx, {
|
|
367
|
+
resource: collections[0]?.slug ?? "*",
|
|
368
|
+
freeText: false
|
|
369
|
+
}),
|
|
370
|
+
children: /* @__PURE__ */ jsx(ChevronsUpDownIcon, { size: iconSize.smallest })
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
}) : /* @__PURE__ */ jsxs(Select, {
|
|
374
|
+
size: "small",
|
|
375
|
+
fullWidth: true,
|
|
376
|
+
className: "flex-1",
|
|
377
|
+
"aria-label": `Resource ${idx + 1}`,
|
|
378
|
+
value: row.resource,
|
|
379
|
+
position: "popper",
|
|
380
|
+
onValueChange: (value) => {
|
|
381
|
+
if (value === SENTINEL_FREE_TEXT) updateRow(idx, {
|
|
382
|
+
freeText: true,
|
|
383
|
+
resource: ""
|
|
384
|
+
});
|
|
385
|
+
else updateRow(idx, {
|
|
386
|
+
resource: value,
|
|
387
|
+
freeText: false
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
renderValue: (value) => /* @__PURE__ */ jsx("span", {
|
|
391
|
+
className: "truncate",
|
|
392
|
+
children: value === "*" ? "Everything" : value === "storage" ? "Storage" : value === "functions" ? "All functions" : String(value).startsWith("functions/") ? `${String(value).slice(FUNCTION_PREFIX.length)}()` : String(value)
|
|
393
|
+
}),
|
|
394
|
+
children: [
|
|
395
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
396
|
+
value: "*",
|
|
397
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
398
|
+
className: "flex flex-col text-left",
|
|
399
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Everything" }), /* @__PURE__ */ jsx("span", {
|
|
400
|
+
className: "text-2xs text-text-secondary dark:text-text-secondary-dark",
|
|
401
|
+
children: "Every collection, every function and storage"
|
|
402
|
+
})]
|
|
403
|
+
})
|
|
404
|
+
}),
|
|
405
|
+
collections.length > 0 && /* @__PURE__ */ jsx(SelectGroup, {
|
|
406
|
+
label: "Collections",
|
|
407
|
+
children: collections.map((col) => /* @__PURE__ */ jsx(SelectItem, {
|
|
408
|
+
value: col.slug,
|
|
409
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
410
|
+
className: "flex flex-col text-left",
|
|
411
|
+
children: [/* @__PURE__ */ jsx("span", { children: col.slug }), col.name && col.name !== col.slug && /* @__PURE__ */ jsx("span", {
|
|
412
|
+
className: "text-2xs text-text-secondary dark:text-text-secondary-dark",
|
|
413
|
+
children: col.name
|
|
414
|
+
})]
|
|
415
|
+
})
|
|
416
|
+
}, col.slug))
|
|
417
|
+
}),
|
|
418
|
+
/* @__PURE__ */ jsxs(SelectGroup, {
|
|
419
|
+
label: "Functions",
|
|
420
|
+
children: [/* @__PURE__ */ jsx(SelectItem, {
|
|
421
|
+
value: RESOURCE_ALL_FUNCTIONS,
|
|
422
|
+
children: "All functions"
|
|
423
|
+
}), functionNames.map((fnName) => /* @__PURE__ */ jsxs(SelectItem, {
|
|
424
|
+
value: `${FUNCTION_PREFIX}${fnName}`,
|
|
425
|
+
children: [fnName, "()"]
|
|
426
|
+
}, fnName))]
|
|
427
|
+
}),
|
|
428
|
+
/* @__PURE__ */ jsx(SelectGroup, {
|
|
429
|
+
label: "Storage",
|
|
430
|
+
children: /* @__PURE__ */ jsx(SelectItem, {
|
|
431
|
+
value: RESOURCE_STORAGE,
|
|
432
|
+
children: "Storage"
|
|
433
|
+
})
|
|
434
|
+
}),
|
|
435
|
+
/* @__PURE__ */ jsx(SelectGroup, {
|
|
436
|
+
label: "Other",
|
|
437
|
+
children: /* @__PURE__ */ jsx(SelectItem, {
|
|
438
|
+
value: SENTINEL_FREE_TEXT,
|
|
439
|
+
children: "Type a name…"
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
]
|
|
443
|
+
})]
|
|
444
|
+
}),
|
|
445
|
+
/* @__PURE__ */ jsx(OperationToggles, {
|
|
446
|
+
row,
|
|
447
|
+
onToggle: (operation, value) => updateRow(idx, { [operation]: value })
|
|
448
|
+
}),
|
|
449
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
450
|
+
title: rows.length > 1 ? "Remove" : "At least one resource is required",
|
|
451
|
+
children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(IconButton, {
|
|
452
|
+
size: "small",
|
|
453
|
+
disabled: rows.length === 1,
|
|
454
|
+
onClick: () => removeRow(idx),
|
|
455
|
+
"aria-label": `Remove resource ${idx + 1}`,
|
|
456
|
+
children: /* @__PURE__ */ jsx(Trash2Icon, { size: iconSize.smallest })
|
|
457
|
+
}) })
|
|
458
|
+
})
|
|
459
|
+
]
|
|
460
|
+
}, idx);
|
|
461
|
+
})
|
|
462
|
+
}),
|
|
463
|
+
/* @__PURE__ */ jsx(Button, {
|
|
464
|
+
size: "small",
|
|
465
|
+
variant: "text",
|
|
466
|
+
onClick: addRow,
|
|
467
|
+
className: "mt-1.5",
|
|
468
|
+
startIcon: /* @__PURE__ */ jsx(PlusIcon, { size: iconSize.smallest }),
|
|
469
|
+
children: "Add resource"
|
|
470
|
+
}),
|
|
471
|
+
/* @__PURE__ */ jsxs("div", {
|
|
472
|
+
className: cls("mt-3 rounded-lg border px-3 py-2.5 bg-surface-accent-50 dark:bg-surface-900", defaultBorderMixin),
|
|
473
|
+
children: [
|
|
474
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
475
|
+
variant: "label",
|
|
476
|
+
gutterBottom: false,
|
|
477
|
+
className: "text-2xs uppercase tracking-wider font-semibold text-surface-600 dark:text-surface-300",
|
|
478
|
+
children: "This key will be able to"
|
|
479
|
+
}),
|
|
480
|
+
effectivePermissions.length === 0 ? /* @__PURE__ */ jsx(Typography, {
|
|
481
|
+
variant: "body2",
|
|
482
|
+
gutterBottom: false,
|
|
483
|
+
className: "mt-1.5 text-[13px] text-surface-500 dark:text-surface-400",
|
|
484
|
+
children: "Nothing yet — pick a resource and at least one operation."
|
|
485
|
+
}) : /* @__PURE__ */ jsxs("ul", {
|
|
486
|
+
className: "mt-1.5 space-y-1",
|
|
487
|
+
children: [effectivePermissions.map((perm, idx) => /* @__PURE__ */ jsxs("li", {
|
|
488
|
+
className: "flex items-start gap-2",
|
|
489
|
+
children: [/* @__PURE__ */ jsx(ResourceIcon, {
|
|
490
|
+
kind: parseResource(perm.collection).kind,
|
|
491
|
+
className: "mt-[3px] shrink-0 text-surface-500 dark:text-surface-400"
|
|
492
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
493
|
+
variant: "body2",
|
|
494
|
+
gutterBottom: false,
|
|
495
|
+
className: "text-[13px] leading-snug",
|
|
496
|
+
children: grantSentence(perm)
|
|
497
|
+
})]
|
|
498
|
+
}, idx)), admin && /* @__PURE__ */ jsxs("li", {
|
|
499
|
+
className: "flex items-start gap-2",
|
|
500
|
+
children: [/* @__PURE__ */ jsx(ShieldIcon, {
|
|
501
|
+
size: iconSize.smallest,
|
|
502
|
+
className: "mt-[3px] shrink-0 text-amber-600 dark:text-amber-400"
|
|
503
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
504
|
+
variant: "body2",
|
|
505
|
+
gutterBottom: false,
|
|
506
|
+
className: "text-[13px] leading-snug",
|
|
507
|
+
children: [
|
|
508
|
+
"Reach every admin route — users, roles, cron, backups, logs — and read through the ",
|
|
509
|
+
/* @__PURE__ */ jsx("span", {
|
|
510
|
+
className: "font-mono text-2xs",
|
|
511
|
+
children: "default_admin"
|
|
512
|
+
}),
|
|
513
|
+
" policies"
|
|
514
|
+
]
|
|
515
|
+
})]
|
|
516
|
+
})]
|
|
517
|
+
}),
|
|
518
|
+
droppedRows > 0 && effectivePermissions.length > 0 && /* @__PURE__ */ jsx(Typography, {
|
|
519
|
+
variant: "caption",
|
|
520
|
+
gutterBottom: false,
|
|
521
|
+
className: "block mt-2 text-2xs text-surface-500 dark:text-surface-400",
|
|
522
|
+
children: droppedRows === 1 ? "1 row grants nothing and will not be saved." : `${droppedRows} rows grant nothing and will not be saved.`
|
|
523
|
+
})
|
|
524
|
+
]
|
|
525
|
+
}),
|
|
526
|
+
hasWildcard && /* @__PURE__ */ jsxs("div", {
|
|
527
|
+
className: "flex items-start gap-2 mt-2 px-1",
|
|
528
|
+
children: [/* @__PURE__ */ jsx(AlertTriangleIcon, {
|
|
529
|
+
size: iconSize.smallest,
|
|
530
|
+
className: "mt-[3px] shrink-0 text-amber-600 dark:text-amber-400"
|
|
531
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
532
|
+
variant: "caption",
|
|
533
|
+
gutterBottom: false,
|
|
534
|
+
className: "text-2xs text-amber-700 dark:text-amber-300 leading-snug max-w-[70ch]",
|
|
535
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
536
|
+
className: "font-semibold",
|
|
537
|
+
children: "Everything"
|
|
538
|
+
}), " is the widest grant there is. It also covers collections and functions you add later, without this key being edited again."]
|
|
539
|
+
})]
|
|
540
|
+
})
|
|
541
|
+
] }),
|
|
542
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(SectionLabel, {
|
|
543
|
+
hint: "Off for almost every key",
|
|
544
|
+
children: "Admin role"
|
|
545
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
546
|
+
className: cls("rounded-lg border px-3 py-2.5 transition-colors duration-150", admin ? "border-amber-500/40 bg-amber-500/[0.06]" : defaultBorderMixin),
|
|
547
|
+
children: [/* @__PURE__ */ jsx(BooleanSwitchWithLabel, {
|
|
548
|
+
size: "small",
|
|
549
|
+
position: "start",
|
|
550
|
+
invisible: true,
|
|
551
|
+
value: admin,
|
|
552
|
+
onValueChange: setAdmin,
|
|
553
|
+
label: /* @__PURE__ */ jsxs("div", {
|
|
554
|
+
className: "flex items-center gap-2",
|
|
555
|
+
children: [/* @__PURE__ */ jsx(ShieldIcon, {
|
|
556
|
+
size: iconSize.smallest,
|
|
557
|
+
className: admin ? "text-amber-600 dark:text-amber-400" : "text-surface-500 dark:text-surface-400"
|
|
558
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
559
|
+
className: "text-[13px]",
|
|
560
|
+
children: "Grant the admin role"
|
|
561
|
+
})]
|
|
562
|
+
})
|
|
563
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
564
|
+
variant: "caption",
|
|
565
|
+
color: "secondary",
|
|
566
|
+
gutterBottom: false,
|
|
567
|
+
className: "block mt-1.5 text-2xs leading-snug max-w-[70ch]",
|
|
568
|
+
children: admin ? "The key passes the admin-gated routes and reads through the default_admin RLS policies — far wider than the resources above. It still cannot manage API keys." : "Without it the key carries only the service role, and RLS grants it nothing unless a collection policy names that role."
|
|
569
|
+
})]
|
|
570
|
+
})] }),
|
|
571
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
572
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Limits" }),
|
|
573
|
+
/* @__PURE__ */ jsxs("div", {
|
|
574
|
+
className: "grid grid-cols-1 sm:grid-cols-2 gap-3 items-end",
|
|
575
|
+
children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(Select, {
|
|
576
|
+
label: "Expires",
|
|
577
|
+
value: expiresIn,
|
|
578
|
+
onValueChange: setExpiresIn,
|
|
579
|
+
size: "small",
|
|
580
|
+
fullWidth: true,
|
|
581
|
+
position: "popper",
|
|
582
|
+
renderValue: (v) => v === "never" ? "Never" : v === "7d" ? "In 7 days" : v === "30d" ? "In 30 days" : v === "90d" ? "In 90 days" : "In 1 year",
|
|
583
|
+
children: [
|
|
584
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
585
|
+
value: "never",
|
|
586
|
+
children: "Never"
|
|
587
|
+
}),
|
|
588
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
589
|
+
value: "7d",
|
|
590
|
+
children: "In 7 days"
|
|
591
|
+
}),
|
|
592
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
593
|
+
value: "30d",
|
|
594
|
+
children: "In 30 days"
|
|
595
|
+
}),
|
|
596
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
597
|
+
value: "90d",
|
|
598
|
+
children: "In 90 days"
|
|
599
|
+
}),
|
|
600
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
601
|
+
value: "1y",
|
|
602
|
+
children: "In 1 year"
|
|
603
|
+
})
|
|
604
|
+
]
|
|
605
|
+
}) }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
606
|
+
htmlFor: "api-key-rate-limit",
|
|
607
|
+
className: "block text-sm font-medium ml-3.5 mb-1 text-text-secondary dark:text-text-secondary-dark",
|
|
608
|
+
children: "Rate limit"
|
|
609
|
+
}), /* @__PURE__ */ jsx(TextField, {
|
|
610
|
+
id: "api-key-rate-limit",
|
|
611
|
+
value: rateLimit,
|
|
612
|
+
onChange: (e) => setRateLimit(e.target.value.replace(/\D/g, "")),
|
|
613
|
+
placeholder: "1000",
|
|
614
|
+
size: "small",
|
|
615
|
+
endAdornment: /* @__PURE__ */ jsx("span", {
|
|
616
|
+
className: "text-2xs text-text-secondary dark:text-text-secondary-dark whitespace-nowrap",
|
|
617
|
+
children: "/ 15 min"
|
|
618
|
+
})
|
|
619
|
+
})] })]
|
|
620
|
+
}),
|
|
621
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
622
|
+
variant: "caption",
|
|
623
|
+
color: "secondary",
|
|
624
|
+
gutterBottom: false,
|
|
625
|
+
className: "block mt-1.5 text-2xs",
|
|
626
|
+
children: "Leave the rate limit empty for the server default of 1000 requests per 15-minute window."
|
|
627
|
+
})
|
|
628
|
+
] })
|
|
629
|
+
]
|
|
630
|
+
}),
|
|
631
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
|
|
632
|
+
variant: "text",
|
|
633
|
+
onClick: onClose,
|
|
634
|
+
disabled: creating,
|
|
635
|
+
children: "Cancel"
|
|
636
|
+
}), /* @__PURE__ */ jsx(Tooltip, {
|
|
637
|
+
title: submitBlockedReason,
|
|
638
|
+
children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(Button, {
|
|
639
|
+
color: "primary",
|
|
640
|
+
onClick: handleCreate,
|
|
641
|
+
disabled: !canSubmit,
|
|
642
|
+
startIcon: creating ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : void 0,
|
|
643
|
+
children: creating ? "Creating…" : "Create key"
|
|
644
|
+
}) })
|
|
645
|
+
})] })
|
|
646
|
+
]
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/components/ApiKeys/ApiKeysView.tsx
|
|
651
|
+
function formatRelative(iso) {
|
|
652
|
+
if (!iso) return "—";
|
|
653
|
+
const d = new Date(iso);
|
|
654
|
+
const now = Date.now();
|
|
655
|
+
const diff = d.getTime() - now;
|
|
656
|
+
const abs = Math.abs(diff);
|
|
657
|
+
if (abs < 6e4) return diff > 0 ? "in <1m" : "<1m ago";
|
|
658
|
+
if (abs < 36e5) {
|
|
659
|
+
const m = Math.round(abs / 6e4);
|
|
660
|
+
return diff > 0 ? `in ${m}m` : `${m}m ago`;
|
|
661
|
+
}
|
|
662
|
+
if (abs < 864e5) {
|
|
663
|
+
const h = Math.round(abs / 36e5);
|
|
664
|
+
return diff > 0 ? `in ${h}h` : `${h}h ago`;
|
|
665
|
+
}
|
|
666
|
+
return d.toLocaleDateString();
|
|
667
|
+
}
|
|
668
|
+
function isExpired(key) {
|
|
669
|
+
return !!(key.expires_at && new Date(key.expires_at) < /* @__PURE__ */ new Date());
|
|
670
|
+
}
|
|
671
|
+
function keyStatus(key) {
|
|
672
|
+
if (key.revoked_at) return {
|
|
673
|
+
label: "Revoked",
|
|
674
|
+
color: "text-red-500"
|
|
675
|
+
};
|
|
676
|
+
if (isExpired(key)) return {
|
|
677
|
+
label: "Expired",
|
|
678
|
+
color: "text-amber-500"
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
label: "Active",
|
|
682
|
+
color: "text-emerald-500"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function ApiKeysView() {
|
|
686
|
+
const client = useRebaseClient();
|
|
687
|
+
const snackbar = useSnackbarController();
|
|
688
|
+
const [keys, setKeys] = useState([]);
|
|
689
|
+
const [loading, setLoading] = useState(true);
|
|
690
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
691
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
692
|
+
const [showSecret, setShowSecret] = useState(null);
|
|
693
|
+
const [revoking, setRevoking] = useState(null);
|
|
694
|
+
const [confirmRevoke, setConfirmRevoke] = useState(null);
|
|
695
|
+
const clientRef = useRef(client);
|
|
696
|
+
clientRef.current = client;
|
|
697
|
+
const snackbarRef = useRef(snackbar);
|
|
698
|
+
snackbarRef.current = snackbar;
|
|
699
|
+
const loadKeys = useCallback(async () => {
|
|
700
|
+
const c = clientRef.current;
|
|
701
|
+
if (!c?.apiKeys) {
|
|
702
|
+
setLoading(false);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
const res = await c.apiKeys.listKeys();
|
|
707
|
+
setKeys(res.keys);
|
|
708
|
+
} catch (e) {
|
|
709
|
+
snackbarRef.current.open({
|
|
710
|
+
type: "error",
|
|
711
|
+
message: e instanceof Error ? e.message : String(e)
|
|
712
|
+
});
|
|
713
|
+
} finally {
|
|
714
|
+
setLoading(false);
|
|
715
|
+
}
|
|
716
|
+
}, []);
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
loadKeys();
|
|
719
|
+
}, [loadKeys]);
|
|
720
|
+
const handleRevoke = async (id) => {
|
|
721
|
+
const c = clientRef.current;
|
|
722
|
+
if (!c?.apiKeys) return;
|
|
723
|
+
setRevoking(id);
|
|
724
|
+
try {
|
|
725
|
+
await c.apiKeys.revokeKey(id);
|
|
726
|
+
snackbarRef.current.open({
|
|
727
|
+
type: "success",
|
|
728
|
+
message: "API key revoked"
|
|
729
|
+
});
|
|
730
|
+
await loadKeys();
|
|
731
|
+
if (selectedId === id) setSelectedId(null);
|
|
732
|
+
} catch (e) {
|
|
733
|
+
snackbarRef.current.open({
|
|
734
|
+
type: "error",
|
|
735
|
+
message: e instanceof Error ? e.message : String(e)
|
|
736
|
+
});
|
|
737
|
+
} finally {
|
|
738
|
+
setRevoking(null);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const handleCreated = (keyWithSecret) => {
|
|
742
|
+
setShowCreate(false);
|
|
743
|
+
setShowSecret(keyWithSecret);
|
|
744
|
+
loadKeys();
|
|
745
|
+
};
|
|
746
|
+
const selectedKey = keys.find((k) => k.id === selectedId);
|
|
747
|
+
const activeKeys = keys.filter((k) => !k.revoked_at && !isExpired(k));
|
|
748
|
+
const inactiveKeys = keys.filter((k) => k.revoked_at || isExpired(k));
|
|
749
|
+
if (loading) return /* @__PURE__ */ jsx("div", {
|
|
750
|
+
className: "flex items-center justify-center h-full",
|
|
751
|
+
children: /* @__PURE__ */ jsx(CircularProgress, {})
|
|
752
|
+
});
|
|
753
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
754
|
+
/* @__PURE__ */ jsxs("div", {
|
|
755
|
+
className: "flex h-full w-full overflow-hidden bg-white dark:bg-surface-950",
|
|
756
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
757
|
+
className: cls("flex flex-col w-[340px] min-w-[280px] border-r h-full", defaultBorderMixin),
|
|
758
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
759
|
+
className: cls("flex items-center justify-between px-4 py-2.5 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin),
|
|
760
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
761
|
+
className: "flex items-center gap-2",
|
|
762
|
+
children: [
|
|
763
|
+
/* @__PURE__ */ jsx(KeyRoundIcon, {
|
|
764
|
+
size: iconSize.smallest,
|
|
765
|
+
className: "text-primary"
|
|
766
|
+
}),
|
|
767
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
768
|
+
variant: "subtitle2",
|
|
769
|
+
className: "font-semibold",
|
|
770
|
+
children: "API Keys"
|
|
771
|
+
}),
|
|
772
|
+
/* @__PURE__ */ jsx(Chip, {
|
|
773
|
+
size: "smallest",
|
|
774
|
+
className: "bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300",
|
|
775
|
+
children: activeKeys.length
|
|
776
|
+
})
|
|
777
|
+
]
|
|
778
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
779
|
+
className: "flex items-center gap-1",
|
|
780
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
781
|
+
size: "small",
|
|
782
|
+
onClick: loadKeys,
|
|
783
|
+
title: "Refresh",
|
|
784
|
+
children: /* @__PURE__ */ jsx(RefreshCwIcon, { size: iconSize.smallest })
|
|
785
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
786
|
+
size: "small",
|
|
787
|
+
color: "primary",
|
|
788
|
+
onClick: () => setShowCreate(true),
|
|
789
|
+
startIcon: /* @__PURE__ */ jsx(PlusIcon, { size: iconSize.smallest }),
|
|
790
|
+
children: "New"
|
|
791
|
+
})]
|
|
792
|
+
})]
|
|
793
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
794
|
+
className: "flex-1 overflow-y-auto p-2 space-y-1",
|
|
795
|
+
children: [
|
|
796
|
+
activeKeys.length === 0 && inactiveKeys.length === 0 && /* @__PURE__ */ jsxs("div", {
|
|
797
|
+
className: "flex flex-col items-center justify-center h-full gap-3 text-center p-6",
|
|
798
|
+
children: [
|
|
799
|
+
/* @__PURE__ */ jsx(KeyRoundIcon, {
|
|
800
|
+
size: iconSize.medium,
|
|
801
|
+
className: "text-surface-300 dark:text-surface-600"
|
|
802
|
+
}),
|
|
803
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
804
|
+
variant: "body2",
|
|
805
|
+
color: "secondary",
|
|
806
|
+
children: "No API keys yet"
|
|
807
|
+
}),
|
|
808
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
809
|
+
variant: "caption",
|
|
810
|
+
color: "disabled",
|
|
811
|
+
children: "Create a key to enable machine-to-machine authentication"
|
|
812
|
+
})
|
|
813
|
+
]
|
|
814
|
+
}),
|
|
815
|
+
activeKeys.map((key) => /* @__PURE__ */ jsx(KeyListItem, {
|
|
816
|
+
apiKey: key,
|
|
817
|
+
selected: selectedId === key.id,
|
|
818
|
+
onClick: () => setSelectedId(key.id)
|
|
819
|
+
}, key.id)),
|
|
820
|
+
inactiveKeys.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
821
|
+
className: "px-2 pt-3 pb-1",
|
|
822
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
823
|
+
variant: "caption",
|
|
824
|
+
color: "disabled",
|
|
825
|
+
className: "text-[10px] uppercase tracking-wider font-medium",
|
|
826
|
+
children: "Revoked / Expired"
|
|
827
|
+
})
|
|
828
|
+
}), inactiveKeys.map((key) => /* @__PURE__ */ jsx(KeyListItem, {
|
|
829
|
+
apiKey: key,
|
|
830
|
+
selected: selectedId === key.id,
|
|
831
|
+
onClick: () => setSelectedId(key.id)
|
|
832
|
+
}, key.id))] })
|
|
833
|
+
]
|
|
834
|
+
})]
|
|
835
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
836
|
+
className: "flex-1 flex flex-col min-w-0 h-full overflow-hidden",
|
|
837
|
+
children: !selectedKey ? /* @__PURE__ */ jsx("div", {
|
|
838
|
+
className: "flex items-center justify-center h-full",
|
|
839
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
840
|
+
variant: "body2",
|
|
841
|
+
color: "disabled",
|
|
842
|
+
children: "Select an API key to view details"
|
|
843
|
+
})
|
|
844
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
845
|
+
/* @__PURE__ */ jsxs("div", {
|
|
846
|
+
className: cls("flex items-center justify-between px-5 py-3 border-b bg-white dark:bg-surface-950 min-h-[56px]", defaultBorderMixin),
|
|
847
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
848
|
+
className: "flex items-center gap-3 min-w-0",
|
|
849
|
+
children: [/* @__PURE__ */ jsx(KeyRoundIcon, {
|
|
850
|
+
size: iconSize.small,
|
|
851
|
+
className: "text-primary shrink-0"
|
|
852
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
853
|
+
className: "min-w-0",
|
|
854
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
855
|
+
className: "flex items-center gap-2 min-w-0",
|
|
856
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
857
|
+
variant: "subtitle1",
|
|
858
|
+
className: "font-semibold truncate",
|
|
859
|
+
children: selectedKey.name
|
|
860
|
+
}), selectedKey.admin && /* @__PURE__ */ jsx(AdminChip, {})]
|
|
861
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
862
|
+
variant: "caption",
|
|
863
|
+
color: "secondary",
|
|
864
|
+
className: "font-mono text-[11px]",
|
|
865
|
+
children: [selectedKey.key_prefix, "•••"]
|
|
866
|
+
})]
|
|
867
|
+
})]
|
|
868
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
869
|
+
className: "flex items-center gap-2 shrink-0",
|
|
870
|
+
children: !selectedKey.revoked_at && /* @__PURE__ */ jsx(Button, {
|
|
871
|
+
size: "small",
|
|
872
|
+
color: "error",
|
|
873
|
+
variant: "outlined",
|
|
874
|
+
onClick: () => setConfirmRevoke(selectedKey),
|
|
875
|
+
disabled: revoking === selectedKey.id,
|
|
876
|
+
startIcon: revoking === selectedKey.id ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(Trash2Icon, { size: iconSize.smallest }),
|
|
877
|
+
children: "Revoke"
|
|
878
|
+
})
|
|
879
|
+
})]
|
|
880
|
+
}),
|
|
881
|
+
/* @__PURE__ */ jsxs("div", {
|
|
882
|
+
className: "px-5 py-4 bg-surface-50 dark:bg-surface-900/50",
|
|
883
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
884
|
+
className: "grid grid-cols-2 md:grid-cols-4 gap-3",
|
|
885
|
+
children: [
|
|
886
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
887
|
+
label: "Status",
|
|
888
|
+
value: keyStatus(selectedKey).label,
|
|
889
|
+
className: keyStatus(selectedKey).color
|
|
890
|
+
}),
|
|
891
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
892
|
+
label: "Created",
|
|
893
|
+
value: formatRelative(selectedKey.created_at)
|
|
894
|
+
}),
|
|
895
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
896
|
+
label: "Last Used",
|
|
897
|
+
value: formatRelative(selectedKey.last_used_at)
|
|
898
|
+
}),
|
|
899
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
900
|
+
label: "Expires",
|
|
901
|
+
value: selectedKey.expires_at ? formatRelative(selectedKey.expires_at) : "Never"
|
|
902
|
+
})
|
|
903
|
+
]
|
|
904
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
905
|
+
className: "grid grid-cols-2 md:grid-cols-3 gap-3 mt-3",
|
|
906
|
+
children: [
|
|
907
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
908
|
+
label: "Role",
|
|
909
|
+
value: selectedKey.admin ? "Admin" : "Service",
|
|
910
|
+
className: selectedKey.admin ? "text-amber-600 dark:text-amber-400" : void 0
|
|
911
|
+
}),
|
|
912
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
913
|
+
label: "Rate Limit",
|
|
914
|
+
value: selectedKey.rate_limit ? `${selectedKey.rate_limit}/15min` : "Default (1000/15min)"
|
|
915
|
+
}),
|
|
916
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
917
|
+
label: "Created By",
|
|
918
|
+
value: selectedKey.created_by,
|
|
919
|
+
mono: true
|
|
920
|
+
})
|
|
921
|
+
]
|
|
922
|
+
})]
|
|
923
|
+
}),
|
|
924
|
+
/* @__PURE__ */ jsxs("div", {
|
|
925
|
+
className: cls("flex items-center gap-2 px-5 py-2 border-y bg-white dark:bg-surface-950", defaultBorderMixin),
|
|
926
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
927
|
+
variant: "subtitle2",
|
|
928
|
+
className: "font-semibold text-[13px]",
|
|
929
|
+
children: "Permissions"
|
|
930
|
+
}), /* @__PURE__ */ jsx(Chip, {
|
|
931
|
+
size: "smallest",
|
|
932
|
+
className: "bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300",
|
|
933
|
+
children: selectedKey.permissions.length
|
|
934
|
+
})]
|
|
935
|
+
}),
|
|
936
|
+
/* @__PURE__ */ jsxs("div", {
|
|
937
|
+
className: "flex-1 overflow-y-auto px-5 py-3",
|
|
938
|
+
children: [selectedKey.admin && /* @__PURE__ */ jsxs("div", {
|
|
939
|
+
className: "flex items-start gap-2 mb-3 px-3 py-2 rounded-lg border border-amber-500/40 bg-amber-500/[0.06]",
|
|
940
|
+
children: [/* @__PURE__ */ jsx(ShieldIcon, {
|
|
941
|
+
size: iconSize.smallest,
|
|
942
|
+
className: "mt-[3px] shrink-0 text-amber-600 dark:text-amber-400"
|
|
943
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
944
|
+
variant: "caption",
|
|
945
|
+
className: "text-[12px] leading-snug text-amber-700 dark:text-amber-300",
|
|
946
|
+
children: [
|
|
947
|
+
"This key holds the ",
|
|
948
|
+
/* @__PURE__ */ jsx("span", {
|
|
949
|
+
className: "font-semibold",
|
|
950
|
+
children: "admin role"
|
|
951
|
+
}),
|
|
952
|
+
": it also passes the admin-gated routes — users, roles, cron, backups, logs — and reads through the",
|
|
953
|
+
/* @__PURE__ */ jsx("span", {
|
|
954
|
+
className: "font-mono",
|
|
955
|
+
children: " default_admin"
|
|
956
|
+
}),
|
|
957
|
+
" RLS policies, beyond the resources listed here."
|
|
958
|
+
]
|
|
959
|
+
})]
|
|
960
|
+
}), selectedKey.permissions.length === 0 ? /* @__PURE__ */ jsx(Typography, {
|
|
961
|
+
variant: "body2",
|
|
962
|
+
color: "disabled",
|
|
963
|
+
children: "No permissions configured"
|
|
964
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
965
|
+
className: "space-y-2",
|
|
966
|
+
children: selectedKey.permissions.map((perm, idx) => /* @__PURE__ */ jsxs("div", {
|
|
967
|
+
className: cls("flex items-center gap-3 px-3 py-2 rounded-lg border", defaultBorderMixin),
|
|
968
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
969
|
+
className: "flex-1 min-w-0",
|
|
970
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
971
|
+
variant: "body2",
|
|
972
|
+
className: "text-[13px] font-medium truncate",
|
|
973
|
+
children: resourceLabel(perm.collection)
|
|
974
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
975
|
+
variant: "caption",
|
|
976
|
+
color: "secondary",
|
|
977
|
+
className: "text-[11px]",
|
|
978
|
+
children: resourcePhrase(perm.collection)
|
|
979
|
+
})]
|
|
980
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
981
|
+
className: "flex items-center gap-1 shrink-0",
|
|
982
|
+
children: perm.operations.map((op) => /* @__PURE__ */ jsx(Chip, {
|
|
983
|
+
size: "smallest",
|
|
984
|
+
className: cls(op === "read" && "bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300", op === "write" && "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300", op === "delete" && "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300"),
|
|
985
|
+
children: op
|
|
986
|
+
}, op))
|
|
987
|
+
})]
|
|
988
|
+
}, idx))
|
|
989
|
+
})]
|
|
990
|
+
})
|
|
991
|
+
] })
|
|
992
|
+
})]
|
|
993
|
+
}),
|
|
994
|
+
/* @__PURE__ */ jsxs(Dialog, {
|
|
995
|
+
open: confirmRevoke !== null,
|
|
996
|
+
onOpenChange: (open) => {
|
|
997
|
+
if (!open && !revoking) setConfirmRevoke(null);
|
|
998
|
+
},
|
|
999
|
+
children: [
|
|
1000
|
+
/* @__PURE__ */ jsx(DialogTitle, {
|
|
1001
|
+
hidden: true,
|
|
1002
|
+
children: "Revoke Confirmation"
|
|
1003
|
+
}),
|
|
1004
|
+
/* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(Typography, {
|
|
1005
|
+
variant: "subtitle1",
|
|
1006
|
+
className: "font-semibold mb-2",
|
|
1007
|
+
children: [
|
|
1008
|
+
"Revoke \"",
|
|
1009
|
+
confirmRevoke?.name,
|
|
1010
|
+
"\"?"
|
|
1011
|
+
]
|
|
1012
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1013
|
+
variant: "body2",
|
|
1014
|
+
color: "secondary",
|
|
1015
|
+
children: "Requests authenticated with this key will stop working immediately. This action cannot be undone."
|
|
1016
|
+
})] }),
|
|
1017
|
+
/* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
|
|
1018
|
+
variant: "text",
|
|
1019
|
+
onClick: () => setConfirmRevoke(null),
|
|
1020
|
+
disabled: revoking !== null,
|
|
1021
|
+
children: "Cancel"
|
|
1022
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1023
|
+
color: "error",
|
|
1024
|
+
disabled: revoking !== null,
|
|
1025
|
+
startIcon: revoking !== null ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : /* @__PURE__ */ jsx(Trash2Icon, { size: iconSize.smallest }),
|
|
1026
|
+
onClick: async () => {
|
|
1027
|
+
if (!confirmRevoke) return;
|
|
1028
|
+
await handleRevoke(confirmRevoke.id);
|
|
1029
|
+
setConfirmRevoke(null);
|
|
1030
|
+
},
|
|
1031
|
+
children: "Revoke"
|
|
1032
|
+
})] })
|
|
1033
|
+
]
|
|
1034
|
+
}),
|
|
1035
|
+
showCreate && /* @__PURE__ */ jsx(CreateApiKeyDialog, {
|
|
1036
|
+
onClose: () => setShowCreate(false),
|
|
1037
|
+
onCreated: handleCreated
|
|
1038
|
+
}),
|
|
1039
|
+
showSecret && /* @__PURE__ */ jsx(SecretDisplayDialog, {
|
|
1040
|
+
keyWithSecret: showSecret,
|
|
1041
|
+
onClose: () => setShowSecret(null)
|
|
1042
|
+
})
|
|
1043
|
+
] });
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Marks a key that carries the admin role.
|
|
1047
|
+
*
|
|
1048
|
+
* Not cosmetic: an admin key reaches the admin routes and the `default_admin`
|
|
1049
|
+
* RLS policies, and without this it is indistinguishable in the list from a
|
|
1050
|
+
* read-only one.
|
|
1051
|
+
*/
|
|
1052
|
+
function AdminChip() {
|
|
1053
|
+
return /* @__PURE__ */ jsx(Tooltip, {
|
|
1054
|
+
title: "Carries the admin role: the admin-gated routes and the default_admin RLS policies",
|
|
1055
|
+
children: /* @__PURE__ */ jsxs(Chip, {
|
|
1056
|
+
size: "smallest",
|
|
1057
|
+
className: "shrink-0 bg-amber-500/12 dark:bg-amber-500/12 text-amber-700 dark:text-amber-300 border-amber-500/30 dark:border-amber-500/30",
|
|
1058
|
+
children: [/* @__PURE__ */ jsx(ShieldIcon, { size: 10 }), "admin"]
|
|
1059
|
+
})
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
function KeyListItem({ apiKey, selected, onClick }) {
|
|
1063
|
+
const status = keyStatus(apiKey);
|
|
1064
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1065
|
+
onClick,
|
|
1066
|
+
className: cls("flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all", selected ? "bg-primary/10 dark:bg-primary/15 ring-1 ring-primary/30" : "hover:bg-surface-100 dark:hover:bg-surface-950"),
|
|
1067
|
+
children: [
|
|
1068
|
+
/* @__PURE__ */ jsx("div", { className: cls("w-2 h-2 rounded-full shrink-0", status.label === "Active" ? "bg-emerald-400" : status.label === "Expired" ? "bg-amber-400" : "bg-red-400") }),
|
|
1069
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1070
|
+
className: "flex-1 min-w-0",
|
|
1071
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1072
|
+
className: "flex items-center gap-1.5 min-w-0",
|
|
1073
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
1074
|
+
variant: "body2",
|
|
1075
|
+
className: "truncate font-medium text-[13px]",
|
|
1076
|
+
children: apiKey.name
|
|
1077
|
+
}), apiKey.admin && /* @__PURE__ */ jsx(AdminChip, {})]
|
|
1078
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
1079
|
+
variant: "caption",
|
|
1080
|
+
color: "secondary",
|
|
1081
|
+
className: "truncate text-[11px] font-mono",
|
|
1082
|
+
children: [apiKey.key_prefix, "•••"]
|
|
1083
|
+
})]
|
|
1084
|
+
}),
|
|
1085
|
+
/* @__PURE__ */ jsx("div", {
|
|
1086
|
+
className: "shrink-0",
|
|
1087
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
1088
|
+
variant: "caption",
|
|
1089
|
+
color: "disabled",
|
|
1090
|
+
className: "text-[10px]",
|
|
1091
|
+
children: permissionSummary(apiKey.permissions)
|
|
1092
|
+
})
|
|
1093
|
+
})
|
|
1094
|
+
]
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
function StatCard({ label, value, mono, className }) {
|
|
1098
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1099
|
+
className: cls("px-3 py-2 rounded-lg border bg-white dark:bg-surface-900", defaultBorderMixin),
|
|
1100
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
1101
|
+
variant: "caption",
|
|
1102
|
+
color: "secondary",
|
|
1103
|
+
className: "text-[10px] uppercase tracking-wider font-medium",
|
|
1104
|
+
children: label
|
|
1105
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1106
|
+
variant: "body2",
|
|
1107
|
+
className: cls("mt-0.5 font-semibold text-[13px]", mono && "font-mono", className),
|
|
1108
|
+
children: value
|
|
1109
|
+
})]
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
function SecretDisplayDialog({ keyWithSecret, onClose }) {
|
|
1113
|
+
const snackbar = useSnackbarController();
|
|
1114
|
+
const [copied, setCopied] = useState(false);
|
|
1115
|
+
const handleCopy = async () => {
|
|
1116
|
+
try {
|
|
1117
|
+
await navigator.clipboard.writeText(keyWithSecret.key);
|
|
1118
|
+
setCopied(true);
|
|
1119
|
+
snackbar.open({
|
|
1120
|
+
type: "success",
|
|
1121
|
+
message: "API key copied to clipboard"
|
|
1122
|
+
});
|
|
1123
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
1124
|
+
} catch {
|
|
1125
|
+
snackbar.open({
|
|
1126
|
+
type: "error",
|
|
1127
|
+
message: "Failed to copy"
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
return /* @__PURE__ */ jsxs(Dialog, {
|
|
1132
|
+
open: true,
|
|
1133
|
+
onOpenChange: (open) => {
|
|
1134
|
+
if (!open) onClose();
|
|
1135
|
+
},
|
|
1136
|
+
maxWidth: "md",
|
|
1137
|
+
children: [
|
|
1138
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: /* @__PURE__ */ jsxs("div", {
|
|
1139
|
+
className: "flex items-center gap-2",
|
|
1140
|
+
children: [/* @__PURE__ */ jsx(CheckCircleIcon, {
|
|
1141
|
+
size: iconSize.small,
|
|
1142
|
+
className: "text-emerald-500"
|
|
1143
|
+
}), "API Key Created"]
|
|
1144
|
+
}) }),
|
|
1145
|
+
/* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
1146
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1147
|
+
className: "p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 mb-4",
|
|
1148
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1149
|
+
className: "flex items-center gap-2 mb-1",
|
|
1150
|
+
children: [/* @__PURE__ */ jsx(AlertCircleIcon, {
|
|
1151
|
+
size: iconSize.smallest,
|
|
1152
|
+
className: "text-amber-600 dark:text-amber-400"
|
|
1153
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1154
|
+
variant: "caption",
|
|
1155
|
+
className: "font-semibold text-amber-700 dark:text-amber-400",
|
|
1156
|
+
children: "Copy your key now — it won't be shown again"
|
|
1157
|
+
})]
|
|
1158
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1159
|
+
variant: "caption",
|
|
1160
|
+
className: "text-amber-600 dark:text-amber-300",
|
|
1161
|
+
children: "This is the only time the full API key will be displayed. Store it securely."
|
|
1162
|
+
})]
|
|
1163
|
+
}),
|
|
1164
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1165
|
+
className: cls("flex items-center gap-2 p-3 rounded-lg border bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
|
|
1166
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
1167
|
+
className: "flex-1 text-[12px] font-mono break-all text-surface-700 dark:text-surface-300 select-all",
|
|
1168
|
+
children: keyWithSecret.key
|
|
1169
|
+
}), /* @__PURE__ */ jsx(Tooltip, {
|
|
1170
|
+
title: copied ? "Copied!" : "Copy",
|
|
1171
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1172
|
+
size: "small",
|
|
1173
|
+
onClick: handleCopy,
|
|
1174
|
+
children: copied ? /* @__PURE__ */ jsx(CheckCircleIcon, {
|
|
1175
|
+
size: iconSize.smallest,
|
|
1176
|
+
className: "text-emerald-500"
|
|
1177
|
+
}) : /* @__PURE__ */ jsx(CopyIcon, { size: iconSize.smallest })
|
|
1178
|
+
})
|
|
1179
|
+
})]
|
|
1180
|
+
}),
|
|
1181
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1182
|
+
className: "mt-4 space-y-1",
|
|
1183
|
+
children: [
|
|
1184
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
1185
|
+
variant: "caption",
|
|
1186
|
+
color: "secondary",
|
|
1187
|
+
children: [
|
|
1188
|
+
/* @__PURE__ */ jsx("strong", { children: "Name:" }),
|
|
1189
|
+
" ",
|
|
1190
|
+
keyWithSecret.name
|
|
1191
|
+
]
|
|
1192
|
+
}),
|
|
1193
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
1194
|
+
variant: "caption",
|
|
1195
|
+
color: "secondary",
|
|
1196
|
+
children: [
|
|
1197
|
+
/* @__PURE__ */ jsx("strong", { children: "Access:" }),
|
|
1198
|
+
" ",
|
|
1199
|
+
permissionSummary(keyWithSecret.permissions)
|
|
1200
|
+
]
|
|
1201
|
+
}),
|
|
1202
|
+
keyWithSecret.admin && /* @__PURE__ */ jsxs(Typography, {
|
|
1203
|
+
variant: "caption",
|
|
1204
|
+
className: "flex items-center gap-1.5 text-amber-700 dark:text-amber-300",
|
|
1205
|
+
children: [/* @__PURE__ */ jsx(ShieldIcon, {
|
|
1206
|
+
size: iconSize.smallest,
|
|
1207
|
+
className: "shrink-0"
|
|
1208
|
+
}), /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("strong", { children: "Admin role granted" }), " — the admin routes and the default_admin policies"] })]
|
|
1209
|
+
})
|
|
1210
|
+
]
|
|
1211
|
+
})
|
|
1212
|
+
] }),
|
|
1213
|
+
/* @__PURE__ */ jsx(DialogActions, { children: /* @__PURE__ */ jsx(Button, {
|
|
1214
|
+
color: "primary",
|
|
1215
|
+
onClick: onClose,
|
|
1216
|
+
children: "Done"
|
|
1217
|
+
}) })
|
|
1218
|
+
]
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
//#endregion
|
|
1222
|
+
export { ApiKeysView };
|
|
1223
|
+
|
|
1224
|
+
//# sourceMappingURL=ApiKeysView-DiCurTEU.js.map
|