from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import delete, select from sqlalchemy.orm import Session, selectinload from app.api import deps from app.core.security import get_password_hash from app.models import GlobalRole, User, UserOrganization from app.schemas.requests import UserCreateRequest, UserProfileUpdateRequest, UserUpdatePasswordRequest from app.schemas.responses import UserResponse router = APIRouter() @router.get("", response_model=list[UserResponse]) async def list_users( current_user: User = Depends(deps.get_current_user), session: Session = Depends(deps.get_session), ): """Returns users. super_admin sees everyone; anyone else sees only users who share at least one organization with them -- enough to find teammates when adding commission members or org roles, without leaking the full cross-organization user directory.""" if current_user.global_role == GlobalRole.SUPER_ADMIN: return session.execute( select(User).options(selectinload(User.organizations)) ).scalars().all() org_ids = [m.organization_id for m in current_user.organizations] if not org_ids: return [current_user] result = session.execute( select(User) .join(UserOrganization, UserOrganization.user_id == User.id) .where(UserOrganization.organization_id.in_(org_ids)) .options(selectinload(User.organizations)) .distinct() ) return result.scalars().all() @router.get("/me", response_model=UserResponse) async def read_current_user( current_user: User = Depends(deps.get_current_user), ): """Get current user""" return current_user @router.patch("/me", response_model=UserResponse) async def update_current_user_profile( payload: UserProfileUpdateRequest, current_user: User = Depends(deps.get_current_user), session: Session = Depends(deps.get_session), ): """Update the caller's own display name and/or contact phone number. This is the only place phone_number is ever written -- commission contact info (Commission.contacts) is derived from it, not stored separately.""" if payload.name is not None: current_user.name = payload.name if payload.phone_number is not None: current_user.phone_number = payload.phone_number session.add(current_user) session.commit() session.refresh(current_user) return current_user @router.delete("/me", status_code=204) async def delete_current_user( current_user: User = Depends(deps.get_current_user), session: Session = Depends(deps.get_session), ): """Delete current user""" session.execute(delete(User).where(User.id == current_user.id)) session.commit() @router.post("/update-password", response_model=UserResponse) async def reset_current_user_password( user_update_password: UserUpdatePasswordRequest, session: Session = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), ): """Update current user password""" current_user.hashed_password = get_password_hash(user_update_password.password) session.add(current_user) session.commit() return current_user @router.post("/register", response_model=UserResponse) async def register_new_user( new_user: UserCreateRequest, session: Session = Depends(deps.get_session), ): """Create new user. Does not attach the user to any organization -- a super_admin must add them via POST /organizations/{id}/members afterward.""" result = session.execute(select(User).where(User.email == new_user.email)) if result.scalars().first() is not None: raise HTTPException(status_code=400, detail="Cannot use this email address") user = User( email=new_user.email, name=new_user.name, hashed_password=get_password_hash(new_user.password), ) session.add(user) session.commit() session.refresh(user) return user