Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions backend/app/api/routes/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from app.config import settings
from app.database import DbSession
from app.schemas import DeveloperRead, DeveloperUpdate
from app.schemas import DeveloperRead, DeveloperUpdate, PasswordChange
from app.schemas.token import TokenResponse
from app.services import DeveloperDep, developer_service, refresh_token_service
from app.utils.security import create_access_token, verify_password
Expand Down Expand Up @@ -59,7 +59,24 @@ async def logout(_developer: DeveloperDep):
return {"message": "Successfully logged out"}


# TODO: Implement /forgot-password and /reset-password
@router.post("/change-password")
async def change_password(
payload: PasswordChange,
db: DbSession,
developer: DeveloperDep,
):
"""Change password for the current authenticated developer."""
# Verify the current password
if not verify_password(payload.current_password, developer.hashed_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Incorrect current password",
)

# Update using existing developer_service logic
developer_service.update_developer_info(db, developer.id, DeveloperUpdate(password=payload.new_password))

return {"message": "Password updated successfully"}


@router.get("/me", response_model=DeveloperRead)
Expand Down
2 changes: 2 additions & 0 deletions backend/app/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
DeveloperRead,
DeveloperUpdate,
DeveloperUpdateInternal,
PasswordChange,
)
from .device_type import DeviceType
from .device_type_priority import (
Expand Down Expand Up @@ -215,6 +216,7 @@
"DeveloperCreateInternal",
"DeveloperUpdateInternal",
"DeveloperUpdate",
"PasswordChange",
"InvitationCreate",
"InvitationRead",
"InvitationAccept",
Expand Down
14 changes: 13 additions & 1 deletion backend/app/schemas/developer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from uuid import UUID, uuid4

from pydantic import BaseModel, ConfigDict, EmailStr, Field
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator


class DeveloperRead(BaseModel):
Expand Down Expand Up @@ -45,3 +45,15 @@ class DeveloperUpdateInternal(BaseModel):
email: EmailStr | None = None
hashed_password: str | None = None
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class PasswordChange(BaseModel):
current_password: str
new_password: str
confirm_password: str

@model_validator(mode="after")
def check_passwords_match(self) -> "PasswordChange":
if self.new_password != self.confirm_password:
raise ValueError("The confirmation password does not match the new password")
return self
52 changes: 52 additions & 0 deletions backend/tests/api/v1/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,58 @@ def test_logout_invalid_token(self, client: TestClient, api_v1_prefix: str) -> N
assert response.status_code == 401


class TestChangePassword:
"""Tests for POST /api/v1/auth/change-password."""

def test_change_password_success(self, client: TestClient, db: Session, api_v1_prefix: str) -> None:
"""Test successful password change with valid data."""
# Arrange
developer = DeveloperFactory(password="OldPassword123")
headers = developer_auth_headers(developer.id)
payload = {
"current_password": "OldPassword123",
"new_password": "NewPassword456",
"confirm_password": "NewPassword456",
}

# Act
response = client.post(f"{api_v1_prefix}/auth/change-password", json=payload, headers=headers)

# Assert
assert response.status_code == 200
assert response.json()["message"] == "Password updated successfully"

def test_change_password_invalid_current(self, client: TestClient, db: Session, api_v1_prefix: str) -> None:
"""Test failure when the current password is wrong."""
developer = DeveloperFactory(password="CorrectOld123")
headers = developer_auth_headers(developer.id)
payload = {
"current_password": "WrongOld123",
"new_password": "NewPassword789",
"confirm_password": "NewPassword789",
}

response = client.post(f"{api_v1_prefix}/auth/change-password", json=payload, headers=headers)

assert response.status_code == 400
assert response.json()["detail"] == "Incorrect current password"

def test_change_password_mismatch(self, client: TestClient, db: Session, api_v1_prefix: str) -> None:
"""Test failure when new_password and confirm_password do not match."""
developer = DeveloperFactory(password="OldPassword123")
headers = developer_auth_headers(developer.id)
payload = {
"current_password": "OldPassword123",
"new_password": "NewPassword123",
"confirm_password": "DifferentPassword123",
}

response = client.post(f"{api_v1_prefix}/auth/change-password", json=payload, headers=headers)

assert response.status_code == 400
assert "The confirmation password does not match" in str(response.json())


class TestGetCurrentDeveloper:
"""Tests for GET /api/v1/me."""

Expand Down