added superuser ips whitelist
This commit is contained in:
@@ -832,7 +832,7 @@ function deleteDropdownItem(data, modalSettings) {
|
||||
try {
|
||||
await app.pb.collections.delete(data.originalCollection.name);
|
||||
|
||||
modalSettings.ondelete?.(JSON.parse(JSON.stringify(data.originalCollection)));
|
||||
modalSettings?.ondelete?.(JSON.parse(JSON.stringify(data.originalCollection)));
|
||||
|
||||
app.utils.removeByKey(app.store.collections, "id", data.originalCollection.id);
|
||||
|
||||
|
||||
+3
-1
@@ -513,7 +513,9 @@ hr {
|
||||
text-decoration: none;
|
||||
border-radius: var(--borderRadius);
|
||||
color: var(--linkColor);
|
||||
transition: color var(--animationSpeed);
|
||||
transition:
|
||||
color var(--animationSpeed),
|
||||
opacity var(--animationSpeed);
|
||||
user-select: none;
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
|
||||
@@ -235,12 +235,4 @@ window.app.checkApiError = function(err, showToast = true) {
|
||||
app.pb.cancelAllRequests();
|
||||
return app.pb.authStore.clear();
|
||||
}
|
||||
|
||||
// forbidden
|
||||
if (statusCode === 403) {
|
||||
app.pb.cancelAllRequests();
|
||||
if (window.location.hash != LOGIN_PATH) {
|
||||
window.location.hash = LOGIN_PATH;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)"),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
+14
-1
@@ -375,7 +375,20 @@ watch(
|
||||
|
||||
removeErrorState(input, container);
|
||||
|
||||
const errMsg = app.utils.getByPath(errs, name)?.message;
|
||||
const errData = app.utils.getByPath(errs, name);
|
||||
let errMsg = errData?.message || "";
|
||||
|
||||
// merge one level nested errors into a single message
|
||||
if (!errMsg && !app.utils.isEmpty(errData)) {
|
||||
const combinedErrs = [];
|
||||
for (let key in errData) {
|
||||
if (errData[key]?.message) {
|
||||
combinedErrs.push(`${key}: ${errData[key]?.message}`);
|
||||
}
|
||||
}
|
||||
errMsg = combinedErrs.join("\n");
|
||||
}
|
||||
|
||||
if (!errMsg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user