feat: enhance organization management; add member registration and validation, update user registration flow, and improve enum handling
This commit is contained in:
+23
-10
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.api.deps import get_auth_service, get_user_repository
|
||||
@@ -19,7 +20,7 @@ class RegisterRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
name: str
|
||||
organization_name: str
|
||||
organization_name: str | None = None
|
||||
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
@@ -37,21 +38,33 @@ async def register_user(
|
||||
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()
|
||||
organization: Organization | None = None
|
||||
if payload.organization_name:
|
||||
existing_org = await repo.session.scalar(
|
||||
select(Organization).where(Organization.name == payload.organization_name)
|
||||
)
|
||||
if existing_org is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Organization 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)
|
||||
if organization is not None:
|
||||
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()
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
"""Organization-related API endpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
from app.api.deps import get_current_user, get_organization_repository
|
||||
from app.api.deps import (
|
||||
get_current_user,
|
||||
get_organization_context,
|
||||
get_organization_repository,
|
||||
get_organization_service,
|
||||
get_user_repository,
|
||||
)
|
||||
from app.models.organization import OrganizationRead
|
||||
from app.models.organization_member import OrganizationMemberRead, OrganizationRole
|
||||
from app.models.user import User
|
||||
from app.repositories.org_repo import OrganizationRepository
|
||||
from app.repositories.user_repo import UserRepository
|
||||
from app.services.organization_service import (
|
||||
OrganizationContext,
|
||||
OrganizationForbiddenError,
|
||||
OrganizationMemberAlreadyExistsError,
|
||||
OrganizationService,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/organizations", tags=["organizations"])
|
||||
|
||||
|
||||
class AddMemberPayload(BaseModel):
|
||||
email: EmailStr
|
||||
role: OrganizationRole = OrganizationRole.MEMBER
|
||||
|
||||
|
||||
@router.get("/me", response_model=list[OrganizationRead])
|
||||
async def list_user_organizations(
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -20,3 +40,26 @@ async def list_user_organizations(
|
||||
|
||||
organizations = await repo.list_for_user(current_user.id)
|
||||
return [OrganizationRead.model_validate(org) for org in organizations]
|
||||
|
||||
|
||||
@router.post("/members", response_model=OrganizationMemberRead, status_code=status.HTTP_201_CREATED)
|
||||
async def add_member_to_organization(
|
||||
payload: AddMemberPayload,
|
||||
context: OrganizationContext = Depends(get_organization_context),
|
||||
service: OrganizationService = Depends(get_organization_service),
|
||||
user_repo: UserRepository = Depends(get_user_repository),
|
||||
) -> OrganizationMemberRead:
|
||||
"""Allow owners/admins to add existing users to their organization."""
|
||||
|
||||
user = await user_repo.get_by_email(payload.email)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
try:
|
||||
membership = await service.add_member(context=context, user_id=user.id, role=payload.role)
|
||||
except OrganizationMemberAlreadyExistsError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
|
||||
except OrganizationForbiddenError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
||||
|
||||
return OrganizationMemberRead.model_validate(membership)
|
||||
|
||||
Reference in New Issue
Block a user