diff --git a/backend_rust/src/api/me.rs b/backend_rust/src/api/me.rs index 753a1e4..3534825 100644 --- a/backend_rust/src/api/me.rs +++ b/backend_rust/src/api/me.rs @@ -5,6 +5,7 @@ use sea_orm::{ActiveModelTrait, EntityTrait, Set}; use serde::{Deserialize, Serialize}; use std::io::Write; use uuid::Uuid; +use bcrypt; use crate::app_data::AppData; use crate::entity::user; @@ -33,6 +34,13 @@ pub struct UpdateSettingsRequest { pub bark_enabled: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChangePasswordRequest { + pub current_password: String, + pub new_password: String, +} + #[get("")] async fn get_me( app_data: web::Data, @@ -208,9 +216,46 @@ async fn upload_avatar( Ok(HttpResponse::Ok().json(UploadAvatarResponse { avatar_url })) } +#[put("/password")] +async fn change_password( + app_data: web::Data, + auth: AuthUser, + body: web::Json, +) -> Result { + if body.new_password.len() < 6 { + return Err(ApiError::BadRequest("New password must be at least 6 characters".to_string())); + } + + let user = user::Entity::find_by_id(auth.user_id) + .one(&app_data.db) + .await? + .ok_or_else(|| ApiError::NotFound("User not found".to_string()))?; + + // Verify current password + let valid = bcrypt::verify(&body.current_password, &user.password_hash) + .map_err(|_| ApiError::Internal("Password verification failed".to_string()))?; + + if !valid { + return Err(ApiError::BadRequest("Current password is incorrect".to_string())); + } + + // Hash new password + let new_password_hash = bcrypt::hash(&body.new_password, 10) + .map_err(|_| ApiError::Internal("Password hashing failed".to_string()))?; + + // Update password + let mut active: user::ActiveModel = user.into(); + active.password_hash = Set(new_password_hash); + active.updated_at = Set(chrono::Utc::now().fixed_offset()); + active.update(&app_data.db).await?; + + Ok(HttpResponse::Ok().json(serde_json::json!({"success": true}))) +} + pub fn routes() -> Scope { web::scope("/api/me") .service(get_me) .service(update_settings) .service(upload_avatar) + .service(change_password) } diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index f0fcdac..da7e9b0 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -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 ( @@ -195,6 +240,53 @@ const SettingsPanel = () => { {saved &&
{t("saved")}
} + + {/* Change Password Section */} +
+

{t("changePassword")}

+
+
+ + setPasswordForm({ ...passwordForm, currentPassword: e.target.value })} + /> +
+
+ + setPasswordForm({ ...passwordForm, newPassword: e.target.value })} + /> +
+
+ + setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })} + /> +
+ {passwordError && ( +
{passwordError}
+ )} + {passwordSuccess && ( +
{t("passwordChanged")}
+ )} + +
+
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index e5d7bff..a454cd0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -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 }) => diff --git a/frontend/src/lib/i18n/context.tsx b/frontend/src/lib/i18n/context.tsx index 3cdf424..ec6c2a1 100644 --- a/frontend/src/lib/i18n/context.tsx +++ b/frontend/src/lib/i18n/context.tsx @@ -40,7 +40,7 @@ export const I18nProvider = ({ children }: { children: ReactNode }) => { const t = useCallback( (key: TranslationKey, params?: Record): 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)); diff --git a/frontend/src/lib/i18n/translations.ts b/frontend/src/lib/i18n/translations.ts index f7f31da..47f730c 100644 --- a/frontend/src/lib/i18n/translations.ts +++ b/frontend/src/lib/i18n/translations.ts @@ -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",