@protobase/ui 0.3.5 → 0.5.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.
Files changed (100) hide show
  1. package/dist/account/account-pages.js +20 -0
  2. package/dist/account/account-section.js +31 -0
  3. package/dist/account/account-security-page.js +59 -0
  4. package/dist/account/backup-codes.js +44 -0
  5. package/dist/account/passkeys-section.js +141 -0
  6. package/dist/account/password-dialog.js +58 -0
  7. package/dist/account/sign-in-policy-form.js +108 -0
  8. package/dist/account/sign-in-policy-labels.js +76 -0
  9. package/dist/account/sign-in-policy-page.js +72 -0
  10. package/dist/account/staff-sign-in-log.js +62 -0
  11. package/dist/account/totp-qr.js +37 -0
  12. package/dist/account/two-factor-section.js +123 -0
  13. package/dist/account/two-factor-setup.js +188 -0
  14. package/dist/activity-chart/activity-chart.js +1 -1
  15. package/dist/activity-chart/activity-tooltip.js +1 -1
  16. package/dist/app/app.js +37 -15
  17. package/dist/app/assistant-shell.js +3 -2
  18. package/dist/app/create/create-page.js +1 -1
  19. package/dist/app/delete/use-delete-flow.js +1 -1
  20. package/dist/app/error-banner.js +1 -1
  21. package/dist/app/list-page.js +1 -1
  22. package/dist/app/list-search.js +1 -1
  23. package/dist/app/meta-gate.js +1 -1
  24. package/dist/app/pages/blocks/modal-form.js +3 -3
  25. package/dist/app/pages/merge-ui.d.ts +9 -0
  26. package/dist/app/pages/merge-ui.js +45 -0
  27. package/dist/app/pages/project-ui-provider.js +15 -0
  28. package/dist/app/pages/project-ui.d.ts +6 -3
  29. package/dist/app/pages/project-ui.js +3 -7
  30. package/dist/app/pages/record-table.js +1 -1
  31. package/dist/app/pages/use-run-action.js +1 -1
  32. package/dist/app/record-page.js +5 -5
  33. package/dist/app/runtime/runtime-update.js +1 -1
  34. package/dist/app/sensitive-field.js +1 -1
  35. package/dist/app/staff-banner.js +67 -0
  36. package/dist/app/use-record-editor.js +1 -1
  37. package/dist/app-shell/breadcrumb.js +1 -1
  38. package/dist/app-shell/global-search.js +1 -1
  39. package/dist/app-shell/profile-menu.js +1 -1
  40. package/dist/app-shell/sidebar-group.js +1 -1
  41. package/dist/app-shell/top-bar.js +1 -1
  42. package/dist/assistant-dock/assistant-dock.d.ts +8 -2
  43. package/dist/assistant-dock/assistant-dock.js +20 -13
  44. package/dist/assistant-dock/index.d.ts +3 -1
  45. package/dist/assistant-dock/part-view.d.ts +1 -1
  46. package/dist/assistant-dock/part-view.js +3 -1
  47. package/dist/assistant-dock/use-backend-data.d.ts +23 -0
  48. package/dist/assistant-dock/use-backend-data.js +67 -0
  49. package/dist/assistant-dock/widget-view.d.ts +1 -0
  50. package/dist/assistant-dock/widget-view.js +44 -0
  51. package/dist/auth/auth-gate.js +42 -4
  52. package/dist/auth/auth-messages.js +48 -0
  53. package/dist/auth/auth-provider.js +40 -14
  54. package/dist/auth/code-input.js +17 -0
  55. package/dist/auth/email-code-form.js +81 -0
  56. package/dist/auth/email-code-page.js +40 -0
  57. package/dist/auth/first-run-page.js +1 -1
  58. package/dist/auth/forgot-password-page.js +1 -1
  59. package/dist/auth/reset-password-page.js +1 -1
  60. package/dist/auth/setup-required-form.js +43 -0
  61. package/dist/auth/setup-required-page.js +51 -0
  62. package/dist/auth/sign-in-form.js +53 -20
  63. package/dist/auth/sign-in-page.js +14 -13
  64. package/dist/auth/staff-link.js +30 -0
  65. package/dist/auth/staff-sign-in-form.js +93 -0
  66. package/dist/auth/staff-sign-in-page.js +41 -0
  67. package/dist/auth/two-factor-form.js +137 -0
  68. package/dist/auth/two-factor-page.js +39 -0
  69. package/dist/auth/user-to-shell.js +5 -2
  70. package/dist/chat/composer.js +1 -1
  71. package/dist/data/api-provider.d.ts +1 -1
  72. package/dist/data/api-provider.js +1 -1
  73. package/dist/data/use-aggregates.js +1 -1
  74. package/dist/data/use-list.js +1 -1
  75. package/dist/data/use-update-record.js +1 -1
  76. package/dist/data-grid/cells.js +3 -3
  77. package/dist/data-grid/grid-header.js +2 -2
  78. package/dist/data-grid/grid-row.js +1 -1
  79. package/dist/data-grid/header-menu.js +1 -1
  80. package/dist/data-grid/row-menu.js +1 -1
  81. package/dist/diff-view/diff-view.js +1 -1
  82. package/dist/filter-panel/active-filter-chips.js +1 -1
  83. package/dist/filter-panel/facet-group.js +1 -1
  84. package/dist/filter-panel/filter-menu.js +1 -1
  85. package/dist/index.d.ts +3 -1
  86. package/dist/index.js +4 -2
  87. package/dist/inline-child-table/inline-child-table.js +1 -1
  88. package/dist/inline-child-table/line-menu.js +1 -1
  89. package/dist/inline-child-table/line-row.js +2 -2
  90. package/dist/inline-child-table/totals-footer.js +1 -1
  91. package/dist/live/use-filter-config.js +1 -1
  92. package/dist/primitives/checkbox.js +1 -1
  93. package/dist/primitives/sheet.js +1 -1
  94. package/dist/primitives/spinner.js +1 -1
  95. package/dist/record-view/field.js +1 -1
  96. package/dist/record-view/record-sidebar.js +1 -1
  97. package/dist/side-panel/side-panel.js +1 -1
  98. package/dist/step-list/step-list.js +1 -1
  99. package/dist/toasts/toast.js +1 -1
  100. package/package.json +6 -5
