Files
microsoft-365-service/app/routes.py
T
JiriUhlirandClaude Fable 5.1 d80193da8b Hlavicky X-MS365-* jako obycejne hodnoty, GET /users/{user_id}/calendar
- credentials: sifrovani AES-GCM/HKDF odstraneno, tri hlavicky se berou
  tak, jak jsou; bez hlavicek se pouziji hodnoty z prostredi
- zrusena promenna MS365_CREDENTIAL_ENCODING_SECRET a zavislost cryptography
- novy endpoint GET /users/{user_id}/calendar (objekt kalendare schranky,
  id a name) pro read-only overeni pristupu pres e-mail schranky
- C# ukazka posila hodnoty primo, CredentialEncoder smazan
- README: sekce o hlavickach vcetne PowerShell ukazky, zaznam zmen
- .gitignore: bin/ a obj/, zaverzovane obj/ soubory ukazky odstraneny z gitu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 12:49:25 +02:00

282 lines
9.8 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")
async def get_calendar(user_id: str, service: CalendarService = Depends(get_calendar_service)) -> Any:
"""user_id is a Graph user id or the mailbox address, e.g. servis@firma.cz."""
return await service.get_calendar(user_id=user_id)
@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)