Files
openclaw-mission-control/backend/app/api/users.py

284 lines
8.6 KiB
Python

"""User self-service API endpoints for profile retrieval and updates."""
from __future__ import annotations
from typing import TYPE_CHECKING
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import col, select
from app.core.auth import AuthContext, delete_clerk_user, get_auth_context
from app.db import crud
from app.db.session import get_session
from app.models.activity_events import ActivityEvent
from app.models.agents import Agent
from app.models.approvals import Approval
from app.models.board_group_memory import BoardGroupMemory
from app.models.board_groups import BoardGroup
from app.models.board_memory import BoardMemory
from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.organization_board_access import OrganizationBoardAccess
from app.models.organization_invite_board_access import OrganizationInviteBoardAccess
from app.models.organization_invites import OrganizationInvite
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
from app.models.task_dependencies import TaskDependency
from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task
from app.models.users import User
from app.schemas.common import OkResponse
from app.schemas.users import UserRead, UserUpdate
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
router = APIRouter(prefix="/users", tags=["users"])
AUTH_CONTEXT_DEP = Depends(get_auth_context)
SESSION_DEP = Depends(get_session)
async def _delete_organization_tree(
session: AsyncSession,
*,
organization_id: UUID,
) -> None:
"""Delete an organization and dependent rows without committing."""
board_ids = select(Board.id).where(col(Board.organization_id) == organization_id)
task_ids = select(Task.id).where(col(Task.board_id).in_(board_ids))
agent_ids = select(Agent.id).where(col(Agent.board_id).in_(board_ids))
member_ids = select(OrganizationMember.id).where(
col(OrganizationMember.organization_id) == organization_id,
)
invite_ids = select(OrganizationInvite.id).where(
col(OrganizationInvite.organization_id) == organization_id,
)
group_ids = select(BoardGroup.id).where(
col(BoardGroup.organization_id) == organization_id,
)
await crud.delete_where(
session,
ActivityEvent,
col(ActivityEvent.task_id).in_(task_ids),
commit=False,
)
await crud.delete_where(
session,
ActivityEvent,
col(ActivityEvent.agent_id).in_(agent_ids),
commit=False,
)
await crud.delete_where(
session,
TaskDependency,
col(TaskDependency.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
TaskFingerprint,
col(TaskFingerprint.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
Approval,
col(Approval.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
BoardMemory,
col(BoardMemory.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
BoardOnboardingSession,
col(BoardOnboardingSession.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
OrganizationBoardAccess,
col(OrganizationBoardAccess.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
OrganizationInviteBoardAccess,
col(OrganizationInviteBoardAccess.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
OrganizationBoardAccess,
col(OrganizationBoardAccess.organization_member_id).in_(member_ids),
commit=False,
)
await crud.delete_where(
session,
OrganizationInviteBoardAccess,
col(OrganizationInviteBoardAccess.organization_invite_id).in_(invite_ids),
commit=False,
)
await crud.delete_where(
session,
Task,
col(Task.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
Agent,
col(Agent.board_id).in_(board_ids),
commit=False,
)
await crud.delete_where(
session,
Board,
col(Board.organization_id) == organization_id,
commit=False,
)
await crud.delete_where(
session,
BoardGroupMemory,
col(BoardGroupMemory.board_group_id).in_(group_ids),
commit=False,
)
await crud.delete_where(
session,
BoardGroup,
col(BoardGroup.organization_id) == organization_id,
commit=False,
)
await crud.delete_where(
session,
Gateway,
col(Gateway.organization_id) == organization_id,
commit=False,
)
await crud.delete_where(
session,
OrganizationInvite,
col(OrganizationInvite.organization_id) == organization_id,
commit=False,
)
await crud.delete_where(
session,
OrganizationMember,
col(OrganizationMember.organization_id) == organization_id,
commit=False,
)
await crud.update_where(
session,
User,
col(User.active_organization_id) == organization_id,
active_organization_id=None,
commit=False,
)
await crud.delete_where(
session,
Organization,
col(Organization.id) == organization_id,
commit=False,
)
@router.get("/me", response_model=UserRead)
async def get_me(auth: AuthContext = AUTH_CONTEXT_DEP) -> UserRead:
"""Return the authenticated user's current profile payload."""
if auth.actor_type != "user" or auth.user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return UserRead.model_validate(auth.user)
@router.patch("/me", response_model=UserRead)
async def update_me(
payload: UserUpdate,
session: AsyncSession = SESSION_DEP,
auth: AuthContext = AUTH_CONTEXT_DEP,
) -> UserRead:
"""Apply partial profile updates for the authenticated user."""
if auth.actor_type != "user" or auth.user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
updates = payload.model_dump(exclude_unset=True)
user: User = auth.user
for key, value in updates.items():
setattr(user, key, value)
session.add(user)
await session.commit()
await session.refresh(user)
return UserRead.model_validate(user)
@router.delete("/me", response_model=OkResponse)
async def delete_me(
session: AsyncSession = SESSION_DEP,
auth: AuthContext = AUTH_CONTEXT_DEP,
) -> OkResponse:
"""Delete the authenticated account and any personal-only organizations."""
if auth.actor_type != "user" or auth.user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
user: User = auth.user
await delete_clerk_user(user.clerk_user_id)
memberships = await OrganizationMember.objects.filter_by(user_id=user.id).all(session)
await crud.update_where(
session,
OrganizationInvite,
col(OrganizationInvite.created_by_user_id) == user.id,
created_by_user_id=None,
commit=False,
)
await crud.update_where(
session,
OrganizationInvite,
col(OrganizationInvite.accepted_by_user_id) == user.id,
accepted_by_user_id=None,
commit=False,
)
await crud.update_where(
session,
Task,
col(Task.created_by_user_id) == user.id,
created_by_user_id=None,
commit=False,
)
for member in memberships:
org_members = await OrganizationMember.objects.filter_by(
organization_id=member.organization_id,
).all(session)
if len(org_members) <= 1:
await _delete_organization_tree(
session,
organization_id=member.organization_id,
)
continue
await crud.delete_where(
session,
OrganizationBoardAccess,
col(OrganizationBoardAccess.organization_member_id) == member.id,
commit=False,
)
await crud.delete_where(
session,
OrganizationMember,
col(OrganizationMember.id) == member.id,
commit=False,
)
await crud.delete_where(
session,
User,
col(User.id) == user.id,
commit=False,
)
await session.commit()
return OkResponse()