From f7e99b5af57abdcf190d34ef4dbb1727693b4b48 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Thu, 15 Jan 2026 04:49:38 +0800 Subject: [PATCH] feat: add toggle for password reset feature in admin settings (#508) --- src/backend/database/routes/users.ts | 94 +++++++++++++++++++ src/locales/en.json | 1 + src/ui/desktop/apps/admin/AdminSettings.tsx | 26 +++++ .../apps/admin/tabs/GeneralSettingsTab.tsx | 29 ++++++ src/ui/main-axios.ts | 22 +++++ 5 files changed, 172 insertions(+) diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 5e7e099e..d286c704 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1746,6 +1746,84 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { } }); +/** + * @openapi + * /users/password-reset-allowed: + * get: + * summary: Get password reset status + * description: Checks if password reset is currently allowed. + * tags: + * - Users + * responses: + * 200: + * description: Password reset status. + * 500: + * description: Failed to get password reset allowed status. + */ +router.get("/password-reset-allowed", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'allow_password_reset'") + .get(); + res.json({ + allowed: row ? (row as { value: string }).value === "true" : true, + }); + } catch (err) { + authLogger.error("Failed to get password reset allowed", err); + res.status(500).json({ error: "Failed to get password reset allowed" }); + } +}); + +/** + * @openapi + * /users/password-reset-allowed: + * patch: + * summary: Set password reset status + * description: Enables or disables password reset. + * tags: + * - Users + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * allowed: + * type: boolean + * responses: + * 200: + * description: Password reset status updated. + * 400: + * description: Invalid value for allowed. + * 403: + * description: Not authorized. + * 500: + * description: Failed to set password reset allowed status. + */ +router.patch("/password-reset-allowed", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); + } + const { allowed } = req.body; + if (typeof allowed !== "boolean") { + return res.status(400).json({ error: "Invalid value for allowed" }); + } + db.$client + .prepare( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('allow_password_reset', ?)", + ) + .run(allowed ? "true" : "false"); + res.json({ allowed }); + } catch (err) { + authLogger.error("Failed to set password reset allowed", err); + res.status(500).json({ error: "Failed to set password reset allowed" }); + } +}); + /** * @openapi * /users/delete-account: @@ -1861,6 +1939,22 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => { * description: Failed to initiate password reset. */ router.post("/initiate-reset", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'allow_password_reset'") + .get(); + if (row && (row as { value: string }).value !== "true") { + return res + .status(403) + .json({ error: "Password reset is currently disabled" }); + } + } catch (e) { + authLogger.warn("Failed to check password reset status", { + operation: "password_reset_check", + error: e, + }); + } + const { username } = req.body; if (!isNonEmptyString(username)) { diff --git a/src/locales/en.json b/src/locales/en.json index 812ea17e..dea8a24d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -539,6 +539,7 @@ "userRegistration": "User Registration", "allowNewAccountRegistration": "Allow new account registration", "allowPasswordLogin": "Allow username/password login", + "allowPasswordReset": "Allow password reset via reset code", "missingRequiredFields": "Missing required fields: {{fields}}", "oidcConfigurationUpdated": "OIDC configuration updated successfully!", "failedToFetchOidcConfig": "Failed to fetch OIDC configuration", diff --git a/src/ui/desktop/apps/admin/AdminSettings.tsx b/src/ui/desktop/apps/admin/AdminSettings.tsx index 8f226339..3883a82e 100644 --- a/src/ui/desktop/apps/admin/AdminSettings.tsx +++ b/src/ui/desktop/apps/admin/AdminSettings.tsx @@ -15,6 +15,7 @@ import { getAdminOIDCConfig, getRegistrationAllowed, getPasswordLoginAllowed, + getPasswordResetAllowed, getUserList, getUserInfo, isElectron, @@ -48,6 +49,7 @@ export function AdminSettings({ const [allowRegistration, setAllowRegistration] = React.useState(true); const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true); + const [allowPasswordReset, setAllowPasswordReset] = React.useState(true); const [oidcConfig, setOidcConfig] = React.useState({ client_id: "", @@ -193,6 +195,28 @@ export function AdminSettings({ }); }, []); + React.useEffect(() => { + if (isElectron()) { + const serverUrl = (window as { configuredServerUrl?: string }) + .configuredServerUrl; + if (!serverUrl) { + return; + } + } + + getPasswordResetAllowed() + .then((res) => { + if (typeof res === "boolean") { + setAllowPasswordReset(res); + } + }) + .catch((err) => { + if (err.code !== "NO_SERVER_CONFIGURED") { + console.warn("Failed to fetch password reset status", err); + } + }); + }, []); + const fetchUsers = async () => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) @@ -367,6 +391,8 @@ export function AdminSettings({ setAllowRegistration={setAllowRegistration} allowPasswordLogin={allowPasswordLogin} setAllowPasswordLogin={setAllowPasswordLogin} + allowPasswordReset={allowPasswordReset} + setAllowPasswordReset={setAllowPasswordReset} oidcConfig={oidcConfig} /> diff --git a/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx b/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx index 51e9fdc2..1b88c8a4 100644 --- a/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx @@ -6,6 +6,7 @@ import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { updateRegistrationAllowed, updatePasswordLoginAllowed, + updatePasswordResetAllowed, } from "@/ui/main-axios.ts"; interface GeneralSettingsTabProps { @@ -13,6 +14,8 @@ interface GeneralSettingsTabProps { setAllowRegistration: (value: boolean) => void; allowPasswordLogin: boolean; setAllowPasswordLogin: (value: boolean) => void; + allowPasswordReset: boolean; + setAllowPasswordReset: (value: boolean) => void; oidcConfig: { client_id: string; client_secret: string; @@ -27,6 +30,8 @@ export function GeneralSettingsTab({ setAllowRegistration, allowPasswordLogin, setAllowPasswordLogin, + allowPasswordReset, + setAllowPasswordReset, oidcConfig, }: GeneralSettingsTabProps): React.ReactElement { const { t } = useTranslation(); @@ -34,6 +39,7 @@ export function GeneralSettingsTab({ const [regLoading, setRegLoading] = React.useState(false); const [passwordLoginLoading, setPasswordLoginLoading] = React.useState(false); + const [passwordResetLoading, setPasswordResetLoading] = React.useState(false); const handleToggleRegistration = async (checked: boolean) => { setRegLoading(true); @@ -96,6 +102,16 @@ export function GeneralSettingsTab({ } }; + const handleTogglePasswordReset = async (checked: boolean) => { + setPasswordResetLoading(true); + try { + await updatePasswordResetAllowed(checked); + setAllowPasswordReset(checked); + } finally { + setPasswordResetLoading(false); + } + }; + return (

{t("admin.userRegistration")}

@@ -120,6 +136,19 @@ export function GeneralSettingsTab({ /> {t("admin.allowPasswordLogin")} +
); } diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 1a747c16..0e322433 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -2501,6 +2501,28 @@ export async function updatePasswordLoginAllowed( } } +export async function getPasswordResetAllowed(): Promise { + try { + const response = await authApi.get("/users/password-reset-allowed"); + return response.data.allowed; + } catch (error) { + handleApiError(error, "get password reset allowed"); + } +} + +export async function updatePasswordResetAllowed( + allowed: boolean, +): Promise<{ allowed: boolean }> { + try { + const response = await authApi.patch("/users/password-reset-allowed", { + allowed, + }); + return response.data; + } catch (error) { + handleApiError(error, "update password reset allowed"); + } +} + export async function updateOIDCConfig( config: Record, ): Promise> {