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", ariaLabel: 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"), ariaHidden: true, }), ), ), ), 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", ariaLabel: 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"), ariaHidden: true, }), ), ), ), ), 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", ariaHidden: true }), ), ), ), 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", ariaHidden: true }), 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]); }, }), ); }