@protobase/ui 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account/account-pages.js +20 -0
- package/dist/account/account-section.js +31 -0
- package/dist/account/account-security-page.js +59 -0
- package/dist/account/backup-codes.js +44 -0
- package/dist/account/passkeys-section.js +141 -0
- package/dist/account/password-dialog.js +58 -0
- package/dist/account/sign-in-policy-form.js +108 -0
- package/dist/account/sign-in-policy-labels.js +76 -0
- package/dist/account/sign-in-policy-page.js +72 -0
- package/dist/account/staff-sign-in-log.js +62 -0
- package/dist/account/totp-qr.js +37 -0
- package/dist/account/two-factor-section.js +123 -0
- package/dist/account/two-factor-setup.js +188 -0
- package/dist/activity-chart/activity-chart.js +1 -1
- package/dist/activity-chart/activity-tooltip.js +1 -1
- package/dist/app/app.js +35 -14
- package/dist/app/assistant-shell.js +1 -1
- package/dist/app/create/create-page.js +1 -1
- package/dist/app/delete/use-delete-flow.js +1 -1
- package/dist/app/error-banner.js +1 -1
- package/dist/app/list-page.js +1 -1
- package/dist/app/list-search.js +1 -1
- package/dist/app/meta-gate.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +3 -3
- package/dist/app/pages/record-table.js +1 -1
- package/dist/app/record-page.js +5 -5
- package/dist/app/runtime/runtime-update.js +1 -1
- package/dist/app/sensitive-field.js +1 -1
- package/dist/app/staff-banner.js +67 -0
- package/dist/app/use-record-editor.js +1 -1
- package/dist/app-shell/breadcrumb.js +1 -1
- package/dist/app-shell/global-search.js +1 -1
- package/dist/app-shell/profile-menu.js +1 -1
- package/dist/app-shell/sidebar-group.js +1 -1
- package/dist/app-shell/top-bar.js +1 -1
- package/dist/assistant-dock/assistant-dock.js +1 -1
- package/dist/auth/auth-gate.js +42 -4
- package/dist/auth/auth-messages.js +48 -0
- package/dist/auth/auth-provider.js +40 -14
- package/dist/auth/code-input.js +17 -0
- package/dist/auth/email-code-form.js +81 -0
- package/dist/auth/email-code-page.js +40 -0
- package/dist/auth/first-run-page.js +1 -1
- package/dist/auth/forgot-password-page.js +1 -1
- package/dist/auth/reset-password-page.js +1 -1
- package/dist/auth/setup-required-form.js +43 -0
- package/dist/auth/setup-required-page.js +51 -0
- package/dist/auth/sign-in-form.js +53 -20
- package/dist/auth/sign-in-page.js +14 -13
- package/dist/auth/staff-link.js +30 -0
- package/dist/auth/staff-sign-in-form.js +93 -0
- package/dist/auth/staff-sign-in-page.js +41 -0
- package/dist/auth/two-factor-form.js +137 -0
- package/dist/auth/two-factor-page.js +39 -0
- package/dist/auth/user-to-shell.js +5 -2
- package/dist/chat/composer.js +1 -1
- package/dist/data/api-provider.d.ts +1 -1
- package/dist/data/api-provider.js +1 -1
- package/dist/data/use-aggregates.js +1 -1
- package/dist/data/use-list.js +1 -1
- package/dist/data/use-update-record.js +1 -1
- package/dist/data-grid/cells.js +3 -3
- package/dist/data-grid/grid-header.js +2 -2
- package/dist/data-grid/grid-row.js +1 -1
- package/dist/data-grid/header-menu.js +1 -1
- package/dist/data-grid/row-menu.js +1 -1
- package/dist/diff-view/diff-view.js +1 -1
- package/dist/filter-panel/active-filter-chips.js +1 -1
- package/dist/filter-panel/facet-group.js +1 -1
- package/dist/filter-panel/filter-menu.js +1 -1
- package/dist/inline-child-table/inline-child-table.js +1 -1
- package/dist/inline-child-table/line-menu.js +1 -1
- package/dist/inline-child-table/line-row.js +2 -2
- package/dist/inline-child-table/totals-footer.js +1 -1
- package/dist/live/use-filter-config.js +1 -1
- package/dist/primitives/checkbox.js +1 -1
- package/dist/primitives/sheet.js +1 -1
- package/dist/primitives/spinner.js +1 -1
- package/dist/record-view/field.js +1 -1
- package/dist/record-view/record-sidebar.js +1 -1
- package/dist/side-panel/side-panel.js +1 -1
- package/dist/step-list/step-list.js +1 -1
- package/dist/toasts/toast.js +1 -1
- package/package.json +6 -5
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { Input } from "../primitives/input.js";
|
|
2
2
|
import { Button } from "../primitives/button.js";
|
|
3
3
|
import { AuthLayout } from "./auth-layout.js";
|
|
4
|
-
import { useState } from "react";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
5
|
+
import { KeyRound, Mail } from "lucide-react";
|
|
5
6
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
//#region src/auth/sign-in-form.tsx
|
|
7
|
-
const
|
|
8
|
+
const Divider = () => /* @__PURE__ */ jsxs("div", {
|
|
9
|
+
className: "my-4 flex items-center gap-3 text-xs text-muted-foreground",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" }),
|
|
12
|
+
"or",
|
|
13
|
+
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
|
|
14
|
+
]
|
|
15
|
+
});
|
|
16
|
+
const SignInForm = ({ onSubmit, busy, error, workspace, notice, methods = ["password"], onSendCode, onPasskey, onForgotPassword, onContinueWithGitHub }) => {
|
|
8
17
|
const [email, setEmail] = useState("");
|
|
9
18
|
const [password, setPassword] = useState("");
|
|
19
|
+
const emailInput = useRef(null);
|
|
20
|
+
const withPassword = methods.includes("password");
|
|
21
|
+
const withCode = methods.includes("emailCode") && onSendCode !== void 0;
|
|
22
|
+
const withPasskey = methods.includes("passkey") && onPasskey !== void 0;
|
|
23
|
+
const sendCode = () => {
|
|
24
|
+
if (!emailInput.current?.reportValidity()) return;
|
|
25
|
+
onSendCode?.(email.trim());
|
|
26
|
+
};
|
|
10
27
|
return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
11
28
|
title: "Sign in to Protobase",
|
|
12
29
|
description: workspace,
|
|
@@ -16,32 +33,39 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, onForgotPassword
|
|
|
16
33
|
className: "mb-4 rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
|
|
17
34
|
children: notice
|
|
18
35
|
}),
|
|
19
|
-
onContinueWithGitHub && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
(onContinueWithGitHub || withPasskey) && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
37
|
+
className: "flex flex-col gap-2",
|
|
38
|
+
children: [onContinueWithGitHub && /* @__PURE__ */ jsx(Button, {
|
|
39
|
+
onClick: onContinueWithGitHub,
|
|
40
|
+
disabled: busy,
|
|
41
|
+
className: "min-h-10 w-full",
|
|
42
|
+
children: "Continue with GitHub"
|
|
43
|
+
}), withPasskey && /* @__PURE__ */ jsxs(Button, {
|
|
44
|
+
onClick: onPasskey,
|
|
45
|
+
disabled: busy,
|
|
46
|
+
className: "min-h-10 w-full",
|
|
47
|
+
children: [/* @__PURE__ */ jsx(KeyRound, { className: "size-4" }), "Sign in with a passkey"]
|
|
48
|
+
})]
|
|
49
|
+
}), withPassword || withCode ? /* @__PURE__ */ jsx(Divider, {}) : error && /* @__PURE__ */ jsx("p", {
|
|
50
|
+
role: "alert",
|
|
51
|
+
className: "mt-4 text-xs font-medium text-danger-text",
|
|
52
|
+
children: error
|
|
31
53
|
})] }),
|
|
32
|
-
/* @__PURE__ */ jsxs("form", {
|
|
54
|
+
(withPassword || withCode) && /* @__PURE__ */ jsxs("form", {
|
|
33
55
|
className: "flex flex-col gap-4",
|
|
34
56
|
onSubmit: (event) => {
|
|
35
57
|
event.preventDefault();
|
|
36
|
-
onSubmit(email.trim(), password);
|
|
58
|
+
if (withPassword) onSubmit(email.trim(), password);
|
|
59
|
+
else sendCode();
|
|
37
60
|
},
|
|
38
61
|
children: [
|
|
39
62
|
/* @__PURE__ */ jsxs("label", {
|
|
40
63
|
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
41
64
|
children: ["Email", /* @__PURE__ */ jsx(Input, {
|
|
65
|
+
ref: emailInput,
|
|
42
66
|
type: "email",
|
|
43
67
|
name: "email",
|
|
44
|
-
autoComplete: "username",
|
|
68
|
+
autoComplete: "username webauthn",
|
|
45
69
|
required: true,
|
|
46
70
|
autoFocus: true,
|
|
47
71
|
value: email,
|
|
@@ -50,7 +74,7 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, onForgotPassword
|
|
|
50
74
|
className: "min-h-6"
|
|
51
75
|
})]
|
|
52
76
|
}),
|
|
53
|
-
/* @__PURE__ */ jsxs("label", {
|
|
77
|
+
withPassword && /* @__PURE__ */ jsxs("label", {
|
|
54
78
|
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
55
79
|
children: ["Password", /* @__PURE__ */ jsx(Input, {
|
|
56
80
|
type: "password",
|
|
@@ -68,14 +92,23 @@ const SignInForm = ({ onSubmit, busy, error, workspace, notice, onForgotPassword
|
|
|
68
92
|
className: "text-xs font-medium text-danger-text",
|
|
69
93
|
children: error
|
|
70
94
|
}),
|
|
71
|
-
/* @__PURE__ */ jsx(Button, {
|
|
95
|
+
withPassword && /* @__PURE__ */ jsx(Button, {
|
|
72
96
|
variant: "primary",
|
|
73
97
|
type: "submit",
|
|
74
98
|
loading: busy,
|
|
75
99
|
className: "min-h-10",
|
|
76
100
|
children: "Sign in"
|
|
77
101
|
}),
|
|
78
|
-
|
|
102
|
+
withCode && /* @__PURE__ */ jsxs(Button, {
|
|
103
|
+
variant: withPassword ? "secondary" : "primary",
|
|
104
|
+
type: withPassword ? "button" : "submit",
|
|
105
|
+
onClick: withPassword ? sendCode : void 0,
|
|
106
|
+
loading: busy && !withPassword,
|
|
107
|
+
disabled: busy,
|
|
108
|
+
className: "min-h-10",
|
|
109
|
+
children: [/* @__PURE__ */ jsx(Mail, { className: "size-4" }), "Email me a sign-in code"]
|
|
110
|
+
}),
|
|
111
|
+
withPassword && onForgotPassword && /* @__PURE__ */ jsx("button", {
|
|
79
112
|
type: "button",
|
|
80
113
|
onClick: onForgotPassword,
|
|
81
114
|
className: "self-center text-[13px] font-medium text-primary-text hover:underline",
|
|
@@ -1,27 +1,22 @@
|
|
|
1
|
+
import { authMessage, signInMessage } from "./auth-messages.js";
|
|
1
2
|
import { useAuth } from "./auth-provider.js";
|
|
2
3
|
import { SignInForm } from "./sign-in-form.js";
|
|
3
4
|
import { useState } from "react";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import { AuthError } from "@protobase/client";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
7
|
//#region src/auth/sign-in-page.tsx
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
|
|
10
|
-
if (error.status === 401 || error.code === "INVALID_EMAIL_OR_PASSWORD") return "The email or password is not right.";
|
|
11
|
-
return error.message;
|
|
12
|
-
};
|
|
13
|
-
const SignInPage = ({ workspace, notice, onForgotPassword }) => {
|
|
14
|
-
const { signIn, signInSocial, socialProviders } = useAuth();
|
|
8
|
+
const SignInPage = ({ workspace, notice, onForgotPassword, onCodeSent }) => {
|
|
9
|
+
const { signIn, signInSocial, socialProviders, signInMethods, sendSignInCode, signInWithPasskey } = useAuth();
|
|
15
10
|
const [busy, setBusy] = useState(false);
|
|
16
11
|
const [error, setError] = useState();
|
|
17
|
-
const attempt = async (start) => {
|
|
12
|
+
const attempt = async (start, message) => {
|
|
18
13
|
setBusy(true);
|
|
19
14
|
setError(void 0);
|
|
20
15
|
try {
|
|
21
16
|
await start();
|
|
22
17
|
} catch (failure) {
|
|
23
18
|
if (!(failure instanceof AuthError)) throw failure;
|
|
24
|
-
setError(
|
|
19
|
+
setError(message(failure));
|
|
25
20
|
setBusy(false);
|
|
26
21
|
}
|
|
27
22
|
};
|
|
@@ -30,9 +25,15 @@ const SignInPage = ({ workspace, notice, onForgotPassword }) => {
|
|
|
30
25
|
error,
|
|
31
26
|
workspace,
|
|
32
27
|
notice,
|
|
28
|
+
methods: signInMethods,
|
|
33
29
|
onForgotPassword,
|
|
34
|
-
onContinueWithGitHub: socialProviders.includes("github") ? () => void attempt(() => signInSocial("github")) : void 0,
|
|
35
|
-
|
|
30
|
+
onContinueWithGitHub: socialProviders.includes("github") ? () => void attempt(() => signInSocial("github"), signInMessage) : void 0,
|
|
31
|
+
onPasskey: () => void attempt(signInWithPasskey, authMessage),
|
|
32
|
+
onSendCode: (email) => void attempt(async () => {
|
|
33
|
+
await sendSignInCode(email);
|
|
34
|
+
onCodeSent(email);
|
|
35
|
+
}, authMessage),
|
|
36
|
+
onSubmit: (email, password) => void attempt(() => signIn(email, password), signInMessage)
|
|
36
37
|
});
|
|
37
38
|
};
|
|
38
39
|
//#endregion
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/auth/staff-link.ts
|
|
2
|
+
/** Marks a link to the staff sign-in page, for example `/?staff-sign-in&email=sanne@example.com&reason=Ticket+4211`. */
|
|
3
|
+
const staffMarker = "staff-sign-in";
|
|
4
|
+
/** Where the server's staff callback says why a staff sign-in did not go through. */
|
|
5
|
+
const errorParam = "staff-error";
|
|
6
|
+
/** The staff sign-in this page was opened for, or `undefined`. */
|
|
7
|
+
const readStaffLink = (href) => {
|
|
8
|
+
const params = new URL(href).searchParams;
|
|
9
|
+
const error = params.get(errorParam);
|
|
10
|
+
if (!params.has("staff-sign-in") && !error) return void 0;
|
|
11
|
+
const marked = params.has(staffMarker);
|
|
12
|
+
return {
|
|
13
|
+
email: marked && params.get("email") || "",
|
|
14
|
+
reason: marked && params.get("reason") || "",
|
|
15
|
+
...error && { error }
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** The address without the staff link's parameters: for the address bar, and the page staff come back to. */
|
|
19
|
+
const withoutStaffLink = (href) => {
|
|
20
|
+
const url = new URL(href);
|
|
21
|
+
if (url.searchParams.has("staff-sign-in")) for (const name of [
|
|
22
|
+
staffMarker,
|
|
23
|
+
"email",
|
|
24
|
+
"reason"
|
|
25
|
+
]) url.searchParams.delete(name);
|
|
26
|
+
url.searchParams.delete(errorParam);
|
|
27
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { readStaffLink, staffMarker, withoutStaffLink };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { Input } from "../primitives/input.js";
|
|
3
|
+
import { Button } from "../primitives/button.js";
|
|
4
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/auth/staff-sign-in-form.tsx
|
|
8
|
+
const minimumReason = 10;
|
|
9
|
+
/** For staff of the operator: sign in as a person of this app, with a reason the app's admins see in their log. */
|
|
10
|
+
const StaffSignInForm = ({ operator, email: givenEmail = "", reason: givenReason = "", onSubmit, onBack, busy, error }) => {
|
|
11
|
+
const [email, setEmail] = useState(givenEmail);
|
|
12
|
+
const [reason, setReason] = useState(givenReason);
|
|
13
|
+
if (!operator) return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
14
|
+
title: "Staff sign-in",
|
|
15
|
+
description: "Staff of the team that runs this app cannot sign in as people here.",
|
|
16
|
+
children: [error && /* @__PURE__ */ jsx("p", {
|
|
17
|
+
role: "alert",
|
|
18
|
+
className: "mb-4 text-xs font-medium text-danger-text",
|
|
19
|
+
children: error
|
|
20
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
21
|
+
variant: "primary",
|
|
22
|
+
className: "min-h-10 w-full",
|
|
23
|
+
onClick: onBack,
|
|
24
|
+
children: "Back to sign in"
|
|
25
|
+
})]
|
|
26
|
+
});
|
|
27
|
+
return /* @__PURE__ */ jsx(AuthLayout, {
|
|
28
|
+
title: "Sign in as a person",
|
|
29
|
+
description: `For staff of ${operator}, to help someone with this app.`,
|
|
30
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
31
|
+
className: "flex flex-col gap-4",
|
|
32
|
+
onSubmit: (event) => {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
onSubmit(email.trim(), reason.trim());
|
|
35
|
+
},
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsxs("label", {
|
|
38
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
39
|
+
children: ["Their email", /* @__PURE__ */ jsx(Input, {
|
|
40
|
+
type: "email",
|
|
41
|
+
name: "email",
|
|
42
|
+
required: true,
|
|
43
|
+
autoFocus: !givenEmail,
|
|
44
|
+
value: email,
|
|
45
|
+
onChange: (event) => setEmail(event.target.value),
|
|
46
|
+
className: "min-h-6"
|
|
47
|
+
})]
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ jsxs("label", {
|
|
50
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
51
|
+
children: ["Reason", /* @__PURE__ */ jsx("textarea", {
|
|
52
|
+
name: "reason",
|
|
53
|
+
required: true,
|
|
54
|
+
minLength: minimumReason,
|
|
55
|
+
rows: 3,
|
|
56
|
+
value: reason,
|
|
57
|
+
placeholder: "Ticket 4211: the invoice totals look wrong",
|
|
58
|
+
onChange: (event) => setReason(event.target.value),
|
|
59
|
+
className: cn("resize-y rounded-md border bg-background px-2.5 py-1.5 text-[13px] font-normal shadow-sm outline-none placeholder:text-faint-foreground focus:border-primary focus:ring-2 focus:ring-primary/20", error ? "border-danger" : "border-border-strong")
|
|
60
|
+
})]
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ jsxs("p", {
|
|
63
|
+
className: "text-xs text-muted-foreground",
|
|
64
|
+
children: [
|
|
65
|
+
"You sign in at ",
|
|
66
|
+
operator,
|
|
67
|
+
" with a passkey or a second step. The session is short, shows a banner, and cannot change their sign-in. The app's admins see who signed in as whom, and why."
|
|
68
|
+
]
|
|
69
|
+
}),
|
|
70
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
71
|
+
role: "alert",
|
|
72
|
+
className: "text-xs font-medium text-danger-text",
|
|
73
|
+
children: error
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
76
|
+
variant: "primary",
|
|
77
|
+
type: "submit",
|
|
78
|
+
loading: busy,
|
|
79
|
+
className: "min-h-10",
|
|
80
|
+
children: ["Continue with ", operator]
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ jsx(Button, {
|
|
83
|
+
variant: "ghost",
|
|
84
|
+
onClick: onBack,
|
|
85
|
+
className: "min-h-10",
|
|
86
|
+
children: "Back to sign in"
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
export { StaffSignInForm };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { staffSignInMessage } from "./auth-messages.js";
|
|
2
|
+
import { useAuth } from "./auth-provider.js";
|
|
3
|
+
import { withoutStaffLink } from "./staff-link.js";
|
|
4
|
+
import { StaffSignInForm } from "./staff-sign-in-form.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { AuthError } from "@protobase/client";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/auth/staff-sign-in-page.tsx
|
|
9
|
+
/** Starts a staff sign-in and leaves for the operator provider, which comes back to this page without the staff link. */
|
|
10
|
+
const StaffSignInPage = ({ link, onBack }) => {
|
|
11
|
+
const { session, staffSignIn } = useAuth();
|
|
12
|
+
const [busy, setBusy] = useState(false);
|
|
13
|
+
const [error, setError] = useState(link.error && staffSignInMessage(link.error));
|
|
14
|
+
const submit = async (email, reason) => {
|
|
15
|
+
setBusy(true);
|
|
16
|
+
setError(void 0);
|
|
17
|
+
try {
|
|
18
|
+
const callbackURL = `${window.location.origin}${withoutStaffLink(window.location.href)}`;
|
|
19
|
+
window.location.assign(await session.staff.start({
|
|
20
|
+
email,
|
|
21
|
+
reason,
|
|
22
|
+
callbackURL
|
|
23
|
+
}));
|
|
24
|
+
} catch (failure) {
|
|
25
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
26
|
+
setError(staffSignInMessage(failure));
|
|
27
|
+
setBusy(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return /* @__PURE__ */ jsx(StaffSignInForm, {
|
|
31
|
+
operator: staffSignIn,
|
|
32
|
+
email: link.email,
|
|
33
|
+
reason: link.reason,
|
|
34
|
+
busy,
|
|
35
|
+
error,
|
|
36
|
+
onBack,
|
|
37
|
+
onSubmit: (email, reason) => void submit(email, reason)
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { StaffSignInPage };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { CodeInput } from "./code-input.js";
|
|
4
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
5
|
+
import { Checkbox } from "../primitives/checkbox.js";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/auth/two-factor-form.tsx
|
|
9
|
+
const instructions = {
|
|
10
|
+
totp: "Enter the 6-digit code from your authenticator app.",
|
|
11
|
+
otp: "Enter the 6-digit code we emailed you. It works for 5 minutes.",
|
|
12
|
+
backup: "Enter one of the backup codes you saved when you set up the authenticator app. Each works once."
|
|
13
|
+
};
|
|
14
|
+
/** The second step of a sign-in, for an account with two-factor authentication. */
|
|
15
|
+
const TwoFactorForm = ({ methods, onVerify, onSendCode, onBack, busy, error, codeSent, initialStep }) => {
|
|
16
|
+
const [step, setStep] = useState(initialStep ?? (methods.includes("totp") ? "totp" : "otp"));
|
|
17
|
+
const [code, setCode] = useState("");
|
|
18
|
+
const [trustDevice, setTrustDevice] = useState(false);
|
|
19
|
+
const waitingForEmail = step === "otp" && !codeSent;
|
|
20
|
+
const switchTo = (next) => {
|
|
21
|
+
setStep(next);
|
|
22
|
+
setCode("");
|
|
23
|
+
if (next === "otp" && !codeSent) onSendCode();
|
|
24
|
+
};
|
|
25
|
+
return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
26
|
+
title: "Two-factor authentication",
|
|
27
|
+
description: "One more step to sign in.",
|
|
28
|
+
children: [waitingForEmail ? /* @__PURE__ */ jsxs("div", {
|
|
29
|
+
className: "flex flex-col gap-4",
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx("p", {
|
|
32
|
+
className: "text-[13px] text-muted-foreground",
|
|
33
|
+
children: "We email you a code to finish signing in."
|
|
34
|
+
}),
|
|
35
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
36
|
+
role: "alert",
|
|
37
|
+
className: "text-xs font-medium text-danger-text",
|
|
38
|
+
children: error
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsx(Button, {
|
|
41
|
+
variant: "primary",
|
|
42
|
+
loading: busy,
|
|
43
|
+
onClick: onSendCode,
|
|
44
|
+
className: "min-h-10",
|
|
45
|
+
children: "Email me a code"
|
|
46
|
+
})
|
|
47
|
+
]
|
|
48
|
+
}) : /* @__PURE__ */ jsxs("form", {
|
|
49
|
+
className: "flex flex-col gap-4",
|
|
50
|
+
onSubmit: (event) => {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
onVerify({
|
|
53
|
+
method: step,
|
|
54
|
+
code: code.trim(),
|
|
55
|
+
trustDevice
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ jsx("p", {
|
|
60
|
+
className: "text-[13px] text-muted-foreground",
|
|
61
|
+
children: instructions[step]
|
|
62
|
+
}),
|
|
63
|
+
/* @__PURE__ */ jsxs("label", {
|
|
64
|
+
className: "flex flex-col gap-1.5 text-[13px] font-medium",
|
|
65
|
+
children: [step === "backup" ? "Backup code" : "Code", step === "backup" ? /* @__PURE__ */ jsx(Input, {
|
|
66
|
+
name: "code",
|
|
67
|
+
autoComplete: "off",
|
|
68
|
+
spellCheck: false,
|
|
69
|
+
required: true,
|
|
70
|
+
autoFocus: true,
|
|
71
|
+
value: code,
|
|
72
|
+
invalid: Boolean(error),
|
|
73
|
+
onChange: (event) => setCode(event.target.value),
|
|
74
|
+
className: "min-h-6 font-mono"
|
|
75
|
+
}) : /* @__PURE__ */ jsx(CodeInput, {
|
|
76
|
+
name: "code",
|
|
77
|
+
required: true,
|
|
78
|
+
autoFocus: true,
|
|
79
|
+
value: code,
|
|
80
|
+
invalid: Boolean(error),
|
|
81
|
+
onChange: (event) => setCode(event.target.value)
|
|
82
|
+
})]
|
|
83
|
+
}),
|
|
84
|
+
/* @__PURE__ */ jsxs("label", {
|
|
85
|
+
className: "flex items-center gap-2 text-[13px] text-muted-foreground",
|
|
86
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
87
|
+
checked: trustDevice,
|
|
88
|
+
onChange: setTrustDevice,
|
|
89
|
+
label: "Trust this browser"
|
|
90
|
+
}), "Don't ask again in this browser for 30 days"]
|
|
91
|
+
}),
|
|
92
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
93
|
+
role: "alert",
|
|
94
|
+
className: "text-xs font-medium text-danger-text",
|
|
95
|
+
children: error
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ jsx(Button, {
|
|
98
|
+
variant: "primary",
|
|
99
|
+
type: "submit",
|
|
100
|
+
loading: busy,
|
|
101
|
+
className: "min-h-10",
|
|
102
|
+
children: "Verify"
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
106
|
+
className: "mt-4 flex flex-col items-center gap-2 border-t pt-4 text-[13px]",
|
|
107
|
+
children: [
|
|
108
|
+
step !== "totp" && methods.includes("totp") && /* @__PURE__ */ jsx("button", {
|
|
109
|
+
type: "button",
|
|
110
|
+
onClick: () => switchTo("totp"),
|
|
111
|
+
className: "font-medium text-primary-text hover:underline",
|
|
112
|
+
children: "Use the authenticator app"
|
|
113
|
+
}),
|
|
114
|
+
step !== "otp" && methods.includes("otp") && /* @__PURE__ */ jsx("button", {
|
|
115
|
+
type: "button",
|
|
116
|
+
onClick: () => switchTo("otp"),
|
|
117
|
+
className: "font-medium text-primary-text hover:underline",
|
|
118
|
+
children: "Email me a code instead"
|
|
119
|
+
}),
|
|
120
|
+
step !== "backup" && methods.includes("totp") && /* @__PURE__ */ jsx("button", {
|
|
121
|
+
type: "button",
|
|
122
|
+
onClick: () => switchTo("backup"),
|
|
123
|
+
className: "font-medium text-primary-text hover:underline",
|
|
124
|
+
children: "Use a backup code"
|
|
125
|
+
}),
|
|
126
|
+
/* @__PURE__ */ jsx("button", {
|
|
127
|
+
type: "button",
|
|
128
|
+
onClick: onBack,
|
|
129
|
+
className: "font-medium text-muted-foreground hover:underline",
|
|
130
|
+
children: "Back to sign in"
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
})]
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
export { TwoFactorForm };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { authMessage } from "./auth-messages.js";
|
|
2
|
+
import { useAuth } from "./auth-provider.js";
|
|
3
|
+
import { TwoFactorForm } from "./two-factor-form.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { AuthError } from "@protobase/client";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/auth/two-factor-page.tsx
|
|
8
|
+
/** The second step after a password or an emailed code, with the methods the server named. */
|
|
9
|
+
const TwoFactorPage = ({ methods, onBack }) => {
|
|
10
|
+
const { verifyTwoFactor, session } = useAuth();
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState();
|
|
13
|
+
const [codeSent, setCodeSent] = useState(false);
|
|
14
|
+
const attempt = async (step) => {
|
|
15
|
+
setBusy(true);
|
|
16
|
+
setError(void 0);
|
|
17
|
+
try {
|
|
18
|
+
await step();
|
|
19
|
+
} catch (failure) {
|
|
20
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
21
|
+
setError(authMessage(failure));
|
|
22
|
+
}
|
|
23
|
+
setBusy(false);
|
|
24
|
+
};
|
|
25
|
+
return /* @__PURE__ */ jsx(TwoFactorForm, {
|
|
26
|
+
methods,
|
|
27
|
+
busy,
|
|
28
|
+
error,
|
|
29
|
+
codeSent,
|
|
30
|
+
onBack,
|
|
31
|
+
onVerify: (input) => void attempt(() => verifyTwoFactor(input)),
|
|
32
|
+
onSendCode: () => void attempt(async () => {
|
|
33
|
+
await session.sendTwoFactorCode();
|
|
34
|
+
setCodeSent(true);
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { TwoFactorPage };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
//#region src/auth/user-to-shell.ts
|
|
2
2
|
const hueOf = (text) => [...text].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) % 360, 7);
|
|
3
3
|
const initialsOf = (name) => name.split(/[\s@.]+/).filter(Boolean).slice(0, 2).map((part) => part[0].toUpperCase()).join("");
|
|
4
|
+
const rolesOf = (user) => (user.role ?? "user").split(",").map((role) => role.trim());
|
|
5
|
+
/** The user holds the `admin` role, among others perhaps. */
|
|
6
|
+
const isAdmin = (user) => rolesOf(user).includes("admin");
|
|
4
7
|
/** What the sidebar's profile menu shows for a signed-in user. */
|
|
5
8
|
const userToShell = (user) => {
|
|
6
|
-
const roles = (user
|
|
9
|
+
const roles = rolesOf(user);
|
|
7
10
|
const admin = roles.includes("admin");
|
|
8
11
|
return {
|
|
9
12
|
name: user.name || user.email,
|
|
@@ -15,4 +18,4 @@ const userToShell = (user) => {
|
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
//#endregion
|
|
18
|
-
export { userToShell };
|
|
21
|
+
export { isAdmin, userToShell };
|
package/dist/chat/composer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Input } from "../primitives/input.js";
|
|
2
2
|
import { Button } from "../primitives/button.js";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
4
|
import { SendHorizontal } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
//#region src/chat/composer.tsx
|
|
7
7
|
/** The text box under a chat. Enter or the button sends the trimmed text and clears the box. */
|
|
8
8
|
const Composer = ({ onSend, busy, placeholder, inputRef }) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
|
-
import { QueryClient } from "@tanstack/react-query";
|
|
3
2
|
import { Client, ClientOptions } from "@protobase/client";
|
|
3
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
4
4
|
//#region src/data/api-provider.d.ts
|
|
5
5
|
export declare const useClient: () => {
|
|
6
6
|
list: <R extends import("@protobase/client").ResourceRef>(resource: R, params?: import("@protobase/client").ListParams) => Promise<import("@protobase/client").ListPage<import("@protobase/client").RecordOf<R>>>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { createContext, useContext, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createClient } from "@protobase/client";
|
|
3
4
|
import { jsx } from "react/jsx-runtime";
|
|
4
5
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
-
import { createClient } from "@protobase/client";
|
|
6
6
|
//#region src/data/api-provider.tsx
|
|
7
7
|
const ApiContext = createContext(null);
|
|
8
8
|
const useClient = () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
|
-
import { useQuery } from "@tanstack/react-query";
|
|
4
3
|
import { filterText } from "@protobase/client";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
5
|
//#region src/data/use-aggregates.ts
|
|
6
6
|
const useFacets = (resource, field, filter, enabled = true) => {
|
|
7
7
|
const client = useClient();
|
package/dist/data/use-list.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
|
-
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
4
3
|
import { filterText } from "@protobase/client";
|
|
4
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
5
5
|
//#region src/data/use-list.ts
|
|
6
6
|
/** Infinite pages of a resource, following `next_page_token`. */
|
|
7
7
|
const useList = (resource, { filter, orderBy, pageSize = 50, fields, startToken = "", enabled = true } = {}) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { keys } from "./query-keys.js";
|
|
2
2
|
import { useClient } from "./api-provider.js";
|
|
3
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
3
|
import { PreconditionFailedError } from "@protobase/client";
|
|
4
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
import { encodeKey } from "@protobase/schema";
|
|
6
6
|
//#region src/data/use-update-record.ts
|
|
7
7
|
/**
|
package/dist/data-grid/cells.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { Avatar } from "../primitives/avatar.js";
|
|
3
3
|
import { Badge } from "../primitives/badge.js";
|
|
4
|
-
import { formatDate, formatDateTime } from "../format/date.js";
|
|
5
|
-
import { userById } from "../mocks/users.js";
|
|
6
4
|
import { formatMoney } from "../format/money.js";
|
|
7
5
|
import { formatInt, formatPercent, formatSigned } from "../format/number.js";
|
|
8
|
-
import {
|
|
6
|
+
import { formatDate, formatDateTime } from "../format/date.js";
|
|
7
|
+
import { userById } from "../mocks/users.js";
|
|
9
8
|
import { Check } from "lucide-react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { enumLabel } from "@protobase/schema";
|
|
11
11
|
//#region src/data-grid/cells.tsx
|
|
12
12
|
const muted = (text) => /* @__PURE__ */ jsx("span", {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import { gridTemplate } from "./grid-template.js";
|
|
3
2
|
import { Checkbox } from "../primitives/checkbox.js";
|
|
3
|
+
import { gridTemplate } from "./grid-template.js";
|
|
4
4
|
import { isRightAligned } from "./column-spec.js";
|
|
5
5
|
import { HeaderMenu } from "./header-menu.js";
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
6
|
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
//#region src/data-grid/grid-header.tsx
|
|
9
9
|
const SortIcon = ({ sorted }) => {
|
|
10
10
|
if (sorted === "asc") return /* @__PURE__ */ jsx(ArrowUp, { className: "size-3 text-foreground" });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { formatInt } from "../format/number.js";
|
|
3
|
-
import { gridTemplate } from "./grid-template.js";
|
|
4
3
|
import { Checkbox } from "../primitives/checkbox.js";
|
|
4
|
+
import { gridTemplate } from "./grid-template.js";
|
|
5
5
|
import { isRightAligned } from "./column-spec.js";
|
|
6
6
|
import { Skeleton } from "../primitives/skeleton.js";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
2
|
import { MoreHorizontal } from "lucide-react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/data-grid/header-menu.tsx
|
|
5
5
|
/** The "..." in a column header: for now, pinning the column to the left edge. */
|
|
6
6
|
const HeaderMenu = ({ header, pinned, onTogglePin }) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
3
|
import { MoreHorizontal } from "lucide-react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
//#region src/data-grid/row-menu.tsx
|
|
7
7
|
/** The actions that apply to a row; a row with none gets no menu at all. */
|