feat: add toggle for password reset feature in admin settings (#508)

This commit was merged in pull request #508.
This commit is contained in:
ZacharyZcR
2026-01-15 04:49:38 +08:00
committed by GitHub
parent 8fa093ae60
commit f7e99b5af5
5 changed files with 172 additions and 0 deletions

View File

@@ -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)) {

View File

@@ -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",

View File

@@ -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}
/>
</TabsContent>

View File

@@ -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 (
<div className="rounded-lg border-2 border-border bg-card p-4 space-y-4">
<h3 className="text-lg font-semibold">{t("admin.userRegistration")}</h3>
@@ -120,6 +136,19 @@ export function GeneralSettingsTab({
/>
{t("admin.allowPasswordLogin")}
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={allowPasswordReset}
onCheckedChange={handleTogglePasswordReset}
disabled={passwordResetLoading || !allowPasswordLogin}
/>
{t("admin.allowPasswordReset")}
{!allowPasswordLogin && (
<span className="text-xs text-muted-foreground">
({t("admin.requiresPasswordLogin")})
</span>
)}
</label>
</div>
);
}

View File

@@ -2501,6 +2501,28 @@ export async function updatePasswordLoginAllowed(
}
}
export async function getPasswordResetAllowed(): Promise<boolean> {
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<string, unknown>,
): Promise<Record<string, unknown>> {