Make auth toggle a settings option instead of env var
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
- Auth enabled state now stored in database setting (auth_enabled) - Add POST /auth/toggle endpoint to enable/disable auth from UI - Add auth toggle section to Settings > General page - Enabling auth without users prompts to create first admin - Disabling auth requires admin password confirmation - Auth setting cached in memory and loaded on startup Removes need for SPOOLMAN_AUTH_ENABLED env var Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { DownloadOutlined, UploadOutlined } from "@ant-design/icons";
|
||||
import { DownloadOutlined, LockOutlined, UploadOutlined } from "@ant-design/icons";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Button, Checkbox, Divider, Form, Input, message, Popconfirm, Space, Upload } from "antd";
|
||||
import { Button, Checkbox, Divider, Form, Input, message, Modal, Popconfirm, Space, Switch, Upload } from "antd";
|
||||
import type { UploadFile } from "antd";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { getAPIURL } from "../../utils/url";
|
||||
import { useGetSettings, useSetSetting } from "../../utils/querySettings";
|
||||
import { ColorModeContext, THEME_COLORS, ThemeColorKey } from "../../contexts/color-mode";
|
||||
import { useAuth } from "../../contexts/auth";
|
||||
|
||||
export function GeneralSettings() {
|
||||
const settings = useGetSettings();
|
||||
@@ -137,6 +138,10 @@ export function GeneralSettings() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<AuthSettings messageApi={messageApi} t={t} />
|
||||
|
||||
<Divider />
|
||||
|
||||
<BackupRestore messageApi={messageApi} t={t} />
|
||||
|
||||
{contextHolder}
|
||||
@@ -144,6 +149,176 @@ export function GeneralSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
interface AuthSettingsProps {
|
||||
messageApi: ReturnType<typeof message.useMessage>[0];
|
||||
t: ReturnType<typeof useTranslate>;
|
||||
}
|
||||
|
||||
function AuthSettings({ messageApi, t }: AuthSettingsProps) {
|
||||
const { authStatus, refreshAuthStatus, setup } = useAuth();
|
||||
const [toggling, setToggling] = useState(false);
|
||||
const [setupModalOpen, setSetupModalOpen] = useState(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||
const [setupForm] = Form.useForm();
|
||||
const [disableForm] = Form.useForm();
|
||||
|
||||
const handleToggle = async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
// If enabling and no users exist, show setup modal
|
||||
if (!authStatus?.has_users) {
|
||||
setSetupModalOpen(true);
|
||||
return;
|
||||
}
|
||||
// If users exist, just enable
|
||||
await toggleAuth(true);
|
||||
} else {
|
||||
// Disabling - show password confirmation modal
|
||||
setDisableModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAuth = async (enabled: boolean, password?: string) => {
|
||||
setToggling(true);
|
||||
try {
|
||||
const res = await fetch(`${getAPIURL()}/auth/toggle`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.detail || "Failed to toggle authentication");
|
||||
}
|
||||
|
||||
await refreshAuthStatus();
|
||||
messageApi.success(
|
||||
enabled
|
||||
? t("settings.auth.enabled_success")
|
||||
: t("settings.auth.disabled_success")
|
||||
);
|
||||
|
||||
if (enabled) {
|
||||
// Redirect to login after enabling
|
||||
window.location.href = "/login";
|
||||
}
|
||||
} catch (err) {
|
||||
messageApi.error(err instanceof Error ? err.message : "Failed to toggle authentication");
|
||||
} finally {
|
||||
setToggling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetup = async (values: { username: string; password: string }) => {
|
||||
setToggling(true);
|
||||
try {
|
||||
// Create the first admin user
|
||||
await setup(values.username, values.password);
|
||||
// Enable auth
|
||||
await toggleAuth(true);
|
||||
setSetupModalOpen(false);
|
||||
setupForm.resetFields();
|
||||
} catch (err) {
|
||||
messageApi.error(err instanceof Error ? err.message : "Setup failed");
|
||||
} finally {
|
||||
setToggling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable = async (values: { password: string }) => {
|
||||
await toggleAuth(false, values.password);
|
||||
setDisableModalOpen(false);
|
||||
disableForm.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "600px", margin: "0 auto" }}>
|
||||
<h3>
|
||||
<LockOutlined style={{ marginRight: 8 }} />
|
||||
{t("settings.auth.title")}
|
||||
</h3>
|
||||
<p style={{ marginBottom: "1em", color: "#888" }}>
|
||||
{t("settings.auth.description")}
|
||||
</p>
|
||||
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<Switch
|
||||
checked={authStatus?.enabled ?? false}
|
||||
onChange={handleToggle}
|
||||
loading={toggling}
|
||||
/>
|
||||
<span>
|
||||
{authStatus?.enabled
|
||||
? t("settings.auth.status_enabled")
|
||||
: t("settings.auth.status_disabled")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Setup Modal - Create first admin user */}
|
||||
<Modal
|
||||
title={t("settings.auth.setup_title")}
|
||||
open={setupModalOpen}
|
||||
onCancel={() => {
|
||||
setSetupModalOpen(false);
|
||||
setupForm.resetFields();
|
||||
}}
|
||||
onOk={() => setupForm.submit()}
|
||||
okText={t("settings.auth.enable_button")}
|
||||
confirmLoading={toggling}
|
||||
>
|
||||
<p style={{ marginBottom: 16 }}>{t("settings.auth.setup_description")}</p>
|
||||
<Form form={setupForm} layout="vertical" onFinish={handleSetup}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={t("settings.users.username")}
|
||||
rules={[
|
||||
{ required: true, message: t("settings.users.username_required") },
|
||||
{ min: 3, message: t("settings.users.username_min") },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t("settings.users.password")}
|
||||
rules={[
|
||||
{ required: true, message: t("settings.users.password_required") },
|
||||
{ min: 6, message: t("settings.users.password_min") },
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Disable Modal - Require password confirmation */}
|
||||
<Modal
|
||||
title={t("settings.auth.disable_title")}
|
||||
open={disableModalOpen}
|
||||
onCancel={() => {
|
||||
setDisableModalOpen(false);
|
||||
disableForm.resetFields();
|
||||
}}
|
||||
onOk={() => disableForm.submit()}
|
||||
okText={t("settings.auth.disable_button")}
|
||||
okButtonProps={{ danger: true }}
|
||||
confirmLoading={toggling}
|
||||
>
|
||||
<p style={{ marginBottom: 16 }}>{t("settings.auth.disable_description")}</p>
|
||||
<Form form={disableForm} layout="vertical" onFinish={handleDisable}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t("settings.auth.admin_password")}
|
||||
rules={[{ required: true, message: t("settings.auth.password_required") }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BackupRestoreProps {
|
||||
messageApi: ReturnType<typeof message.useMessage>[0];
|
||||
t: ReturnType<typeof useTranslate>;
|
||||
|
||||
Reference in New Issue
Block a user