feat(panel-access): add safe custom-domain reset and gate SSL availability
This commit is contained in:
1 parent
6eb9b47290
commit
b358138f77
3 files changed
+115
-3
No files matched your search
@@ -6,6 +6,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Globe2,
|
Globe2,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
RotateCcw,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
@@ -60,7 +61,7 @@ export default function PanelAccessSettingsPanel({
|
|||||||
const [panelDomain, setPanelDomain] = useState(initialDomain);
|
const [panelDomain, setPanelDomain] = useState(initialDomain);
|
||||||
const [panelProxy, setPanelProxy] = useState<PanelAccessProxy>("NGINX");
|
const [panelProxy, setPanelProxy] = useState<PanelAccessProxy>("NGINX");
|
||||||
const [panelAutoSsl, setPanelAutoSsl] = useState(
|
const [panelAutoSsl, setPanelAutoSsl] = useState(
|
||||||
settings.general.panelUrl.startsWith("https://") || !initialDomain,
|
Boolean(initialDomain) && settings.general.panelUrl.startsWith("https://"),
|
||||||
);
|
);
|
||||||
const [capabilities, setCapabilities] =
|
const [capabilities, setCapabilities] =
|
||||||
useState<PanelAccessCapabilities | null>(null);
|
useState<PanelAccessCapabilities | null>(null);
|
||||||
@@ -72,6 +73,13 @@ export default function PanelAccessSettingsPanel({
|
|||||||
const selectedCapability = capabilities?.proxies.find(
|
const selectedCapability = capabilities?.proxies.find(
|
||||||
(proxy) => proxy.type === panelProxy,
|
(proxy) => proxy.type === panelProxy,
|
||||||
);
|
);
|
||||||
|
const hasProvisionableProxy = Boolean(
|
||||||
|
capabilities?.proxies.some(
|
||||||
|
(proxy) => proxy.available && proxy.supportsProvisioning,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const sslDisabled = !panelDomain.trim() || !hasProvisionableProxy;
|
||||||
|
const hasCustomDomain = Boolean(initialDomain);
|
||||||
const canProvision = Boolean(
|
const canProvision = Boolean(
|
||||||
panelDomain.trim() &&
|
panelDomain.trim() &&
|
||||||
selectedCapability?.available &&
|
selectedCapability?.available &&
|
||||||
@@ -121,7 +129,7 @@ export default function PanelAccessSettingsPanel({
|
|||||||
const response = await settingsApi.provisionPanelDomain({
|
const response = await settingsApi.provisionPanelDomain({
|
||||||
domain: panelDomain.trim(),
|
domain: panelDomain.trim(),
|
||||||
proxy: panelProxy,
|
proxy: panelProxy,
|
||||||
autoSsl: panelAutoSsl,
|
autoSsl: panelAutoSsl && !sslDisabled,
|
||||||
});
|
});
|
||||||
setMessage(response.message || response.data.provision.message);
|
setMessage(response.message || response.data.provision.message);
|
||||||
onProvisioned?.(response.data.settings, response.message);
|
onProvisioned?.(response.data.settings, response.message);
|
||||||
@@ -134,6 +142,34 @@ export default function PanelAccessSettingsPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetPanelDomain = async () => {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Reset the custom panel domain? This only removes it from Doktainer settings; reverse-proxy and SSL configuration on the server will remain unchanged.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProvisioning(true);
|
||||||
|
setError(null);
|
||||||
|
setMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.resetPanelDomain();
|
||||||
|
setPanelDomain("");
|
||||||
|
setPanelAutoSsl(false);
|
||||||
|
setMessage(response.message || "Custom panel domain reset");
|
||||||
|
onProvisioned?.(response.data, response.message);
|
||||||
|
} catch (err) {
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to reset panel domain",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setProvisioning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card"
|
className="card"
|
||||||
@@ -396,10 +432,25 @@ export default function PanelAccessSettingsPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={panelAutoSsl}
|
checked={panelAutoSsl && !sslDisabled}
|
||||||
onChange={() => setPanelAutoSsl((current) => !current)}
|
onChange={() => setPanelAutoSsl((current) => !current)}
|
||||||
|
disabled={sslDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{sslDisabled ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: -8,
|
||||||
|
fontSize: 11,
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
lineHeight: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!panelDomain.trim()
|
||||||
|
? "Enter a domain to enable HTTPS / SSL."
|
||||||
|
: "HTTPS / SSL is unavailable because no reverse proxy can be provisioned."}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{capabilities?.autoSsl.reason ? (
|
{capabilities?.autoSsl.reason ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -423,6 +474,21 @@ export default function PanelAccessSettingsPanel({
|
|||||||
paddingTop: 2,
|
paddingTop: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
disabled={!hasCustomDomain || provisioning}
|
||||||
|
onClick={() => void resetPanelDomain()}
|
||||||
|
style={{
|
||||||
|
minWidth: 88,
|
||||||
|
opacity: hasCustomDomain && !provisioning ? 1 : 0.55,
|
||||||
|
cursor:
|
||||||
|
hasCustomDomain && !provisioning ? "pointer" : "not-allowed",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
|||||||
@@ -1163,6 +1163,12 @@ export const settingsApi = {
|
|||||||
message?: string;
|
message?: string;
|
||||||
}>("/settings/panel-access/provision", body, { timeoutMs: 30000 }),
|
}>("/settings/panel-access/provision", body, { timeoutMs: 30000 }),
|
||||||
|
|
||||||
|
resetPanelDomain: () =>
|
||||||
|
post<{ success: boolean; data: SettingsRecord; message?: string }>(
|
||||||
|
"/settings/panel-access/reset",
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
|
||||||
update: (body: UpdateSettingsBody) =>
|
update: (body: UpdateSettingsBody) =>
|
||||||
patch<{ success: boolean; data: SettingsRecord; message?: string }>(
|
patch<{ success: boolean; data: SettingsRecord; message?: string }>(
|
||||||
"/settings",
|
"/settings",
|
||||||
|
|||||||
@@ -1032,6 +1032,46 @@ export async function settingsRoutes(app: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/panel-access/reset",
|
||||||
|
{ preHandler: [authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
// Do not use request headers here: this route can be reached through the
|
||||||
|
// custom domain that is being removed.
|
||||||
|
const panelUrl = resolvePublicAppOrigin({
|
||||||
|
fallbackOrigin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.userSettings.upsert({
|
||||||
|
where: { userId: req.userId! },
|
||||||
|
update: { panelUrl },
|
||||||
|
create: { userId: req.userId!, panelUrl },
|
||||||
|
});
|
||||||
|
|
||||||
|
const saved = await ensureModularSettings(
|
||||||
|
req.userId!,
|
||||||
|
req.organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await auditLog({
|
||||||
|
userId: req.userId,
|
||||||
|
organizationId: req.organizationId,
|
||||||
|
action: "PANEL_DOMAIN_RESET",
|
||||||
|
category: "SYSTEM",
|
||||||
|
level: "WARNING",
|
||||||
|
message: "Custom panel domain was removed from Panel Access settings",
|
||||||
|
meta: { panelUrl },
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
data: serializeSettings(saved),
|
||||||
|
message:
|
||||||
|
"Custom panel domain was reset. Existing reverse-proxy and SSL configuration was left unchanged.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
app.patch(
|
app.patch(
|
||||||
"/",
|
"/",
|
||||||
{ preHandler: [authenticate], bodyLimit: 512 * 1024 },
|
{ preHandler: [authenticate], bodyLimit: 512 * 1024 },
|
||||||
|
|||||||
Reference in new issue
Block a user