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}
+
)}
+
router.push("/change-password")}
+ className="text-sm text-gray-500 hover:text-blue-600 transition"
+ title="Đổi mật khẩu"
+ >
+
+
(null); // populated from api.me(); used to identify own vs remote objects
const [userRole, setUserRole] = useState<"admin" | "teacher" | "student">("student");
const userRoleRef = useRef<"admin" | "teacher" | "student">("student");
+ const [currentUsername, setCurrentUsername] = useState("");
+ const currentUsernameRef = useRef("");
+ const userMapRef = useRef
{/* ── Color picker portal \u2014 rendered into document.body to escape overflow:hidden ── */}
+ {/* Annotation owner tooltip */}
+ {annotationTooltip && (
+
+
+ {annotationTooltip.text}
+
+ )}
+
{colorPickerOpen && colorPickerPos && typeof document !== "undefined" && createPortal(
request("/api/auth/register", { method: "POST", body: JSON.stringify(body) }),
logout: () => request("/api/auth/logout", { method: "POST" }),
+ changePassword: (body: { current_password: string; new_password: string }) =>
+ request("/api/auth/change-password", { method: "POST", body: JSON.stringify(body) }),
// PDFs
listPdfs: () => request("/api/pdfs"),