Files
pocketbase/ui/src/auth/pageInstaller.js
2026-04-18 22:11:58 +03:00

269 lines
9.3 KiB
JavaScript

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]);
},
}),
);
}