merge newui branch

This commit is contained in:
Gani Georgiev
2026-04-18 16:29:34 +03:00
parent 58f605e90c
commit 4c44044c0c
804 changed files with 58660 additions and 56663 deletions

View File

@@ -0,0 +1,129 @@
import PocketBase, { getTokenPayload } from "pocketbase";
export function pageConfirmEmailChange(route) {
const token = route.params?.token || "";
const tokenPayload = getTokenPayload(token);
if (!tokenPayload.newEmail || !tokenPayload.collectionId) {
app.toasts.error("Invalid or expired email change token.");
window.location.hash = "#/";
return;
}
app.store.title = "Confirm email change";
const data = store({
password: "",
isSubmitting: false,
isSuccess: false,
showPassword: false,
});
async function submit() {
if (data.isSubmitting) {
return;
}
data.isSubmitting = true;
// init a custom client to avoid interfering with the superuser state
const client = new PocketBase(app.pb.baseURL);
try {
await client.collection(tokenPayload.collectionId).confirmEmailChange(token, data.password);
data.isSuccess = true;
} catch (err) {
app.checkApiError(err);
data.isSuccess = false;
}
data.isSubmitting = false;
}
return t.div(
{
pbEvent: "pageConfirmEmailChange",
className: "wrapper sm m-auto p-b-base",
},
t.header(
{ className: "txt-center m-b-base" },
t.img({ className: "main-logo", src: () => app.store.mainLogo, ariaHidden: true, alt: "App logo" }),
t.h5({ className: "m-t-10" }, () => app.store.title),
),
() => {
if (data.isSuccess) {
return t.div(
{
pbEvent: "confirmEmailChangeAlert",
className: "alert success txt-center",
},
t.p(null, "The email was successfully changed."),
t.p(null, "You can go back and sign in with your new email address."),
);
}
return t.form(
{
pbEvent: "confirmEmailChangeForm",
className: "grid confirm-email-change-form",
onsubmit: (e) => {
e.preventDefault();
submit();
},
},
t.div(
{ className: "col-12" },
t.div(
{ className: "content txt-center m-b-sm" },
"Type your password to confirm changing your email address to ",
t.strong(null, tokenPayload.newEmail),
":",
),
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "password_confirm" }, "Password"),
t.input({
id: "password_confirm",
name: "password",
required: true,
autofocus: true,
type: () => (data.showPassword ? "text" : "password"),
value: () => data.password,
oninput: (e) => (data.password = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showPassword ? "Hide password" : "Show password"
),
onclick: () => (data.showPassword = !data.showPassword),
},
t.i({
className: () => (data.showPassword ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg block ${data.isSubmitting ? "loading" : ""}`,
disabled: () => data.isSubmitting,
},
t.span({ className: "txt" }, "Confirm new email"),
),
),
);
},
);
}

View File

@@ -0,0 +1,167 @@
import PocketBase, { getTokenPayload } from "pocketbase";
export function pageConfirmPasswordReset(route) {
const token = route.params?.token || "";
const tokenPayload = getTokenPayload(token);
if (!tokenPayload.email || !tokenPayload.collectionId) {
app.toasts.error("Invalid or expired password reset token.");
window.location.hash = "#/";
return;
}
app.store.title = "Confirm password reset";
const data = store({
newPassword: "",
newPasswordConfirm: "",
showNewPassword: false,
showNewPasswordConfirm: false,
isSubmitting: false,
isSuccess: false,
});
async function submit() {
if (data.isSubmitting) {
return;
}
data.isSubmitting = true;
// init a custom client to avoid interfering with the superuser state
const client = new PocketBase(app.pb.baseURL);
try {
await client
.collection(tokenPayload.collectionId)
.confirmPasswordReset(token, data.newPassword, data.newPasswordConfirm);
data.isSuccess = true;
} catch (err) {
app.checkApiError(err);
}
data.isSubmitting = false;
}
return t.div(
{
pbEvent: "pageConfirmPasswordReset",
className: "wrapper sm m-auto p-b-base",
},
t.header(
{ className: "txt-center m-b-base" },
t.img({ className: "main-logo", src: () => app.store.mainLogo, ariaHidden: true, alt: "App logo" }),
t.h5({ className: "m-t-10" }, () => app.store.title),
),
() => {
if (data.isSuccess) {
return t.div(
{ pbEvent: "confirmPasswordResetAlert", className: "alert success txt-center" },
t.p(null, "The password was successfully changed."),
t.p(null, "You can go back to sign in with your new password."),
);
}
return t.form(
{
pbEvent: "confirmPasswordResetForm",
className: "grid confirm-password-reset-form",
onsubmit: (e) => {
e.preventDefault();
submit();
},
},
t.div(
{ className: "col-12" },
t.div(
{ className: "content txt-center m-b-sm" },
"Type your new password for ",
t.strong(null, tokenPayload.email),
":",
),
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "newPassword" }, "New password"),
t.input({
id: "newPassword",
name: "password",
required: true,
autofocus: true,
autocomplete: "new-password",
type: () => (data.showNewPassword ? "text" : "password"),
value: () => data.newPassword,
oninput: (e) => (data.newPassword = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showNewPassword ? "Hide password" : "Show password"
),
onclick: () => (data.showNewPassword = !data.showNewPassword),
},
t.i({
className: () => (data.showNewPassword ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "newPasswordConfirm" }, "New password confirm"),
t.input({
id: "newPasswordConfirm",
name: "passwordConfirm",
required: true,
autocomplete: "new-password",
type: () => (data.showNewPasswordConfirm ? "text" : "password"),
value: () => data.newPasswordConfirm,
oninput: (e) => (data.newPasswordConfirm = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showNewPasswordConfirm ? "Hide password" : "Show password"
),
onclick: () => (data.showNewPasswordConfirm = !data.showNewPasswordConfirm),
},
t.i({
className: () => (data.showNewPasswordConfirm ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg block ${data.isSubmitting ? "loading" : ""}`,
disabled: () => data.isSubmitting,
},
t.span({ className: "txt" }, "Set new password"),
),
),
);
},
);
}

