"""API routes for chat room management FastAPI router with all room-related endpoints """ from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime from app.core.database import get_db from app.modules.auth import get_current_user from app.modules.chat_room import schemas from app.modules.chat_room.models import MemberRole, RoomStatus from app.modules.chat_room.services.room_service import room_service from app.modules.chat_room.services.membership_service import membership_service from app.modules.chat_room.dependencies import ( get_current_room, require_room_permission, validate_room_owner, require_admin, get_user_effective_role ) from app.modules.realtime.websocket_manager import manager as ws_manager router = APIRouter(prefix="/api/rooms", tags=["Chat Rooms"]) # Room CRUD Endpoints @router.post("", response_model=schemas.RoomResponse, status_code=status.HTTP_201_CREATED) async def create_room( room_data: schemas.CreateRoomRequest, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Create a new incident room""" user_email = current_user["username"] room = room_service.create_room(db, user_email, room_data) # Get user role for response role = membership_service.get_user_role_in_room(db, room.room_id, user_email) return schemas.RoomResponse( **room.__dict__, current_user_role=role, is_member=True # Creator is always a member ) @router.get("", response_model=schemas.RoomListResponse) async def list_rooms( status: Optional[RoomStatus] = None, incident_type: Optional[schemas.IncidentType] = None, severity: Optional[schemas.SeverityLevel] = None, search: Optional[str] = None, my_rooms: bool = Query(False, description="Filter to show only rooms where user is a member"), limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """List all rooms for authenticated user Returns all rooms by default. Use my_rooms=true to filter to only rooms where the current user is a member. Each room includes is_member and current_user_role fields. """ user_email = current_user["username"] is_admin = membership_service.is_system_admin(user_email) # Create filter params filters = schemas.RoomFilterParams( status=status, incident_type=incident_type, severity=severity, search=search, my_rooms=my_rooms, limit=limit, offset=offset ) rooms, total = room_service.list_user_rooms(db, user_email, filters, is_admin) # Add user role and membership status to each room room_responses = [] for room in rooms: role = membership_service.get_user_role_in_room(db, room.room_id, user_email) is_member = role is not None room_response = schemas.RoomResponse( **room.__dict__, current_user_role=role, is_member=is_member, is_admin_view=is_admin ) room_responses.append(room_response) return schemas.RoomListResponse( rooms=room_responses, total=total, limit=limit, offset=offset ) @router.get("/{room_id}", response_model=schemas.RoomResponse) async def get_room_details( room_id: str, room = Depends(get_current_room), role: Optional[MemberRole] = Depends(get_user_effective_role), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Get room details including members""" # Load members members = membership_service.get_room_members(db, room.room_id) member_responses = [schemas.MemberResponse.from_orm(m) for m in members] is_admin = membership_service.is_system_admin(current_user["username"]) is_member = role is not None return schemas.RoomResponse( **room.__dict__, members=member_responses, current_user_role=role, is_member=is_member, is_admin_view=is_admin ) @router.patch("/{room_id}", response_model=schemas.RoomResponse) async def update_room( room_id: str, updates: schemas.UpdateRoomRequest, _: None = Depends(require_room_permission("update_metadata")), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Update room metadata""" try: room = room_service.update_room(db, room_id, updates) if not room: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found" ) role = membership_service.get_user_role_in_room(db, room_id, current_user["username"]) return schemas.RoomResponse(**room.__dict__, current_user_role=role, is_member=True) except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) @router.delete("/{room_id}", response_model=schemas.SuccessResponse) async def delete_room( room_id: str, _: None = Depends(validate_room_owner), db: Session = Depends(get_db) ): """Soft delete (archive) a room""" success = room_service.delete_room(db, room_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found" ) return schemas.SuccessResponse(message="Room archived successfully") @router.delete("/{room_id}/permanent", response_model=schemas.SuccessResponse) async def permanent_delete_room( room_id: str, _: None = Depends(require_admin), db: Session = Depends(get_db) ): """Permanently delete a room and all associated data (admin only) This is an irreversible operation that deletes: - All room members - All messages and reactions - All uploaded files (including MinIO storage) - All generated reports (including MinIO storage) - The room itself Only system administrators can perform this operation. """ # Broadcast room_deleted event to all connected users BEFORE deleting await ws_manager.broadcast_to_room(room_id, { "type": "system", "event": "room_deleted", "room_id": room_id, "timestamp": datetime.utcnow().isoformat() }) success, error = room_service.permanent_delete_room(db, room_id) if not success: if error == "Room not found": raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found" ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete room: {error}" ) return schemas.SuccessResponse(message="Room permanently deleted") # Self-Join Endpoint @router.post("/{room_id}/join", response_model=schemas.MemberResponse) async def join_room( room_id: str, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Self-join a room as a viewer Any authenticated user can join any non-archived room. User will be added with VIEWER role. """ user_email = current_user["username"] member, error_code, existing = membership_service.self_join_room( db, room_id, user_email ) if error_code == "room_not_found": raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found" ) elif error_code == "room_archived": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot join archived room" ) elif error_code == "already_member": raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ "message": "Already a member of this room", "current_role": existing.role.value, "added_at": existing.added_at.isoformat() } ) return schemas.MemberResponse.from_orm(member) # Membership Endpoints @router.get("/{room_id}/members", response_model=List[schemas.MemberResponse]) async def list_room_members( room_id: str, _ = Depends(get_current_room), db: Session = Depends(get_db) ): """List all members of a room""" members = membership_service.get_room_members(db, room_id) return [schemas.MemberResponse.from_orm(m) for m in members] @router.post("/{room_id}/members", response_model=schemas.MemberResponse) async def add_member( room_id: str, request: schemas.AddMemberRequest, _: None = Depends(require_room_permission("manage_members")), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Add a member to the room""" member = membership_service.add_member( db, room_id, request.user_id, request.role, current_user["username"] ) if not member: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User is already a member" ) # Update room activity room_service.update_room_activity(db, room_id) return schemas.MemberResponse.from_orm(member) @router.patch("/{room_id}/members/{user_id}", response_model=schemas.MemberResponse) async def update_member_role( room_id: str, user_id: str, request: schemas.UpdateMemberRoleRequest, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Update a member's role Permission rules: - OWNER can change any role - EDITOR can upgrade VIEWER → EDITOR only - EDITOR cannot downgrade, remove, or set OWNER role """ changer_id = current_user["username"] # Get target member's current role current_role = membership_service.get_user_role_in_room(db, room_id, user_id) if not current_role: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Member not found" ) # Check permission allowed, error_msg = membership_service.can_change_member_role( db, room_id, changer_id, user_id, current_role, request.role ) if not allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=error_msg ) member = membership_service.update_member_role( db, room_id, user_id, request.role ) if not member: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Member not found" ) # Update room activity room_service.update_room_activity(db, room_id) return schemas.MemberResponse.from_orm(member) @router.delete("/{room_id}/members/{user_id}", response_model=schemas.SuccessResponse) async def remove_member( room_id: str, user_id: str, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Remove a member from the room Permission rules: - OWNER can remove any member - EDITOR cannot remove members (only owner can) - Users can remove themselves (leave room) unless they're the only owner """ remover_id = current_user["username"] # Check permission allowed, error_msg = membership_service.can_remove_member( db, room_id, remover_id, user_id ) if not allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=error_msg ) success = membership_service.remove_member(db, room_id, user_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Member not found" ) # Update room activity room_service.update_room_activity(db, room_id) return schemas.SuccessResponse(message="Member removed successfully") @router.post("/{room_id}/transfer-ownership", response_model=schemas.SuccessResponse) async def transfer_ownership( room_id: str, request: schemas.TransferOwnershipRequest, _: None = Depends(validate_room_owner), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Transfer room ownership to another member""" success = membership_service.transfer_ownership( db, room_id, current_user["username"], request.new_owner_id ) if not success: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="New owner must be an existing room member" ) return schemas.SuccessResponse(message="Ownership transferred successfully") # Permission Endpoints @router.get("/{room_id}/permissions", response_model=schemas.PermissionResponse) async def get_user_permissions( room_id: str, role: Optional[MemberRole] = Depends(get_user_effective_role), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """Get current user's permissions in the room""" user_email = current_user["username"] is_admin = membership_service.is_system_admin(user_email) if is_admin: # Admin has all permissions return schemas.PermissionResponse( role=role or MemberRole.OWNER, is_admin=True, can_read=True, can_write=True, can_manage_members=True, can_transfer_ownership=True, can_update_status=True, can_delete=True ) if not role: # Not a member return schemas.PermissionResponse( role=None, is_admin=False, can_read=False, can_write=False, can_manage_members=False, can_transfer_ownership=False, can_update_status=False, can_delete=False ) # Return permissions based on role permissions = { MemberRole.OWNER: schemas.PermissionResponse( role=role, is_admin=False, can_read=True, can_write=True, can_manage_members=True, can_transfer_ownership=True, can_update_status=True, can_delete=True ), MemberRole.EDITOR: schemas.PermissionResponse( role=role, is_admin=False, can_read=True, can_write=True, can_manage_members=False, can_transfer_ownership=False, can_update_status=False, can_delete=False ), MemberRole.VIEWER: schemas.PermissionResponse( role=role, is_admin=False, can_read=True, can_write=False, can_manage_members=False, can_transfer_ownership=False, can_update_status=False, can_delete=False ) } return permissions[role] # LOT Endpoints @router.post("/{room_id}/lots", response_model=List[str]) async def add_lot( room_id: str, request: schemas.AddLotRequest, _: None = Depends(require_room_permission("update_metadata")), db: Session = Depends(get_db) ): """Add a LOT batch number to the room""" from app.modules.chat_room.models import IncidentRoom room = db.query(IncidentRoom).filter(IncidentRoom.room_id == room_id).first() if not room: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found") # Get current lots or initialize empty list current_lots = room.lots or [] # Prevent duplicates if request.lot not in current_lots: current_lots.append(request.lot) room.lots = current_lots room.last_updated_at = datetime.utcnow() db.commit() db.refresh(room) return room.lots @router.delete("/{room_id}/lots/{lot}", response_model=List[str]) async def remove_lot( room_id: str, lot: str, _: None = Depends(require_room_permission("update_metadata")), db: Session = Depends(get_db) ): """Remove a LOT batch number from the room""" from app.modules.chat_room.models import IncidentRoom room = db.query(IncidentRoom).filter(IncidentRoom.room_id == room_id).first() if not room: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found") current_lots = room.lots or [] if lot in current_lots: current_lots.remove(lot) room.lots = current_lots room.last_updated_at = datetime.utcnow() db.commit() db.refresh(room) return room.lots