From 41e344f06f99ad4df7b0891f884bb7fc7b45d86a Mon Sep 17 00:00:00 2001 From: Artem Kashaev Date: Thu, 27 Nov 2025 14:14:15 +0500 Subject: [PATCH] feat: implement user registration and login functionality with JWT issuance --- app/api/v1/auth/views.py | 68 ++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/app/api/v1/auth/views.py b/app/api/v1/auth/views.py index 61635f7..ed07679 100644 --- a/app/api/v1/auth/views.py +++ b/app/api/v1/auth/views.py @@ -2,9 +2,15 @@ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.exc import IntegrityError -from app.api.deps import get_auth_service +from app.api.deps import get_auth_service, get_user_repository +from app.core.security import password_hasher +from app.models.organization import Organization +from app.models.organization_member import OrganizationMember, OrganizationRole from app.models.token import LoginRequest, TokenResponse +from app.models.user import UserCreate +from app.repositories.user_repo import UserRepository from app.services.auth_service import AuthService, InvalidCredentialsError from .models import RegisterRequest @@ -12,20 +18,56 @@ from .models import RegisterRequest router = APIRouter(prefix="/auth", tags=["auth"]) -def _stub(detail: str) -> dict[str, str]: - return {"detail": detail} +@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) +async def register_user( + payload: RegisterRequest, + repo: UserRepository = Depends(get_user_repository), + auth_service: AuthService = Depends(get_auth_service), +) -> TokenResponse: + """Register a new owner along with the first organization and return JWT.""" + + existing = await repo.get_by_email(payload.email) + if existing is not None: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="User already exists") + + organization = Organization(name=payload.organization_name) + repo.session.add(organization) + await repo.session.flush() + + user_data = UserCreate(email=payload.email, password=payload.password, name=payload.name) + hashed_password = password_hasher.hash(payload.password) + + try: + user = await repo.create(data=user_data, hashed_password=hashed_password) + membership = OrganizationMember( + organization_id=organization.id, + user_id=user.id, + role=OrganizationRole.OWNER, + ) + repo.session.add(membership) + await repo.session.commit() + except IntegrityError as exc: + await repo.session.rollback() + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Organization or user already exists", + ) from exc + + await repo.session.refresh(user) + return auth_service.create_access_token(user) -@router.post("/register", status_code=status.HTTP_501_NOT_IMPLEMENTED) -async def register_user(_: RegisterRequest) -> dict[str, str]: - """Placeholder for user plus organization registration flow.""" - return _stub("POST /auth/register is not implemented yet") - - -@router.post("/login", status_code=status.HTTP_501_NOT_IMPLEMENTED) -async def login(_: LoginRequest) -> dict[str, str]: - """Placeholder for login shortcut endpoint defined in the spec.""" - return _stub("POST /auth/login is not implemented yet") +@router.post("/login", response_model=TokenResponse) +async def login( + credentials: LoginRequest, + service: AuthService = Depends(get_auth_service), +) -> TokenResponse: + """Authenticate user credentials and issue a JWT.""" + try: + user = await service.authenticate(credentials.email, credentials.password) + except InvalidCredentialsError as exc: # pragma: no cover - thin API + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc + return service.create_access_token(user) @router.post("/token", response_model=TokenResponse)