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
|
* @openapi
|
||||||
* /users/delete-account:
|
* /users/delete-account:
|
||||||
@@ -1861,6 +1939,22 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => {
|
|||||||
* description: Failed to initiate password reset.
|
* description: Failed to initiate password reset.
|
||||||
*/
|
*/
|
||||||
router.post("/initiate-reset", async (req, res) => {
|
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;
|
const { username } = req.body;
|
||||||
|
|
||||||
if (!isNonEmptyString(username)) {
|
if (!isNonEmptyString(username)) {
|
||||||
|
|||||||
@@ -539,6 +539,7 @@
|
|||||||
"userRegistration": "User Registration",
|
"userRegistration": "User Registration",
|
||||||
"allowNewAccountRegistration": "Allow new account registration",
|
"allowNewAccountRegistration": "Allow new account registration",
|
||||||
"allowPasswordLogin": "Allow username/password login",
|
"allowPasswordLogin": "Allow username/password login",
|
||||||
|
"allowPasswordReset": "Allow password reset via reset code",
|
||||||
"missingRequiredFields": "Missing required fields: {{fields}}",
|
"missingRequiredFields": "Missing required fields: {{fields}}",
|
||||||
"oidcConfigurationUpdated": "OIDC configuration updated successfully!",
|
"oidcConfigurationUpdated": "OIDC configuration updated successfully!",
|
||||||
"failedToFetchOidcConfig": "Failed to fetch OIDC configuration",
|
"failedToFetchOidcConfig": "Failed to fetch OIDC configuration",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
getAdminOIDCConfig,
|
getAdminOIDCConfig,
|
||||||
getRegistrationAllowed,
|
getRegistrationAllowed,
|
||||||
getPasswordLoginAllowed,
|
getPasswordLoginAllowed,
|
||||||
|
getPasswordResetAllowed,
|
||||||
getUserList,
|
getUserList,
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
isElectron,
|
isElectron,
|
||||||
@@ -48,6 +49,7 @@ export function AdminSettings({
|
|||||||
|
|
||||||
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
||||||
const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true);
|
const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true);
|
||||||
|
const [allowPasswordReset, setAllowPasswordReset] = React.useState(true);
|
||||||
|
|
||||||
const [oidcConfig, setOidcConfig] = React.useState({
|
const [oidcConfig, setOidcConfig] = React.useState({
|
||||||
client_id: "",
|
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 () => {
|
const fetchUsers = async () => {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
const serverUrl = (window as { configuredServerUrl?: string })
|
const serverUrl = (window as { configuredServerUrl?: string })
|
||||||
@@ -367,6 +391,8 @@ export function AdminSettings({
|
|||||||
setAllowRegistration={setAllowRegistration}
|
setAllowRegistration={setAllowRegistration}
|
||||||
allowPasswordLogin={allowPasswordLogin}
|
allowPasswordLogin={allowPasswordLogin}
|
||||||
setAllowPasswordLogin={setAllowPasswordLogin}
|
setAllowPasswordLogin={setAllowPasswordLogin}
|
||||||
|
allowPasswordReset={allowPasswordReset}
|
||||||
|
setAllowPasswordReset={setAllowPasswordReset}
|
||||||
oidcConfig={oidcConfig}
|
oidcConfig={oidcConfig}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
|||||||
import {
|
import {
|
||||||
updateRegistrationAllowed,
|
updateRegistrationAllowed,
|
||||||
updatePasswordLoginAllowed,
|
updatePasswordLoginAllowed,
|
||||||
|
updatePasswordResetAllowed,
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
interface GeneralSettingsTabProps {
|
interface GeneralSettingsTabProps {
|
||||||
@@ -13,6 +14,8 @@ interface GeneralSettingsTabProps {
|
|||||||
setAllowRegistration: (value: boolean) => void;
|
setAllowRegistration: (value: boolean) => void;
|
||||||
allowPasswordLogin: boolean;
|
allowPasswordLogin: boolean;
|
||||||
setAllowPasswordLogin: (value: boolean) => void;
|
setAllowPasswordLogin: (value: boolean) => void;
|
||||||
|
allowPasswordReset: boolean;
|
||||||
|
setAllowPasswordReset: (value: boolean) => void;
|
||||||
oidcConfig: {
|
oidcConfig: {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret: string;
|
client_secret: string;
|
||||||
@@ -27,6 +30,8 @@ export function GeneralSettingsTab({
|
|||||||
setAllowRegistration,
|
setAllowRegistration,
|
||||||
allowPasswordLogin,
|
allowPasswordLogin,
|
||||||
setAllowPasswordLogin,
|
setAllowPasswordLogin,
|
||||||
|
allowPasswordReset,
|
||||||
|
setAllowPasswordReset,
|
||||||
oidcConfig,
|
oidcConfig,
|
||||||
}: GeneralSettingsTabProps): React.ReactElement {
|
}: GeneralSettingsTabProps): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -34,6 +39,7 @@ export function GeneralSettingsTab({
|
|||||||
|
|
||||||
const [regLoading, setRegLoading] = React.useState(false);
|
const [regLoading, setRegLoading] = React.useState(false);
|
||||||
const [passwordLoginLoading, setPasswordLoginLoading] = React.useState(false);
|
const [passwordLoginLoading, setPasswordLoginLoading] = React.useState(false);
|
||||||
|
const [passwordResetLoading, setPasswordResetLoading] = React.useState(false);
|
||||||
|
|
||||||
const handleToggleRegistration = async (checked: boolean) => {
|
const handleToggleRegistration = async (checked: boolean) => {
|
||||||
setRegLoading(true);
|
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 (
|
return (
|
||||||
<div className="rounded-lg border-2 border-border bg-card p-4 space-y-4">
|
<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>
|
<h3 className="text-lg font-semibold">{t("admin.userRegistration")}</h3>
|
||||||
@@ -120,6 +136,19 @@ export function GeneralSettingsTab({
|
|||||||
/>
|
/>
|
||||||
{t("admin.allowPasswordLogin")}
|
{t("admin.allowPasswordLogin")}
|
||||||
</label>
|
</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>
|
</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(
|
export async function updateOIDCConfig(
|
||||||
config: Record<string, unknown>,
|
config: Record<string, unknown>,
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user