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:
@@ -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>
|
||||
|
||||
@@ -67,6 +67,9 @@ export const api = {
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
// Password management
|
||||
changePassword: (payload: { currentPassword: string; newPassword: string }) =>
|
||||
request("/api/me/password", { method: "PUT", body: JSON.stringify(payload) }),
|
||||
// Invite management
|
||||
getInvites: () => request("/api/invites"),
|
||||
createInvite: (payload: { maxUses?: number; expiresInDays?: number }) =>
|
||||
|
||||
@@ -40,7 +40,7 @@ export const I18nProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const t = useCallback(
|
||||
(key: TranslationKey, params?: Record<string, string | number>): string => {
|
||||
let text = translations[locale][key] || translations.zh[key] || key;
|
||||
let text: string = translations[locale][key] || translations.zh[key] || key;
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([paramKey, value]) => {
|
||||
text = text.replace(`{${paramKey}}`, String(value));
|
||||
|
||||
@@ -120,6 +120,17 @@ export const translations = {
|
||||
barkAlerts: "Bark 推送",
|
||||
barkAlertsDesc: "Bark 推送开关",
|
||||
|
||||
// Change Password
|
||||
changePassword: "修改密码",
|
||||
currentPassword: "当前密码",
|
||||
newPassword: "新密码",
|
||||
confirmNewPassword: "确认新密码",
|
||||
passwordChanged: "密码修改成功",
|
||||
passwordMismatch: "两次输入的密码不一致",
|
||||
passwordTooShort: "密码至少需要6个字符",
|
||||
currentPasswordRequired: "请输入当前密码",
|
||||
newPasswordRequired: "请输入新密码",
|
||||
|
||||
// Recurrence
|
||||
hourly: "每小时",
|
||||
daily: "每天",
|
||||
@@ -333,6 +344,17 @@ export const translations = {
|
||||
barkAlerts: "Bark Alerts",
|
||||
barkAlertsDesc: "Bark push notification toggle",
|
||||
|
||||
// Change Password
|
||||
changePassword: "Change Password",
|
||||
currentPassword: "Current Password",
|
||||
newPassword: "New Password",
|
||||
confirmNewPassword: "Confirm New Password",
|
||||
passwordChanged: "Password changed successfully",
|
||||
passwordMismatch: "Passwords do not match",
|
||||
passwordTooShort: "Password must be at least 6 characters",
|
||||
currentPasswordRequired: "Please enter current password",
|
||||
newPasswordRequired: "Please enter new password",
|
||||
|
||||
// Recurrence
|
||||
hourly: "Hourly",
|
||||
daily: "Daily",
|
||||
|
||||
Reference in New Issue
Block a user