Calendar a Planner endpointy, admin consent URL, C# ukazka (1.1.0)

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>
This commit is contained in:
JiriUhlir
2026-09-09 11:52:52 +02:00
co-authored by Claude Fable 5.1
parent 275bd6c467
commit fe3c3a736a
20 changed files with 877 additions and 9 deletions
+147 -3
View File
@@ -1,12 +1,21 @@
from typing import Any
from urllib.parse import urlencode
from fastapi import APIRouter, Body, Depends, Query, Response, status
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, DriveUploadRequest, SendMailRequest
from .services import CalendarService, DriveService, GroupsService, MailService, UsersService
from .schemas import (
CalendarEventRequest,
CalendarEventUpdateRequest,
DriveUploadRequest,
PlannerTaskCreateRequest,
PlannerTaskUpdateRequest,
ScheduleRequest,
SendMailRequest,
)
from .services import CalendarService, DriveService, GroupsService, MailService, PlannerService, UsersService
router = APIRouter(tags=["microsoft365"])
@@ -35,6 +44,10 @@ def get_groups_service(graph: MicrosoftGraphClient = Depends(get_graph_client))
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 {
@@ -46,6 +59,31 @@ def microsoft365_status(request_settings: Settings = Depends(get_request_setting
}
@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),
@@ -99,6 +137,56 @@ async def create_event(
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,
@@ -129,3 +217,59 @@ async def list_groups(
@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)