# Sprint 2 Plan A — Auth subsystem (A2/A3 + A7) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Remove the orphaned `/recovery` RecoveryCodesView page (dead code) and make the `/legal/offer` & `/legal/privacy` footer links resolve to real stub pages instead of a 404.

**Architecture:** Two independent, atomic changes. (1) Delete the `/recovery` route + `RecoveryCodesView.vue` + its story + spec — the real 2FA recovery-codes flow lives entirely in Settings → Безопасность (`TwoFactorCard.vue` setup wizard shows codes inline; `RecoveryCodesCard.vue` regenerates them), and nothing in the UI navigates to `/recovery`. (2) Add a single DRY `LegalDocView.vue` served by a `/legal/:doc(offer|privacy)` route, rendering an honest "document being finalized" stub (real legal text needs юр. редактура — реестр K3 / blocker Б-1). Upgrade the AuthLayout footer links from raw `<a href>` to `<RouterLink>`.

**Tech Stack:** Vue 3 `<script setup>`, Vuetify 3, vue-router 4 (`createWebHistory`, lazy imports), Vitest + `@vue/test-utils`, Laravel 13 (`routes/web.php` SPA `Route::view`).

---

## Context for the implementer

- **Working directory:** `c:\моя\проекты\портал crm\Документация`. The Laravel app is under `app/`. Frontend = `app/resources/js/`, frontend tests = `app/tests/Frontend/`.
- **Branch:** `main` (consistent with Sprint 1 — implementers commit atomically directly to `main`). Do **not** push (user pushes manually).
- **Commands** (run from `app/`):
  - Vitest: `npm run test:vue`
  - Type-check: `npm run type-check`
  - ESLint: `npm run lint:vue`
- **Baseline (fresh, 2026-05-15):** Pest `742/739/3sk/0`, Vitest `92 files / 774 passed / 3 skipped / 0 failed`, vue-tsc `0`, ESLint `0`.
- **Lefthook pre-commit** runs gitleaks / markdownlint / cspell / eslint-vue / pint / larastan on staged files — must stay green. Do **not** bypass hooks (`--no-verify` forbidden).
- **`app/dev-indices.json`** is a pre-existing **uncommitted** dev artifact (audit I2 — Sprint 6 cleanup). Do **NOT** stage or modify it. Stage only the exact files each task names.
- **Why `/recovery` is orphaned (verified):** grep across `app/resources/js` finds **no** `router.push('/recovery')` and **no** `<RouterLink to="/recovery">`. The only references are the route definition itself, the Histoire stub, the `.story.vue`, and the view file. `TwoFactorView.vue:126` links `/recovery-use` (a different page — consuming a code to log in), which is **kept**. The real codes-display is `TwoFactorCard.vue` setup wizard step `'codes'` and `RecoveryCodesCard.vue` regeneration — both already use the real API (`twoFactorConfirm` / `twoFactorRegenerateRecoveryCodes`).

---

## File Structure

| File | Action | Responsibility |
|---|---|---|
| `app/resources/js/views/auth/RecoveryCodesView.vue` | **Delete** | Orphaned mock recovery-codes page |
| `app/resources/js/views/auth/RecoveryCodesView.story.vue` | **Delete** | Histoire story for the orphan |
| `app/tests/Frontend/RecoveryCodesView.spec.ts` | **Delete** | Spec for the orphan (4 tests) |
| `app/resources/js/router/index.ts` | Modify | Remove `/recovery` route; add `/legal/:doc` route |
| `app/routes/web.php` | Modify | Remove `Route::view('/recovery')`; add 2 legal `Route::view` |
| `app/resources/js/histoire.setup.ts` | Modify | Remove `/recovery` Histoire stub route |
| `app/resources/js/views/legal/LegalDocView.vue` | **Create** | One DRY view for offer + privacy stub docs |
| `app/tests/Frontend/LegalDocView.spec.ts` | **Create** | Spec for the legal view (4 tests) |
| `app/resources/js/layouts/AuthLayout.vue` | Modify | Footer links `<a href>` → `<RouterLink>` |

---

## Task 1: Delete orphaned RecoveryCodesView (closes A2 + A3)

**Files:**

- Delete: `app/resources/js/views/auth/RecoveryCodesView.vue`
- Delete: `app/resources/js/views/auth/RecoveryCodesView.story.vue`
- Delete: `app/tests/Frontend/RecoveryCodesView.spec.ts`
- Modify: `app/resources/js/router/index.ts`
- Modify: `app/routes/web.php`
- Modify: `app/resources/js/histoire.setup.ts`

