diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 5f1d3be..317812f 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -5,7 +5,7 @@ import os from ..database import get_db from ..dependencies import get_current_user from ..models import User, UserRole, UserStatus -from ..schemas import UserLogin, UserOut, UserRegister, UserApprove +from ..schemas import UserLogin, UserOut, UserRegister, UserApprove, ChangePassword from ..security import ( ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token, @@ -117,3 +117,17 @@ def get_token(access_token: str | None = Cookie(default=None)): if not access_token: raise HTTPException(status_code=401, detail="Not authenticated.") return {"access_token": access_token} + + +# ── POST /auth/change-password ──────────────────────────────────────────────── + +@router.post("/change-password", status_code=status.HTTP_204_NO_CONTENT) +def change_password( + body: ChangePassword, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + if not verify_password(body.current_password, current_user.password_hash): + raise HTTPException(status_code=400, detail="Mật khẩu hiện tại không đúng.") + current_user.password_hash = hash_password(body.new_password) + db.commit() diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 347b4f5..b5e77cf 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -55,6 +55,18 @@ class UserRoleUpdate(BaseModel): role: UserRole +class ChangePassword(BaseModel): + current_password: str + new_password: str + + @field_validator("new_password") + @classmethod + def password_strength(cls, v: str) -> str: + if len(v) < 8: + raise ValueError("Password must be at least 8 characters.") + return v + + class TokenPayload(BaseModel): sub: int # user id exp: int diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index d3f54af..2b66ba1 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -151,6 +151,15 @@ export default function AdminPage() {
{me?.username} + +

Đổi mật khẩu

+
+ + {/* Success */} + {success && ( +
+ + + + Đổi mật khẩu thành công! +
+ )} + + {/* Error */} + {error && ( +
+ {error} + +
+ )} + +
+ {/* Current password */} +
+ +
+ setForm(f => ({ ...f, current_password: e.target.value }))} + required + className="w-full border border-gray-300 rounded-lg px-3 py-2.5 pr-10 text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + placeholder="Nhập mật khẩu hiện tại" + /> + +
+
+ + {/* New password */} +
+ +
+ setForm(f => ({ ...f, new_password: e.target.value }))} + required + minLength={8} + className="w-full border border-gray-300 rounded-lg px-3 py-2.5 pr-10 text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + placeholder="Ít nhất 8 ký tự" + /> + +
+ {/* Strength indicator */} + {form.new_password && ( +
+ {[1,2,3,4].map(i => { + const len = form.new_password.length; + const hasUpper = /[A-Z]/.test(form.new_password); + const hasSpecial = /[^A-Za-z0-9]/.test(form.new_password); + const score = (len >= 8 ? 1 : 0) + (len >= 12 ? 1 : 0) + (hasUpper ? 1 : 0) + (hasSpecial ? 1 : 0); + const active = i <= score; + const color = score <= 1 ? "bg-red-400" : score === 2 ? "bg-yellow-400" : score === 3 ? "bg-blue-400" : "bg-green-500"; + return
; + })} +
+ )} +
+ + {/* Confirm password */} +
+ +
+ setForm(f => ({ ...f, confirm_password: e.target.value }))} + required + className={`w-full border rounded-lg px-3 py-2.5 pr-10 text-sm focus:outline-none focus:ring-1 ${ + form.confirm_password && form.confirm_password !== form.new_password + ? "border-red-400 focus:border-red-400 focus:ring-red-300" + : "border-gray-300 focus:border-blue-500 focus:ring-blue-500" + }`} + placeholder="Nhập lại mật khẩu mới" + /> + +
+ {form.confirm_password && form.confirm_password !== form.new_password && ( +

Mật khẩu không khớp.

+ )} +
+ + + +
+ + ); +} diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 988668d..87bd68c 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -79,6 +79,15 @@ export default function DashboardPage() { Quản trị )} +