feat: add toggle for password reset feature in admin settings (#508)
This commit was merged in pull request #508.
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
Reference in New Issue
Block a user