@@ -0,0 +1,48 @@
1
+ //#region src/auth/auth-messages.ts
2
+ const tooManyRequests = "Too many attempts. Wait a minute and try again.";
3
+ const passkeyClosed = "The passkey prompt closed before it finished. Try again.";
4
+ const messages = {
5
+ INVALID_EMAIL_OR_PASSWORD: "The email or password is not right.",
6
+ INVALID_PASSWORD: "The password is not right.",
7
+ INVALID_OTP: "The code is not right.",
8
+ OTP_EXPIRED: "The code has expired. Ask for a new one.",
9
+ TOO_MANY_ATTEMPTS: "Too many wrong codes. Ask for a new one.",
10
+ INVALID_CODE: "The code is not right.",
11
+ INVALID_BACKUP_CODE: "The backup code is not right, or was used before.",
12
+ TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE: "Too many wrong codes. Sign in again.",
13
+ ACCOUNT_TEMPORARILY_LOCKED: "Too many wrong codes. Wait 15 minutes and try again.",
14
+ INVALID_TWO_FACTOR_COOKIE: "This sign-in took too long. Sign in again.",
15
+ AUTH_CANCELLED: passkeyClosed,
16
+ ERROR_CEREMONY_ABORTED: passkeyClosed,
17
+ REGISTRATION_CANCELLED: passkeyClosed,
18
+ PASSKEY_NOT_FOUND: "This passkey is not registered for an account here.",
19
+ AUTHENTICATION_FAILED: "The passkey could not be checked. Try again.",
20
+ PREVIOUSLY_REGISTERED: "This passkey is added already.",
21
+ ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: "This passkey is added already.",
22
+ SESSION_NOT_FRESH: "For this change, sign out and sign in again first."
23
+ };
24
+ const known = (error) => error.code ? messages[error.code] : void 0;
25
+ /** Better Auth's error from a sign-in step or an account change, in words for the person at the keyboard. */
26
+ const authMessage = (error) => known(error) ?? (error.status === 429 ? tooManyRequests : error.message);
27
+ /** As `authMessage`, for the password form: any other 401 is a wrong email or password. */
28
+ const signInMessage = (error) => known(error) ?? (error.status === 429 ? tooManyRequests : error.status === 401 ? messages.INVALID_EMAIL_OR_PASSWORD : error.message);
29
+ const staffMessages = {
30
+ STAFF_PERMISSION_MISSING: "Your staff account has no permission to sign in as people here.",
31
+ STAFF_SIGN_IN_NOT_STRONG: "Sign in to your staff account with a passkey or a second step, then try again.",
32
+ STAFF_SIGN_IN_STALE: "Your staff sign-in is too old. Sign in to your staff account again.",
33
+ STAFF_SIGN_IN_EXPIRED: "This staff sign-in took too long or was used already. Start again.",
34
+ STAFF_SIGN_IN_FAILED: "The staff sign-in did not go through at the provider. Try again.",
35
+ STAFF_USER_UNAVAILABLE: "There is no account with this address, or it is disabled.",
36
+ STAFF_REASON_REQUIRED: "Say why you sign in as this person, in at least 10 characters.",
37
+ SIGN_IN_METHOD_FORBIDDEN: "Staff sign-in is turned off for this app.",
38
+ INVALID_CALLBACK_URL: "This page is not on an address the server trusts."
39
+ };
40
+ /** Why a staff sign-in did not go through, from the code it came back with or an `AuthError` of its start. */
41
+ const staffSignInMessage = (failure) => {
42
+ const code = typeof failure === "string" ? failure : failure.code;
43
+ if (code && staffMessages[code]) return staffMessages[code];
44
+ if (typeof failure === "string") return "The staff sign-in did not go through. Try again.";
45
+ return failure.status === 429 ? tooManyRequests : failure.message;
46
+ };
47
+ //#endregion
48
+ export { authMessage, signInMessage, staffSignInMessage };
@@ -1,6 +1,6 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
- import { jsx } from "react/jsx-runtime";
3
2
  import { createAuthSession } from "@protobase/client";
