@rebasepro/app 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/RlsSimulatorPanel-DOfmVgM3.js +403 -0
- package/dist/RlsSimulatorPanel-DOfmVgM3.js.map +1 -0
- package/dist/collections/form-layout.d.ts +30 -27
- package/dist/components/LanguageToggle.d.ts +9 -0
- package/dist/components/LoginView/index.d.ts +7 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/studio/RlsSimulatorBar.d.ts +39 -0
- package/dist/components/studio/RlsSimulatorPanel.d.ts +29 -0
- package/dist/contexts/RlsSimulationContext.d.ts +47 -0
- package/dist/contexts/index.d.ts +1 -0
- package/dist/core/effective_role_state.d.ts +9 -0
- package/dist/{de-BMgOgwue.js → de-3yzzpuZj.js} +175 -1
- package/dist/de-3yzzpuZj.js.map +1 -0
- package/dist/debug.js +96 -46
- package/dist/{es-DSOOSgPW.js → es-mnyTaRkK.js} +175 -1
- package/dist/es-mnyTaRkK.js.map +1 -0
- package/dist/{fr-CnXidmZf.js → fr-nJHADzlI.js} +175 -1
- package/dist/fr-nJHADzlI.js.map +1 -0
- package/dist/{hi-lTvNsFMr.js → hi-B9_iunnL.js} +175 -1
- package/dist/hi-B9_iunnL.js.map +1 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +9 -3
- package/dist/hooks/usePermissions.d.ts +4 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +7 -1
- package/dist/i18n/RebaseI18nProvider.d.ts +11 -0
- package/dist/index.es.js +2814 -2555
- package/dist/index.es.js.map +1 -1
- package/dist/{it-BoT_vBlx.js → it-DoEtz-OK.js} +175 -1
- package/dist/it-DoEtz-OK.js.map +1 -0
- package/dist/{pt-D9mSchbY.js → pt-DT26AmBU.js} +175 -1
- package/dist/pt-DT26AmBU.js.map +1 -0
- package/package.json +7 -7
- package/dist/de-BMgOgwue.js.map +0 -1
- package/dist/es-DSOOSgPW.js.map +0 -1
- package/dist/fr-CnXidmZf.js.map +0 -1
- package/dist/hi-lTvNsFMr.js.map +0 -1
- package/dist/it-BoT_vBlx.js.map +0 -1
- package/dist/pt-D9mSchbY.js.map +0 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ This package is **framework-agnostic** in the sense that it doesn't depend on an
|
|
|
58
58
|
| `useAuthSubscription` | Subscribe to auth state changes |
|
|
59
59
|
| `usePermissions` | Check permissions for collections/actions |
|
|
60
60
|
| `useAdminModeController` / `useBuildAdminModeController` | Admin mode toggle |
|
|
61
|
-
| `useEffectiveRoleController`
|
|
61
|
+
| `useEffectiveRoleController` | The role Studio's navigation is listed for (`useBuildEffectiveRoleController` is deprecated: use Simulate access to test permissions) |
|
|
62
62
|
|
|
63
63
|
### Hooks — UI & State
|
|
64
64
|
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { useApiBase, useApiConfig, useCanSimulateAccess, useRebaseClient, useRlsSimulation, useTranslation } from "./index.es.js";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { Alert, Button, CircularProgress, EyeIcon, IconButton, KeyRoundIcon, Popover, ShieldIcon, Tab, Tabs, TextField, Typography, UserIcon, cls, iconSize } from "@rebasepro/ui";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/studio/RlsSimulatorPanel.tsx
|
|
6
|
+
/**
|
|
7
|
+
* The "Simulate access" pill and banner themselves. Loaded on demand by the
|
|
8
|
+
* wrappers in `RlsSimulatorBar.tsx`: only an administrator ever sees them, so
|
|
9
|
+
* nobody else downloads them before the login screen paints.
|
|
10
|
+
*/
|
|
11
|
+
/** How many users one search of the picker lists. */
|
|
12
|
+
var USER_PAGE_SIZE = 20;
|
|
13
|
+
/**
|
|
14
|
+
* A request to the simulator's admin surface with the administrator's own
|
|
15
|
+
* token — never through the simulated client, whose headers the admin routes
|
|
16
|
+
* refuse.
|
|
17
|
+
*/
|
|
18
|
+
function useSimulatorRequest() {
|
|
19
|
+
const apiConfig = useApiConfig();
|
|
20
|
+
const apiBase = useApiBase();
|
|
21
|
+
return useCallback(async (path, init) => {
|
|
22
|
+
if (!apiBase) throw new Error("No API URL is configured.");
|
|
23
|
+
const token = await apiConfig?.getAuthToken?.();
|
|
24
|
+
const response = await fetch(`${apiBase}/admin/rls-simulator${path}`, {
|
|
25
|
+
method: init?.method ?? "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
29
|
+
},
|
|
30
|
+
...init?.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
|
|
31
|
+
});
|
|
32
|
+
const json = await response.json().catch(() => void 0);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const message = typeof json === "object" && json !== null && "error" in json && typeof json.error === "object" && json.error !== null && "message" in json.error && typeof json.error.message === "string" ? json.error.message : response.statusText;
|
|
35
|
+
throw new Error(message);
|
|
36
|
+
}
|
|
37
|
+
return json;
|
|
38
|
+
}, [apiBase, apiConfig]);
|
|
39
|
+
}
|
|
40
|
+
/** One choosable row of the popover's lists. */
|
|
41
|
+
function ChoiceRow({ selected, onClick, icon, title, detail }) {
|
|
42
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
43
|
+
variant: "text",
|
|
44
|
+
size: "small",
|
|
45
|
+
fullWidth: true,
|
|
46
|
+
onClick,
|
|
47
|
+
className: cls("justify-start", { "bg-surface-active": selected }),
|
|
48
|
+
children: [
|
|
49
|
+
icon,
|
|
50
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
51
|
+
variant: "body2",
|
|
52
|
+
noWrap: true,
|
|
53
|
+
className: "truncate",
|
|
54
|
+
children: title
|
|
55
|
+
}),
|
|
56
|
+
detail && /* @__PURE__ */ jsx(Typography, {
|
|
57
|
+
variant: "caption",
|
|
58
|
+
color: "secondary",
|
|
59
|
+
noWrap: true,
|
|
60
|
+
className: "truncate",
|
|
61
|
+
children: detail
|
|
62
|
+
})
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* "Simulate access": the app bar pill that runs every data view as a role, a
|
|
68
|
+
* user or a service key, so an administrator sees exactly the rows and fields
|
|
69
|
+
* that identity's row-level security lets it read.
|
|
70
|
+
*
|
|
71
|
+
* Shown only to an administrator, and only when the data views ride a client
|
|
72
|
+
* that can derive a simulated one. The server checks the first again on every
|
|
73
|
+
* request: anyone else sending the simulation headers gets a 403.
|
|
74
|
+
*
|
|
75
|
+
* @group Components
|
|
76
|
+
*/
|
|
77
|
+
function RlsSimulatorBarPanel({ className, compact, side, align = "end", portalContainer }) {
|
|
78
|
+
const { t } = useTranslation();
|
|
79
|
+
const controller = useRlsSimulation();
|
|
80
|
+
const canSimulate = useCanSimulateAccess();
|
|
81
|
+
const request = useSimulatorRequest();
|
|
82
|
+
const client = useRebaseClient();
|
|
83
|
+
const [open, setOpen] = useState(false);
|
|
84
|
+
const [tab, setTab] = useState("role");
|
|
85
|
+
const [options, setOptions] = useState(null);
|
|
86
|
+
const [optionsError, setOptionsError] = useState(null);
|
|
87
|
+
const [role, setRole] = useState(null);
|
|
88
|
+
const [roleUserId, setRoleUserId] = useState("");
|
|
89
|
+
const [userSearch, setUserSearch] = useState("");
|
|
90
|
+
const [users, setUsers] = useState([]);
|
|
91
|
+
const [usersLoading, setUsersLoading] = useState(false);
|
|
92
|
+
const [usersError, setUsersError] = useState(null);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!open || options) return;
|
|
95
|
+
let cancelled = false;
|
|
96
|
+
setOptionsError(null);
|
|
97
|
+
request("/options").then((loaded) => {
|
|
98
|
+
if (!cancelled) setOptions(loaded);
|
|
99
|
+
}).catch((error) => {
|
|
100
|
+
if (!cancelled) setOptionsError(error instanceof Error ? error.message : String(error));
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
};
|
|
105
|
+
}, [
|
|
106
|
+
open,
|
|
107
|
+
options,
|
|
108
|
+
request
|
|
109
|
+
]);
|
|
110
|
+
const adminApi = client?.admin;
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!open || tab !== "user" || !adminApi) return;
|
|
113
|
+
let cancelled = false;
|
|
114
|
+
setUsersLoading(true);
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
adminApi.listUsersPaginated({
|
|
117
|
+
search: userSearch || void 0,
|
|
118
|
+
limit: USER_PAGE_SIZE
|
|
119
|
+
}).then(({ users: found }) => {
|
|
120
|
+
if (cancelled) return;
|
|
121
|
+
setUsersError(null);
|
|
122
|
+
setUsers(found.filter((user) => !user.disabled).map((user) => ({
|
|
123
|
+
uid: user.uid,
|
|
124
|
+
label: user.email ?? user.displayName ?? user.uid,
|
|
125
|
+
detail: user.roles?.length ? user.roles.join(", ") : void 0
|
|
126
|
+
})));
|
|
127
|
+
}).catch((error) => {
|
|
128
|
+
if (cancelled) return;
|
|
129
|
+
setUsers([]);
|
|
130
|
+
setUsersError(error instanceof Error ? error.message : String(error));
|
|
131
|
+
}).finally(() => {
|
|
132
|
+
if (!cancelled) setUsersLoading(false);
|
|
133
|
+
});
|
|
134
|
+
}, 250);
|
|
135
|
+
return () => {
|
|
136
|
+
cancelled = true;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
};
|
|
139
|
+
}, [
|
|
140
|
+
open,
|
|
141
|
+
tab,
|
|
142
|
+
userSearch,
|
|
143
|
+
adminApi
|
|
144
|
+
]);
|
|
145
|
+
const start = (simulation, label) => {
|
|
146
|
+
controller.setSimulation(simulation, label);
|
|
147
|
+
setOpen(false);
|
|
148
|
+
};
|
|
149
|
+
if (!canSimulate) return null;
|
|
150
|
+
const active = controller.simulation !== null;
|
|
151
|
+
return /* @__PURE__ */ jsx(Popover, {
|
|
152
|
+
trigger: compact ? /* @__PURE__ */ jsxs(IconButton, {
|
|
153
|
+
"aria-label": t("rls_simulate_access"),
|
|
154
|
+
className: cls("relative", className),
|
|
155
|
+
children: [/* @__PURE__ */ jsx(EyeIcon, { size: iconSize.small }), active && /* @__PURE__ */ jsx("span", {
|
|
156
|
+
"aria-hidden": true,
|
|
157
|
+
className: "absolute top-1 right-1 w-2 h-2 rounded-full bg-amber-500"
|
|
158
|
+
})]
|
|
159
|
+
}) : /* @__PURE__ */ jsxs(Button, {
|
|
160
|
+
variant: "outlined",
|
|
161
|
+
size: "small",
|
|
162
|
+
"aria-label": t("rls_simulate_access"),
|
|
163
|
+
className: cls("rounded-full whitespace-nowrap", className),
|
|
164
|
+
children: [active ? /* @__PURE__ */ jsx("span", {
|
|
165
|
+
"aria-hidden": true,
|
|
166
|
+
className: "w-2 h-2 rounded-full bg-amber-500"
|
|
167
|
+
}) : /* @__PURE__ */ jsx(EyeIcon, { size: iconSize.smallest }), t("rls_simulate_access")]
|
|
168
|
+
}),
|
|
169
|
+
open,
|
|
170
|
+
onOpenChange: setOpen,
|
|
171
|
+
side,
|
|
172
|
+
align,
|
|
173
|
+
portalContainer,
|
|
174
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
175
|
+
className: "flex flex-col gap-3 p-3 w-[22rem]",
|
|
176
|
+
children: [
|
|
177
|
+
/* @__PURE__ */ jsxs("div", {
|
|
178
|
+
className: "flex flex-col gap-1",
|
|
179
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
180
|
+
variant: "label",
|
|
181
|
+
children: t("rls_simulate_access")
|
|
182
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
183
|
+
variant: "caption",
|
|
184
|
+
color: "secondary",
|
|
185
|
+
children: t("rls_simulate_access_description")
|
|
186
|
+
})]
|
|
187
|
+
}),
|
|
188
|
+
/* @__PURE__ */ jsxs(Tabs, {
|
|
189
|
+
value: tab,
|
|
190
|
+
onValueChange: (value) => setTab(value === "user" || value === "apiKey" ? value : "role"),
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ jsx(Tab, {
|
|
193
|
+
value: "role",
|
|
194
|
+
children: t("rls_simulate_tab_role")
|
|
195
|
+
}),
|
|
196
|
+
/* @__PURE__ */ jsx(Tab, {
|
|
197
|
+
value: "user",
|
|
198
|
+
children: t("rls_simulate_tab_user")
|
|
199
|
+
}),
|
|
200
|
+
/* @__PURE__ */ jsx(Tab, {
|
|
201
|
+
value: "apiKey",
|
|
202
|
+
children: t("rls_simulate_tab_api_key")
|
|
203
|
+
})
|
|
204
|
+
]
|
|
205
|
+
}),
|
|
206
|
+
optionsError && /* @__PURE__ */ jsx(Alert, {
|
|
207
|
+
color: "error",
|
|
208
|
+
size: "small",
|
|
209
|
+
role: null,
|
|
210
|
+
children: t("rls_simulate_options_error", { message: optionsError })
|
|
211
|
+
}),
|
|
212
|
+
!options && !optionsError && tab !== "user" && /* @__PURE__ */ jsx("div", {
|
|
213
|
+
className: "flex justify-center p-2",
|
|
214
|
+
children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
|
|
215
|
+
}),
|
|
216
|
+
tab === "role" && options && /* @__PURE__ */ jsxs("div", {
|
|
217
|
+
className: "flex flex-col gap-2",
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ jsx("div", {
|
|
220
|
+
className: "flex flex-col max-h-60 overflow-auto",
|
|
221
|
+
children: options.roles.map((entry) => /* @__PURE__ */ jsx(ChoiceRow, {
|
|
222
|
+
selected: role === entry.id,
|
|
223
|
+
onClick: () => setRole(entry.id),
|
|
224
|
+
icon: /* @__PURE__ */ jsx(ShieldIcon, { size: iconSize.smallest }),
|
|
225
|
+
title: entry.id,
|
|
226
|
+
detail: entry.name !== entry.id ? entry.name : void 0
|
|
227
|
+
}, entry.id))
|
|
228
|
+
}),
|
|
229
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
230
|
+
size: "small",
|
|
231
|
+
value: roleUserId,
|
|
232
|
+
placeholder: t("rls_simulate_user_id_optional"),
|
|
233
|
+
onChange: (event) => setRoleUserId(event.target.value)
|
|
234
|
+
}),
|
|
235
|
+
/* @__PURE__ */ jsx(Button, {
|
|
236
|
+
color: "primary",
|
|
237
|
+
size: "small",
|
|
238
|
+
disabled: !role,
|
|
239
|
+
onClick: () => {
|
|
240
|
+
if (!role) return;
|
|
241
|
+
const userId = roleUserId.trim();
|
|
242
|
+
start({
|
|
243
|
+
role,
|
|
244
|
+
...userId ? { userId } : {}
|
|
245
|
+
}, {
|
|
246
|
+
kind: "role",
|
|
247
|
+
role,
|
|
248
|
+
...userId ? { userId } : {}
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
children: t("rls_simulate_apply")
|
|
252
|
+
})
|
|
253
|
+
]
|
|
254
|
+
}),
|
|
255
|
+
tab === "user" && /* @__PURE__ */ jsxs("div", {
|
|
256
|
+
className: "flex flex-col gap-2",
|
|
257
|
+
children: [/* @__PURE__ */ jsx(TextField, {
|
|
258
|
+
size: "small",
|
|
259
|
+
value: userSearch,
|
|
260
|
+
placeholder: t("rls_simulate_search_users"),
|
|
261
|
+
onChange: (event) => setUserSearch(event.target.value)
|
|
262
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
263
|
+
className: "flex flex-col max-h-60 overflow-auto",
|
|
264
|
+
children: [
|
|
265
|
+
usersLoading && users.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
266
|
+
className: "flex justify-center p-2",
|
|
267
|
+
children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
|
|
268
|
+
}),
|
|
269
|
+
!usersLoading && usersError && /* @__PURE__ */ jsx(Alert, {
|
|
270
|
+
color: "error",
|
|
271
|
+
size: "small",
|
|
272
|
+
role: null,
|
|
273
|
+
children: t("rls_simulate_users_error", { message: usersError })
|
|
274
|
+
}),
|
|
275
|
+
!usersLoading && !usersError && users.length === 0 && /* @__PURE__ */ jsx(Typography, {
|
|
276
|
+
variant: "caption",
|
|
277
|
+
color: "secondary",
|
|
278
|
+
className: "p-2",
|
|
279
|
+
children: t("rls_simulate_no_users")
|
|
280
|
+
}),
|
|
281
|
+
users.map((user) => /* @__PURE__ */ jsx(ChoiceRow, {
|
|
282
|
+
selected: controller.simulation?.userId === user.uid && !controller.simulation.role,
|
|
283
|
+
onClick: () => start({ userId: user.uid }, {
|
|
284
|
+
kind: "user",
|
|
285
|
+
user: user.label
|
|
286
|
+
}),
|
|
287
|
+
icon: /* @__PURE__ */ jsx(UserIcon, { size: iconSize.smallest }),
|
|
288
|
+
title: user.label,
|
|
289
|
+
detail: user.detail
|
|
290
|
+
}, user.uid))
|
|
291
|
+
]
|
|
292
|
+
})]
|
|
293
|
+
}),
|
|
294
|
+
tab === "apiKey" && options && /* @__PURE__ */ jsxs("div", {
|
|
295
|
+
className: "flex flex-col max-h-60 overflow-auto",
|
|
296
|
+
children: [options.apiKeys.length === 0 && /* @__PURE__ */ jsx(Typography, {
|
|
297
|
+
variant: "caption",
|
|
298
|
+
color: "secondary",
|
|
299
|
+
className: "p-2",
|
|
300
|
+
children: t("rls_simulate_no_api_keys")
|
|
301
|
+
}), options.apiKeys.map((key) => /* @__PURE__ */ jsx(ChoiceRow, {
|
|
302
|
+
selected: controller.simulation?.apiKeyId === key.id,
|
|
303
|
+
onClick: () => start({ apiKeyId: key.id }, {
|
|
304
|
+
kind: "apiKey",
|
|
305
|
+
name: key.name,
|
|
306
|
+
keyPrefix: key.keyPrefix
|
|
307
|
+
}),
|
|
308
|
+
icon: /* @__PURE__ */ jsx(KeyRoundIcon, { size: iconSize.smallest }),
|
|
309
|
+
title: key.name,
|
|
310
|
+
detail: `${key.keyPrefix}…`
|
|
311
|
+
}, key.id))]
|
|
312
|
+
}),
|
|
313
|
+
active && /* @__PURE__ */ jsx(Button, {
|
|
314
|
+
variant: "text",
|
|
315
|
+
size: "small",
|
|
316
|
+
onClick: () => {
|
|
317
|
+
controller.setSimulation(null);
|
|
318
|
+
setOpen(false);
|
|
319
|
+
},
|
|
320
|
+
children: t("rls_simulate_stop")
|
|
321
|
+
})
|
|
322
|
+
]
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The banner every screen shows while access is simulated: who the data views
|
|
328
|
+
* run as, how many collections show that identity nothing, how many fields its
|
|
329
|
+
* roles cannot read — and the way out.
|
|
330
|
+
*
|
|
331
|
+
* @group Components
|
|
332
|
+
*/
|
|
333
|
+
function RlsSimulationBannerPanel({ className }) {
|
|
334
|
+
const { t } = useTranslation();
|
|
335
|
+
const { simulation, label, setSimulation } = useRlsSimulation();
|
|
336
|
+
const request = useSimulatorRequest();
|
|
337
|
+
const [summary, setSummary] = useState(null);
|
|
338
|
+
const [summaryError, setSummaryError] = useState(null);
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
setSummary(null);
|
|
341
|
+
setSummaryError(null);
|
|
342
|
+
if (!simulation) return;
|
|
343
|
+
let cancelled = false;
|
|
344
|
+
request("/preview", {
|
|
345
|
+
method: "POST",
|
|
346
|
+
body: simulation
|
|
347
|
+
}).then(({ summary: loaded }) => {
|
|
348
|
+
if (!cancelled) setSummary(loaded);
|
|
349
|
+
}).catch((error) => {
|
|
350
|
+
console.warn("Could not preview the simulated access", error);
|
|
351
|
+
if (!cancelled) setSummaryError(error instanceof Error ? error.message : String(error));
|
|
352
|
+
});
|
|
353
|
+
return () => {
|
|
354
|
+
cancelled = true;
|
|
355
|
+
};
|
|
356
|
+
}, [simulation, request]);
|
|
357
|
+
if (!simulation) return null;
|
|
358
|
+
const title = (() => {
|
|
359
|
+
if (label?.kind === "apiKey") return t("rls_simulation_viewing_as_api_key", {
|
|
360
|
+
name: label.name,
|
|
361
|
+
prefix: label.keyPrefix
|
|
362
|
+
});
|
|
363
|
+
if (label?.kind === "user") return t("rls_simulation_viewing_as_user", { user: label.user });
|
|
364
|
+
if (simulation.role && simulation.userId) return t("rls_simulation_viewing_as_role_user", {
|
|
365
|
+
role: simulation.role,
|
|
366
|
+
user: simulation.userId
|
|
367
|
+
});
|
|
368
|
+
if (simulation.role) return t("rls_simulation_viewing_as_role", { role: simulation.role });
|
|
369
|
+
return t("rls_simulation_viewing_as_user", { user: simulation.userId ?? simulation.apiKeyId ?? "" });
|
|
370
|
+
})();
|
|
371
|
+
const counts = summary ? [summary.hiddenCollections === 1 ? t("rls_simulation_hidden_collections_one") : t("rls_simulation_hidden_collections", { count: String(summary.hiddenCollections) }), summary.maskedColumns === 1 ? t("rls_simulation_masked_columns_one") : t("rls_simulation_masked_columns", { count: String(summary.maskedColumns) })].join(", ") : summaryError !== null ? t("rls_simulation_count_failed", { message: summaryError }) : t("rls_simulation_counting");
|
|
372
|
+
return /* @__PURE__ */ jsx("div", {
|
|
373
|
+
role: "status",
|
|
374
|
+
"aria-live": "polite",
|
|
375
|
+
className,
|
|
376
|
+
children: /* @__PURE__ */ jsx(Alert, {
|
|
377
|
+
color: "warning",
|
|
378
|
+
size: "small",
|
|
379
|
+
role: null,
|
|
380
|
+
action: /* @__PURE__ */ jsx(Button, {
|
|
381
|
+
variant: "text",
|
|
382
|
+
size: "small",
|
|
383
|
+
onClick: () => setSimulation(null),
|
|
384
|
+
children: t("rls_simulate_stop")
|
|
385
|
+
}),
|
|
386
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
387
|
+
className: "flex flex-col gap-0.5",
|
|
388
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
389
|
+
variant: "label",
|
|
390
|
+
children: `${title} — ${counts}`
|
|
391
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
392
|
+
variant: "body2",
|
|
393
|
+
color: "secondary",
|
|
394
|
+
children: t("rls_simulation_note")
|
|
395
|
+
})]
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
export { RlsSimulationBannerPanel, RlsSimulatorBarPanel };
|
|
402
|
+
|
|
403
|
+
//# sourceMappingURL=RlsSimulatorPanel-DOfmVgM3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RlsSimulatorPanel-DOfmVgM3.js","names":[],"sources":["../src/components/studio/RlsSimulatorPanel.tsx"],"sourcesContent":["/**\n * The \"Simulate access\" pill and banner themselves. Loaded on demand by the\n * wrappers in `RlsSimulatorBar.tsx`: only an administrator ever sees them, so\n * nobody else downloads them before the login screen paints.\n */\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport {\n Alert,\n Button,\n CircularProgress,\n cls,\n EyeIcon,\n IconButton,\n iconSize,\n KeyRoundIcon,\n Popover,\n ShieldIcon,\n Tab,\n Tabs,\n TextField,\n Typography,\n UserIcon\n} from \"@rebasepro/ui\";\nimport type { AccessSimulation, RebaseClient } from \"@rebasepro/types\";\nimport { useRlsSimulation, type RlsSimulationLabel } from \"../../contexts/RlsSimulationContext\";\nimport { useApiBase, useApiConfig } from \"../../hooks/ApiConfigContext\";\nimport { useRebaseClient } from \"../../hooks/useRebaseClient\";\nimport { useTranslation } from \"../../hooks/useTranslation\";\nimport { useCanSimulateAccess, type RlsSimulatorBarProps } from \"./RlsSimulatorBar\";\n\n/** A role `GET /admin/rls-simulator/options` offers. */\ninterface SimulatorRole {\n id: string;\n name: string;\n}\n\n/** A live service key `GET /admin/rls-simulator/options` offers. */\ninterface SimulatorApiKey {\n id: string;\n name: string;\n keyPrefix: string;\n}\n\ninterface SimulatorOptions {\n roles: SimulatorRole[];\n apiKeys: SimulatorApiKey[];\n}\n\n/** What `POST /admin/rls-simulator/preview` sums up. */\ninterface SimulationSummary {\n hiddenCollections: number;\n maskedColumns: number;\n}\n\ntype SimulatorTab = \"role\" | \"user\" | \"apiKey\";\n\n/** How many users one search of the picker lists. */\nconst USER_PAGE_SIZE = 20;\n\n/**\n * A request to the simulator's admin surface with the administrator's own\n * token — never through the simulated client, whose headers the admin routes\n * refuse.\n */\nfunction useSimulatorRequest() {\n const apiConfig = useApiConfig();\n const apiBase = useApiBase();\n return useCallback(async <T, >(path: string, init?: { method?: string; body?: unknown }): Promise<T> => {\n if (!apiBase) throw new Error(\"No API URL is configured.\");\n const token = await apiConfig?.getAuthToken?.();\n const response = await fetch(`${apiBase}/admin/rls-simulator${path}`, {\n method: init?.method ?? \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token ? { Authorization: `Bearer ${token}` } : {})\n },\n ...(init?.body !== undefined ? { body: JSON.stringify(init.body) } : {})\n });\n const json: unknown = await response.json().catch(() => undefined);\n if (!response.ok) {\n const message = typeof json === \"object\" && json !== null && \"error\" in json\n && typeof json.error === \"object\" && json.error !== null && \"message\" in json.error\n && typeof json.error.message === \"string\"\n ? json.error.message\n : response.statusText;\n throw new Error(message);\n }\n return json as T;\n }, [apiBase, apiConfig]);\n}\n\n/** One choosable row of the popover's lists. */\nfunction ChoiceRow({ selected, onClick, icon, title, detail }: {\n selected: boolean;\n onClick: () => void;\n icon: React.ReactNode;\n title: string;\n detail?: string;\n}) {\n return (\n <Button\n variant=\"text\"\n size=\"small\"\n fullWidth\n onClick={onClick}\n className={cls(\"justify-start\", { \"bg-surface-active\": selected })}>\n {icon}\n <Typography variant=\"body2\" noWrap className=\"truncate\">{title}</Typography>\n {detail && <Typography variant=\"caption\" color=\"secondary\" noWrap className=\"truncate\">{detail}</Typography>}\n </Button>\n );\n}\n\n/**\n * \"Simulate access\": the app bar pill that runs every data view as a role, a\n * user or a service key, so an administrator sees exactly the rows and fields\n * that identity's row-level security lets it read.\n *\n * Shown only to an administrator, and only when the data views ride a client\n * that can derive a simulated one. The server checks the first again on every\n * request: anyone else sending the simulation headers gets a 403.\n *\n * @group Components\n */\nexport function RlsSimulatorBarPanel({ className, compact, side, align = \"end\", portalContainer }: RlsSimulatorBarProps) {\n const { t } = useTranslation();\n const controller = useRlsSimulation();\n const canSimulate = useCanSimulateAccess();\n const request = useSimulatorRequest();\n const client = useRebaseClient<RebaseClient>();\n\n const [open, setOpen] = useState(false);\n const [tab, setTab] = useState<SimulatorTab>(\"role\");\n const [options, setOptions] = useState<SimulatorOptions | null>(null);\n const [optionsError, setOptionsError] = useState<string | null>(null);\n const [role, setRole] = useState<string | null>(null);\n const [roleUserId, setRoleUserId] = useState(\"\");\n const [userSearch, setUserSearch] = useState(\"\");\n const [users, setUsers] = useState<Array<{ uid: string; label: string; detail?: string }>>([]);\n const [usersLoading, setUsersLoading] = useState(false);\n // A failed search is not a search that found no one.\n const [usersError, setUsersError] = useState<string | null>(null);\n\n useEffect(() => {\n if (!open || options) return;\n let cancelled = false;\n // An error from an earlier open is about that request, not this one.\n setOptionsError(null);\n request<SimulatorOptions>(\"/options\")\n .then((loaded) => {\n if (!cancelled) setOptions(loaded);\n })\n .catch((error: unknown) => {\n if (!cancelled) setOptionsError(error instanceof Error ? error.message : String(error));\n });\n return () => {\n cancelled = true;\n };\n }, [open, options, request]);\n\n const adminApi = client?.admin;\n useEffect(() => {\n if (!open || tab !== \"user\" || !adminApi) return;\n let cancelled = false;\n setUsersLoading(true);\n const timer = setTimeout(() => {\n adminApi.listUsersPaginated({ search: userSearch || undefined, limit: USER_PAGE_SIZE })\n .then(({ users: found }) => {\n if (cancelled) return;\n setUsersError(null);\n setUsers(found.filter(user => !user.disabled).map(user => ({\n uid: user.uid,\n label: user.email ?? user.displayName ?? user.uid,\n detail: user.roles?.length ? user.roles.join(\", \") : undefined\n })));\n })\n .catch((error: unknown) => {\n if (cancelled) return;\n setUsers([]);\n setUsersError(error instanceof Error ? error.message : String(error));\n })\n .finally(() => {\n if (!cancelled) setUsersLoading(false);\n });\n }, 250);\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [open, tab, userSearch, adminApi]);\n\n const start = (simulation: AccessSimulation, label: RlsSimulationLabel) => {\n controller.setSimulation(simulation, label);\n setOpen(false);\n };\n\n if (!canSimulate) return null;\n\n const active = controller.simulation !== null;\n\n const trigger = compact ? (\n <IconButton aria-label={t(\"rls_simulate_access\")} className={cls(\"relative\", className)}>\n <EyeIcon size={iconSize.small}/>\n {active && <span aria-hidden className=\"absolute top-1 right-1 w-2 h-2 rounded-full bg-amber-500\"/>}\n </IconButton>\n ) : (\n <Button\n variant=\"outlined\"\n size=\"small\"\n aria-label={t(\"rls_simulate_access\")}\n className={cls(\"rounded-full whitespace-nowrap\", className)}>\n {active\n ? <span aria-hidden className=\"w-2 h-2 rounded-full bg-amber-500\"/>\n : <EyeIcon size={iconSize.smallest}/>}\n {t(\"rls_simulate_access\")}\n </Button>\n );\n\n return (\n <Popover trigger={trigger} open={open} onOpenChange={setOpen} side={side} align={align} portalContainer={portalContainer}>\n <div className=\"flex flex-col gap-3 p-3 w-[22rem]\">\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"label\">{t(\"rls_simulate_access\")}</Typography>\n <Typography variant=\"caption\" color=\"secondary\">{t(\"rls_simulate_access_description\")}</Typography>\n </div>\n\n <Tabs value={tab} onValueChange={(value) => setTab(value === \"user\" || value === \"apiKey\" ? value : \"role\")}>\n <Tab value=\"role\">{t(\"rls_simulate_tab_role\")}</Tab>\n <Tab value=\"user\">{t(\"rls_simulate_tab_user\")}</Tab>\n <Tab value=\"apiKey\">{t(\"rls_simulate_tab_api_key\")}</Tab>\n </Tabs>\n\n {optionsError && (\n <Alert color=\"error\" size=\"small\" role={null}>\n {t(\"rls_simulate_options_error\", { message: optionsError })}\n </Alert>\n )}\n\n {!options && !optionsError && tab !== \"user\" && (\n <div className=\"flex justify-center p-2\"><CircularProgress size=\"small\"/></div>\n )}\n\n {tab === \"role\" && options && (\n <div className=\"flex flex-col gap-2\">\n <div className=\"flex flex-col max-h-60 overflow-auto\">\n {options.roles.map(entry => (\n <ChoiceRow\n key={entry.id}\n selected={role === entry.id}\n onClick={() => setRole(entry.id)}\n icon={<ShieldIcon size={iconSize.smallest}/>}\n title={entry.id}\n detail={entry.name !== entry.id ? entry.name : undefined}/>\n ))}\n </div>\n <TextField\n size=\"small\"\n value={roleUserId}\n placeholder={t(\"rls_simulate_user_id_optional\")}\n onChange={(event) => setRoleUserId(event.target.value)}/>\n <Button\n color=\"primary\"\n size=\"small\"\n disabled={!role}\n onClick={() => {\n if (!role) return;\n const userId = roleUserId.trim();\n start(\n { role, ...(userId ? { userId } : {}) },\n { kind: \"role\", role, ...(userId ? { userId } : {}) }\n );\n }}>\n {t(\"rls_simulate_apply\")}\n </Button>\n </div>\n )}\n\n {tab === \"user\" && (\n <div className=\"flex flex-col gap-2\">\n <TextField\n size=\"small\"\n value={userSearch}\n placeholder={t(\"rls_simulate_search_users\")}\n onChange={(event) => setUserSearch(event.target.value)}/>\n <div className=\"flex flex-col max-h-60 overflow-auto\">\n {usersLoading && users.length === 0 && (\n <div className=\"flex justify-center p-2\"><CircularProgress size=\"small\"/></div>\n )}\n {!usersLoading && usersError && (\n <Alert color=\"error\" size=\"small\" role={null}>\n {t(\"rls_simulate_users_error\", { message: usersError })}\n </Alert>\n )}\n {!usersLoading && !usersError && users.length === 0 && (\n <Typography variant=\"caption\" color=\"secondary\" className=\"p-2\">{t(\"rls_simulate_no_users\")}</Typography>\n )}\n {users.map(user => (\n <ChoiceRow\n key={user.uid}\n selected={controller.simulation?.userId === user.uid && !controller.simulation.role}\n onClick={() => start({ userId: user.uid }, { kind: \"user\", user: user.label })}\n icon={<UserIcon size={iconSize.smallest}/>}\n title={user.label}\n detail={user.detail}/>\n ))}\n </div>\n </div>\n )}\n\n {tab === \"apiKey\" && options && (\n <div className=\"flex flex-col max-h-60 overflow-auto\">\n {options.apiKeys.length === 0 && (\n <Typography variant=\"caption\" color=\"secondary\" className=\"p-2\">{t(\"rls_simulate_no_api_keys\")}</Typography>\n )}\n {options.apiKeys.map(key => (\n <ChoiceRow\n key={key.id}\n selected={controller.simulation?.apiKeyId === key.id}\n onClick={() => start({ apiKeyId: key.id }, { kind: \"apiKey\", name: key.name, keyPrefix: key.keyPrefix })}\n icon={<KeyRoundIcon size={iconSize.smallest}/>}\n title={key.name}\n detail={`${key.keyPrefix}…`}/>\n ))}\n </div>\n )}\n\n {active && (\n <Button\n variant=\"text\"\n size=\"small\"\n onClick={() => {\n controller.setSimulation(null);\n setOpen(false);\n }}>\n {t(\"rls_simulate_stop\")}\n </Button>\n )}\n </div>\n </Popover>\n );\n}\n\n/**\n * The banner every screen shows while access is simulated: who the data views\n * run as, how many collections show that identity nothing, how many fields its\n * roles cannot read — and the way out.\n *\n * @group Components\n */\nexport function RlsSimulationBannerPanel({ className }: { className?: string }) {\n const { t } = useTranslation();\n const { simulation, label, setSimulation } = useRlsSimulation();\n const request = useSimulatorRequest();\n const [summary, setSummary] = useState<SimulationSummary | null>(null);\n // Why the count failed: without it the banner said \"Counting…\" for as long\n // as the simulation ran.\n const [summaryError, setSummaryError] = useState<string | null>(null);\n\n useEffect(() => {\n setSummary(null);\n setSummaryError(null);\n if (!simulation) return;\n let cancelled = false;\n request<{ summary: SimulationSummary }>(\"/preview\", { method: \"POST\", body: simulation })\n .then(({ summary: loaded }) => {\n if (!cancelled) setSummary(loaded);\n })\n .catch((error: unknown) => {\n console.warn(\"Could not preview the simulated access\", error);\n if (!cancelled) setSummaryError(error instanceof Error ? error.message : String(error));\n });\n return () => {\n cancelled = true;\n };\n }, [simulation, request]);\n\n if (!simulation) return null;\n\n const title = (() => {\n if (label?.kind === \"apiKey\") return t(\"rls_simulation_viewing_as_api_key\", { name: label.name, prefix: label.keyPrefix });\n if (label?.kind === \"user\") return t(\"rls_simulation_viewing_as_user\", { user: label.user });\n if (simulation.role && simulation.userId) {\n return t(\"rls_simulation_viewing_as_role_user\", { role: simulation.role, user: simulation.userId });\n }\n if (simulation.role) return t(\"rls_simulation_viewing_as_role\", { role: simulation.role });\n return t(\"rls_simulation_viewing_as_user\", { user: simulation.userId ?? simulation.apiKeyId ?? \"\" });\n })();\n\n const counts = summary\n ? [\n summary.hiddenCollections === 1\n ? t(\"rls_simulation_hidden_collections_one\")\n : t(\"rls_simulation_hidden_collections\", { count: String(summary.hiddenCollections) }),\n summary.maskedColumns === 1\n ? t(\"rls_simulation_masked_columns_one\")\n : t(\"rls_simulation_masked_columns\", { count: String(summary.maskedColumns) })\n ].join(\", \")\n : summaryError !== null\n ? t(\"rls_simulation_count_failed\", { message: summaryError })\n : t(\"rls_simulation_counting\");\n\n return (\n <div role=\"status\" aria-live=\"polite\" className={className}>\n <Alert\n color=\"warning\"\n size=\"small\"\n role={null}\n action={<Button variant=\"text\" size=\"small\" onClick={() => setSimulation(null)}>{t(\"rls_simulate_stop\")}</Button>}>\n <div className=\"flex flex-col gap-0.5\">\n <Typography variant=\"label\">{`${title} — ${counts}`}</Typography>\n <Typography variant=\"body2\" color=\"secondary\">{t(\"rls_simulation_note\")}</Typography>\n </div>\n </Alert>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Turns a collection into the shape the entity form renders: an ordered list of
|
|
5
5
|
* sections holding grid-spanned fields, plus the metadata rail beside them.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* A field takes the full row unless its property sets `admin.span`, with one
|
|
8
|
+
* exception: two neighbouring fields whose editors are both small and of the
|
|
9
|
+
* same kind — two numbers, two switches, two dates, two short selects — share a
|
|
10
|
+
* row. Nothing wider is ever guessed. Pairing by type alone put Namespace beside
|
|
11
|
+
* Custom Domain and Git Branch beside Git Repository URL, values that sat
|
|
12
|
+
* together only because both were strings; a string's editor says nothing about
|
|
13
|
+
* how long its value is, so a free-text field is never paired. Nothing here
|
|
14
|
+
* looks at which property is the record's title either.
|
|
14
15
|
*/
|
|
15
16
|
import type { AdminCollection, PropertySpan } from "@rebasepro/cms-types";
|
|
16
17
|
import type { Property } from "@rebasepro/types";
|
|
@@ -21,10 +22,7 @@ export interface ResolvedFormField {
|
|
|
21
22
|
span: PropertySpan;
|
|
22
23
|
/** True for an `additionalFields` entry rather than a property. */
|
|
23
24
|
additional: boolean;
|
|
24
|
-
/**
|
|
25
|
-
* The span was written on the property rather than derived from its type.
|
|
26
|
-
* Row filling leaves these alone: an author who wrote a width meant it.
|
|
27
|
-
*/
|
|
25
|
+
/** The span was written on the property. Pairing leaves these alone. */
|
|
28
26
|
spanExplicit?: boolean;
|
|
29
27
|
}
|
|
30
28
|
export interface ResolvedFormSection {
|
|
@@ -65,14 +63,6 @@ export interface ResolveFormLayoutParams<M extends Record<string, unknown>> {
|
|
|
65
63
|
*/
|
|
66
64
|
status: "new" | "existing" | "copy";
|
|
67
65
|
}
|
|
68
|
-
/**
|
|
69
|
-
* The span a property gets when nothing is declared.
|
|
70
|
-
*
|
|
71
|
-
* Deliberately coarse: three buckets, chosen by how much room the *editor*
|
|
72
|
-
* needs, not by how important the field is. Importance is what `admin.form`
|
|
73
|
-
* sections are for.
|
|
74
|
-
*/
|
|
75
|
-
export declare function deriveSpan(property: Property, isTitleProperty: boolean): PropertySpan;
|
|
76
66
|
/**
|
|
77
67
|
* Can the user still type this id?
|
|
78
68
|
*
|
|
@@ -92,17 +82,29 @@ export declare function isIdPropertyEditable(property: Property, status: "new" |
|
|
|
92
82
|
* screen.
|
|
93
83
|
*/
|
|
94
84
|
export declare function isAuditTimestamp(property: Property | undefined): boolean;
|
|
85
|
+
/** What a field's editor is, for the purpose of sitting beside another one. */
|
|
86
|
+
type PairKind = "number" | "switch" | "date" | "date_time" | "select";
|
|
87
|
+
/**
|
|
88
|
+
* The kind of compact editor this property renders, or `undefined` when its
|
|
89
|
+
* editor needs the row — or when nothing about it says how long its value is.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately narrow. A plain string is never compact: "Namespace" and "Git
|
|
92
|
+
* Branch" are both strings and nothing in the schema tells a 6-character branch
|
|
93
|
+
* from a 60-character URL. Neither is a relation, whose preview is the related
|
|
94
|
+
* record's own text. Only editors whose width is bounded by what they are —
|
|
95
|
+
* a number, a switch, a date picker, a select over short options — qualify.
|
|
96
|
+
*/
|
|
97
|
+
export declare function pairKind(property: Property): PairKind | undefined;
|
|
95
98
|
/**
|
|
96
|
-
*
|
|
99
|
+
* Put neighbouring fields of the same compact kind two to a row.
|
|
97
100
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* with an explicit span are never resized.
|
|
101
|
+
* Greedy and in declaration order: a run of three numbers is a pair and a full
|
|
102
|
+
* row, never a pair with the third pulled out of order. A pair only starts at
|
|
103
|
+
* the beginning of a row — the grid flows, so a pair placed after an author's
|
|
104
|
+
* `span: 2` would split across two rows. Fields with a span the author wrote are
|
|
105
|
+
* left as written, and nothing is widened to close a gap they leave.
|
|
104
106
|
*/
|
|
105
|
-
export declare function
|
|
107
|
+
export declare function pairCompactFields(fields: ResolvedFormField[], propertyOf: (key: string) => Property | undefined): ResolvedFormField[];
|
|
106
108
|
/**
|
|
107
109
|
* Resolve the layout for a collection's generated form.
|
|
108
110
|
*
|
|
@@ -111,3 +113,4 @@ export declare function fillRows(fields: ResolvedFormField[]): ResolvedFormField
|
|
|
111
113
|
* in the panel.
|
|
112
114
|
*/
|
|
113
115
|
export declare function resolveFormLayout<M extends Record<string, unknown>>({ collection, fieldKeys, status }: ResolveFormLayoutParams<M>): ResolvedFormLayout;
|
|
116
|
+
export {};
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A language's name in that language. One Rebase does not ship is named by
|
|
4
|
+
* the browser (`nl` → "Nederlands"), and by its code when the browser cannot.
|
|
5
|
+
*/
|
|
6
|
+
export declare function languageName(locale: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* The language menu: every language the admin can be shown in, which is the
|
|
9
|
+
* shipped ones plus any the app adds through `<Rebase translations>`.
|
|
10
|
+
*/
|
|
2
11
|
export declare function LanguageToggle(): React.JSX.Element;
|
|
@@ -2,3 +2,10 @@ export { LoginView } from "./LoginView.js";
|
|
|
2
2
|
export type { LoginViewProps } from "./LoginView.js";
|
|
3
3
|
export { readEmailLinkAction } from "./email-link.js";
|
|
4
4
|
export type { EmailLinkAction } from "./email-link.js";
|
|
5
|
+
export { startOAuthRedirect, consumeOAuthCallback } from "./oauth-redirect-flow.js";
|
|
6
|
+
export type { OAuthRedirectRequest, OAuthCallbackResult } from "./oauth-redirect-flow.js";
|
|
7
|
+
export { readMfaRequired, canAnswerMfa, classifyMfaRefusal } from "./mfa-required.js";
|
|
8
|
+
export type { PendingMfaSignIn } from "./mfa-required.js";
|
|
9
|
+
export { authErrorMessage } from "./auth-error-message.js";
|
|
10
|
+
export { appAddressOfEmailLink } from "./email-link.js";
|
|
11
|
+
export { emailChangeErrorMessage } from "../email-change-message.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface RlsSimulatorBarProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
/** An icon button instead of the pill — for a collapsed drawer rail. */
|
|
5
|
+
compact?: boolean;
|
|
6
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
7
|
+
align?: "start" | "center" | "end";
|
|
8
|
+
/** Where the popover mounts — inside a floating drawer, so hovering it keeps the drawer open. */
|
|
9
|
+
portalContainer?: HTMLElement | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether the signed-in person may simulate access here: an administrator,
|
|
13
|
+
* with data views that ride a client able to derive a simulated one.
|
|
14
|
+
*
|
|
15
|
+
* @group Hooks and utilities
|
|
16
|
+
*/
|
|
17
|
+
export declare function useCanSimulateAccess(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* "Simulate access": the app bar pill that runs every data view as a role, a
|
|
20
|
+
* user or a service key, so an administrator sees exactly the rows and fields
|
|
21
|
+
* that identity's row-level security lets it read.
|
|
22
|
+
*
|
|
23
|
+
* Shown only to an administrator, and only when the data views ride a client
|
|
24
|
+
* that can derive a simulated one. The server checks the first again on every
|
|
25
|
+
* request: anyone else sending the simulation headers gets a 403.
|
|
26
|
+
*
|
|
27
|
+
* @group Components
|
|
28
|
+
*/
|
|
29
|
+
export declare function RlsSimulatorBar(props: RlsSimulatorBarProps): React.JSX.Element | null;
|
|
30
|
+
/**
|
|
31
|
+
* The banner every screen shows while access is simulated: who the data views
|
|
32
|
+
* run as, how many collections show that identity nothing, how many fields its
|
|
33
|
+
* roles cannot read — and the way out.
|
|
34
|
+
*
|
|
35
|
+
* @group Components
|
|
36
|
+
*/
|
|
37
|
+
export declare function RlsSimulationBanner({ className }: {
|
|
38
|
+
className?: string;
|
|
39
|
+
}): React.JSX.Element | null;
|