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>
18 KiB
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 underapp/. Frontend =app/resources/js/, frontend tests =app/tests/Frontend/. - Branch:
main(consistent with Sprint 1 — implementers commit atomically directly tomain). 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
- Vitest:
- Baseline (fresh, 2026-05-15): Pest
742/739/3sk/0, Vitest92 files / 774 passed / 3 skipped / 0 failed, vue-tsc0, ESLint0. - Lefthook pre-commit runs gitleaks / markdownlint / cspell / eslint-vue / pint / larastan on staged files — must stay green. Do not bypass hooks (
--no-verifyforbidden). app/dev-indices.jsonis 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
/recoveryis orphaned (verified): grep acrossapp/resources/jsfinds norouter.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:126links/recovery-use(a different page — consuming a code to log in), which is kept. The real codes-display isTwoFactorCard.vuesetup wizard step'codes'andRecoveryCodesCard.vueregeneration — 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
/recoveryroute fromapp/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.)
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:
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: 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
<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
--parallelunchanged from baseline:742/739/3sk/0(no backend files touched — runcomposer test:parallelonce 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 |