from typing import Any from urllib.parse import urlencode from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Response, status from .config import Settings from .credentials import get_request_settings from .graph_client import MicrosoftGraphClient from .schemas import ( CalendarEventRequest, CalendarEventUpdateRequest, DriveUploadRequest, PlannerTaskCreateRequest, PlannerTaskUpdateRequest, ScheduleRequest, SendMailRequest, ) from .services import CalendarService, DriveService, GroupsService, MailService, PlannerService, UsersService router = APIRouter(tags=["microsoft365"]) def get_graph_client(request_settings: Settings = Depends(get_request_settings)) -> MicrosoftGraphClient: return MicrosoftGraphClient(request_settings) def get_users_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> UsersService: return UsersService(graph) def get_mail_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> MailService: return MailService(graph) def get_calendar_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> CalendarService: return CalendarService(graph) def get_drive_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> DriveService: return DriveService(graph) def get_groups_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> GroupsService: return GroupsService(graph) def get_planner_service(graph: MicrosoftGraphClient = Depends(get_graph_client)) -> PlannerService: return PlannerService(graph) @router.get("/status") def microsoft365_status(request_settings: Settings = Depends(get_request_settings)) -> dict[str, Any]: return { "configured": request_settings.is_configured, "tenant_id": bool(request_settings.tenant_id), "client_id": bool(request_settings.client_id), "client_secret": bool(request_settings.client_secret), "graph_base_url": request_settings.graph_base_url, } @router.get("/admin-consent/url") def admin_consent_url( redirect_uri: str = Query(..., description="HTTPS callback URL registered on the Entra app registration."), state: str | None = Query(None, description="Opaque value returned to the callback, e.g. customer id."), request_settings: Settings = Depends(get_request_settings), ) -> dict[str, Any]: """Build the admin consent URL a client tenant administrator must open to approve the multitenant app.""" if not request_settings.tenant_id or not request_settings.client_id: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Tenant id and client id are required to build the admin consent URL.", ) query: dict[str, str] = { "client_id": request_settings.client_id, "scope": request_settings.graph_scope, "redirect_uri": redirect_uri, } if state: query["state"] = state return { "tenant_id": request_settings.tenant_id, "url": f"https://login.microsoftonline.com/{request_settings.tenant_id}/v2.0/adminconsent?{urlencode(query)}", } @router.get("/users") async def list_users( top: int = Query(25, ge=1, le=999), search: str | None = None, service: UsersService = Depends(get_users_service), ) -> Any: return await service.list_users(top=top, search=search) @router.get("/users/{user_id}") async def get_user(user_id: str, service: UsersService = Depends(get_users_service)) -> Any: return await service.get_user(user_id) @router.get("/users/{user_id}/mail/messages") async def list_messages( user_id: str, folder: str = "Inbox", top: int = Query(25, ge=1, le=100), service: MailService = Depends(get_mail_service), ) -> Any: return await service.list_messages(user_id=user_id, folder=folder, top=top) @router.post("/users/{user_id}/mail/send", status_code=status.HTTP_202_ACCEPTED) async def send_mail( user_id: str, request: SendMailRequest, response: Response, service: MailService = Depends(get_mail_service), ) -> None: await service.send_mail(user_id=user_id, request=request) response.status_code = status.HTTP_202_ACCEPTED @router.get("/users/{user_id}/calendar/events") async def list_events( user_id: str, top: int = Query(25, ge=1, le=100), service: CalendarService = Depends(get_calendar_service), ) -> Any: return await service.list_events(user_id=user_id, top=top) @router.post("/users/{user_id}/calendar/events", status_code=status.HTTP_201_CREATED) async def create_event( user_id: str, request: CalendarEventRequest, service: CalendarService = Depends(get_calendar_service), ) -> Any: return await service.create_event(user_id=user_id, request=request) @router.get("/users/{user_id}/calendar/view") async def list_calendar_view( user_id: str, start: str = Query(..., description="ISO 8601 with offset, e.g. 2026-09-08T08:00:00+02:00."), end: str = Query(..., description="ISO 8601 with offset, e.g. 2026-09-08T18:00:00+02:00."), top: int = Query(25, ge=1, le=100), time_zone: str | None = Query(None, description="Time zone for returned dates, e.g. Europe/Prague."), service: CalendarService = Depends(get_calendar_service), ) -> Any: return await service.list_calendar_view(user_id=user_id, start=start, end=end, top=top, time_zone=time_zone) @router.post("/users/{user_id}/calendar/schedule") async def get_schedule( user_id: str, request: ScheduleRequest, service: CalendarService = Depends(get_calendar_service), ) -> Any: return await service.get_schedule(user_id=user_id, request=request) @router.get("/users/{user_id}/calendar/events/{event_id}") async def get_event( user_id: str, event_id: str, service: CalendarService = Depends(get_calendar_service), ) -> Any: return await service.get_event(user_id=user_id, event_id=event_id) @router.patch("/users/{user_id}/calendar/events/{event_id}") async def update_event( user_id: str, event_id: str, request: CalendarEventUpdateRequest, service: CalendarService = Depends(get_calendar_service), ) -> Any: return await service.update_event(user_id=user_id, event_id=event_id, request=request) @router.delete("/users/{user_id}/calendar/events/{event_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_event( user_id: str, event_id: str, service: CalendarService = Depends(get_calendar_service), ) -> Response: await service.delete_event(user_id=user_id, event_id=event_id) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/users/{user_id}/drive/root/children") async def list_drive_root_children( user_id: str, top: int = Query(25, ge=1, le=200), service: DriveService = Depends(get_drive_service), ) -> Any: return await service.list_root_children(user_id=user_id, top=top) @router.put("/users/{user_id}/drive/root/{path:path}") async def upload_small_file( user_id: str, path: str, request: DriveUploadRequest = Body(...), service: DriveService = Depends(get_drive_service), ) -> Any: return await service.upload_small_file(user_id=user_id, path=path, request=request) @router.get("/groups") async def list_groups( top: int = Query(25, ge=1, le=999), service: GroupsService = Depends(get_groups_service), ) -> Any: return await service.list_groups(top=top) @router.get("/teams/{team_id}/channels") async def list_team_channels(team_id: str, service: GroupsService = Depends(get_groups_service)) -> Any: return await service.list_team_channels(team_id=team_id) @router.get("/groups/{group_id}/planner/plans") async def list_group_plans(group_id: str, service: PlannerService = Depends(get_planner_service)) -> Any: return await service.list_group_plans(group_id=group_id) @router.get("/planner/plans/{plan_id}/buckets") async def list_plan_buckets(plan_id: str, service: PlannerService = Depends(get_planner_service)) -> Any: return await service.list_plan_buckets(plan_id=plan_id) @router.get("/planner/plans/{plan_id}/tasks") async def list_plan_tasks(plan_id: str, service: PlannerService = Depends(get_planner_service)) -> Any: return await service.list_plan_tasks(plan_id=plan_id) @router.post("/planner/tasks", status_code=status.HTTP_201_CREATED) async def create_planner_task( request: PlannerTaskCreateRequest, service: PlannerService = Depends(get_planner_service), ) -> Any: return await service.create_task(request=request) @router.get("/planner/tasks/{task_id}") async def get_planner_task(task_id: str, service: PlannerService = Depends(get_planner_service)) -> Any: return await service.get_task(task_id=task_id) @router.patch("/planner/tasks/{task_id}") async def update_planner_task( task_id: str, request: PlannerTaskUpdateRequest, if_match: str | None = Header( None, alias="If-Match", description="Task ETag (@odata.etag). When omitted, the service loads the current ETag first.", ), service: PlannerService = Depends(get_planner_service), ) -> Any: return await service.update_task(task_id=task_id, request=request, if_match=if_match) @router.delete("/planner/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_planner_task( task_id: str, if_match: str | None = Header( None, alias="If-Match", description="Task ETag (@odata.etag). When omitted, the service loads the current ETag first.", ), service: PlannerService = Depends(get_planner_service), ) -> Response: await service.delete_task(task_id=task_id, if_match=if_match) return Response(status_code=status.HTTP_204_NO_CONTENT)