View File

@@ -0,0 +1,112 @@
import PocketBase, { getTokenPayload } from "pocketbase";
export function pageConfirmVerification(route) {
const token = route.params?.token || "";
const tokenPayload = getTokenPayload(token);
if (!tokenPayload.email || !tokenPayload.collectionId) {
app.toasts.error("Invalid or expired verification token.");
window.location.hash = "#/";
return;
}
app.store.title = "Confirm verification";
const data = store({
isConfirming: false,
isConfirmSuccess: false,
// ---
isResending: false,
isResendSuccess: false,
});
confirm();
async function confirm() {
if (data.isConfirming) {
return;
}
data.isConfirming = true;
// init a custom client to avoid interfering with the superuser state
const client = new PocketBase(app.pb.baseURL);
try {
await client.collection(tokenPayload.collectionId).confirmVerification(token);
data.isConfirmSuccess = true;
} catch (err) {
data.isConfirmSuccess = false;
}
data.isConfirming = false;
}
async function resend() {
if (data.isResending) {
return;
}
data.isResending = true;
// init a custom client to avoid interfering with the superuser state
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
try {
await client.collection(tokenPayload.collectionId).requestVerification(tokenPayload.email);
data.isResendSuccess = true;
} catch (err) {
app.checkApiError(err);
data.isResendSuccess = false;
}
data.isResending = false;
}
return t.div(
{
pbEvent: "pageConfirmVerification",
className: "wrapper sm m-auto p-b-base",
},
t.header(
{ className: "txt-center m-b-base" },
t.img({ className: "main-logo", src: () => app.store.mainLogo, ariaHidden: true, alt: "App logo" }),
t.h5({ className: "m-t-10" }, () => app.store.title),
),
() => {
if (data.isConfirming) {
return t.div({ className: "block txt-center" }, t.span({ className: "loader" }, "Please wait..."));
}
if (data.isConfirmSuccess) {
return t.div(
{ pbEvent: "confirmVerificationSuccessAlert", className: "alert success txt-center" },
t.p(null, "Successfully verified ", t.strong(null, tokenPayload.email), "."),
);
}
if (data.isResendSuccess) {
return t.div(
{ pbEvent: "confirmVerificationResendAlert", className: "alert success txt-center" },
t.p(null, "Please check your email for the new verification link."),
);
}
return [
t.div(
{ pbEvent: "confirmVerificationErrorAlert", className: "alert danger txt-center m-b-base" },
t.p(null, "Invalid or expired verification token."),
),
t.button(
{
type: "button",
className: () => `btn transparent lg block ${data.isResending ? "loading" : ""}`,
disabled: () => data.isResending,
onclick: () => resend(),
},
t.span({ className: "txt" }, "Resend"),
),
];
},
);
}

