@protobase/ui 0.46.7 → 0.46.9

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 (27) hide show
  1. package/dist/account/change-email-dialog.js +1 -1
  2. package/dist/account/two-factor-setup.js +1 -1
  3. package/dist/app/confirm-sign-in-messages.js +22 -0
  4. package/dist/app/confirm-sign-in.js +88 -0
  5. package/dist/app/sensitive-field.js +10 -2
  6. package/dist/auth/auth-messages.js +6 -3
  7. package/dist/auth/auth-provider.js +3 -1
  8. package/dist/auth/code-form.js +1 -1
  9. package/dist/auth/reset-password-page.js +2 -1
  10. package/dist/auth/two-factor-form.js +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/package.js +1 -1
  13. package/dist/shell/assets/app-axeIZ-10.js +9 -0
  14. package/dist/shell/assets/{index-Cjh9OH3F.js → index-1Ue_dZiA.js} +2 -2
  15. package/dist/shell/assets/{records-map-DFiu2MjE.js → records-map-DPA8Y3C8.js} +2 -2
  16. package/dist/shell/assets/shared-protobase-ui-dWIf89ff.js +2 -0
  17. package/dist/shell/index.html +3 -3
  18. package/dist/shell/protobase.shell.json +1 -1
  19. package/dist/shell/protobase.sw.js +2 -2
  20. package/dist/shell-source-maps/assets/app-axeIZ-10.js.map +1 -0
  21. package/dist/shell-source-maps/assets/{index-Cjh9OH3F.js.map → index-1Ue_dZiA.js.map} +1 -1
  22. package/dist/shell-source-maps/assets/{records-map-DFiu2MjE.js.map → records-map-DPA8Y3C8.js.map} +1 -1
  23. package/dist/shell-source-maps/assets/{shared-protobase-ui-CLCawVYs.js.map → shared-protobase-ui-dWIf89ff.js.map} +1 -1
  24. package/package.json +4 -4
  25. package/dist/shell/assets/app-BClUNT0O.js +0 -9
  26. package/dist/shell/assets/shared-protobase-ui-CLCawVYs.js +0 -2
  27. package/dist/shell-source-maps/assets/app-BClUNT0O.js.map +0 -1
@@ -2,9 +2,9 @@ import { text } from "./messages.js";
2
2
  import { Button } from "../primitives/button.js";
3
3
  import { Dialog } from "../primitives/dialog.js";
4
4
  import { Input } from "../primitives/input.js";
5
+ import { OtpInput } from "../primitives/otp-input.js";
5
6
  import { SectionError } from "./account-section.js";
6
7
  import { authMessage } from "../auth/auth-messages.js";
7
- import { OtpInput } from "../primitives/otp-input.js";
8
8
  import { useState } from "react";
9
9
  import { AuthError } from "@protobase/client";
10
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -2,8 +2,8 @@ import { text } from "./messages.js";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { Button } from "../primitives/button.js";
4
4
  import { Input } from "../primitives/input.js";
5
- import { authMessage } from "../auth/auth-messages.js";
6
5
  import { OtpInput } from "../primitives/otp-input.js";
6
+ import { authMessage } from "../auth/auth-messages.js";
7
7
  import { BackupCodes } from "./backup-codes.js";
8
8
  import { TotpQr, totpKey } from "./totp-qr.js";
9
9
  import { useState } from "react";
