Files
portal/docs/superpowers/plans/2026-05-15-sprint2a-auth.md
T
Дмитрий 50dbee8a97 docs(plan): Sprint 2 Plan A — Auth (A2/A3 orphan delete + A7 legal pages)
Sprint 2 (P1 wave 1) split into 3 sub-plans per writing-plans
scope-check (Auth / Settings / Billing — independent subsystems).
Plan A covers the Auth subsystem:
- A2/A3: delete orphaned /recovery RecoveryCodesView (real flow lives
  in Settings -> Безопасность; user-approved deletion 2026-05-15).
- A7: /legal/offer + /legal/privacy stub pages via one DRY LegalDocView.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:21:30 +03:00

18 KiB
Raw Blame History

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

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

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

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:

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

Keep the /recovery-use stub line.

  • Step 6: Verify no dangling references
grep -rn "RecoveryCodesView" app/resources/js app/tests

Expected: no output (zero matches).

  • Step 7: Run frontend verification
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.

cd app && npm run type-check

Expected: 0 errors.

cd app && npm run lint:vue

Expected: 0 errors.

  • Step 8: Commit
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.)


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:

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
cd app && npx vitest run tests/Frontend/LegalDocView.spec.ts

Expected: FAILFailed 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
<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):

    {
        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
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');):

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

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

with RouterLinks (SPA navigation, no full page reload):

                            <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
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.

cd app && npm run type-check

Expected: 0 errors.

cd app && npm run lint:vue

Expected: 0 errors.

  • Step 9: Commit
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