from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.database import get_db from app.dependencies import get_current_user from app.models.user import User from app.models.tenant import WorkspaceInvitation from app.schemas.tenant import ( InvitationCreate, InvitationResponse, MemberResponse, RoleUpdate, AuditEventResponse, WorkspaceCreate, WorkspaceResponse, ) from app.config import settings from app.services.tenant_service import ( accept_invitation, can_manage, create_workspace, get_membership, get_workspace, invite_member, list_audit_events, revoke_invitation, transfer_ownership, list_members, list_workspaces, update_member_role, ) router = APIRouter(prefix="/tenants", tags=["tenants"]) def workspace_response(row) -> WorkspaceResponse: workspace, organization, role = row return WorkspaceResponse( id=workspace.id, organization_id=organization.id, organization_name=organization.name, name=workspace.name, slug=workspace.slug, role=role, created_at=workspace.created_at, ) @router.get("/workspaces", response_model=list[WorkspaceResponse]) def get_workspaces(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): return [workspace_response(row) for row in list_workspaces(db, current_user.id)] @router.post("/workspaces", response_model=WorkspaceResponse, status_code=status.HTTP_201_CREATED) def post_workspace(data: WorkspaceCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): workspace = create_workspace(db, current_user, data) row = get_workspace(db, workspace.id, current_user.id) return workspace_response(row) @router.get("/workspaces/{workspace_id}/members", response_model=list[MemberResponse]) def get_workspace_members(workspace_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): if get_workspace(db, workspace_id, current_user.id) is None: raise HTTPException(status_code=403, detail="Workspace not found or access denied") return [ MemberResponse(user_id=user.id, email=user.email, full_name=user.full_name, username=user.username, role=membership.role, joined_at=membership.created_at) for user, membership in list_members(db, workspace_id) ] @router.post("/workspaces/{workspace_id}/invitations", response_model=InvitationResponse, status_code=201) def post_invitation(workspace_id: int, data: InvitationCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = get_workspace(db, workspace_id, current_user.id) if row is None or not can_manage(row[2]): raise HTTPException(status_code=403, detail="Only workspace owners and admins can invite members") invitation = invite_member(db, workspace_id, current_user.id, str(data.email), data.role) return InvitationResponse( id=invitation.id, workspace_id=invitation.workspace_id, email=invitation.email, role=invitation.role, expires_at=invitation.expires_at, accepted=invitation.accepted, token=invitation.token, invite_url=f"{settings.frontend_url}/invite/{invitation.token}", ) @router.delete("/workspaces/{workspace_id}/invitations/{invitation_id}", status_code=204) def delete_invitation(workspace_id: int, invitation_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = get_workspace(db, workspace_id, current_user.id) if row is None or not can_manage(row[2]) or not revoke_invitation(db, workspace_id, invitation_id, current_user.id): raise HTTPException(status_code=404, detail="Invitation not found") @router.post("/workspaces/{workspace_id}/invitations/{invitation_id}/resend", response_model=InvitationResponse) def resend_invitation(workspace_id: int, invitation_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = get_workspace(db, workspace_id, current_user.id) invitation = db.query(WorkspaceInvitation).filter_by(id=invitation_id, workspace_id=workspace_id, accepted=False).first() if row is None or not can_manage(row[2]) or invitation is None: raise HTTPException(status_code=404, detail="Invitation not found") invitation.accepted = True db.commit() replacement = invite_member(db, workspace_id, current_user.id, invitation.email, invitation.role) return InvitationResponse( id=replacement.id, workspace_id=replacement.workspace_id, email=replacement.email, role=replacement.role, expires_at=replacement.expires_at, accepted=replacement.accepted, token=replacement.token, invite_url=f"{settings.frontend_url}/invite/{replacement.token}", ) @router.post("/workspaces/{workspace_id}/ownership/{user_id}", response_model=WorkspaceResponse) def post_transfer_ownership(workspace_id: int, user_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = get_workspace(db, workspace_id, current_user.id) if row is None or row[2] != "owner" or not transfer_ownership(db, workspace_id, current_user.id, user_id): raise HTTPException(status_code=403, detail="Only the workspace owner can transfer ownership") return workspace_response(get_workspace(db, workspace_id, user_id)) @router.get("/workspaces/{workspace_id}/audit", response_model=list[AuditEventResponse]) def get_audit(workspace_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = get_workspace(db, workspace_id, current_user.id) if row is None or not can_manage(row[2]): raise HTTPException(status_code=403, detail="Only workspace admins can view audit history") return [AuditEventResponse.model_validate(event, from_attributes=True) for event in list_audit_events(db, workspace_id)] @router.post("/invitations/{token}/accept", response_model=MemberResponse) def post_accept_invitation(token: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): membership = accept_invitation(db, token, current_user) if membership is None: raise HTTPException(status_code=400, detail="Invitation is invalid, expired, or not addressed to this account") return MemberResponse(user_id=current_user.id, email=current_user.email, full_name=current_user.full_name, username=current_user.username, role=membership.role, joined_at=membership.created_at) @router.patch("/workspaces/{workspace_id}/members/{user_id}", response_model=MemberResponse) def patch_member_role(workspace_id: int, user_id: int, data: RoleUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): row = get_workspace(db, workspace_id, current_user.id) if row is None or not can_manage(row[2]): raise HTTPException(status_code=403, detail="Only workspace owners and admins can manage roles") membership = update_member_role(db, workspace_id, user_id, data.role) target = db.query(User).filter(User.id == user_id).first() if membership is None or target is None: raise HTTPException(status_code=400, detail="Member role cannot be changed") return MemberResponse(user_id=target.id, email=target.email, full_name=target.full_name, username=target.username, role=membership.role, joined_at=membership.created_at)