""" Client endpoints for managing users within their organization. """ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_owner, get_current_user from app.core.database import get_db from app.models.user import User from app.schemas.user import UserCreate, UserListResponse, UserResponse, UserUpdate from app.services import user_service router = APIRouter() class ChangePasswordRequest(BaseModel): """Request schema for changing user password.""" new_password: str @router.get("", response_model=UserListResponse) async def list_organization_users( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Max records to return"), role: str | None = Query(None, description="Filter by role"), status: str | None = Query(None, description="Filter by status"), ): """ List users in current user's organization. All authenticated users can view users in their organization. """ if not current_user.organization_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User is not assigned to any organization", ) users, total = await user_service.list_users( db, skip=skip, limit=limit, organization_id=current_user.organization_id, role=role, status=status, ) return UserListResponse( users=users, total=total, ) @router.get("/{user_id}", response_model=UserResponse) async def get_organization_user( user_id: int, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): """ Get user details from current organization. Users can view other users in their organization. """ user = await user_service.get_user(db, user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Check if user belongs to same organization if user.organization_id != current_user.organization_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot view users from other organizations", ) return user @router.post( "", response_model=UserResponse, status_code=status.HTTP_201_CREATED ) async def create_organization_user( data: UserCreate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_owner)], ): """ Create a new user in current organization (owner/admin only). Only owners and admins can create users. User will be created in the same organization as the current user. """ if not current_user.organization_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User is not assigned to any organization", ) # Override organization_id to current user's organization data.organization_id = current_user.organization_id # Check if email already exists existing_user = await user_service.get_user_by_email(db, data.email) if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered", ) user = await user_service.create_user(db, data) return user @router.patch("/{user_id}", response_model=UserResponse) async def update_organization_user( user_id: int, data: UserUpdate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_owner)], ): """ Update user in current organization (owner/admin only). Only owners and admins can update users. Cannot update users from other organizations. """ user = await user_service.get_user(db, user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Check if user belongs to same organization if user.organization_id != current_user.organization_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot update users from other organizations", ) # Cannot update yourself via this endpoint if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot update yourself, use profile endpoint", ) updated_user = await user_service.update_user(db, user_id, data) return updated_user @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_organization_user( user_id: int, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_owner)], ): """ Delete user from current organization (owner/admin only). Only owners and admins can delete users. Cannot delete users from other organizations. Cannot delete yourself. """ user = await user_service.get_user(db, user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Check if user belongs to same organization if user.organization_id != current_user.organization_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot delete users from other organizations", ) # Cannot delete yourself if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete yourself", ) await user_service.delete_user(db, user_id) @router.post("/{user_id}/change-password", response_model=UserResponse) async def change_organization_user_password( user_id: int, data: ChangePasswordRequest, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_owner)], ): """ Change password for user in current organization (owner/admin only). Only owners and admins can change passwords for other users. """ user = await user_service.get_user(db, user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # Check if user belongs to same organization if user.organization_id != current_user.organization_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot change password for users from other organizations", ) updated_user = await user_service.change_user_password( db, user_id, data.new_password ) return updated_user