@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.
- 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 +37 -15
- package/dist/app/assistant-shell.js +3 -2
- 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/merge-ui.d.ts +9 -0
- package/dist/app/pages/merge-ui.js +45 -0
- package/dist/app/pages/project-ui-provider.js +15 -0
- package/dist/app/pages/project-ui.d.ts +6 -3
- package/dist/app/pages/project-ui.js +3 -7
- package/dist/app/pages/record-table.js +1 -1
- package/dist/app/pages/use-run-action.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.d.ts +8 -2
- package/dist/assistant-dock/assistant-dock.js +20 -13
- package/dist/assistant-dock/index.d.ts +3 -1
- package/dist/assistant-dock/part-view.d.ts +1 -1
- package/dist/assistant-dock/part-view.js +3 -1
- package/dist/assistant-dock/use-backend-data.d.ts +23 -0
- package/dist/assistant-dock/use-backend-data.js +67 -0
- package/dist/assistant-dock/widget-view.d.ts +1 -0
- package/dist/assistant-dock/widget-view.js +44 -0
- 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/index.d.ts +3 -1
- package/dist/index.js +4 -2
- 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
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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 };
|