diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 3c2303cc..e032555e 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -848,6 +848,22 @@ router.post("/login", async (req, res) => { return res.status(400).json({ error: "Invalid username or password" }); } + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'") + .get(); + if (row && (row as any).value !== "true") { + return res + .status(403) + .json({ error: "Password authentication is currently disabled" }); + } + } catch (e) { + authLogger.warn("Failed to check password login status", { + operation: "login_check", + error: e, + }); + } + try { const user = await db .select() @@ -1098,6 +1114,43 @@ router.patch("/registration-allowed", authenticateJWT, async (req, res) => { } }); +// Route: Get password login allowed status (public - needed for login page) +// GET /users/password-login-allowed +router.get("/password-login-allowed", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'") + .get(); + res.json({ allowed: row ? (row as any).value === "true" : true }); + } catch (err) { + authLogger.error("Failed to get password login allowed", err); + res.status(500).json({ error: "Failed to get password login allowed" }); + } +}); + +// Route: Set password login allowed status (admin only) +// PATCH /users/password-login-allowed +router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { + const userId = (req as any).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("UPDATE settings SET value = ? WHERE key = 'allow_password_login'") + .run(allowed ? "true" : "false"); + res.json({ allowed }); + } catch (err) { + authLogger.error("Failed to set password login allowed", err); + res.status(500).json({ error: "Failed to set password login allowed" }); + } +}); + // Route: Delete user account // DELETE /users/delete-account router.delete("/delete-account", authenticateJWT, async (req, res) => { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index d3aff79d..6f5ae638 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -409,10 +409,12 @@ "general": "General", "userRegistration": "User Registration", "allowNewAccountRegistration": "Allow new account registration", + "allowPasswordLogin": "Allow username/password login", "missingRequiredFields": "Missing required fields: {{fields}}", "oidcConfigurationUpdated": "OIDC configuration updated successfully!", "failedToFetchOidcConfig": "Failed to fetch OIDC configuration", "failedToFetchRegistrationStatus": "Failed to fetch registration status", + "failedToFetchPasswordLoginStatus": "Failed to fetch password login status", "failedToFetchUsers": "Failed to fetch users", "oidcConfigurationDisabled": "OIDC configuration disabled successfully!", "failedToUpdateOidcConfig": "Failed to update OIDC configuration", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 0cd92395..174b4b87 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -395,10 +395,12 @@ "general": "常规", "userRegistration": "用户注册", "allowNewAccountRegistration": "允许新账户注册", + "allowPasswordLogin": "允许用户名/密码登录", "missingRequiredFields": "缺少必填字段:{{fields}}", "oidcConfigurationUpdated": "OIDC 配置更新成功!", "failedToFetchOidcConfig": "获取 OIDC 配置失败", "failedToFetchRegistrationStatus": "获取注册状态失败", + "failedToFetchPasswordLoginStatus": "获取密码登录状态失败", "failedToFetchUsers": "获取用户列表失败", "oidcConfigurationDisabled": "OIDC 配置禁用成功!", "failedToUpdateOidcConfig": "更新 OIDC 配置失败", diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx index e176e234..7f144e84 100644 --- a/src/ui/Desktop/Admin/AdminSettings.tsx +++ b/src/ui/Desktop/Admin/AdminSettings.tsx @@ -37,8 +37,10 @@ import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { getOIDCConfig, getRegistrationAllowed, + getPasswordLoginAllowed, getUserList, updateRegistrationAllowed, + updatePasswordLoginAllowed, updateOIDCConfig, disableOIDCConfig, makeUserAdmin, @@ -62,6 +64,9 @@ export function AdminSettings({ const [allowRegistration, setAllowRegistration] = React.useState(true); const [regLoading, setRegLoading] = React.useState(false); + const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true); + const [passwordLoginLoading, setPasswordLoginLoading] = React.useState(false); + const [oidcConfig, setOidcConfig] = React.useState({ client_id: "", client_secret: "", @@ -141,6 +146,27 @@ export function AdminSettings({ }); }, []); + React.useEffect(() => { + if (isElectron()) { + const serverUrl = (window as any).configuredServerUrl; + if (!serverUrl) { + return; + } + } + + getPasswordLoginAllowed() + .then((res) => { + if (typeof res?.allowed === "boolean") { + setAllowPasswordLogin(res.allowed); + } + }) + .catch((err) => { + if (!err.message?.includes("No server configured")) { + toast.error(t("admin.failedToFetchPasswordLoginStatus")); + } + }); + }, []); + const fetchUsers = async () => { if (isElectron()) { const serverUrl = (window as any).configuredServerUrl; @@ -172,6 +198,16 @@ export function AdminSettings({ } }; + const handleTogglePasswordLogin = async (checked: boolean) => { + setPasswordLoginLoading(true); + try { + await updatePasswordLoginAllowed(checked); + setAllowPasswordLogin(checked); + } finally { + setPasswordLoginLoading(false); + } + }; + const handleOIDCConfigSubmit = async (e: React.FormEvent) => { e.preventDefault(); setOidcLoading(true); @@ -483,6 +519,14 @@ export function AdminSettings({ /> {t("admin.allowNewAccountRegistration")} + diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index c1f2a660..7622c83d 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1605,6 +1605,15 @@ export async function getRegistrationAllowed(): Promise<{ allowed: boolean }> { } } +export async function getPasswordLoginAllowed(): Promise<{ allowed: boolean }> { + try { + const response = await authApi.get("/users/password-login-allowed"); + return response.data; + } catch (error) { + handleApiError(error, "check password login status"); + } +} + export async function getOIDCConfig(): Promise { try { const response = await authApi.get("/users/oidc-config"); @@ -1752,6 +1761,19 @@ export async function updateRegistrationAllowed( } } +export async function updatePasswordLoginAllowed( + allowed: boolean, +): Promise { + try { + const response = await authApi.patch("/users/password-login-allowed", { + allowed, + }); + return response.data; + } catch (error) { + handleApiError(error, "update password login allowed"); + } +} + export async function updateOIDCConfig(config: any): Promise { try { const response = await authApi.post("/users/oidc-config", config);