apiblaze 0.18.1 → 0.18.3
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/index.js +1 -1
- package/dist/react/index.js +96 -4
- package/dist/react/index.mjs +96 -4
- package/dist/server/index.d.mts +8 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +10 -1
- package/dist/server/index.mjs +10 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -78,6 +78,7 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
78
78
|
const t = { ...DEFAULTS, ...theme ?? {} };
|
|
79
79
|
const headerBg = t.headerBackground || t.surface;
|
|
80
80
|
const [access, setAccess] = React.useState("ok");
|
|
81
|
+
const [myEmail, setMyEmail] = React.useState(null);
|
|
81
82
|
const [groups, setGroups] = React.useState(null);
|
|
82
83
|
const [users, setUsers] = React.useState([]);
|
|
83
84
|
const [observed, setObserved] = React.useState([]);
|
|
@@ -95,6 +96,9 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
95
96
|
const [addSel, setAddSel] = React.useState("");
|
|
96
97
|
const [addRole, setAddRole] = React.useState("member");
|
|
97
98
|
const [nestSel, setNestSel] = React.useState("");
|
|
99
|
+
const [admins, setAdmins] = React.useState([]);
|
|
100
|
+
const [showAdmins, setShowAdmins] = React.useState(false);
|
|
101
|
+
const [newAdmin, setNewAdmin] = React.useState("");
|
|
98
102
|
const call = React.useCallback(async (body) => {
|
|
99
103
|
const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
|
|
100
104
|
const data = await res.json().catch(() => ({}));
|
|
@@ -106,9 +110,14 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
106
110
|
try {
|
|
107
111
|
const d = await call();
|
|
108
112
|
setAccess(d.access === "pending" ? "pending" : "ok");
|
|
113
|
+
setMyEmail(d.email ?? null);
|
|
109
114
|
setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
|
|
110
115
|
setUsers(d.users ?? []);
|
|
111
116
|
setObserved(d.observed ?? []);
|
|
117
|
+
if (d.access !== "pending") {
|
|
118
|
+
const a = await call({ action: "list-admins" });
|
|
119
|
+
setAdmins(a.admin_emails ?? []);
|
|
120
|
+
}
|
|
112
121
|
} catch (e) {
|
|
113
122
|
setLoadError(e instanceof Error ? e.message : "Could not load groups.");
|
|
114
123
|
setGroups(null);
|
|
@@ -170,6 +179,11 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
170
179
|
setCreating(false);
|
|
171
180
|
}
|
|
172
181
|
}
|
|
182
|
+
async function addAdmin() {
|
|
183
|
+
const email = newAdmin.trim().toLowerCase();
|
|
184
|
+
if (!email) return;
|
|
185
|
+
await act("add-admin", { action: "add-admin", email }, () => setNewAdmin(""));
|
|
186
|
+
}
|
|
173
187
|
const vars = {
|
|
174
188
|
// @ts-expect-error CSS custom properties
|
|
175
189
|
"--abz-accent": t.accent,
|
|
@@ -211,10 +225,34 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
211
225
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 15 }, children: title }),
|
|
212
226
|
access === "ok" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("primary", true), disabled: busy, onClick: () => setShowCreate((v) => !v), "aria-label": "Create a new group", children: "+ New group" })
|
|
213
227
|
] }),
|
|
214
|
-
access === "pending" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: [
|
|
215
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight:
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
access === "pending" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted, lineHeight: 1.5 }, children: [
|
|
229
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 700, color: t.text, marginBottom: 6, fontSize: 15 }, children: "Admin access required" }),
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 12 }, children: [
|
|
231
|
+
"Managing users & groups is limited to ",
|
|
232
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "workspace admins" }),
|
|
233
|
+
". You\u2019re signed in",
|
|
234
|
+
myEmail ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
235
|
+
" as ",
|
|
236
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
|
|
237
|
+
] }) : "",
|
|
238
|
+
", but this account isn\u2019t an admin yet."
|
|
239
|
+
] }),
|
|
240
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { textAlign: "left", maxWidth: 420, margin: "0 auto 12px", paddingLeft: 18 }, children: [
|
|
241
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
242
|
+
"If your API provider just made you an admin, it activates the first time you open this \u2014 ",
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Refresh" }),
|
|
244
|
+
"."
|
|
245
|
+
] }),
|
|
246
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
247
|
+
"Otherwise, ask your API provider to add",
|
|
248
|
+
myEmail ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
249
|
+
" ",
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
|
|
251
|
+
] }) : " your email",
|
|
252
|
+
" to the workspace\u2019s admin list."
|
|
253
|
+
] })
|
|
254
|
+
] }),
|
|
255
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: load, children: "Refresh" })
|
|
218
256
|
] }),
|
|
219
257
|
access === "ok" && showCreate && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, padding: "12px 16px", borderBottom: `1px solid ${t.border}` }, children: [
|
|
220
258
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -413,6 +451,60 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
413
451
|
] })
|
|
414
452
|
] }, g.id);
|
|
415
453
|
}) }),
|
|
454
|
+
access === "ok" && groups !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
|
|
455
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
456
|
+
"button",
|
|
457
|
+
{
|
|
458
|
+
onClick: () => setShowAdmins((v) => !v),
|
|
459
|
+
"aria-expanded": showAdmins,
|
|
460
|
+
style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 13, textAlign: "left" },
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: showAdmins ? "\u25BE" : "\u25B8" }),
|
|
463
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { flex: 1 }, children: "Admins" }),
|
|
464
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, children: admins.length })
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
),
|
|
468
|
+
showAdmins && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "0 16px 14px 34px" }, children: [
|
|
469
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: [
|
|
470
|
+
admins.map((a) => {
|
|
471
|
+
const removable = !a.pinned && admins.length > 1;
|
|
472
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
|
|
473
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, minWidth: 160 }, children: a.email }),
|
|
474
|
+
a.pinned && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, title: "Set by the API provider \u2014 you can\u2019t remove this one", children: "provider" }),
|
|
475
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...chip, ...a.active ? { color: t.success, borderColor: t.success } : {} }, children: a.active ? "active" : "pending sign-in" }),
|
|
476
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
477
|
+
"button",
|
|
478
|
+
{
|
|
479
|
+
style: { ...btn("danger", true), ...removable ? {} : { opacity: 0.4, cursor: "not-allowed" } },
|
|
480
|
+
disabled: busy || !removable,
|
|
481
|
+
title: a.pinned ? "Provider-set admins can only be removed by the API provider" : admins.length <= 1 ? "A tenant must keep at least one admin" : `Remove ${a.email} as an admin`,
|
|
482
|
+
onClick: () => act(`rm-admin:${a.email}`, { action: "remove-admin", email: a.email }),
|
|
483
|
+
children: "Remove"
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
] }, a.email);
|
|
487
|
+
}),
|
|
488
|
+
admins.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { color: t.muted, fontSize: 13, padding: "6px 0" }, children: "No admins yet." })
|
|
489
|
+
] }),
|
|
490
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children: [
|
|
491
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
492
|
+
"input",
|
|
493
|
+
{
|
|
494
|
+
style: { ...inputStyle, flex: 1 },
|
|
495
|
+
type: "email",
|
|
496
|
+
placeholder: "Add an admin by email",
|
|
497
|
+
value: newAdmin,
|
|
498
|
+
onChange: (e) => setNewAdmin(e.target.value),
|
|
499
|
+
onKeyDown: (e) => e.key === "Enter" && addAdmin(),
|
|
500
|
+
"aria-label": "New admin email"
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("primary", true), disabled: busy || !newAdmin.trim(), onClick: addAdmin, children: "Add admin" })
|
|
504
|
+
] }),
|
|
505
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: t.muted, marginTop: 8 }, children: "A new admin gets access the first time they sign in. Admins are granted by email \u2014 never by adding someone to a group." })
|
|
506
|
+
] })
|
|
507
|
+
] }),
|
|
416
508
|
access === "ok" && groups !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
|
|
417
509
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
418
510
|
"button",
|
package/dist/react/index.mjs
CHANGED
|
@@ -43,6 +43,7 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
43
43
|
const t = { ...DEFAULTS, ...theme ?? {} };
|
|
44
44
|
const headerBg = t.headerBackground || t.surface;
|
|
45
45
|
const [access, setAccess] = React.useState("ok");
|
|
46
|
+
const [myEmail, setMyEmail] = React.useState(null);
|
|
46
47
|
const [groups, setGroups] = React.useState(null);
|
|
47
48
|
const [users, setUsers] = React.useState([]);
|
|
48
49
|
const [observed, setObserved] = React.useState([]);
|
|
@@ -60,6 +61,9 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
60
61
|
const [addSel, setAddSel] = React.useState("");
|
|
61
62
|
const [addRole, setAddRole] = React.useState("member");
|
|
62
63
|
const [nestSel, setNestSel] = React.useState("");
|
|
64
|
+
const [admins, setAdmins] = React.useState([]);
|
|
65
|
+
const [showAdmins, setShowAdmins] = React.useState(false);
|
|
66
|
+
const [newAdmin, setNewAdmin] = React.useState("");
|
|
63
67
|
const call = React.useCallback(async (body) => {
|
|
64
68
|
const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
|
|
65
69
|
const data = await res.json().catch(() => ({}));
|
|
@@ -71,9 +75,14 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
71
75
|
try {
|
|
72
76
|
const d = await call();
|
|
73
77
|
setAccess(d.access === "pending" ? "pending" : "ok");
|
|
78
|
+
setMyEmail(d.email ?? null);
|
|
74
79
|
setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
|
|
75
80
|
setUsers(d.users ?? []);
|
|
76
81
|
setObserved(d.observed ?? []);
|
|
82
|
+
if (d.access !== "pending") {
|
|
83
|
+
const a = await call({ action: "list-admins" });
|
|
84
|
+
setAdmins(a.admin_emails ?? []);
|
|
85
|
+
}
|
|
77
86
|
} catch (e) {
|
|
78
87
|
setLoadError(e instanceof Error ? e.message : "Could not load groups.");
|
|
79
88
|
setGroups(null);
|
|
@@ -135,6 +144,11 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
135
144
|
setCreating(false);
|
|
136
145
|
}
|
|
137
146
|
}
|
|
147
|
+
async function addAdmin() {
|
|
148
|
+
const email = newAdmin.trim().toLowerCase();
|
|
149
|
+
if (!email) return;
|
|
150
|
+
await act("add-admin", { action: "add-admin", email }, () => setNewAdmin(""));
|
|
151
|
+
}
|
|
138
152
|
const vars = {
|
|
139
153
|
// @ts-expect-error CSS custom properties
|
|
140
154
|
"--abz-accent": t.accent,
|
|
@@ -176,10 +190,34 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
176
190
|
/* @__PURE__ */ jsx("strong", { style: { fontSize: 15 }, children: title }),
|
|
177
191
|
access === "ok" && /* @__PURE__ */ jsx("button", { style: btn("primary", true), disabled: busy, onClick: () => setShowCreate((v) => !v), "aria-label": "Create a new group", children: "+ New group" })
|
|
178
192
|
] }),
|
|
179
|
-
access === "pending" && /* @__PURE__ */ jsxs("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: [
|
|
180
|
-
/* @__PURE__ */ jsx("div", { style: { fontWeight:
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
access === "pending" && /* @__PURE__ */ jsxs("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted, lineHeight: 1.5 }, children: [
|
|
194
|
+
/* @__PURE__ */ jsx("div", { style: { fontWeight: 700, color: t.text, marginBottom: 6, fontSize: 15 }, children: "Admin access required" }),
|
|
195
|
+
/* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
|
|
196
|
+
"Managing users & groups is limited to ",
|
|
197
|
+
/* @__PURE__ */ jsx("strong", { children: "workspace admins" }),
|
|
198
|
+
". You\u2019re signed in",
|
|
199
|
+
myEmail ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
200
|
+
" as ",
|
|
201
|
+
/* @__PURE__ */ jsx("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
|
|
202
|
+
] }) : "",
|
|
203
|
+
", but this account isn\u2019t an admin yet."
|
|
204
|
+
] }),
|
|
205
|
+
/* @__PURE__ */ jsxs("ul", { style: { textAlign: "left", maxWidth: 420, margin: "0 auto 12px", paddingLeft: 18 }, children: [
|
|
206
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
207
|
+
"If your API provider just made you an admin, it activates the first time you open this \u2014 ",
|
|
208
|
+
/* @__PURE__ */ jsx("strong", { children: "Refresh" }),
|
|
209
|
+
"."
|
|
210
|
+
] }),
|
|
211
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
212
|
+
"Otherwise, ask your API provider to add",
|
|
213
|
+
myEmail ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
214
|
+
" ",
|
|
215
|
+
/* @__PURE__ */ jsx("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
|
|
216
|
+
] }) : " your email",
|
|
217
|
+
" to the workspace\u2019s admin list."
|
|
218
|
+
] })
|
|
219
|
+
] }),
|
|
220
|
+
/* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: load, children: "Refresh" })
|
|
183
221
|
] }),
|
|
184
222
|
access === "ok" && showCreate && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, padding: "12px 16px", borderBottom: `1px solid ${t.border}` }, children: [
|
|
185
223
|
/* @__PURE__ */ jsx(
|
|
@@ -378,6 +416,60 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
|
|
|
378
416
|
] })
|
|
379
417
|
] }, g.id);
|
|
380
418
|
}) }),
|
|
419
|
+
access === "ok" && groups !== null && /* @__PURE__ */ jsxs("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
|
|
420
|
+
/* @__PURE__ */ jsxs(
|
|
421
|
+
"button",
|
|
422
|
+
{
|
|
423
|
+
onClick: () => setShowAdmins((v) => !v),
|
|
424
|
+
"aria-expanded": showAdmins,
|
|
425
|
+
style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 13, textAlign: "left" },
|
|
426
|
+
children: [
|
|
427
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: showAdmins ? "\u25BE" : "\u25B8" }),
|
|
428
|
+
/* @__PURE__ */ jsx("strong", { style: { flex: 1 }, children: "Admins" }),
|
|
429
|
+
/* @__PURE__ */ jsx("span", { style: chip, children: admins.length })
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
),
|
|
433
|
+
showAdmins && /* @__PURE__ */ jsxs("div", { style: { padding: "0 16px 14px 34px" }, children: [
|
|
434
|
+
/* @__PURE__ */ jsxs("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: [
|
|
435
|
+
admins.map((a) => {
|
|
436
|
+
const removable = !a.pinned && admins.length > 1;
|
|
437
|
+
return /* @__PURE__ */ jsxs("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
|
|
438
|
+
/* @__PURE__ */ jsx("span", { style: { flex: 1, minWidth: 160 }, children: a.email }),
|
|
439
|
+
a.pinned && /* @__PURE__ */ jsx("span", { style: chip, title: "Set by the API provider \u2014 you can\u2019t remove this one", children: "provider" }),
|
|
440
|
+
/* @__PURE__ */ jsx("span", { style: { ...chip, ...a.active ? { color: t.success, borderColor: t.success } : {} }, children: a.active ? "active" : "pending sign-in" }),
|
|
441
|
+
/* @__PURE__ */ jsx(
|
|
442
|
+
"button",
|
|
443
|
+
{
|
|
444
|
+
style: { ...btn("danger", true), ...removable ? {} : { opacity: 0.4, cursor: "not-allowed" } },
|
|
445
|
+
disabled: busy || !removable,
|
|
446
|
+
title: a.pinned ? "Provider-set admins can only be removed by the API provider" : admins.length <= 1 ? "A tenant must keep at least one admin" : `Remove ${a.email} as an admin`,
|
|
447
|
+
onClick: () => act(`rm-admin:${a.email}`, { action: "remove-admin", email: a.email }),
|
|
448
|
+
children: "Remove"
|
|
449
|
+
}
|
|
450
|
+
)
|
|
451
|
+
] }, a.email);
|
|
452
|
+
}),
|
|
453
|
+
admins.length === 0 && /* @__PURE__ */ jsx("li", { style: { color: t.muted, fontSize: 13, padding: "6px 0" }, children: "No admins yet." })
|
|
454
|
+
] }),
|
|
455
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children: [
|
|
456
|
+
/* @__PURE__ */ jsx(
|
|
457
|
+
"input",
|
|
458
|
+
{
|
|
459
|
+
style: { ...inputStyle, flex: 1 },
|
|
460
|
+
type: "email",
|
|
461
|
+
placeholder: "Add an admin by email",
|
|
462
|
+
value: newAdmin,
|
|
463
|
+
onChange: (e) => setNewAdmin(e.target.value),
|
|
464
|
+
onKeyDown: (e) => e.key === "Enter" && addAdmin(),
|
|
465
|
+
"aria-label": "New admin email"
|
|
466
|
+
}
|
|
467
|
+
),
|
|
468
|
+
/* @__PURE__ */ jsx("button", { style: btn("primary", true), disabled: busy || !newAdmin.trim(), onClick: addAdmin, children: "Add admin" })
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: t.muted, marginTop: 8 }, children: "A new admin gets access the first time they sign in. Admins are granted by email \u2014 never by adding someone to a group." })
|
|
471
|
+
] })
|
|
472
|
+
] }),
|
|
381
473
|
access === "ok" && groups !== null && /* @__PURE__ */ jsxs("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
|
|
382
474
|
/* @__PURE__ */ jsxs(
|
|
383
475
|
"button",
|
package/dist/server/index.d.mts
CHANGED
|
@@ -116,6 +116,14 @@ type GroupsAction = {
|
|
|
116
116
|
email?: string;
|
|
117
117
|
displayName?: string;
|
|
118
118
|
groups?: string[];
|
|
119
|
+
} | {
|
|
120
|
+
action: 'list-admins';
|
|
121
|
+
} | {
|
|
122
|
+
action: 'add-admin';
|
|
123
|
+
email: string;
|
|
124
|
+
} | {
|
|
125
|
+
action: 'remove-admin';
|
|
126
|
+
email: string;
|
|
119
127
|
};
|
|
120
128
|
declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): {
|
|
121
129
|
handler: (req: Request) => Promise<Response>;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -116,6 +116,14 @@ type GroupsAction = {
|
|
|
116
116
|
email?: string;
|
|
117
117
|
displayName?: string;
|
|
118
118
|
groups?: string[];
|
|
119
|
+
} | {
|
|
120
|
+
action: 'list-admins';
|
|
121
|
+
} | {
|
|
122
|
+
action: 'add-admin';
|
|
123
|
+
email: string;
|
|
124
|
+
} | {
|
|
125
|
+
action: 'remove-admin';
|
|
126
|
+
email: string;
|
|
119
127
|
};
|
|
120
128
|
declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): {
|
|
121
129
|
handler: (req: Request) => Promise<Response>;
|
package/dist/server/index.js
CHANGED
|
@@ -107,7 +107,7 @@ function createApiblazeGroups(cfg) {
|
|
|
107
107
|
iam("GET", "/users", user, tenant),
|
|
108
108
|
iam("GET", "/observed-identities", user, tenant)
|
|
109
109
|
]);
|
|
110
|
-
if (groupsRes.status === 403) return { status: 200, data: { access: "pending", groups: [], users: [], observed: [] } };
|
|
110
|
+
if (groupsRes.status === 403) return { status: 200, data: { access: "pending", email: user.email ?? null, groups: [], users: [], observed: [] } };
|
|
111
111
|
if (!groupsRes.ok) return { status: groupsRes.status, data: { error: await safeErr(groupsRes) } };
|
|
112
112
|
const g = await groupsRes.json();
|
|
113
113
|
const u = usersRes.ok ? await usersRes.json() : { users: [] };
|
|
@@ -172,6 +172,15 @@ function createApiblazeGroups(cfg) {
|
|
|
172
172
|
display_name: op.displayName,
|
|
173
173
|
groups: op.groups ?? []
|
|
174
174
|
}));
|
|
175
|
+
// Tenant-admin allowlist — appoint/remove CO-admins by email. This is the
|
|
176
|
+
// ONLY way admin membership changes (apiblaze_admins is sealed); the plane
|
|
177
|
+
// enforces the guardrails (producer-pinned + last-admin).
|
|
178
|
+
case "list-admins":
|
|
179
|
+
return pass(await iam("GET", "/admin-emails", user, tenant));
|
|
180
|
+
case "add-admin":
|
|
181
|
+
return pass(await iam("POST", "/admin-emails", user, tenant, { email: op.email }));
|
|
182
|
+
case "remove-admin":
|
|
183
|
+
return pass(await iam("DELETE", `/admin-emails/${encodeURIComponent(op.email)}`, user, tenant));
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
async function handler(req) {
|
package/dist/server/index.mjs
CHANGED
|
@@ -79,7 +79,7 @@ function createApiblazeGroups(cfg) {
|
|
|
79
79
|
iam("GET", "/users", user, tenant),
|
|
80
80
|
iam("GET", "/observed-identities", user, tenant)
|
|
81
81
|
]);
|
|
82
|
-
if (groupsRes.status === 403) return { status: 200, data: { access: "pending", groups: [], users: [], observed: [] } };
|
|
82
|
+
if (groupsRes.status === 403) return { status: 200, data: { access: "pending", email: user.email ?? null, groups: [], users: [], observed: [] } };
|
|
83
83
|
if (!groupsRes.ok) return { status: groupsRes.status, data: { error: await safeErr(groupsRes) } };
|
|
84
84
|
const g = await groupsRes.json();
|
|
85
85
|
const u = usersRes.ok ? await usersRes.json() : { users: [] };
|
|
@@ -144,6 +144,15 @@ function createApiblazeGroups(cfg) {
|
|
|
144
144
|
display_name: op.displayName,
|
|
145
145
|
groups: op.groups ?? []
|
|
146
146
|
}));
|
|
147
|
+
// Tenant-admin allowlist — appoint/remove CO-admins by email. This is the
|
|
148
|
+
// ONLY way admin membership changes (apiblaze_admins is sealed); the plane
|
|
149
|
+
// enforces the guardrails (producer-pinned + last-admin).
|
|
150
|
+
case "list-admins":
|
|
151
|
+
return pass(await iam("GET", "/admin-emails", user, tenant));
|
|
152
|
+
case "add-admin":
|
|
153
|
+
return pass(await iam("POST", "/admin-emails", user, tenant, { email: op.email }));
|
|
154
|
+
case "remove-admin":
|
|
155
|
+
return pass(await iam("DELETE", `/admin-emails/${encodeURIComponent(op.email)}`, user, tenant));
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
async function handler(req) {
|