added superuser ips whitelist

This commit is contained in:
Gani Georgiev committed 2026-05-05 13:04:06 +03:00
1 parent fe2d90641c
commit 21a5524fed
34 files changed
+1224 -47

No files matched your search

@@ -8,7 +8,7 @@ export function batchAccordion(pageData) {
t.summary(
null,
t.i({ className: "ri-archive-stack-line", ariaHidden: true }),
t.span({ className: "txt" }, "Batch API"),
t.span({ className: "txt" }, "Batch Web API"),
t.div({ className: "flex-fill" }),
() => {
if (pageData.formSettings.batch.enabled) {
@@ -1,6 +1,7 @@
import { settingsSidebar } from "../settingsSidebar";
import { batchAccordion } from "./batchAccordion";
import { rateLimitAccordion, sortRules } from "./rateLimitAccordion";
import { superuserAccordion } from "./superuserAccordion";
import { trustedProxyAccordion } from "./trustedProxyAccordion";
export function pageApplicationSettings() {
@@ -37,6 +38,53 @@ export function pageApplicationSettings() {
}
}
function hasSuperuserIPsChanged() {
return JSON.stringify(data.formSettings?.superuserIPs)
!= JSON.stringify(data.originalFormSettings?.superuserIPs);
}
async function saveWithConfirm() {
const superuserIPs = app.utils.toArray(data.formSettings?.superuserIPs);
if (!superuserIPs.length) {
return save();
}
return app.modals.confirm(
t.div(
{ className: "txt-center" },
t.h6(
null,
"The ONLY allowed superuser IPs will change to: ",
t.br(),
t.strong(null, superuserIPs.join(", ")),
),
t.p(null, "Please make sure that your IP is in the list or you'll be locked."),
t.p(
{ className: "txt-hint" },
"In case of lockout, you can reset the setting with the ",
t.a(
{
href: import.meta.env.PB_SUPERUSER_IPS_RESET_DOCS,
target: "_blank",
rel: "noopener noreferrer",
className: "link-primary txt-bold txt-sm",
},
t.code(
null,
"superuser ips",
t.i({ ariaHidden: true, className: "ri-arrow-right-up-line txt-sm" }),
),
),
" console command.",
),
),
() => save(),
null,
{ yesButton: "Yes, save changes" },
);
}
async function save() {
if (data.isSaving || !data.hasChanges) {
return;
@@ -48,8 +96,19 @@ export function pageApplicationSettings() {
try {
const redacted = app.utils.filterRedactedProps(data.formSettings);
const settings = await app.pb.settings.update(redacted);
init(settings);
const updatedSettings = await app.pb.settings.update(redacted);
// reauthenticate to ensure that the superuser has still access
if (hasSuperuserIPsChanged()) {
try {
await app.pb.collection("_superusers").authRefresh();
} catch (_) {
app.pb.authStore.clear();
}
}
init(updatedSettings);
app.toasts.success("Successfully saved application settings.");
} catch (err) {
@@ -73,6 +132,7 @@ export function pageApplicationSettings() {
}
data.originalFormSettings = {
superuserIPs: settings.superuserIPs || [],
meta: settings.meta || {},
batch: settings.batch || {},
trustedProxy: settings.trustedProxy || { headers: [] },
@@ -118,7 +178,7 @@ export function pageApplicationSettings() {
inert: () => data.isSaving,
onsubmit: (e) => {
e.preventDefault();
save();
saveWithConfirm();
},
},
t.div(
@@ -158,9 +218,10 @@ export function pageApplicationSettings() {
),
t.div(
{ className: "col-lg-12" },
() => batchAccordion(data),
() => trustedProxyAccordion(data),
() => rateLimitAccordion(data),
() => batchAccordion(data),
() => superuserAccordion(data),
),
t.div(
{ className: "col-lg-12" },
@@ -249,7 +249,6 @@ export function rateLimitAccordion(pageData) {
t.label(
{ htmlFor: "rateLimits.enabled" },
t.span({ className: "txt" }, "Enable"),
t.small({ className: "txt-hint" }, " (experimental)"),
),
),
),
@@ -0,0 +1,158 @@
export function superuserAccordion(pageData) {
const info = store({
isLoading: false,
realIP: "",
});
async function loadInfo() {
info.isLoading = true;
try {
const health = await app.pb.health.check({ requestKey: "loadSuperuserIPsInfo" });
info.realIP = health.data?.realIP || "";
info.isLoading = false;
} catch (err) {
if (!err.isAbort) {
app.checkApiError(err);
info.isLoading = false;
}
}
}
return t.details(
{
pbEvent: "superuserAccordion",
className: "accordion superuser-accordion",
name: "settingsAccordion",
onmount: (el) => {
el._ipwatcher?.unwatch();
el._ipwatcher = watch(
() => JSON.stringify(app.store.settings?.trustedProxy?.headers),
(newHash, oldHash) => {
if (newHash != oldHash) {
loadInfo();
}
},
);
},
onunmount: (el) => {
el._ipwatcher?.unwatch();
},
},
t.summary(
null,
t.i({ className: "ri-fingerprint-2-line", ariaHidden: true }),
t.span({ className: "txt" }, "Superuser IPs"),
t.div({ className: "flex-fill" }),
() => {
if (pageData.formSettings?.superuserIPs?.length) {
return t.span({ className: "label success" }, "Enabled");
}
return t.span({ className: "label" }, "Disabled");
},
() => {
if (!app.utils.isEmpty(app.store.errors?.batch)) {
return t.i({
className: "ri-error-warning-fill txt-danger",
ariaDescription: app.attrs.tooltip("Has errors", "left"),
});
}
},
),
t.div(
{ className: "content m-b-sm" },
t.p(null, "A comma separated list of superusers allowed IPs and subnets."),
t.p(
null,
"Enabling this option greatly helps hardening the security of your application because even if someone manage to get their hands on a superuser auth token they will not be able to use it.",
),
t.p(
null,
"In case your IP changes, you can always reset the field value with the ",
t.a(
{
href: import.meta.env.PB_SUPERUSER_IPS_RESET_DOCS,
target: "_blank",
rel: "noopener noreferrer",
className: "link-primary txt-bold txt-sm",
},
t.code(
null,
"superuser ips",
t.i({ ariaHidden: true, className: "ri-arrow-right-up-line txt-sm" }),
),
),
" console command.",
),
),
t.div(
{ className: "fields" },
t.div(
{ className: "field" },
t.label(
{ htmlFor: "superuserIPs" },
t.span({ className: "txt" }, "Superuser IPs and subnets"),
),
t.input({
id: "superuserIPs",
name: "superuserIPs",
type: "text",
placeholder: "Leave empty for no restriction",
value: () => app.utils.joinNonEmpty(pageData.formSettings.superuserIPs),
oninput: (e) => {
const newValue = app.utils.splitNonEmpty(e.target.value, ",");
const newStr = app.utils.joinNonEmpty(newValue);
const oldStr = app.utils.joinNonEmpty(pageData.formSettings.superuserIPs);
// has an actual change
if (oldStr != newStr) {
pageData.formSettings.superuserIPs = newValue;
}
},
}),
),
t.div(
{ className: "field addon" },
t.button(
{
type: "button",
className: () =>
`btn sm secondary transparent ${
app.utils.isEmpty(pageData.formSettings.superuserIPs) ? "hidden" : ""
}`,
onclick: () => {
pageData.formSettings.superuserIPs = [];
if (app.store.errors?.superuserIPs) {
delete app.store.errors.superuserIPs;
}
},
},
t.span({ className: "txt" }, "Clear"),
),
),
),
t.div(
{ className: "field-help" },
"Comma separated list of IPs and subnets such as: ",
t.div(
{ className: "inline-flex gap-5" },
t.div({
role: "button",
className: "label sm link-primary",
onclick: () => {
if (info.isLoading) {
return;
}
const ips = app.utils.toArray(pageData.formSettings.superuserIPs);
app.utils.pushUnique(ips, info.realIP);
pageData.formSettings.superuserIPs = ips;
},
textContent: () => info.isLoading ? "..." : (info.realIP + " (you)"),
}),
),
),
);
}