wiki/c-work/2-build/2026-04-msft-google-calendar-contacts.md
2026-04-29 00:35:17 +02:00

189 lines
9.5 KiB
Markdown

<!-- status: build -->
<!-- started: 2026-04-28 -->
<!-- component: gateway, frontend-nyla -->
# MSFT- und Google-Connector: CalendarAdapter + ContactsAdapter, plus Reconnect-Button
## Beschreibung und Kontext
Der Infomaniak-Connector liefert seit dem 2026-04-28 die drei Services
**kDrive**, **Calendar** und **Contacts**. Die analogen Services fuer Microsoft
und Google fehlten bislang in der UDB. Ziel dieses Builds: vier neue
ServiceAdapter (MSFT-Calendar, MSFT-Contacts, Google-Calendar,
Google-Contacts), so dass die UDB pro Connection einen einheitlichen
Service-Pool zeigt und der Agent fuer Calendar/Contacts gegen alle drei
Provider gleichermassen arbeiten kann.
Zusaetzliche Anforderung: ein **Reconnect-Button** im Frontend. Bestehende
MSFT-/Google-Connections wurden mit einem kleineren Scope-Set autorisiert.
Beim Hinzufuegen neuer Scopes (`Calendars.Read`, `Contacts.Read`,
`calendar.readonly`, `contacts.readonly`) liefern die Provider auf einer
Re-Authorisierung sonst still **dieselben** alten Tokens, weil sie
`include_granted_scopes` (Google) bzw. `prompt=select_account` (MSFT)
default-maessig nutzen -- die neuen Scopes wuerden nie das Tokenset
erreichen.
## Architektur
- `oauthProviderConfig.googleDataScopes` += `calendar.readonly`,
`contacts.readonly`
- `oauthProviderConfig.msftDataScopes` += `Calendars.Read`, `Contacts.Read`
- `connectorMsft.CalendarAdapter` (neu)
- Endpoints: `me/calendars`, `me/calendars/{id}/events`
- Pagination via `@odata.nextLink` (Helper `_stripGraphBase` aus dem Modul)
- Pfad: `""` -> Calendars; `"/{calendarId}"` -> Events
- Download: synthetisches RFC-5545 VCALENDAR/VEVENT (Graph hat keinen
`$value`-Endpoint fuer Events)
- `connectorMsft.ContactsAdapter` (neu)
- Endpoints: `me/contactFolders`, `me/contactFolders/{id}/contacts`,
plus virtueller Default-Folder `default` -> `me/contacts`
- Download: vCard 3.0 (N/FN/ORG/TITLE/EMAIL/TEL/ADR/NOTE)
- Helper: `_eventToIcs`, `_contactToVcard`, `_safeFileName`,
`_personLabel`, `_icsEscape`, `_icsDateTime` (alle modullokal)
- `connectorMsft.MsftConnector._SERVICE_MAP` += `"calendar"` + `"contact"`
- `connectorGoogle.CalendarAdapter` (neu)
- Endpoints: `users/me/calendarList`,
`calendars/{id}/events?singleEvents=true&orderBy=startTime`
- Download: `.ics` aus Event-Detail synthetisiert
- Search: `events?q=...` per Calendar-ID (Default `primary`)
- `connectorGoogle.ContactsAdapter` (neu)
- Endpoints: `contactGroups` + virtueller `all` -> `people/me/connections`
- Gruppen-Mitglieder via `contactGroups/{id}` -> `memberResourceNames` ->
`people:batchGet?resourceNames=...&personFields=...`
- Search: `people:searchContacts`
- Download: vCard 3.0 aus `personFields=names,emailAddresses,phoneNumbers,
organizations,addresses,biographies,memberships`
- `connectorGoogle.GoogleConnector._SERVICE_MAP` += `"calendar"` +
`"contact"`
- `routeFeatureWorkspace` und `routeFeatureGraphicalEditor`
Service-Label-/Icon-Maps += `kdrive`, `calendar`, `contact` (so dass die
UDB sinnvolle Bezeichnungen anzeigt -- vorher war Infomaniak-kDrive
ungelabelt durchgerutscht)
## Reconnect-Flow
- `routeDataConnections.connect_service` (`POST /api/connections/{id}/connect`)
akzeptiert optionalen Body `{"reauth": true}` und haengt `&reauth=1` an
die zurueckgegebene `auth_url`.
- `routeSecurityMsft.auth_connect` setzt bei `reauth=1`
`prompt=consent` (statt `select_account` / `login`). Das erzwingt den
Microsoft-Consent-Screen und liefert die neuen Scopes nach.
- `routeSecurityGoogle.auth_connect` droppt bei `reauth=1`
`include_granted_scopes=true` (default-`true` macht Google "vergesslich"
fuer neu hinzugekommene Scopes -- ohne diesen Drop blieben die neuen
Scopes still aussen vor).
- ClickUp ignoriert den Param (FastAPI laesst unbekannte Query-Args
durchrutschen) -- das ist OK, weil ClickUp keine Scope-Erweiterung hat.
- Frontend:
- `connectionApi.connectService(id, reauth?)` haengt den Body an.
- `useConnections.connectWithPopup(id, reauth?)` reicht durch (auch
der separate `useOAuthConnect`-Hook).
- `ConnectionsPage` hat ein neues `customAction` `reconnect` mit
`FaSyncAlt`-Icon, eigenem `reconnectingConnections`-Set und
`visible`-Filter `status === 'active' && authority in {msft, google,
clickup}`. Der Refresh-Button bleibt fuer Token-Refresh ohne
Re-Consent erhalten.
## Fokus und kritische Details
- **Pagination**: MSFT Calendar/Contacts paginieren bei `$top<=100`; wir
ziehen `@odata.nextLink` durch wie OutlookAdapter und stoppen bei
`effectiveLimit`.
- **iCal-Bauen**: Wir ueberlassen das nicht einer Library, weil RFC-5545
fuer einzelne VEVENT-Faelle trivial ist und der Overhead einer
Dependency (`icalendar`) nicht gerechtfertigt ist. Escape und
DTSTAMP/DTSTART/DTEND in UTC.
- **vCard-Bauen**: vCard 3.0 weil universell von Outlook/Google/macOS
Contacts akzeptiert. ADR-Felder: leere Felder werden als leere Slots
gerendert (nicht weggelassen), damit das Format gueltig bleibt.
- **Default-Contacts-Folder**: Outlook hat einen unsichtbaren
Default-Ordner -- den simulieren wir mit dem Pseudo-Folder `default`,
der intern auf `me/contacts` mappt. Ohne diesen Eintrag waere der
Default-Ordner aus der UDB nicht erreichbar.
- **People-API Group-Resolution**: `contactGroups/{id}` liefert keine
vollstaendigen Person-Records, nur `memberResourceNames`. Wir batchen
diese in 200er-Chunks via `people:batchGet`. Limit fuer Resource-Names
pro Batch ist 200 (Google API spec).
- **Reconnect ohne Token-Vernichtung**: Wir loeschen die alte Token nicht
-- der Auth-Callback ueberschreibt sie atomar nach erfolgreichem
Code-Exchange. Bei abgebrochenem Reconnect bleibt der bisherige Token
gueltig.
## Entscheidungen
- **Calendar/Contacts read-only:** Wir nehmen `Calendars.Read` /
`calendar.readonly` / `Contacts.Read` / `contacts.readonly` -- nicht
ReadWrite. Der UDB-Use-Case ist ausschliesslich Lesen + Suche +
Download. Schreibrechte werden bei einem konkreten Bedarf separat
diskutiert (mehr Scopes erhoehen Consent-Aufwand und Risk-Profile
unnoetig).
- **Reconnect statt eigenem Endpoint:** Wir spendieren keinen separaten
`/reconnect`-Endpoint, sondern einen Body-Param am bestehenden
`/connect`. Vorteil: ein einziger Authorisierungspfad, identische
Callback-Behandlung, kein doppeltes State-Management.
- **`include_granted_scopes` Drop bei Google:** Default `true` ist
freundlich fuer Token-Reuse, aber die offizielle Google-Doku warnt
ausdruecklich, dass es bei Scope-Erweiterung ueberraschend Tokens
ohne neue Scopes ausstellt. `false` bei Reconnect ist die robustere
Wahl.
- **`prompt=consent` bei MSFT:** Microsoft erkennt zusaetzliche Scopes
zwar normalerweise selbststaendig und zeigt einen Consent-Screen,
aber das ist nur best-effort. Mit `prompt=consent` ist es
garantiert.
- **vCard 3.0 statt 4.0:** 4.0 hat noch immer schlechte Outlook-
Kompatibilitaet beim Import; 3.0 ist der Common-Denominator.
- **`.ics` per Hand bauen statt `iCalendar`-Lib:** Eine externe
Dependency lohnt fuer einen einzigen VEVENT je Download nicht.
## Umsetzungs-Checkliste
- [x] `oauthProviderConfig.py`: `googleDataScopes` + `msftDataScopes`
ergaenzt
- [x] `connectorMsft.CalendarAdapter` (browse/download/search)
- [x] `connectorMsft.ContactsAdapter` (browse/download/search)
- [x] `connectorMsft._SERVICE_MAP`: `calendar`, `contact` registriert
- [x] `connectorMsft._eventToIcs`, `_contactToVcard`, Helpers
- [x] `connectorGoogle.CalendarAdapter` (browse/download/search)
- [x] `connectorGoogle.ContactsAdapter` (browse/download/search inkl.
Group-Resolution)
- [x] `connectorGoogle._SERVICE_MAP`: `calendar`, `contact` registriert
- [x] `connectorGoogle._googleEventToIcs`, `_googlePersonToVcard`,
Helpers
- [x] `routeFeatureWorkspace` Service-Label/Icon-Maps erweitert
- [x] `routeFeatureGraphicalEditor` Service-Label/Icon-Maps erweitert
- [x] `routeDataConnections.connect_service`: optionaler `reauth`-Body
- [x] `routeSecurityMsft.auth_connect`: `reauth` -> `prompt=consent`
- [x] `routeSecurityGoogle.auth_connect`: `reauth` -> drop
`include_granted_scopes`
- [x] Frontend `connectionApi.connectService(id, reauth?)`
- [x] Frontend `useConnections.connectWithPopup(id, reauth?)` (beide
Hooks)
- [x] Frontend `ConnectionsPage`: `Reconnect`-Button mit
`FaSyncAlt`-Icon und eigenem Loading-Set
- [ ] **Manuelle Verifikation:** existierende MSFT-Connection neu
durchklicken (Reconnect), Calendar+Contacts in der UDB
verifizieren; existierende Google-Connection genauso
- [ ] **Manuelle Verifikation:** Calendar-Download liefert eine
importierbare `.ics` (Outlook-Test); Contacts-Download liefert eine
importierbare `.vcf`
- [ ] Doc-Sync: `wiki/b-reference/auth-and-rbac.md`,
`wiki/b-reference/connectors.md` aktualisieren (neue Scopes,
Reconnect-Pfad), Topics-Liste pruefen
## Akzeptanzkriterien
1. Bestehende MSFT-/Google-Connection -> Reconnect-Button -> nach
Consent erscheinen `Calendar` und `Contacts` als zusaetzliche
Service-Knoten in der UDB.
2. Calendar-Browse listet die User-Calendars; Klick auf einen
Calendar listet die juengsten Events; Download eines Events
liefert eine `.ics` mit DTSTART/DTEND/SUMMARY/LOCATION.
3. Contacts-Browse listet ContactFolders (MSFT) bzw. ContactGroups
(Google); Klick listet die enthaltenen Contacts; Download liefert
eine `.vcf` mit FN, ORG, EMAIL, TEL.
4. Frischer Connect (neue MSFT-Connection) zieht die neuen Scopes ohne
Reconnect (weil sie im initialen `msftDataScopes` /
`googleDataScopes` enthalten sind).
5. ClickUp- und FTP-Connections sind unbeeintraechtigt; bei
Infomaniak ist der Reconnect-Button korrekt **nicht** sichtbar
(Authority-Whitelist).