Podle zadani "MS365 prava konektoru": - Calendar: calendarView, getSchedule, get/patch/delete udalosti - Planner: plany skupiny, buckety, tasky, create/patch/delete s ETag - GET /admin-consent/url pro onboarding klientskeho tenantu - examples/csharp/ListUsersTop10: ukazka volani GET /users?top=10 vcetne sifrovani hlavicek X-MS365-* - README: multitenant napojeni, endpointy, opravneni, zaznam zmen Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
276 lines
9.5 KiB
Python
276 lines
9.5 KiB
Python
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)
|