@protobase/ui 0.3.4 → 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.d.ts +3 -1
- package/dist/app/app.js +50 -18
- 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-status.js +25 -0
- package/dist/app/runtime/runtime-update.js +77 -0
- package/dist/app/runtime/use-runtime-status.js +59 -0
- 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/meta-data.d.ts +4 -0
- package/dist/data/use-aggregates.js +1 -1
- package/dist/data/use-list.js +1 -1
- package/dist/data/use-meta.js +2 -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/lib/use-visual-viewport.js +27 -0
- 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.d.ts +5 -1
- package/dist/side-panel/side-panel.js +49 -35
- package/dist/step-list/step-list.js +1 -1
- package/dist/toasts/toast.js +1 -1
- package/package.json +10 -8
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { authMessage } from "./auth-messages.js";
|
|
2
|
+
import { useAuth } from "./auth-provider.js";
|
|
3
|
+
import { EmailCodeForm } from "./email-code-form.js";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { AuthError } from "@protobase/client";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/auth/email-code-page.tsx
|
|
8
|
+
/** Signs in with the code emailed to `email`; the account's second step follows when it has one. */
|
|
9
|
+
const EmailCodePage = ({ email, onBack }) => {
|
|
10
|
+
const { signInWithCode, sendSignInCode } = useAuth();
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState();
|
|
13
|
+
const [notice, setNotice] = useState();
|
|
14
|
+
const attempt = async (step) => {
|
|
15
|
+
setBusy(true);
|
|
16
|
+
setError(void 0);
|
|
17
|
+
setNotice(void 0);
|
|
18
|
+
try {
|
|
19
|
+
await step();
|
|
20
|
+
} catch (failure) {
|
|
21
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
22
|
+
setError(authMessage(failure));
|
|
23
|
+
}
|
|
24
|
+
setBusy(false);
|
|
25
|
+
};
|
|
26
|
+
return /* @__PURE__ */ jsx(EmailCodeForm, {
|
|
27
|
+
email,
|
|
28
|
+
busy,
|
|
29
|
+
error,
|
|
30
|
+
notice,
|
|
31
|
+
onBack,
|
|
32
|
+
onSubmit: (code) => void attempt(() => signInWithCode(email, code)),
|
|
33
|
+
onResend: () => void attempt(async () => {
|
|
34
|
+
await sendSignInCode(email);
|
|
35
|
+
setNotice("A new code is on its way. Only the newest one works.");
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { EmailCodePage };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Button } from "../primitives/button.js";
|
|
2
2
|
import { AuthLayout } from "./auth-layout.js";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
4
|
import { Check, Copy } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
//#region src/auth/first-run-page.tsx
|
|
7
7
|
const createAdminCommand = "protobase users create you@example.com";
|
|
8
8
|
/**
|
|
@@ -2,8 +2,8 @@ import { useAuth } from "./auth-provider.js";
|
|
|
2
2
|
import { ForgotPasswordForm } from "./forgot-password-form.js";
|
|
3
3
|
import { resetLinkTarget } from "./reset-link.js";
|
|
4
4
|
import { useState } from "react";
|
|
5
|
-
import { jsx } from "react/jsx-runtime";
|
|
6
5
|
import { AuthError } from "@protobase/client";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
//#region src/auth/forgot-password-page.tsx
|
|
8
8
|
/** Asks the server to email a reset link that leads back to this page. */
|
|
9
9
|
const ForgotPasswordPage = ({ onBack }) => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useAuth } from "./auth-provider.js";
|
|
2
2
|
import { ResetPasswordForm } from "./reset-password-form.js";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
5
4
|
import { AuthError } from "@protobase/client";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
//#region src/auth/reset-password-page.tsx
|
|
7
7
|
/** Better Auth's answers to a new password, in words for the person at the keyboard. */
|
|
8
8
|
const resetPasswordMessage = (error) => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { TwoFactorSetup } from "../account/two-factor-setup.js";
|
|
3
|
+
import { AuthLayout } from "./auth-layout.js";
|
|
4
|
+
import { KeyRound } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/auth/setup-required-form.tsx
|
|
7
|
+
const titles = {
|
|
8
|
+
twoFactor: "Turn on two-factor authentication",
|
|
9
|
+
passkey: "Add a passkey"
|
|
10
|
+
};
|
|
11
|
+
/** Shown after signing in while the account lacks something the sign-in policy requires; the app opens once it is set up. */
|
|
12
|
+
const SetupRequiredForm = ({ step, twoFactor, onAddPasskey, busy, error, onSignOut }) => /* @__PURE__ */ jsxs(AuthLayout, {
|
|
13
|
+
title: titles[step],
|
|
14
|
+
description: "This app requires it for every account.",
|
|
15
|
+
children: [step === "twoFactor" ? /* @__PURE__ */ jsx(TwoFactorSetup, { ...twoFactor }) : /* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: "flex flex-col gap-4",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ jsx("p", {
|
|
19
|
+
className: "text-[13px] text-muted-foreground",
|
|
20
|
+
children: "A passkey signs you in with Face ID, Touch ID, Windows Hello, your phone or a security key. Your browser asks which one to use."
|
|
21
|
+
}),
|
|
22
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
23
|
+
role: "alert",
|
|
24
|
+
className: "text-xs font-medium text-danger-text",
|
|
25
|
+
children: error
|
|
26
|
+
}),
|
|
27
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
28
|
+
variant: "primary",
|
|
29
|
+
loading: busy,
|
|
30
|
+
onClick: onAddPasskey,
|
|
31
|
+
className: "min-h-10",
|
|
32
|
+
children: [/* @__PURE__ */ jsx(KeyRound, { className: "size-4" }), "Add a passkey"]
|
|
33
|
+
})
|
|
34
|
+
]
|
|
35
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
36
|
+
type: "button",
|
|
37
|
+
onClick: onSignOut,
|
|
38
|
+
className: "mt-4 w-full text-center text-[13px] font-medium text-muted-foreground hover:underline",
|
|
39
|
+
children: "Sign out"
|
|
40
|
+
})]
|
|
41
|
+
});
|
|
42
|
+
//#endregion
|
|
43
|
+
export { SetupRequiredForm };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Spinner } from "../primitives/spinner.js";
|
|
2
|
+
import { authMessage } from "./auth-messages.js";
|
|
3
|
+
import { useAuth } from "./auth-provider.js";
|
|
4
|
+
import { SetupRequiredForm } from "./setup-required-form.js";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { AuthError } from "@protobase/client";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/auth/setup-required-page.tsx
|
|
9
|
+
/** Sets up what the sign-in policy requires, one thing at a time, then reads the account again to open the app. */
|
|
10
|
+
const SetupRequiredPage = ({ missing }) => {
|
|
11
|
+
const { session, recheck, signOut } = useAuth();
|
|
12
|
+
const [methods, setMethods] = useState();
|
|
13
|
+
const [busy, setBusy] = useState(false);
|
|
14
|
+
const [error, setError] = useState();
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
session.account.signInMethods().then(setMethods, (failure) => setError(failure instanceof AuthError ? authMessage(failure) : "Could not read your account"));
|
|
17
|
+
}, [session]);
|
|
18
|
+
const addPasskey = async () => {
|
|
19
|
+
setBusy(true);
|
|
20
|
+
setError(void 0);
|
|
21
|
+
try {
|
|
22
|
+
await session.account.addPasskey();
|
|
23
|
+
recheck();
|
|
24
|
+
} catch (failure) {
|
|
25
|
+
if (!(failure instanceof AuthError)) throw failure;
|
|
26
|
+
setError(authMessage(failure));
|
|
27
|
+
setBusy(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (!methods && !error) return /* @__PURE__ */ jsxs("div", {
|
|
31
|
+
className: "flex h-full min-h-48 items-center justify-center gap-2 text-[13px] text-muted-foreground",
|
|
32
|
+
children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
|
|
33
|
+
});
|
|
34
|
+
return /* @__PURE__ */ jsx(SetupRequiredForm, {
|
|
35
|
+
step: missing[0],
|
|
36
|
+
busy,
|
|
37
|
+
error,
|
|
38
|
+
onAddPasskey: () => void addPasskey(),
|
|
39
|
+
onSignOut: () => void signOut(),
|
|
40
|
+
twoFactor: {
|
|
41
|
+
offerEmailedCodes: methods?.mail ?? false,
|
|
42
|
+
needsPassword: methods?.account.password ?? true,
|
|
43
|
+
onStartApp: (password) => session.account.startAuthenticatorApp(password),
|
|
44
|
+
onConfirmApp: (code) => session.account.confirmAuthenticatorApp(code),
|
|
45
|
+
onEmailedCodes: (password) => session.account.turnOnEmailedCodes(password),
|
|
46
|
+
onDone: recheck
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { SetupRequiredPage };
|
|
@@ -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 };
|