This task has no "failing test first" — it is a verified dead-code removal. Discipline = confirm-then-delete-then-verify-suite-green.

- [ ] **Step 1: Confirm orphan status**

Run (from repo root):

```bash
grep -rn "RecoveryCodesView\|'/recovery'\|\"/recovery\"\|to=\"/recovery\"\|push('/recovery')" app/resources/js app/routes app/tests
```

Expected: matches **only** in `router/index.ts` (the `/recovery` route), `web.php` (`Route::view('/recovery', ...)`), `histoire.setup.ts` (the stub), `RecoveryCodesView.vue` itself, and `RecoveryCodesView.story.vue`. There must be **no** `router.push('/recovery')` and **no** `<RouterLink to="/recovery">`. (`/recovery-use` matches are a different, kept page — ignore them.) If a real navigation to `/recovery` is found, **STOP** and report — the orphan assumption is wrong.

- [ ] **Step 2: Delete the three orphan files**

```bash
rm "app/resources/js/views/auth/RecoveryCodesView.vue"
rm "app/resources/js/views/auth/RecoveryCodesView.story.vue"
rm "app/tests/Frontend/RecoveryCodesView.spec.ts"
```

- [ ] **Step 3: Remove the `/recovery` route from `app/resources/js/router/index.ts`**

Delete this exact route object from the `routes` array (it sits between the `/forgot` route and the `/recovery-use` route):

```ts
    {
        path: '/recovery',
        name: 'recovery',
        component: () => import('../views/auth/RecoveryCodesView.vue'),
        meta: { layout: 'auth', title: 'Резервные коды', devIndex: 6, devLabel: 'Recovery codes' },
    },
```

Keep the `/recovery-use` route (`UseRecoveryCodeView.vue`) untouched. Leaving a gap in `devIndex` numbering (5 → 7) is acceptable — `devIndex` is a temporary feature (audit I1/I3, removed in a later sprint); do **not** renumber other routes.

- [ ] **Step 4: Remove the SPA route from `app/routes/web.php`**

Delete the line:

```php
Route::view('/recovery', 'welcome');
```

Keep `Route::view('/recovery-use', 'welcome');`.

- [ ] **Step 5: Remove the Histoire stub from `app/resources/js/histoire.setup.ts`**

Delete the line:

```ts
            { path: '/recovery', component: { template: '<div />' } },
```

Keep the `/recovery-use` stub line.

- [ ] **Step 6: Verify no dangling references**

```bash
grep -rn "RecoveryCodesView" app/resources/js app/tests
```

Expected: **no output** (zero matches).

- [ ] **Step 7: Run frontend verification**

```bash
cd app && npm run test:vue
```

Expected: `91 files / 770 passed / 3 skipped / 0 failed` (baseline `92/774` minus the deleted `RecoveryCodesView.spec.ts` = 1 file / 4 tests). **0 failures.**

```bash
cd app && npm run type-check
```

Expected: `0 errors`.

```bash
cd app && npm run lint:vue
```

Expected: `0 errors`.

- [ ] **Step 8: Commit**

```bash
git add "app/resources/js/views/auth/RecoveryCodesView.vue" \
        "app/resources/js/views/auth/RecoveryCodesView.story.vue" \
        "app/tests/Frontend/RecoveryCodesView.spec.ts" \
        app/resources/js/router/index.ts \
        app/routes/web.php \
        app/resources/js/histoire.setup.ts
git commit -m "refactor(auth): remove orphaned /recovery RecoveryCodesView page (closes A2, A3)

Audit A2/A3: RecoveryCodesView (route /recovery) had a TODO no-op
continue handler and 8 hardcoded mock codes. Recon found the page is
orphaned — nothing in the UI navigates to /recovery. The real 2FA
recovery-codes flow lives entirely in Settings -> Безопасность
(TwoFactorCard setup wizard + RecoveryCodesCard regeneration), both
already wired to the real API. Per user decision (2026-05-15) the
orphan is deleted rather than polished.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

(`git add` only the 6 named paths — `git rm`-tracked deletions are picked up by `git add` on the path. Do **not** stage `app/dev-indices.json`.)

---

## Task 2: Legal stub pages — `/legal/offer` + `/legal/privacy` (closes A7)

**Files:**

- Create: `app/resources/js/views/legal/LegalDocView.vue`
- Create: `app/tests/Frontend/LegalDocView.spec.ts`
- Modify: `app/resources/js/router/index.ts`
- Modify: `app/routes/web.php`
- Modify: `app/resources/js/layouts/AuthLayout.vue`

- [ ] **Step 1: Write the failing test**

Create `app/tests/Frontend/LegalDocView.spec.ts` with exactly:

```ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory, type Router } from 'vue-router';

