feat(identity): account-linking UI + Telegram-as-identity surface (FH-4.7, FH-4.6, FH-4.8)
FH-4.7 - AccountIdentitiesComponent under features/website/account/identities/. Lists linked identities from GET /me/identities, offers attach buttons only for OAuth providers not already linked (reusing SocialLoginButtonComponent), detaches through unlink(). Refuses to detach the last remaining identity - it is the only way back in - with the control disabled and an explanatory title, matching the backend's last-identity 409. Loading / error / ready states; a load failure surfaces an error rather than rendering an empty account, and a slot carries the identity-conflict message from PHASE-8 §2.3. 6 unit tests. Not wired into a route: the storefront has no customer account area yet and no live OAuth application to authorize against (FH-0.1). This is the surface both depend on, buildable and tested now. FH-4.6 (client + contract) - the gateway now separates the two provider sets. SocialProvider (vk | yandex) is what has an OAuth authorize redirect; ExternalIdentityProvider (adds telegram | max) is what can be listed and unlinked. unlink() widened to the latter so Telegram detaches through the same path as VK, with no second code path. The dev local gateway seeds a Telegram identity so the linking screen is exercisable before any real provider exists. PHASE-8 §2.6 specifies the backend migration: a Telegram login writes an ExternalIdentity row under the same uniqueness and identity-conflict rule as VK, appears in /me/identities, is removable subject to the last-identity 409, and keeps customer (marketplace_session) and admin (bo_session) sessions as distinct cookies - closing the shared customer/admin Telegram session the audit flagged. The identity row and the messaging BotConversationBinding stay separate records. FH-4.8 - PHASE-8 §3 now states email/phone OTP's position explicitly: recovery when a linked messenger is unreachable and an addable second factor, never the primary login, and one more identity on the same customer rather than a parallel account. 262 tests pass. Build green, boundaries and cycles green, bundle scan clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -205,14 +205,16 @@ The **client half and the contract are done** (2026-08-21). What remains is back
|
||||
A second strategy object against the same surface — roughly a day once VK works.
|
||||
*Confirm exact parameter and scope names against live provider docs; both providers revised their flows recently.*
|
||||
|
||||
- [ ] **FH-4.6 — Migrate Telegram onto `ExternalIdentity`** · M · Lane B + C
|
||||
Telegram becomes one provider among several rather than the schema's only key. Ends the shared customer/admin Telegram session their audit flagged.
|
||||
- [~] **FH-4.6 — Migrate Telegram onto `ExternalIdentity`** · M · **client + contract done 2026-08-21; backend write path pending**
|
||||
Client: the gateway now separates the two provider sets — `SocialProvider` (`vk`/`yandex`, has an OAuth authorize) vs `ExternalIdentityProvider` (adds `telegram`/`max`, listable and unlinkable). `unlink()` takes the wider type, so the linking UI detaches Telegram through the same path as VK. The dev local gateway seeds a Telegram identity so the surface is exercisable now.
|
||||
Contract: PHASE-8 §2.6 — Telegram login writes an `ExternalIdentity` row under the same uniqueness/conflict rule as VK, appears in `/me/identities`, is removable subject to the last-identity `409`, and — the audit finding — keeps customer (`marketplace_session`) and admin (`bo_session`) sessions as separate cookies so a Telegram customer never satisfies an admin guard. Identity row vs messaging `BotConversationBinding` kept distinct.
|
||||
Backend still owns: the actual write-on-login and the session split enforcement. Telegram login itself lives in `@marketplaces/auth`.
|
||||
|
||||
- [ ] **FH-4.7 — Account linking UI** · M
|
||||
`/me/identities` — show linked providers, link, unlink, and surface the conflict-resolution path from FH-4.3.
|
||||
- [x] **FH-4.7 — Account linking UI** · M · **done 2026-08-21**
|
||||
`AccountIdentitiesComponent` (`src/app/features/website/account/identities/`): lists linked identities from `GET /me/identities`, offers attach buttons only for OAuth providers not yet linked (reusing `SocialLoginButtonComponent`), detaches through `unlink()`, disables the detach control on the last remaining identity with an explanatory title, and has a slot for the §2.3 conflict message. Loading / error / ready states, error surfaced rather than shown as an empty account. 6 unit tests. Not yet wired into a route — the storefront has no customer account area and no live OAuth app (FH-0.1) — but fully built and tested behind that.
|
||||
|
||||
- [ ] **FH-4.8 — Email/phone OTP repositioned as recovery** · S · Lane E
|
||||
Our approved [email/phone login spec](superpowers/specs/2026-08-15-email-phone-login-design.md) stays valid but drops below VK ID and becomes the fallback when a messenger channel is unavailable, per v3.1 §14.
|
||||
- [x] **FH-4.8 — Email/phone OTP repositioned as recovery** · S · **done 2026-08-21**
|
||||
PHASE-8 §3 now states it explicitly: OTP is a way back in when a linked messenger is unreachable and a second factor a customer may add, never the front-and-centre first login option, and one more `ExternalIdentity`/`ContactMethod` on the same customer rather than a parallel account. The [email/phone spec](superpowers/specs/2026-08-15-email-phone-login-design.md) stays valid; only its priority relative to VK ID moves.
|
||||
|
||||
---
|
||||
|
||||
@@ -268,10 +270,10 @@ The **client half and the contract are done** (2026-08-21). What remains is back
|
||||
| 1 — Live defects | 2 | — | 2 | FH-1.2 / FH-1.4 sit in files another session owns |
|
||||
| 2 — Contracts | 14 | — | 0 | 1 rejected (FH-2.12) |
|
||||
| 3 — Proof | 3 | 1 | 1 | see note below |
|
||||
| 4 — Identity | 2 | 3 | 3 | OAuth apps, which FH-0.1 gates |
|
||||
| 4 — Identity | 4 | 3 | 1 | OAuth apps, which FH-0.1 gates |
|
||||
| Ops | 1 | — | 2 | — |
|
||||
| Process | 5 | — | 1 | — |
|
||||
| **Total** | **27** | **4** | **11** | 1 rejected |
|
||||
| **Total** | **29** | **4** | **9** | 1 rejected |
|
||||
|
||||
**Landed 2026-08-21**
|
||||
|
||||
|
||||
@@ -131,10 +131,23 @@ profile GET https://login.yandex.ru/info?format=json
|
||||
|
||||
Yandex is a second strategy object against the same surface, not a second integration. Build it after VK works.
|
||||
|
||||
## 3. Sprint 8.3 — Email/phone OTP (after VK ID)
|
||||
### 2.6 Migrating Telegram onto `ExternalIdentity` (FH-4.6)
|
||||
|
||||
Added 2026-08-21. Telegram is the current app's only customer login and it does not go through §2's OAuth surface — it authenticates via a QR/bot flow owned by `@marketplaces/auth`. The migration makes it *one identity among several* without changing how it authenticates:
|
||||
|
||||
- On a successful Telegram login the backend writes (or updates) an `ExternalIdentity` with `provider: 'telegram'` and `providerUserId` = the Telegram user id, under the same `UNIQUE (provider, providerUserId)` and same identity-conflict rule as §2.3. Telegram stops being a special-cased column and becomes a row like any other provider.
|
||||
- Telegram appears in `GET /me/identities` and is removable through `POST /api/identity/v1/telegram/unlink`, subject to the **last-identity rule**: the backend refuses to unlink a customer's only remaining identity (`409`, a login they cannot get back). The frontend gateway's `unlink()` already accepts any `ExternalIdentityProvider`, not just the OAuth ones, so the linking UI (FH-4.7) drives this without a second code path.
|
||||
- **The customer/admin session split their audit flagged is closed here.** A Telegram customer session (`marketplace_session`) and an admin session (`bo_session`) are different cookies checked by different guards ([Track S §2.1](TRACK-S-SECURITY-RBAC-CONTRACT.md)); the same Telegram account authenticating as a customer must never satisfy an admin guard, and vice versa. Migrating Telegram to `ExternalIdentity` on the customer side does not grant it any admin scope — admin membership is a separate axis.
|
||||
- Telegram's messaging binding (§4 `BotConversationBinding`, delivery updates) is a *contact channel*, distinct from the *identity* row added here. One provider can be both; they are separate records so unlinking the identity does not silently kill an order's delivery conversation, and vice versa.
|
||||
|
||||
Client state today: `SocialProvider` (`vk` | `yandex`) is the set with an OAuth authorize redirect; `ExternalIdentityProvider` (`vk_id` | `yandex_id` | `telegram` | `max`) is the set that can be listed and unlinked. `unlink()` takes the wider type; `getAuthorizeUrl()` takes the narrower one. See `src/app/core/identity/services/social-identity-gateway.interface.ts`.
|
||||
|
||||
## 3. Sprint 8.3 — Email/phone OTP (after VK ID), positioned as recovery (FH-4.8)
|
||||
|
||||
Implements the already-approved [email/phone login spec](../superpowers/specs/2026-08-15-email-phone-login-design.md). Per v3.1 §14, position this as **recovery/fallback** when a messenger channel is unavailable — not the primary login path. No new contract beyond that spec; this section exists only to fix its place in the build order relative to VK ID.
|
||||
|
||||
Concretely, "recovery/fallback" means: email/phone OTP is offered as a way back in when a customer's linked messenger identity is unreachable, and as a second factor a customer can add — never as the front-and-centre first option on the login surface, which is VK ID (and Telegram, where it is already the norm). It is one more `ExternalIdentity`/`ContactMethod` on the same customer, not a parallel account.
|
||||
|
||||
## 4. Sprint 8.4 — MAX + Telegram bot channels
|
||||
|
||||
```ts
|
||||
|
||||
Reference in New Issue
Block a user