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:
@@ -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