import LegalDocView from '../../resources/js/views/legal/LegalDocView.vue';

const vuetify = createVuetify();

const buildRouter = (): Router =>
    createRouter({
        history: createMemoryHistory(),
        routes: [
            { path: '/legal/:doc(offer|privacy)', name: 'legal', component: LegalDocView },
            { path: '/login', name: 'login', component: { template: '<div>login</div>' } },
        ],
    });

const mountAt = async (path: string) => {
    const router = buildRouter();
    await router.push(path);
    await router.isReady();
    return mount(LegalDocView, { global: { plugins: [vuetify, router] } });
};

describe('LegalDocView.vue', () => {
    it('рендерит «Договор-оферта» на /legal/offer', async () => {
        const wrapper = await mountAt('/legal/offer');
        expect(wrapper.text()).toContain('Договор-оферта');
    });

    it('рендерит «Политика конфиденциальности» на /legal/privacy', async () => {
        const wrapper = await mountAt('/legal/privacy');
        expect(wrapper.text()).toContain('Политика конфиденциальности');
    });

    it('показывает честную заглушку «документ готовится», а не фейк-текст', async () => {
        const wrapper = await mountAt('/legal/offer');
        const notice = wrapper.find('[data-testid="legal-stub-notice"]');
        expect(notice.exists()).toBe(true);
        expect(notice.text()).toContain('готовится');
    });

    it('содержит ссылку возврата ко входу', async () => {
        const wrapper = await mountAt('/legal/privacy');
        const back = wrapper.find('a.legal-back');
        expect(back.exists()).toBe(true);
        expect(back.attributes('href')).toBe('/login');
    });
});
```

- [ ] **Step 2: Run the test to verify it fails**

```bash
cd app && npx vitest run tests/Frontend/LegalDocView.spec.ts
```

Expected: **FAIL** — `Failed to resolve import "../../resources/js/views/legal/LegalDocView.vue"` (the view does not exist yet).

- [ ] **Step 3: Create the view `app/resources/js/views/legal/LegalDocView.vue`**

```vue
<script setup lang="ts">
/**
 * Правовые документы — заглушки оферты и политики конфиденциальности.
 *
 * Audit A7: ссылки /legal/offer и /legal/privacy в подвале AuthLayout вели
 * на 404 (catch-all). Финальные тексты документов требуют юридической
 * редактуры (реестр K3 / блокер Б-1) — до этого страницы показывают честную
 * заглушку «документ готовится», а не фейк-текст (юридический риск).
 *
 * Один view на оба документа (DRY): контент выбирается по route.params.doc.
 * Маршрут /legal/:doc(offer|privacy) — иные значения отсекает regex-constraint,
 * уходя в catch-all 404.
 */
import { computed } from 'vue';
import { useRoute } from 'vue-router';

interface LegalDoc {
    title: string;
    intro: string;
}

const DOCS: Record<'offer' | 'privacy', LegalDoc> = {
    offer: {
        title: 'Договор-оферта',
        intro: 'Публичная оферта на оказание услуг сервиса «Лидерра» — условия использования платформы, права и обязанности сторон, порядок оплаты.',
    },
    privacy: {
        title: 'Политика конфиденциальности',
        intro: 'Порядок обработки и защиты персональных данных пользователей сервиса «Лидерра» в соответствии с Федеральным законом № 152-ФЗ «О персональных данных».',
    },
};

const route = useRoute();
const doc = computed<LegalDoc>(() => (String(route.params.doc) === 'privacy' ? DOCS.privacy : DOCS.offer));
</script>

<template>
    <v-card variant="flat" :max-width="480" width="100%" color="transparent" class="legal-card">
        <header class="legal-header">
            <h1 class="text-h5 mb-1">{{ doc.title }}</h1>
            <p class="text-body-2 text-medium-emphasis ma-0">{{ doc.intro }}</p>
        </header>

        <v-alert type="info" variant="tonal" density="compact" data-testid="legal-stub-notice">
            Финальная редакция документа готовится и будет опубликована до запуска сервиса.
        </v-alert>

        <RouterLink to="/login" class="text-body-2 text-primary legal-back"> ← Вернуться ко входу </RouterLink>
    </v-card>
</template>