@@ -0,0 +1,22 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/app/confirm-sign-in-messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ confirmTitle: "Confirm it's you",
6
+ confirmWhy: (minutes) => `Showing this needs a passkey or your authenticator app in the last ${minutes} minutes.`,
7
+ usePasskey: "Use a passkey",
8
+ authenticatorCode: "Code from your authenticator app",
9
+ confirm: "Confirm",
10
+ nothingToConfirmWith: "Add a passkey or an authenticator app on your account page first."
11
+ },
12
+ nl: {
13
+ confirmTitle: "Bevestig dat jij het bent",
14
+ confirmWhy: (minutes) => `Dit tonen vraagt in de laatste ${minutes} minuten een passkey of je authenticator-app.`,
15
+ usePasskey: "Passkey gebruiken",
16
+ authenticatorCode: "Code uit je authenticator-app",
17
+ confirm: "Bevestigen",
18
+ nothingToConfirmWith: "Voeg eerst een passkey of een authenticator-app toe op je accountpagina."
19
+ }
20
+ });
21
+ //#endregion
22
+ export { text };
@@ -0,0 +1,88 @@
1
+ import { useOptionalAuth } from "../auth/auth-provider.js";
2
+ import { Button } from "../primitives/button.js";
3
+ import { OtpInput } from "../primitives/otp-input.js";
4
+ import { text } from "./confirm-sign-in-messages.js";
5
+ import { useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { useMutation, useQuery } from "@tanstack/react-query";
8
+ //#region src/app/confirm-sign-in.tsx
9
+ /**
10
+ * Asks the person to confirm it's them, with a passkey or their authenticator app, after the server refused something
11
+ * that needs a recent strong sign-in (`strong-sign-in-required`). It offers only what the account has; a passkey signs
12
+ * the browser in anew, a code confirms the session it has.
13
+ */
14
+ const ConfirmSignIn = ({ minutes, onConfirmed }) => {
15
+ const auth = useOptionalAuth();
16
+ const [code, setCode] = useState("");
17
+ const methods = useQuery({
18
+ queryKey: ["account-sign-in-methods"],
19
+ queryFn: () => auth.session.account.signInMethods(),
20
+ enabled: Boolean(auth)
21
+ });
22
+ const passkey = useMutation({
23
+ mutationFn: () => auth.signInWithPasskey(),
24
+ onSuccess: onConfirmed
25
+ });
26
+ const authenticator = useMutation({
27
+ mutationFn: (entered) => auth.verifyTwoFactor({
28
+ method: "totp",
29
+ code: entered
30
+ }),
31
+ onSuccess: onConfirmed
32
+ });
33
+ const account = methods.data?.account;
34
+ const error = passkey.error ?? authenticator.error ?? methods.error;
35
+ return /* @__PURE__ */ jsxs("section", {
36
+ "aria-label": text("confirmTitle"),
37
+ className: "mt-2 flex flex-col gap-2 rounded-md border border-border bg-surface p-3",
38
+ children: [
39
+ /* @__PURE__ */ jsx("p", {
40
+ className: "font-medium",
41
+ children: text("confirmTitle")
42
+ }),
43
+ /* @__PURE__ */ jsx("p", {
44
+ className: "text-muted-foreground",
45
+ children: text("confirmWhy", minutes)
46
+ }),
47
+ account && account.passkeys > 0 && /* @__PURE__ */ jsx(Button, {
48
+ variant: "primary",
49
+ size: "sm",
50
+ className: "self-start",
51
+ loading: passkey.isPending,
52
+ onClick: () => passkey.mutate(),
53
+ children: text("usePasskey")
54
+ }),
55
+ account?.authenticatorApp && /* @__PURE__ */ jsxs("form", {
56
+ className: "flex flex-wrap items-end gap-2",
57
+ onSubmit: (event) => {
58
+ event.preventDefault();
59
+ authenticator.mutate(code.trim());
60
+ },
61
+ children: [/* @__PURE__ */ jsxs("label", {
62
+ className: "flex flex-col gap-1.5",
63
+ children: [text("authenticatorCode"), /* @__PURE__ */ jsx(OtpInput, {
64
+ name: "code",
65
+ required: true,
66
+ value: code,
67
+ invalid: authenticator.isError,
68
+ onChange: setCode,
69
+ onComplete: (entered) => authenticator.mutate(entered)
70
+ })]
71
+ }), /* @__PURE__ */ jsx(Button, {
72
+ type: "submit",
73
+ size: "sm",
74
+ loading: authenticator.isPending,
75
+ children: text("confirm")
76
+ })]
77
+ }),
78
+ account && account.passkeys === 0 && !account.authenticatorApp && /* @__PURE__ */ jsx("p", { children: text("nothingToConfirmWith") }),
79
+ error && /* @__PURE__ */ jsx("p", {
80
+ role: "alert",
81
+ className: "font-medium text-danger-text",
82
+ children: error.message
83
+ })
84
+ ]
85
+ });
86
+ };
87
+ //#endregion
88
+ export { ConfirmSignIn };
@@ -6,15 +6,19 @@ import { displayValue } from "../live/display-value.js";
6
6
  import { text } from "./messages.js";
7
7
  import { toDraft } from "../live/field-values.js";
8
8
  import { ErrorBanner } from "./error-banner.js";
9
+ import { ConfirmSignIn } from "./confirm-sign-in.js";
9
10
  import { useState } from "react";
10
11
  import { Eye, EyeOff } from "lucide-react";
12
+ import { isApiError } from "@protobase/client";
11
13
  import { jsx, jsxs } from "react/jsx-runtime";
12
14
  import { useMutation } from "@tanstack/react-query";
13
15
  //#region src/app/sensitive-field.tsx
14
16
  const mask = "••••••••••";
17
+ const strongSignInMinutes = (error) => isApiError(error) && error.slug === "strong-sign-in-required" ? error.problem.minutes ?? 15 : void 0;
15
18
  /**
16
19
  * A value hidden like a password. Showing a stored one asks the server for it, once per page visit; a write-only one has
17
- * none to show, so its eye only unmasks what was typed.
20
+ * none to show, so its eye only unmasks what was typed. When the server wants a recent strong sign-in first, the field
21
+ * asks the person to confirm it's them, then shows the value.
18
22
  */
19
23
  const SensitiveField = ({ field, hints, value, reveal, locked, invalid, placeholder, onChange }) => {
20
24
  const [shown, setShown] = useState(false);
@@ -38,7 +42,11 @@ const SensitiveField = ({ field, hints, value, reveal, locked, invalid, placehol
38
42
  onClick: toggle,
39
43
  children: !stored.isPending && (shown ? /* @__PURE__ */ jsx(EyeOff, { className: "size-3.5" }) : /* @__PURE__ */ jsx(Eye, { className: "size-3.5" }))
40
44
  });
41
- const error = stored.error && /* @__PURE__ */ jsx("div", {
45
+ const minutes = strongSignInMinutes(stored.error);
46
+ const error = minutes !== void 0 ? /* @__PURE__ */ jsx(ConfirmSignIn, {
47
+ minutes,
48
+ onConfirmed: () => stored.mutate()
49
+ }) : stored.error && /* @__PURE__ */ jsx("div", {
42
50
  className: "mt-2",
43
51
  children: /* @__PURE__ */ jsx(ErrorBanner, {
44
52
  title: text("couldNotShow", label),
@@ -26,7 +26,8 @@ const text = defineMessages({
26
26
  staffReasonRequired: "Say why you sign in as this person, in at least 10 characters.",
27
27
  staffForbidden: "Staff sign-in is turned off for this app.",
28
28
  untrustedCallback: "This page is not on an address the server trusts.",
29
- staffFailed: "The staff sign-in did not go through. Try again."
29
+ staffFailed: "The staff sign-in did not go through. Try again.",
30
+ passwordLeaked: "This password appears in a known data breach. Choose a different one."
30
31
  },
31
32
  nl: {
32
33
  tooManyRequests: "Te veel pogingen. Wacht een minuut en probeer het opnieuw.",
@@ -53,7 +54,8 @@ const text = defineMessages({
53
54
  staffReasonRequired: "Zeg in minstens 10 tekens waarom je als deze persoon inlogt.",
54
55
  staffForbidden: "Inloggen als medewerker staat uit voor deze app.",
55
56
  untrustedCallback: "Deze pagina staat niet op een adres dat de server vertrouwt.",
56
- staffFailed: "De inlog als medewerker lukte niet. Probeer het opnieuw."
57
+ staffFailed: "De inlog als medewerker lukte niet. Probeer het opnieuw.",
58
+ passwordLeaked: "Dit wachtwoord komt voor in een bekend datalek. Kies een ander wachtwoord."
57
59
  }
58
60
  });
59
61
  const providerText = defineMessages({
@@ -94,7 +96,8 @@ const messages = {
94
96
  AUTHENTICATION_FAILED: "passkeyFailed",
95
97
  PREVIOUSLY_REGISTERED: "passkeyAdded",
96
98
  ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: "passkeyAdded",
97
- SESSION_NOT_FRESH: "notFresh"
99
+ SESSION_NOT_FRESH: "notFresh",
100
+ PASSWORD_COMPROMISED: "passwordLeaked"
98
101
  };
99
102
  const lookup = (ids, code) => {
100
103
  const id = code ? ids[code] : void 0;
@@ -12,6 +12,8 @@ const useAuth = () => {
12
12
  if (!api) throw new Error("useAuth must be used inside AuthProvider");
13
13
  return api;
14
14
  };
15
+ /** The auth state and steps inside `AuthProvider`, `null` outside it, for a part that also works without one. */
16
+ const useOptionalAuth = () => useContext(AuthContext);
15
17
  /**
16
18
  * Finds out whether the server has a login and who is signed in, and keeps that state for the app. Without a `session`,
17
19
  * it makes one on `origin`, the page's own by default; in the iOS or Android app, with its `nativeBridge`, the session
@@ -216,4 +218,4 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
216
218
  });
217
219
  };
218
220
  //#endregion
219
- export { AuthProvider, useAuth };
221
+ export { AuthProvider, useAuth, useOptionalAuth };
@@ -1,7 +1,7 @@
1
1
  import { text } from "./messages.js";
2
2
  import { Button } from "../primitives/button.js";
3
- import { AuthLayout } from "./auth-layout.js";
4
3
  import { OtpInput } from "../primitives/otp-input.js";
4
+ import { AuthLayout } from "./auth-layout.js";
5
5
  import { ResendCodeButton } from "./resend-code-button.js";
6
6
  import { useEffect, useRef, useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,5 +1,6 @@
1
1
  import { text } from "./messages.js";
2
2
  import { useAuth } from "./auth-provider.js";
3
+ import { authMessage } from "./auth-messages.js";
3
4
  import { ResetPasswordForm } from "./reset-password-form.js";
4
5
  import { useState } from "react";
5
6
  import { AuthError } from "@protobase/client";
@@ -10,7 +11,7 @@ const resetPasswordMessage = (error) => {
10
11
  if (error.status === 429) return text("tooManyAttempts");
11
12
  if (error.code === "PASSWORD_TOO_SHORT") return text("useAtLeast", 12);
12
13
  if (error.code === "PASSWORD_TOO_LONG") return text("passwordTooLong");
13
- return error.message;
14
+ return authMessage(error);
14
15
  };
15
16
  /** Sets a new password with the token of an emailed reset link. */
16
17
  const ResetPasswordPage = ({ token, onDone, onRequestNewLink, onBack }) => {
@@ -1,9 +1,9 @@
1
1
  import { text } from "./messages.js";
2
2
  import { Button } from "../primitives/button.js";
3
3
  import { Input } from "../primitives/input.js";
4
+ import { OtpInput } from "../primitives/otp-input.js";
4
5
  import { Checkbox } from "../primitives/checkbox.js";
5
6
  import { AuthLayout } from "./auth-layout.js";
6
- import { OtpInput } from "../primitives/otp-input.js";
7
7
  import { useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/auth/two-factor-form.tsx
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { AppLink, useAppAddress } from "./app/use-app-address.js";
12
12
  import { Segmented } from "./primitives/segmented.js";
13
13
  import { Textarea } from "./primitives/textarea.js";
14
14
  import { useList } from "./data/use-list.js";
15
+ import { OtpInput } from "./primitives/otp-input.js";
15
16
  import { definePlugin, defineUi } from "./app/pages/project-ui.js";
16
17
  import { Markdown } from "./markdown/markdown.js";
17
18
  import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
@@ -22,7 +23,6 @@ import { SignInPrompt } from "./auth/sign-in-prompt.js";
22
23
  import { useListDisplay } from "./app/displays/display-context.js";
23
24
  import { MenuSelect } from "./primitives/menu-select.js";
24
25
  import { AuthLayout } from "./auth/auth-layout.js";
25
- import { OtpInput } from "./primitives/otp-input.js";
26
26
  import { allowDelayMs, isFramed, useArmedAfterFocus, useGuardedPress } from "./connect/interaction-guard.js";
27
27
  import { ResendCodeButton } from "./auth/resend-code-button.js";
28
28
  import { CodeForm } from "./auth/code-form.js";
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.46.7";
2
+ var version = "0.46.9";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };