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,
|
||||
Globe2,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -60,7 +61,7 @@ export default function PanelAccessSettingsPanel({
|
||||
const [panelDomain, setPanelDomain] = useState(initialDomain);
|
||||
const [panelProxy, setPanelProxy] = useState<PanelAccessProxy>("NGINX");
|
||||
const [panelAutoSsl, setPanelAutoSsl] = useState(
|
||||
settings.general.panelUrl.startsWith("https://") || !initialDomain,
|
||||
Boolean(initialDomain) && settings.general.panelUrl.startsWith("https://"),
|
||||
);
|
||||
const [capabilities, setCapabilities] =
|
||||
useState<PanelAccessCapabilities | null>(null);
|
||||
@@ -72,6 +73,13 @@ export default function PanelAccessSettingsPanel({
|
||||
const selectedCapability = capabilities?.proxies.find(
|
||||
(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(
|
||||
panelDomain.trim() &&
|
||||
selectedCapability?.available &&
|
||||
@@ -121,7 +129,7 @@ export default function PanelAccessSettingsPanel({
|
||||
const response = await settingsApi.provisionPanelDomain({
|
||||
domain: panelDomain.trim(),
|
||||
proxy: panelProxy,
|
||||
autoSsl: panelAutoSsl,
|
||||
autoSsl: panelAutoSsl && !sslDisabled,
|
||||
});
|
||||
setMessage(response.message || response.data.provision.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 (
|
||||
<div
|
||||
className="card"
|
||||
@@ -396,10 +432,25 @@ export default function PanelAccessSettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={panelAutoSsl}
|
||||
checked={panelAutoSsl && !sslDisabled}
|
||||
onChange={() => setPanelAutoSsl((current) => !current)}
|
||||
disabled={sslDisabled}
|
||||
/>
|
||||
</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 ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -423,6 +474,21 @@ export default function PanelAccessSettingsPanel({
|
||||
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
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
|
||||
@@ -1163,6 +1163,12 @@ export const settingsApi = {
|
||||
message?: string;
|
||||
}>("/settings/panel-access/provision", body, { timeoutMs: 30000 }),
|
||||
|
||||
resetPanelDomain: () =>
|
||||
post<{ success: boolean; data: SettingsRecord; message?: string }>(
|
||||
"/settings/panel-access/reset",
|
||||
{},
|
||||
),
|
||||
|
||||
update: (body: UpdateSettingsBody) =>
|
||||
patch<{ success: boolean; data: SettingsRecord; message?: string }>(
|
||||
"/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(
|
||||
"/",
|
||||
{ preHandler: [authenticate], bodyLimit: 512 * 1024 },
|
||||
|
||||
Reference in new issue
Block a user