- Shipped
- September 5, 2026 at 6:31 AM UTC
- Author
- Kamo
- Commit
- cbc238a
The CalDAV/CardDAV tab's Connect via OAuth button raised an alert saying the flow was not implemented and to configure credentials through the API. It now runs a real connect, and the constraint that shaped it is that an OAuth redirect URI lives in a THIRD PARTY's console: Google and Entra each hold exactly one address per app registration, typed in by whoever registered it, so a second flow cannot have a second callback. Both flows therefore land on the address already registered and are told apart by a `flow` marker inside `state` -- OAuthFlowKind, absent meaning EMAIL so callbacks in flight keep completing where they always did. No console change, no new gateway route, no new session exemption for a public path. What the new flow shares with the mailbox flow it shares deliberately: - The app registration. GroupwareOAuthService asks PlatformOAuthClientResolver the same question the Provider Setup tab asks -- the organization's OWN Google or Entra app if it registered one there, else Kamo's platform app -- so credentials entered once serve both, and an org that has neither is told so rather than sent to a consent screen that rejects a blank client_id. - The redirect URI, read from **************** rather than restated. One copy, because a second would be a redirect_uri_mismatch waiting for somebody to move a callback, and it is already covered by OAuthRedirectUriTest against the shared OAuthCallbackPaths table. What it does not share is the grant. It asks for Calendar and Contacts rather than Gmail and the Admin SDK, and stores its tokens on the org's ContactIntegration rather than its email provider row, so connecting calendars cannot disturb a working mailbox. Read-write scopes, not .readonly: two-way sync is a switch the member flips after consenting, and scopes are fixed at consent. The connect proves itself before reporting success -- it reads the account that granted access and lists one calendar. A grant that cannot read calendars, or that came back with no refresh token, is stored and flagged with what to do about it; both are otherwise invisible until a sync run days later. Credentials are written in the shape the four existing refreshers already read by string key (accessToken / refreshToken / tokenExpiry as epoch millis / clientId / clientSecret / tenantId), so a connection stays alive with no further work, and GroupwareOAuthServiceTest pins those names: renaming one compiles, deploys, connects, and stops working an hour later. Alongside, three fixes in the same surface: - GET /settings/integrations/org returned the entity, which put the org's AES-encrypted credentials blob in a response body, a browser cache and every proxy log between here and the member, on every load of a screen that never reads the field. Both org endpoints now return an explicit view of the row, reporting only WHETHER a credential is on file. - applyConfig read `bidirectional` while every settings screen has always sent `bidirectionalSync`, so the two-way-sync switch never persisted. Accepts both. - clearOrgCredentials, so Disconnect forgets the grant without resetting the sync options the member chose -- which deleteOrgIntegration would. The calendars-and-contacts sync itself is unchanged and still unbuilt: the Google and Microsoft GroupwareProviders are stubs delegating to the local DB, ProviderRegistry binds KamoGroupwareProvider whatever the provider type, and DaemonService's sync jobs only bump a syncToken. This lands the credential those will need, and nothing yet reads it.