269 lines
9.3 KiB
JavaScript
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]);
|
|
},
|
|
}),
|
|
);
|
|
}
|