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);