Change password route was added.

This commit is contained in:
febux 2026-09-02 21:58:45 +03:00
parent 73187742f2
commit 6ae7edc8f5
3 changed files with 48 additions and 0 deletions

View file

@ -8,10 +8,12 @@ from fastapi import APIRouter, HTTPException, status
from src.contract_check.api.deps import (
AsyncSessionDep,
CurrentUserDep,
NotificationPublisherDep,
RefreshStoreDep,
create_email_user,
fetch_user_by_email,
fetch_user_by_id_full,
)
from src.contract_check.api.routes.auth.support import (
_build_reset_link,
@ -20,6 +22,7 @@ from src.contract_check.api.routes.auth.support import (
_require_web_auth_enabled,
)
from src.contract_check.api.schemas import (
ChangePasswordRequest,
ForgotPasswordRequest,
LoginRequest,
LogoutRequest,
@ -225,3 +228,41 @@ async def reset_password(
log.info("user_password_reset", user_id=str(user_id))
return OkResponse(ok=True, detail="password updated")
@router.post(
"/change-password",
response_model=OkResponse,
status_code=status.HTTP_200_OK,
)
async def change_password(
session: AsyncSessionDep,
refresh_store: RefreshStoreDep,
user: CurrentUserDep,
body: ChangePasswordRequest,
) -> OkResponse:
"""Rotate the password of the authenticated user.
Requires the current password. On success all active refresh tokens are
revoked, forcing re-login on other devices.
"""
_require_web_auth_enabled()
db_user = await fetch_user_by_id_full(session, user.user_id)
if db_user is None or not db_user.password_hash:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="no password set")
if not verify_password(body.current_password, db_user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
await UserRepository(session).set_password(user.user_id, hash_password(body.new_password))
await session.commit()
# Best-effort revoke of existing sessions; ignore Redis hiccups so the
# password itself is still rotated.
try:
await refresh_store.revoke_all(user.user_id)
except Exception as exc: # noqa: BLE001
log.warning("change_password_revoke_failed", user_id=str(user.user_id), error=str(exc))
log.info("user_password_changed", user_id=str(user.user_id))
return OkResponse(ok=True, detail="password updated")

View file

@ -15,6 +15,7 @@ from src.contract_check.api.schemas.analysis_result import (
)
from src.contract_check.api.schemas.auth import (
AuthResponse,
ChangePasswordRequest,
ForgotPasswordRequest,
LoginRequest,
LogoutRequest,
@ -106,6 +107,7 @@ __all__ = [
"LogoutRequest",
"ForgotPasswordRequest",
"ResetPasswordRequest",
"ChangePasswordRequest",
"OkResponse",
"PasskeyRegistrationOptions",
"PasskeyAuthenticationOptions",

View file

@ -105,6 +105,11 @@ class ResetPasswordRequest(BaseModel):
password: str = Field(..., min_length=8, max_length=128)
class ChangePasswordRequest(BaseModel):
current_password: str = Field(..., min_length=1, max_length=128)
new_password: str = Field(..., min_length=8, max_length=128)
class OkResponse(BaseModel):
"""Generic `{ok: true, ...}` payload for state-mutating auth endpoints."""