diff --git a/backend/app/api/routes/me.py b/backend/app/api/routes/me.py index bd9e0dd..0b43821 100644 --- a/backend/app/api/routes/me.py +++ b/backend/app/api/routes/me.py @@ -1,7 +1,11 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from app.core.db import get_db from app.core.deps import get_current_user +from app.core.security import hash_password, verify_password from app.models.models import User -from app.schemas.user import UserOut +from app.schemas.user import UserOut, UserPasswordChange +from app.services.audit import write_audit_log router = APIRouter() @@ -9,3 +13,21 @@ router = APIRouter() @router.get("/me", response_model=UserOut) async def me(user: User = Depends(get_current_user)) -> UserOut: return UserOut.model_validate(user) + + +@router.post("/me/password") +async def change_password( + payload: UserPasswordChange, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + if not verify_password(payload.current_password, user.password_hash): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Current password is incorrect") + + if verify_password(payload.new_password, user.password_hash): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="New password must be different") + + user.password_hash = hash_password(payload.new_password) + await db.commit() + await write_audit_log(db, action="auth.password_change", user_id=user.id, payload={}) + return {"status": "ok"} diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 57e643c..f04aa50 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -2,7 +2,7 @@ from functools import lru_cache from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict -NEXAPG_VERSION = "0.1.1" +NEXAPG_VERSION = "0.1.2" class Settings(BaseSettings): diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 0c52067..ad725b6 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -1,5 +1,5 @@ from datetime import datetime -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, field_validator class UserOut(BaseModel): @@ -21,3 +21,15 @@ class UserUpdate(BaseModel): email: EmailStr | None = None password: str | None = None role: str | None = None + + +class UserPasswordChange(BaseModel): + current_password: str + new_password: str + + @field_validator("new_password") + @classmethod + def validate_new_password(cls, value: str) -> str: + if len(value) < 8: + raise ValueError("new_password must be at least 8 characters") + return value diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d2689a6..ea8d3fc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,7 @@ import { QueryInsightsPage } from "./pages/QueryInsightsPage"; import { AlertsPage } from "./pages/AlertsPage"; import { AdminUsersPage } from "./pages/AdminUsersPage"; import { ServiceInfoPage } from "./pages/ServiceInfoPage"; +import { UserSettingsPage } from "./pages/UserSettingsPage"; function Protected({ children }) { const { tokens } = useAuth(); @@ -102,6 +103,9 @@ function Layout({ children }) {
Manage your personal account security settings.
+ {error &&