Add change password feature to settings page

- Add PUT /api/me/password endpoint in backend
- Add changePassword API function in frontend
- Add change password form UI in SettingsPanel
- Add i18n translations for change password (zh/en)
- Fix TypeScript type error in i18n context

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Dong
2026-02-05 18:30:57 +08:00
parent 403843acfd
commit 86d3a8c419
5 changed files with 163 additions and 1 deletions

View File

@@ -34,6 +34,14 @@ const SettingsPanel = () => {
});
const [saved, setSaved] = useState(false);
const [uploading, setUploading] = useState(false);
const [passwordForm, setPasswordForm] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: "",
});
const [passwordError, setPasswordError] = useState("");
const [passwordSuccess, setPasswordSuccess] = useState(false);
const [changingPassword, setChangingPassword] = useState(false);
useEffect(() => {
api
@@ -92,6 +100,43 @@ const SettingsPanel = () => {
}
};
const handleChangePassword = async () => {
setPasswordError("");
setPasswordSuccess(false);
if (!passwordForm.currentPassword) {
setPasswordError(t("currentPasswordRequired"));
return;
}
if (!passwordForm.newPassword) {
setPasswordError(t("newPasswordRequired"));
return;
}
if (passwordForm.newPassword.length < 6) {
setPasswordError(t("passwordTooShort"));
return;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setPasswordError(t("passwordMismatch"));
return;
}
try {
setChangingPassword(true);
await api.changePassword({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword,
});
setPasswordSuccess(true);
setPasswordForm({ currentPassword: "", newPassword: "", confirmPassword: "" });
setTimeout(() => setPasswordSuccess(false), 3000);
} catch (error) {
setPasswordError(error instanceof Error ? error.message : "Failed to change password");
} finally {
setChangingPassword(false);
}
};
return (
<Card className="bg-white">
<CardHeader>
@@ -195,6 +240,53 @@ const SettingsPanel = () => {
</Button>
{saved && <div className="text-sm text-primary">{t("saved")}</div>}
</div>
{/* Change Password Section */}
<div className="mt-8 pt-6 border-t border-slate-200">
<h3 className="text-lg font-medium mb-4">{t("changePassword")}</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword">{t("currentPassword")}</Label>
<Input
id="currentPassword"
type="password"
value={passwordForm.currentPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, currentPassword: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="newPassword">{t("newPassword")}</Label>
<Input
id="newPassword"
type="password"
value={passwordForm.newPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">{t("confirmNewPassword")}</Label>
<Input
id="confirmPassword"
type="password"
value={passwordForm.confirmPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
/>
</div>
{passwordError && (
<div className="text-sm text-red-500">{passwordError}</div>
)}
{passwordSuccess && (
<div className="text-sm text-green-600">{t("passwordChanged")}</div>
)}
<Button
variant="outline"
onClick={handleChangePassword}
disabled={changingPassword}
>
{changingPassword ? t("loading") : t("changePassword")}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>