feat(api+client): linked-identities panel on the account screen #216

Closed
opened 2026-06-21 16:38:30 +00:00 by james · 0 comments
Owner

Context

The Universal client's Account screen (apps/client/app/(app)/account.tsx) ports the Personal Access Tokens panel but omits the OAuth identity-link/unlink panel the previous Next.js PWA shipped. There is no /api/account/identities endpoint and no UI for users to see which providers are linked, link a new provider, or unlink one with the safety guard that the last sign-in method cannot be removed.

The PWA had this via apps/api/app/(app)/account/page.tsx + unlink-action.ts (since deleted by #185) calling LocalIdentitiesRepository and OauthIdentitiesRepository directly from a server component. The universal client must do it through an API endpoint.

Source

apps/client/app/(app)/account.tsx lines 31-34:

Provider linking and the OAuth "Connect another" flow live on the web PWA today; this slice ports the tokens panel and shows the signed-in identity. A follow-up will surface linked identities through a hook against /api/account/identities.

apps/api/app/(app)/account/page.tsx and unlink-action.ts (deleted in PR #209 / #185) are the historical reference for the unlink behaviour, including the "do not strand the user without a sign-in method" check.

Scope

  • Add GET /api/account/identities returning { local: { email } | null, oauth: [{ id, provider, email }] } scoped to the authenticated user.
  • Add DELETE /api/account/identities/oauth/:id enforcing the "last sign-in method" check from unlink-action.ts: refuse if removing the identity would leave the user with zero sign-in methods.
  • Surface "Connect another" entries that link out to /api/auth/oauth/start?provider=...&intent=link (the start route already supports the linking intent).
  • Add @carol/api-client hooks: useAccountIdentities, useUnlinkAccountIdentity.
  • Build the panel in apps/client/app/(app)/account.tsx next to the existing PATs panel: signed-in identity (already shown) → linked providers → connectable providers.
  • All strings via react-i18next; tokens via useTheme().

Acceptance criteria

  • GET /api/account/identities returns the per-user list; cross-user reads 404.
  • DELETE /api/account/identities/oauth/:id enforces the last-method check and returns 409 last_sign_in_method on refusal.
  • OpenAPI spec includes both routes; drift gate green.
  • Account screen on the Expo client shows linked + connectable providers; unlink confirms inline.
  • Tests cover happy paths, last-method refusal, and cross-user 404.

Composes with

  • ADR-0015 (OAuth account linking).
  • #176 (universal client epic).
## Context The Universal client's Account screen (`apps/client/app/(app)/account.tsx`) ports the Personal Access Tokens panel but omits the OAuth identity-link/unlink panel the previous Next.js PWA shipped. There is no `/api/account/identities` endpoint and no UI for users to see which providers are linked, link a new provider, or unlink one with the safety guard that the last sign-in method cannot be removed. The PWA had this via `apps/api/app/(app)/account/page.tsx` + `unlink-action.ts` (since deleted by #185) calling `LocalIdentitiesRepository` and `OauthIdentitiesRepository` directly from a server component. The universal client must do it through an API endpoint. ## Source `apps/client/app/(app)/account.tsx` lines 31-34: > Provider linking and the OAuth "Connect another" flow live on the web PWA today; this slice ports the tokens panel and shows the signed-in identity. A follow-up will surface linked identities through a hook against /api/account/identities. `apps/api/app/(app)/account/page.tsx` and `unlink-action.ts` (deleted in PR #209 / #185) are the historical reference for the unlink behaviour, including the "do not strand the user without a sign-in method" check. ## Scope - Add `GET /api/account/identities` returning `{ local: { email } | null, oauth: [{ id, provider, email }] }` scoped to the authenticated user. - Add `DELETE /api/account/identities/oauth/:id` enforcing the "last sign-in method" check from `unlink-action.ts`: refuse if removing the identity would leave the user with zero sign-in methods. - Surface "Connect another" entries that link out to `/api/auth/oauth/start?provider=...&intent=link` (the start route already supports the linking intent). - Add `@carol/api-client` hooks: `useAccountIdentities`, `useUnlinkAccountIdentity`. - Build the panel in `apps/client/app/(app)/account.tsx` next to the existing PATs panel: signed-in identity (already shown) → linked providers → connectable providers. - All strings via `react-i18next`; tokens via `useTheme()`. ## Acceptance criteria - [ ] `GET /api/account/identities` returns the per-user list; cross-user reads 404. - [ ] `DELETE /api/account/identities/oauth/:id` enforces the last-method check and returns `409 last_sign_in_method` on refusal. - [ ] OpenAPI spec includes both routes; drift gate green. - [ ] Account screen on the Expo client shows linked + connectable providers; unlink confirms inline. - [ ] Tests cover happy paths, last-method refusal, and cross-user 404. ## Composes with - ADR-0015 (OAuth account linking). - #176 (universal client epic).
james closed this issue 2026-06-23 13:26:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#216
No description provided.