<style scoped>
.legal-card {
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.legal-header h1 {
    font-variation-settings: 'opsz' 24;
    letter-spacing: -0.01em;
}
.legal-back {
    text-decoration: none;
}
.legal-back:hover {
    text-decoration: underline;
}
</style>
```

- [ ] **Step 4: Register the route in `app/resources/js/router/index.ts`**

Add this route object to the `routes` array **before** the catch-all `/:pathMatch(.*)*` route (placing it just after the `/recovery-use` route, with the other auth-layout routes, is fine):

```ts
    {
        path: '/legal/:doc(offer|privacy)',
        name: 'legal',
        component: () => import('../views/legal/LegalDocView.vue'),
        meta: { layout: 'auth', title: 'Правовые документы' },
    },
```

No `requiresAuth` / `guestOnly` — legal pages are public (a logged-in user may also read them). No `devIndex` — the page is not part of the temporary dev-index walkthrough.

- [ ] **Step 5: Run the test to verify it passes**

```bash
cd app && npx vitest run tests/Frontend/LegalDocView.spec.ts
```

Expected: **PASS** — 4/4 tests.

- [ ] **Step 6: Add SPA routes to `app/routes/web.php`**

Add these two lines next to the other auth `Route::view` entries (e.g. after `Route::view('/recovery-use', 'welcome');`):

```php
Route::view('/legal/offer', 'welcome');
Route::view('/legal/privacy', 'welcome');
```

This makes a hard browser load / refresh of `/legal/offer` serve the SPA shell (consistent with the project's explicit-route convention).

- [ ] **Step 7: Upgrade the footer links in `app/resources/js/layouts/AuthLayout.vue`**

Replace the raw anchors (currently at lines ~47-48):

```vue
                            <a href="/legal/offer">Оферта</a>
                            <a href="/legal/privacy">Политика</a>
```

with `RouterLink`s (SPA navigation, no full page reload):

```vue
                            <RouterLink to="/legal/offer">Оферта</RouterLink>
                            <RouterLink to="/legal/privacy">Политика</RouterLink>
```

Leave the `<span>v8 · Forest</span>` line and the `.bp-foot` wrapper unchanged. `RouterLink` renders a plain `<a>` element, so any existing `.bp-foot a { ... }` style rules continue to apply unchanged — no `<style>` edit needed.

- [ ] **Step 8: Run full frontend verification**

```bash
cd app && npm run test:vue
```

Expected: `92 files / 774 passed / 3 skipped / 0 failed` (Task 1 left it at `91/770`; this task adds `LegalDocView.spec.ts` = +1 file / +4 tests → `92/774`). **0 failures.**

```bash
cd app && npm run type-check
```

Expected: `0 errors`.

```bash
cd app && npm run lint:vue
```

Expected: `0 errors`.

- [ ] **Step 9: Commit**

```bash
git add app/resources/js/views/legal/LegalDocView.vue \
        app/tests/Frontend/LegalDocView.spec.ts \
        app/resources/js/router/index.ts \
        app/routes/web.php \
        app/resources/js/layouts/AuthLayout.vue
git commit -m "feat(auth): /legal/offer + /legal/privacy stub pages (closes A7)

Audit A7: the «Оферта» / «Политика» links in the AuthLayout footer were
raw <a href> pointing at unrouted paths -> 404 via the SPA catch-all.
Adds a single DRY LegalDocView served by /legal/:doc(offer|privacy),
rendering an honest «document being finalized» stub (real legal text
needs юр. редактура — реестр K3 / blocker Б-1). Footer links upgraded
to <RouterLink> for SPA navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

---

## Final Acceptance — Plan A

After both tasks:

- [ ] Pest `--parallel` unchanged from baseline: `742/739/3sk/0` (no backend files touched — run `composer test:parallel` once to confirm).
- [ ] Vitest: `92 files / 774 passed / 3 skipped / 0 failed` (net: −`RecoveryCodesView.spec.ts` +`LegalDocView.spec.ts` = 0 file delta, −4 +4 = 0 test delta vs baseline).
- [ ] vue-tsc: `0 errors`.
- [ ] ESLint: `0 errors`.
- [ ] `grep -rn "RecoveryCodesView" app/` → no matches.
- [ ] Lefthook pre-commit green on both commits.
- [ ] 2 atomic commits on `main`; not pushed.

## Spec coverage (audit `2026-05-15-portal-audit-design.md`)

| Audit ID | Spec line | This plan |
|---|---|---|
| A2 | RecoveryCodesView continue button → redirect | Task 1 — superseded: page deleted (orphan, per user decision) |
| A3 | RecoveryCodesView → real API | Task 1 — superseded: page deleted (real flow already in Settings → Безопасность) |
| A7 | `/legal/offer` + `/legal/privacy` routes + stub | Task 2 |