View File

@@ -0,0 +1,266 @@
import { getTokenPayload, isTokenExpired } from "pocketbase";
export function pageInstaller(route) {
const token = route.params?.token || "";
const tokenPayload = getTokenPayload(token);
if (tokenPayload.type != "auth" || isTokenExpired(token)) {
app.toasts.error("The installer token is invalid or has expired.");
window.location.hash = "#/";
return;
}
app.store.title = "Setup your PocketBase instance";
const data = store({
email: "",
password: "",
passwordConfirm: "",
showPassword: false,
showPasswordConfirm: false,
isSubmitting: false,
isUploading: false,
get isBusy() {
return data.isSubmitting || data.isUploading;
},
});
async function submit() {
if (data.isBusy) {
return;
}
data.isSubmitting = true;
try {
await app.pb.collection("_superusers").create(
{
email: data.email,
password: data.password,
passwordConfirm: data.passwordConfirm,
},
{
headers: { Authorization: token },
},
);
await app.pb.collection("_superusers").authWithPassword(data.email, data.password);
window.location.hash = "#/";
} catch (err) {
app.checkApiError(err);
}
data.isSubmitting = false;
}
const fileInputId = "backupFileInput";
function resetSelectedBackupFile() {
const input = document.getElementById(fileInputId);
if (input) {
input.value = "";
}
}
function uploadBackupConfirm(file) {
if (!file) {
return;
}
app.modals.confirm(
t.h6(
null,
`Note that we don't perform validations for the uploaded backup files. Proceed with caution and only if you trust the file source.\n\n`
+ `Do you really want to upload and initialize "${file.name}"?`,
),
() => {
uploadBackup(file);
},
() => {
resetSelectedBackupFile();
},
);
}
async function uploadBackup(file) {
if (!file || data.isBusy) {
return;
}
data.isUploading = true;
try {
await app.pb.backups.upload(
{ file: file },
{
headers: { Authorization: token },
},
);
await app.pb.backups.restore(file.name, {
headers: { Authorization: token },
});
app.toasts.info("Please wait while extracting the uploaded archive!");
// optimistic restore completion
await new Promise((r) => setTimeout(r, 3000));
window.location.href = "#/";
} catch (err) {
app.checkApiError(err);
}
resetSelectedBackupFile();
data.isUploading = false;
}
return t.div(
{
pbEvent: "pageInstaller",
className: "wrapper sm m-auto p-b-base",
},
t.header(
{ className: "txt-center m-b-base" },
t.img({ className: "main-logo", src: () => app.store.mainLogo, ariaHidden: true, alt: "App logo" }),
t.h5({ className: "m-t-10" }, () => app.store.title),
),
t.form(
{
pbEvent: "installerForm",
className: "grid installer-form",
onsubmit: (e) => {
e.preventDefault();
submit(data);
},
},
t.div({ className: "col-12 txt-center" }, "Create your first superuser account in order to continue:"),
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: "superuser_email" }, "Email"),
t.input({
id: "superuser_email",
name: "email",
type: "email",
required: true,
autofocus: true,
autocomplete: "off",
disabled: () => data.isBusy,
value: () => data.email,
oninput: (e) => (data.email = e.target.value),
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "superuser_password" }, "Password"),
t.input({
id: "superuser_password",
name: "password",
min: 10,
required: true,
disabled: () => data.isBusy,
type: () => (data.showPassword ? "text" : "password"),
value: () => data.password,
oninput: (e) => (data.password = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showPassword ? "Hide password" : "Show password"
),
onclick: () => (data.showPassword = !data.showPassword),
},
t.i({
className: () => (data.showPassword ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
t.div({ className: "field-help" }, "Recommended at least 10 characters."),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "superuser_password_confirm" }, "Password confirm"),
t.input({
id: "superuser_password_confirm",
name: "passwordConfirm",
required: true,
disabled: () => data.isBusy,
type: () => (data.showPasswordConfirm ? "text" : "password"),
value: () => data.passwordConfirm,
oninput: (e) => (data.passwordConfirm = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showPasswordConfirm ? "Hide password" : "Show password"
),
onclick: () => (data.showPasswordConfirm = !data.showPasswordConfirm),
},
t.i({
className: () => (data.showPasswordConfirm ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg next block ${data.isSubmitting ? "loading" : ""}`,
disabled: () => data.isBusy,
},
t.span({ className: "txt" }, "Create superuser and login"),
t.i({ className: "ri-arrow-right-line" }),
),
),
),
t.hr(),
t.label(
{
htmlFor: fileInputId,
className: () =>
`btn secondary transparent lg block ${data.isBusy ? "disabled" : ""} ${
data.isUploading ? "loading" : ""
}`,
},
t.i({ className: "ri-upload-cloud-line" }),
t.span({ className: "txt" }, "Or initialize from backup"),
),
t.input({
id: fileInputId,
type: "file",
className: "hidden",
accept: ".zip",
onchange: (e) => {
uploadBackupConfirm(e.target?.files?.[0]);
},
}),
);
}

View File

@@ -0,0 +1,20 @@
export function pageOAuth2RedirectFailure(route) {
app.store.title = "OAuth2 auth failed";
window.close();
return t.div(
{ pbEvent: "pageOAuth2RedirectFailure", className: "page" },
t.div(
{ className: "page-content" },
t.header(
{ className: "txt-center p-base" },
t.h3({ className: "primary-heading m-b-sm" }, "Auth failed."),
t.h6(
{ className: "secondary-heading" },
"You can close this window and go back to the app to try again.",
),
),
),
);
}

View File

@@ -0,0 +1,17 @@
export function pageOAuth2RedirectSuccess(route) {
app.store.title = "OAuth2 auth completed";
window.close();
return t.div(
{ pbEvent: "pageOAuth2RedirectSuccess", className: "page" },
t.div(
{ className: "page-content" },
t.header(
{ className: "txt-center p-base" },
t.h3({ className: "primary-heading m-b-sm" }, "Auth completed."),
t.h6({ className: "secondary-heading" }, "You can close this window and go back to the app."),
),
),
);
}

View File

@@ -0,0 +1,92 @@
export function pageRequestSuperuserPasswordReset(route) {
app.store.title = "Forgotten superuser password";
const data = store({
email: "",
isSubmitting: false,
success: false,
});
async function submit() {
if (data.isSubmitting) {
return;
}
data.isSubmitting = true;
try {
await app.pb.collection("_superusers").requestPasswordReset(data.email);
data.success = true;
} catch (err) {
app.checkApiError(err);
}
data.isSubmitting = false;
}
return t.div(
{
pbEvent: "pageSuperuserPasswordReset",
className: "wrapper sm m-auto p-b-base",
},
t.header(
{ className: "txt-center m-b-base" },
t.img({ className: "main-logo", src: () => app.store.mainLogo, ariaHidden: true, alt: "App logo" }),
t.h5({ className: "m-t-10" }, () => app.store.title),
),
() => {
if (data.success) {
return t.div(
{ pbEvent: "superuserPasswordResetAlert", className: "alert success txt-center" },
t.p(null, "Check ", t.strong(null, data.email), " for the recovery link!"),
);
}
return t.form(
{
pbEvent: "superuserPasswordResetForm",
className: "grid request-password-reset-form",
onsubmit: (e) => {
e.preventDefault();
submit();
},
},
t.div(
{ className: "col-12" },
t.div(
{ className: "content txt-center m-b-sm" },
t.p(null, "Enter the email associated with your account and we'll send you a recovery link:"),
),
t.div(
{ className: "field" },
t.label({ htmlFor: "password_reset_email" }, "Email"),
t.input({
id: "password_reset_email",
name: "email",
type: "email",
required: true,
autofocus: true,
value: () => data.email,
oninput: (e) => (data.email = e.target.value),
}),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg block ${data.isSubmitting ? "loading" : ""}`,
disabled: () => data.isSubmitting,
},
t.i({ className: "ri-mail-send-line" }),
t.span({ className: "txt" }, "Send recovery link"),
),
),
);
},
t.div(
{ className: "block m-t-sm txt-center" },
t.a({ href: "#/login", className: "link-hint" }, "Back to login"),
),
);
}