3
+ import { jsx } from "react/jsx-runtime";
4
4
  //#region src/auth/auth-provider.tsx
5
5
  const AuthContext = createContext(null);
6
6
  const useAuth = () => {
@@ -11,45 +11,67 @@ const useAuth = () => {
11
11
  /** Finds out whether the server has a login and who is signed in, and keeps that state for the app. */
12
12
  const AuthProvider = ({ session: given, children }) => {
13
13
  const [state, setState] = useState({ kind: "loading" });
14
+ const [signInMethods, setSignInMethods] = useState(["password"]);
14
15
  const [passwordReset, setPasswordReset] = useState(false);
15
16
  const [socialProviders, setSocialProviders] = useState([]);
17
+ const [staffSignIn, setStaffSignIn] = useState();
16
18
  const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
17
19
  const signedOut = useRef(markSignedOut);
18
20
  signedOut.current = markSignedOut;
19
21
  const session = useMemo(() => given ?? createAuthSession({ onSignedOut: () => signedOut.current() }), [given]);
22
+ const enter = useCallback(async (user) => {
23
+ const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
24
+ setState(missing.length > 0 ? {
25
+ kind: "setup-required",
26
+ user,
27
+ missing
28
+ } : {
29
+ kind: "signed-in",
30
+ user,
31
+ ...staff && { staff }
32
+ });
33
+ }, [session]);
34
+ const finish = useCallback(async (result) => result.kind === "two-factor" ? setState({
35
+ kind: "two-factor",
36
+ methods: result.methods
37
+ }) : enter(result.user), [enter]);
20
38
  const recheck = useCallback(() => {
21
39
  setState({ kind: "loading" });
22
40
  session.status().then(async (status) => {
41
+ setSignInMethods(status.signInMethods);
23
42
  setPasswordReset(status.passwordReset);
24
43
  setSocialProviders(status.socialProviders);
44
+ setStaffSignIn(status.staffSignIn);
25
45
  if (status.needsAdmin) return setState({ kind: "needs-admin" });
26
46
  const user = await session.session();
27
- setState(user ? {
28
- kind: "signed-in",
29
- user
30
- } : { kind: "signed-out" });
31
- }, (error) => setState({
47
+ return user ? enter(user) : setState({ kind: "signed-out" });
48
+ }).catch((error) => setState({
32
49
  kind: "error",
33
50
  message: error instanceof Error ? error.message : "The server did not answer"
34
51
  }));
35
- }, [session]);
52
+ }, [session, enter]);
36
53
  useEffect(recheck, [recheck]);
37
54
  const api = useMemo(() => ({
38
55
  state,
39
56
  session,
57
+ signInMethods,
40
58
  passwordReset,
41
59
  socialProviders,
60
+ staffSignIn,
42
61
  markSignedOut,
43
62
  recheck,
44
- signIn: async (email, password) => {
45
- setState({
46
- kind: "signed-in",
47
- user: await session.signIn(email, password)
48
- });
49
- },
63
+ signIn: async (email, password) => finish(await session.signIn(email, password)),
64
+ sendSignInCode: (email) => session.sendSignInCode(email),
65
+ signInWithCode: async (email, code) => finish(await session.signInWithCode(email, code)),
66
+ signInWithPasskey: async () => enter(await session.signInWithPasskey()),
67
+ verifyTwoFactor: async (input) => enter(await session.verifyTwoFactor(input)),
50
68
  signInSocial: async (provider) => {
51
69
  window.location.assign(await session.signInSocial(provider, window.location.href));
52
70
  },
71
+ stopStaffSession: async () => {
72
+ await session.staff.stop();
73
+ setState({ kind: "signed-out" });
74
+ },
53
75
  signOut: async () => {
54
76
  await session.signOut();
55
77
  setState({ kind: "signed-out" });
@@ -57,10 +79,14 @@ const AuthProvider = ({ session: given, children }) => {
57
79
  }), [
58
80
  state,
59
81
  session,
82
+ signInMethods,
60
83
  passwordReset,
61
84
  socialProviders,
85
+ staffSignIn,
62
86
  markSignedOut,
63
- recheck
87
+ recheck,
88
+ finish,
89
+ enter
64
90
  ]);
65
91
  return /* @__PURE__ */ jsx(AuthContext.Provider, {
66
92
  value: api,
@@ -0,0 +1,17 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { Input } from "../primitives/input.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/auth/code-input.tsx
5
+ /** A 6-digit one-time code field: digits on phone keyboards, filled from the mail app where the system offers it. */
6
+ const CodeInput = ({ className, ...rest }) => /* @__PURE__ */ jsx(Input, {
7
+ type: "text",
8
+ inputMode: "numeric",
9
+ autoComplete: "one-time-code",
10
+ pattern: "[0-9]{6}",
11
+ maxLength: 6,
12
+ spellCheck: false,
13
+ className: cn("min-h-6 font-mono tracking-[0.3em]", className),
14
+ ...rest
15
+ });
16
+ //#endregion
17
+ export { CodeInput };
@@ -0,0 +1,81 @@
1
+ import { Button } from "../primitives/button.js";
2
+ import { CodeInput } from "./code-input.js";
3
+ import { AuthLayout } from "./auth-layout.js";
4
+ import { useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/auth/email-code-form.tsx
7
+ /** The second half of signing in with an emailed code: the code from the email. */
8
+ const EmailCodeForm = ({ email, onSubmit, onResend, onBack, busy, error, notice }) => {
9
+ const [code, setCode] = useState("");
10
+ return /* @__PURE__ */ jsxs(AuthLayout, {
11
+ title: "Check your email",
12
+ description: "Enter the code we sent to sign in.",
13
+ children: [
14
+ /* @__PURE__ */ jsxs("p", {
15
+ className: "mb-4 text-[13px] text-muted-foreground",
16
+ children: [
17
+ "If ",
18
+ /* @__PURE__ */ jsx("span", {
19
+ className: "font-medium text-foreground",
20
+ children: email
21
+ }),
22
+ " has an account, a 6-digit code is on its way. It works for 5 minutes, once."
23
+ ]
24
+ }),
25
+ notice && /* @__PURE__ */ jsx("p", {
26
+ role: "status",
27
+ className: "mb-4 rounded-md bg-success-soft px-3 py-2 text-xs font-medium text-success-text",
28
+ children: notice
29
+ }),
30
+ /* @__PURE__ */ jsxs("form", {
31
+ className: "flex flex-col gap-4",
32
+ onSubmit: (event) => {
33
+ event.preventDefault();
34
+ onSubmit(code.trim());
35
+ },
36
+ children: [
37
+ /* @__PURE__ */ jsxs("label", {
38
+ className: "flex flex-col gap-1.5 text-[13px] font-medium",
39
+ children: ["Code", /* @__PURE__ */ jsx(CodeInput, {
40
+ name: "code",
41
+ required: true,
42
+ autoFocus: true,
43
+ value: code,
44
+ invalid: Boolean(error),
45
+ onChange: (event) => setCode(event.target.value)
46
+ })]
47
+ }),
48
+ error && /* @__PURE__ */ jsx("p", {
49
+ role: "alert",
50
+ className: "text-xs font-medium text-danger-text",
51
+ children: error
52
+ }),
53
+ /* @__PURE__ */ jsx(Button, {
54
+ variant: "primary",
55
+ type: "submit",
56
+ loading: busy,
57
+ className: "min-h-10",
58
+ children: "Sign in"
59
+ }),
60
+ /* @__PURE__ */ jsxs("div", {
61
+ className: "flex justify-between gap-2",
62
+ children: [/* @__PURE__ */ jsx(Button, {
63
+ variant: "ghost",
64
+ onClick: onBack,
65
+ className: "min-h-10",
66
+ children: "Back to sign in"
67
+ }), /* @__PURE__ */ jsx(Button, {
68
+ variant: "ghost",
69
+ onClick: onResend,
70
+ disabled: busy,
71
+ className: "min-h-10",
72
+ children: "Send a new code"
73
+ })]
74
+ })
75
+ ]
76
+ })
77
+ ]
78
+ });
79
+ };
80
+ //#endregion
81
+ export { EmailCodeForm };
@@ -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 SignInForm = ({ onSubmit, busy, error, workspace, notice, onForgotPassword, onContinueWithGitHub }) => {
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__ */ jsx(Button, {
20
- onClick: onContinueWithGitHub,
21
- disabled: busy,
22
- className: "min-h-10 w-full",
23
- children: "Continue with GitHub"
24
- }), /* @__PURE__ */ jsxs("div", {
25
- className: "my-4 flex items-center gap-3 text-xs text-muted-foreground",
26
- children: [
27
- /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" }),
28
- "or",
29
- /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border" })
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
- onForgotPassword && /* @__PURE__ */ jsx("button", {
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
- /** Better Auth's messages, in words for the person at the keyboard. */
8
- const signInMessage = (error) => {
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(signInMessage(failure));
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
- onSubmit: (email, password) => void attempt(() => signIn(email, password))
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 };