View File

@@ -0,0 +1,420 @@
export function pageSuperuserLogin(route) {
app.store.title = "Superuser login";
const data = store({
authMethods: {},
identity: route.query.demoEmail?.[0] || "",
password: route.query.demoPassword?.[0] || "",
showPassword: false,
otpId: "",
lastOTPId: "",
otpEmail: "",
otpPassword: "",
mfaId: "",
totalSteps: 1,
get currentStep() {
return 1 + !!data.mfaId + !!data.otpId;
},
isAuthMethodsLoading: true,
isPasswordAuthSubmitting: false,
isOTPRequestSubmitting: false,
isOTPAuthSubmitting: false,
});
async function loadAuthMethods() {
data.isAuthMethodsLoading = true;
try {
data.authMethods = await app.pb.collection("_superusers").listAuthMethods();
data.totalSteps = 1;
data.mfaId = "";
data.otpId = "";
if (data.authMethods.mfa?.enabled && data.authMethods.otp?.enabled) {
data.totalSteps += 2; // otp request + auth
}
} catch (err) {
app.checkApiError(err);
}
data.isAuthMethodsLoading = false;
}
loadAuthMethods();
return t.div(
{
pbEvent: "pageSuperuserLogin",
className: "wrapper sm m-auto p-b-base",
},
t.header(
{ className: "txt-center m-b-base" },
t.img({ className: "main-logo", src: () => app.store.mainLogo, ariaHidden: true, alt: "App logo" }),
t.h5(
{ className: "m-t-10" },
t.span(null, () => app.store.title),
() => {
if (data.totalSteps > 1) {
return t.span(null, () => ` (${data.currentStep}/${data.totalSteps})`);
}
},
),
),
() => {
if (data.isAuthMethodsLoading) {
return t.div({ className: "block txt-center" }, t.span({ className: "loader lg" }));
}
if (data.authMethods.password?.enabled && !data.mfaId) {
return authWithPasswordForm(data);
}
if (data.authMethods.otp?.enabled) {
if (!data.otpId) {
return requestOTPForm(data);
}
return authWithOTPForm(data);
}
},
);
}
// Auth with password
// -------------------------------------------------------------------
async function authWithPassword(data) {
if (data.isPasswordAuthSubmitting) {
return;
}
data.isPasswordAuthSubmitting = true;
try {
await app.pb.collection("_superusers").authWithPassword(data.identity, data.password);
app.toasts.removeAll();
app.store.errors = null;
app.utils.toRememberedPath();
} catch (err) {
if (err.status == 401) {
data.mfaId = err.response.mfaId;
// show the otp forms
if (
// if the identity field is just the email use it to directly send an otp request
data.authMethods?.password?.identityFields?.length == 1
&& data.authMethods.password.identityFields[0] == "email"
) {
// prefill and request
data.otpEmail = data.identity;
await requestOTP(data);
} else if (/^[^@\s]+@[^@\s]+$/.test(data.identity)) {
// only prefill
data.otpEmail = data.identity;
}
} else if (err.status != 400) {
app.checkApiError(err);
} else {
app.toasts.error("Invalid login credentials.");
}
}
data.isPasswordAuthSubmitting = false;
}
function authWithPasswordForm(data) {
return t.form(
{
pbEvent: "authWithPasswordForm",
className: "grid auth-with-password-form",
onsubmit: (e) => {
e.preventDefault();
authWithPassword(data);
},
},
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label(
{ htmlFor: "login_identity" },
() => app.utils.sentenize(data.authMethods.password.identityFields.join(" or "), false),
),
t.input({
id: "login_identity",
name: "identity",
type: () => {
if (
data.authMethods.password.identityFields.length == 1
&& data.authMethods.password.identityFields[0] == "email"
) {
return "email";
}
return "text";
},
required: true,
autofocus: true,
value: () => data.identity,
oninput: (e) => (data.identity = e.target.value),
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "login_pass" }, "Password"),
t.input({
id: "login_pass",
name: "password",
required: true,
type: () => (data.showPassword ? "text" : "password"),
value: () => data.password,
oninput: (e) => (data.password = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showPassword ? "Hide password" : "Show password"
),
onclick: () => (data.showPassword = !data.showPassword),
},
t.i({
className: () => (data.showPassword ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
t.a(
{
href: "#/request-password-reset",
className: "link-hint m-t-5",
onclick: (e) => e.stopPropagation(),
},
t.small(null, "Forgotten password"),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg block next ${data.isPasswordAuthSubmitting ? "loading" : ""}`,
disabled: () => data.isPasswordAuthSubmitting,
},
t.span({ className: "txt" }, () => (data.totalSteps > 1 ? "Next" : "Login")),
t.i({ className: "ri-arrow-right-line" }),
),
),
);
}
// Request OTP
// -------------------------------------------------------------------
async function requestOTP(data) {
if (data.isOTPRequestSubmitting) {
return;
}
data.isOTPRequestSubmitting = true;
try {
const result = await app.pb.collection("_superusers").requestOTP(data.otpEmail);
data.otpId = result.otpId;
data.lastOTPId = data.otpId;
app.toasts.removeAll();
app.store.errors = null;
} catch (err) {
// reset the form
if (err.status == 429) {
data.otpId = data.lastOTPId;
}
app.checkApiError(err);
}
data.isOTPRequestSubmitting = false;
}
function requestOTPForm(data) {
return t.form(
{
pbEvent: "requestOTPForm",
className: "grid request-otp-form",
onsubmit: (e) => {
e.preventDefault();
requestOTP(data);
},
},
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: "otp_email" }, "Email"),
t.input({
id: "otp_email",
name: "email",
type: "email",
required: true,
autofocus: true,
value: () => data.otpEmail,
oninput: (e) => (data.otpEmail = e.target.value),
}),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg block ${data.isOTPRequestSubmitting ? "loading" : ""}`,
disabled: () => data.isOTPRequestSubmitting,
},
t.i({ className: "ri-mail-send-line" }),
t.span({ className: "txt" }, "Send OTP"),
),
),
);
}
// Auth with OTP
// -------------------------------------------------------------------
async function authWithOTP(data) {
if (data.isOTPAuthSubmitting) {
return;
}
data.isOTPAuthSubmitting = true;
try {
await app.pb.collection("_superusers").authWithOTP(data.otpId || data.lastOTPId, data.otpPassword, {
mfaId: data.mfaId,
});
app.toasts.removeAll();
app.store.errors = null;
app.utils.toRememberedPath();
} catch (err) {
app.checkApiError(err);
}
data.isOTPAuthSubmitting = false;
}
function authWithOTPForm(data) {
return t.form(
{
pbEvent: "authWithOTPForm",
className: "grid auht-with-otp-form",
onsubmit: (e) => {
e.preventDefault();
authWithOTP(data);
},
},
() => {
if (data.otpEmail) {
return t.div(
{ className: "col-12" },
t.div(
{ className: "content txt-center" },
"Check your ",
t.strong(null, data.otpEmail),
" inbox and enter below the received One-time password (OTP).",
),
);
}
},
t.div(
{ className: "col-12" },
t.div(
{ className: "field" },
t.label({ htmlFor: "otp_id" }, "Id"),
t.input({
id: "otp_id",
name: "otpId",
type: "text",
required: true,
placeholder: () => data.lastOTPId,
value: () => data.otpId,
onchange: (e) => {
data.otpId = e.target.value || data.lastOTPId;
e.target.value = data.otpId;
},
}),
),
),
t.div(
{ className: "col-12" },
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label({ htmlFor: "otp_password" }, "One-time password"),
t.input({
id: "otp_password",
name: "password",
required: true,
type: () => (data.showPassword ? "text" : "password"),
value: () => data.otpPassword,
oninput: (e) => (data.otpPassword = e.target.value),
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
tabIndex: -1,
className: "btn sm transparent secondary circle tooltip-right",
ariaDescription: app.attrs.tooltip(() =>
data.showPassword ? "Hide password" : "Show password"
),
onclick: () => (data.showPassword = !data.showPassword),
},
t.i({
className: () => (data.showPassword ? "ri-eye-off-line" : "ri-eye-line"),
}),
),
),
),
),
t.div(
{ className: "col-12" },
t.button(
{
className: () => `btn lg block next ${data.isOTPAuthSubmitting ? "loading" : ""}`,
disabled: () => data.isOTPAuthSubmitting,
},
t.span({ className: "txt" }, "Login"),
t.i({ className: "ri-arrow-right-line" }),
),
t.div(
{ className: "block m-t-sm txt-center" },
t.button(
{
type: "button",
className: "link-hint txt-sm",
disabled: () => data.isOTPAuthSubmitting,
onclick: () => {
data.otpId = "";
data.otpPassword = "";
},
},
"Request another OTP",
),
),
),
);
}