From 6d28f415114c3e6a3a54921ba099bb4ea494f1ec Mon Sep 17 00:00:00 2001 From: Aleksandr Fenin Date: Thu, 23 Oct 2025 17:23:46 +0300 Subject: [PATCH 1/6] feature/auth-improvements feat: implement comprehensive authentication system with modular architecture --- .cursor/commands/commit.md | 46 ++ .cursor/commands/component.md | 62 ++ .cursor/commands/execute-plan-auto.md | 58 ++ .cursor/commands/execute-plan.md | 67 ++ .cursor/commands/feature.md | 82 +++ .cursor/commands/fix-linter.md | 21 + .cursor/commands/fsd.md | 112 +++ .cursor/commands/plan.md | 49 ++ .cursor/commands/review.md | 29 + .cursor/commands/route.md | 66 ++ .cursor/commands/service.md | 62 ++ .cursor/commands/taiga-ui.md | 110 +++ .cursor/commands/test.md | 36 + CLAUDE.md | 31 + .../006-project-improvements-analysis.md | 118 ---- docs/plans/007-auth-service-improvements.md | 258 ++----- docs/plans/011-auth-ux-improvements.md | 31 + ...3-backend-calorie-calculation-migration.md | 645 ++++++++++++++++++ docs/plans/014-offline-mode-implementation.md | 251 +++++++ .../015-toast-messages-api-error-handling.md | 579 ++++++++++++++++ .../calorie-service-frontend-integration.md | 60 -- src/app/app.component.html | 35 +- src/app/app.component.scss | 70 ++ src/app/app.component.spec.ts | 15 +- src/app/app.component.ts | 42 +- src/app/app.config.ts | 6 +- src/app/interceptors/auth.interceptor.spec.ts | 5 + src/app/interceptors/auth.interceptor.ts | 63 +- .../interceptors/credentials.interceptor.ts | 3 +- .../ui/user-menu/user-menu.component.html | 32 +- src/features/auth/guards/auth.guard.spec.ts | 3 +- src/features/auth/index.ts | 4 + .../auth/models/auth-provider.types.ts | 17 + .../auth/services/auth.service.spec.ts | 125 +++- src/features/auth/services/auth.service.ts | 88 ++- .../email-password-provider.service.spec.ts | 123 ++++ .../email-password-provider.service.ts | 28 + src/shared/index.ts | 1 + src/shared/lib/taiga-ui.ts | 12 + .../services/user/user-store.service.ts | 8 +- 40 files changed, 2998 insertions(+), 455 deletions(-) create mode 100644 .cursor/commands/commit.md create mode 100644 .cursor/commands/component.md create mode 100644 .cursor/commands/execute-plan-auto.md create mode 100644 .cursor/commands/execute-plan.md create mode 100644 .cursor/commands/feature.md create mode 100644 .cursor/commands/fix-linter.md create mode 100644 .cursor/commands/fsd.md create mode 100644 .cursor/commands/plan.md create mode 100644 .cursor/commands/review.md create mode 100644 .cursor/commands/route.md create mode 100644 .cursor/commands/service.md create mode 100644 .cursor/commands/taiga-ui.md create mode 100644 .cursor/commands/test.md delete mode 100644 docs/plans/006-project-improvements-analysis.md create mode 100644 docs/plans/013-backend-calorie-calculation-migration.md create mode 100644 docs/plans/014-offline-mode-implementation.md create mode 100644 docs/plans/015-toast-messages-api-error-handling.md delete mode 100644 docs/plans/calorie-service-frontend-integration.md create mode 100644 src/features/auth/models/auth-provider.types.ts create mode 100644 src/features/auth/services/email-password-provider.service.spec.ts create mode 100644 src/features/auth/services/email-password-provider.service.ts create mode 100644 src/shared/lib/taiga-ui.ts diff --git a/.cursor/commands/commit.md b/.cursor/commands/commit.md new file mode 100644 index 0000000..eb7d11f --- /dev/null +++ b/.cursor/commands/commit.md @@ -0,0 +1,46 @@ +Создай commit message для текущих изменений: + +**Формат:** + +``` +[branch-name] type: short description +``` + +**Требования:** + +- В начале всегда указывать название текущей ветки +- Использовать conventional commits (feat, fix, refactor, docs, test, chore) +- Краткое описание на английском (до 50 символов) +- Понятное описание WHY, а не только WHAT +- **ВАЖНО:** Коммит должен содержать ТОЛЬКО одну строку с кратким описанием +- **НЕ ДОБАВЛЯТЬ** детальное описание изменений, списки файлов или bullet points +- Commit message должен быть максимально лаконичным + +**Шаги:** + +1. **ОБЯЗАТЕЛЬНО:** Запусти команду "Review" для проверки качества кода +2. **ОБЯЗАТЕЛЬНО:** Изучи изменения в текущей ветки и обнови документацию: + - Обнови CLAUDE.md (правила разработки, архитектура) + - Обнови структуру проекта в CLAUDE.md (если изменилась структура файлов/папок) +3. Посмотри git status и git diff +4. Определи текущую ветку +5. Если находимся в ветке master - создай новую ветку с названием согласно разрабатываемой фиче +6. Сформируй КОРОТКИЙ commit message (одна строка) +7. Предложи команду для коммита + +**Пример правильного коммита:** + +``` +feature/user-auth feat: add JWT authentication +``` + +**Пример НЕПРАВИЛЬНОГО коммита (слишком детальный):** + +``` +feature/user-auth feat: add JWT authentication + +- Add JWT token generation +- Create auth middleware +- Update user service +- Add refresh token logic +``` diff --git a/.cursor/commands/component.md b/.cursor/commands/component.md new file mode 100644 index 0000000..70b5fb4 --- /dev/null +++ b/.cursor/commands/component.md @@ -0,0 +1,62 @@ +Создай Angular компонент согласно FSD архитектуре: + +**Требования:** + +- Используй standalone компоненты без NgModules +- Используй signals для state management +- Используй inject() вместо constructor injection +- Используй OnPush change detection strategy +- Следуй FSD архитектуре (app → pages → widgets → features → entities → shared) +- Используй Taiga UI компоненты для UI +- Используй BEM методологию для CSS классов +- Все функции с явными типами возвращаемых значений +- БЕЗ КОММЕНТАРИЕВ в коде +- Все строки на английском языке + +**Структура компонента:** + +```typescript +@Component({ + selector: 'app-component-name', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, TuiButton, TuiInputModule], + templateUrl: './component-name.component.html', + styleUrls: ['./component-name.component.scss'] +}) +export class ComponentNameComponent { + private readonly service = inject(SomeService); + + readonly inputData = input.required(); + readonly optionalInput = input(false); + readonly dataChanged = output(); + + readonly computedValue = computed(() => + this.inputData().toUpperCase() + ); +} +``` + +**Шаги:** + +1. Определи в каком слое FSD должен быть компонент +2. Создай структуру файлов: + - `component-name.component.ts` + - `component-name.component.html` + - `component-name.component.scss` + - `component-name.component.spec.ts` + - `index.ts` (barrel export) +3. Реализуй компонент с современным Angular синтаксисом +4. Добавь типизацию и signals +5. Создай тесты с `configureZonelessTestingModule()` +6. Обнови barrel exports + +**Примеры компонентов:** + +- Страница: `src/pages/dashboard/ui/dashboard.component.ts` +- Виджет: `src/widgets/calorie-widget/calorie-widget.component.ts` +- Фича: `src/features/calorie-calculation/ui/basic-data-form/basic-data-form.component.ts` +- Сущность: `src/entities/calorie/ui/calorie-display.component.ts` +- Общий: `src/shared/ui/back-layout/back-layout.component.ts` + +Создай компонент и покажи результат. diff --git a/.cursor/commands/execute-plan-auto.md b/.cursor/commands/execute-plan-auto.md new file mode 100644 index 0000000..280f9fe --- /dev/null +++ b/.cursor/commands/execute-plan-auto.md @@ -0,0 +1,58 @@ +Выполни план разработки маленькой фичи автоматически: + +**Требования:** + +- Прочитай план из папки `plans/` +- Следуй правилам проекта из CLAUDE.md +- Выполняй все шаги автоматически без остановок +- Применяй все правила проекта: + - Все функции с явными типами возвращаемых значений + - Современный Angular синтаксис (standalone компоненты, signals, inject()) + - БЕЗ КОММЕНТАРИЕВ в коде (код должен быть самодокументируемым) + - Naming без слов-паразитов (data, info) - используй конкретные, описательные названия + - **КРИТИЧЕСКИ ВАЖНО: Принцип YAGNI** - создавай ТОЛЬКО те функции, компоненты, методы и утилиты, которые реально используются в текущей задаче + - **ВСЕГДА используй алиасы (@/) для импортов** вместо относительных путей + - Имена переменных, функций, типов — только английский + - Тексты в пользовательском интерфейсе (UI) — только русский + - Используй standalone компоненты без NgModules + - Используй signals для state management + - Используй inject() вместо constructor injection + - Используй OnPush change detection strategy + - Следуй FSD архитектуре (Feature-Sliced Design) + +**Шаги:** + +1. Найди и прочитай указанный план в папке `plans/` +2. Изучи правила проекта из CLAUDE.md и проверь соответствие плана этим правилам. Если информация в плане противоречит правилам, согласуй со мной изменения в плане +3. Покажи список всех шагов из плана +4. **АВТОМАТИЧЕСКИ** выполни ВСЕ шаги подряд: + - Реализуй каждый шаг полностью + - Покажи что было сделано после каждого шага + - НЕ останавливайся для подтверждения + - Переходи к следующему шагу сразу +5. После завершения всех шагов: + - Запусти линтер и TypeScript проверку + - Покажи итоговый статус выполнения плана +6. перед финальной проверкой: + - Убедись, что все пункты плана выполнены + - Удали файл плана из папки `plans/` + +**Формат отчёта после каждого шага:** + +``` +✅ Шаг [N]: [название шага] + +Выполнено: +- [действие 1] +- [действие 2] + +Созданные/изменённые файлы: +- [файл 1] +- [файл 2] +``` + +**Важно:** + +- Выполняй ВСЕ шаги подряд без остановок +- Показывай прогресс после каждого шага +- Если возникнут ошибки - остановись и покажи проблему diff --git a/.cursor/commands/execute-plan.md b/.cursor/commands/execute-plan.md new file mode 100644 index 0000000..76a04a3 --- /dev/null +++ b/.cursor/commands/execute-plan.md @@ -0,0 +1,67 @@ +Выполни план разработки фичи пошагово: + +**Требования:** + +- Прочитай план из папки `plans/` +- Следуй правилам проекта из CLAUDE.md +- Выполняй шаги последовательно +- **ВАЖНО: После каждого шага с изменениями в коде ОБЯЗАТЕЛЬНО спрашивай подтверждение перед переходом к следующему** +- Применяй все правила проекта: + - Все функции с явными типами возвращаемых значений + - Современный Angular синтаксис (standalone компоненты, signals, inject()) + - БЕЗ КОММЕНТАРИЕВ в коде (код должен быть самодокументируемым) + - Naming без слов-паразитов (data, info) - используй конкретные, описательные названия + - **КРИТИЧЕСКИ ВАЖНО: Принцип YAGNI** - создавай ТОЛЬКО те функции, компоненты, методы и утилиты, которые реально используются в текущей задаче + - **ВСЕГДА используй алиасы (@/) для импортов** вместо относительных путей + - Имена переменных, функций, типов — только английский + - Тексты в пользовательском интерфейсе (UI) — только русский + - Используй standalone компоненты без NgModules + - Используй signals для state management + - Используй inject() вместо constructor injection + - Используй OnPush change detection strategy + - Следуй FSD архитектуре (Feature-Sliced Design) + +**Шаги:** + +1. Найди и прочитай указанный план в папке `plans/` +2. Изучи правила проекта из CLAUDE.md и проверь соответствие плана этим правилам. Если информация в плане противоречит правилам, согласуй со мной изменения в плане +3. Покажи список всех шагов из плана +4. Начни выполнение первого шага: + - Реализуй шаг полностью + - Покажи что было сделано + - **Если были изменения в коде, спроси подтверждение: "Шаг [N] завершён. Перейти к шагу [N+1]?"** + - Дождись ответа пользователя (только при наличии изменений в коде) +5. Переходи к следующему шагу ТОЛЬКО после подтверждения (при наличии изменений в коде) +6. Повторяй пункт 4-5 для каждого шага +7. После завершения всех шагов: + - Запусти линтер и TypeScript проверку + - Покажи итоговый статус выполнения плана +8. **АВТОМАТИЧЕСКИ** (без подтверждения пользователя) выполни финальную проверку: + - Убедись, что все пункты плана выполнены + - Удали файл плана из папки `plans/` + +**Формат отчёта после каждого шага:** + +``` +✅ Шаг [N]: [название шага] + +Выполнено: +- [действие 1] +- [действие 2] + +Созданные/изменённые файлы: +- [файл 1] +- [файл 2] + +Готов перейти к следующему шагу? +``` + +**Важно:** + +- НЕ выполняй несколько шагов подряд без подтверждения (кроме финальной проверки) +- **Подтверждение требуется ТОЛЬКО при наличии изменений в коде** +- Давай пользователю возможность проверить результат каждого шага с изменениями +- Если пользователь попросит изменения - внеси их перед переходом к следующему шагу +- **ИСКЛЮЧЕНИЯ:** + - Шаги без изменений в коде выполняются без подтверждения + - После успешной проверки линтера и TypeScript автоматически выполни финальную проверку и удали файл плана diff --git a/.cursor/commands/feature.md b/.cursor/commands/feature.md new file mode 100644 index 0000000..5d643ac --- /dev/null +++ b/.cursor/commands/feature.md @@ -0,0 +1,82 @@ +Реализуй фичу автоматически: + +**Требования:** + +- Применяй ВСЕ правила проекта из CLAUDE.md +- Выполняй фичу полностью автоматически без остановок +- Создавай только то, что реально используется (принцип YAGNI) +- Применяй все правила кодирования: + - Все функции с явными типами возвращаемых значений + - Современный Angular синтаксис (standalone компоненты, signals, inject()) + - БЕЗ КОММЕНТАРИЕВ в коде (код должен быть самодокументируемым) + - Naming без слов-паразитов (data, info) - используй конкретные, описательные названия + - **КРИТИЧЕСКИ ВАЖНО: Принцип YAGNI** - создавай ТОЛЬКО те функции, компоненты, методы и утилиты, которые реально используются в текущей задаче + - **ВСЕГДА используй алиасы (@/) для импортов** вместо относительных путей + - Имена переменных, функций, типов — только английский + - Тексты в пользовательском интерфейсе (UI) — только русский + - Используй standalone компоненты без NgModules + - Используй signals для state management + - Используй inject() вместо constructor injection + - Используй OnPush change detection strategy + - Следуй FSD архитектуре (Feature-Sliced Design) + +**Шаги выполнения:** + +1. **Анализ задачи** - определи что именно нужно реализовать +2. **Планирование** - определи какие файлы нужно создать/изменить +3. **Реализация** - создай/измени все необходимые файлы: + - Компоненты с правильной типизацией + - Сервисы с типизированными методами + - Типы и интерфейсы + - Интеграция с существующими store + - Обновление barrel exports +4. **Проверка** - запусти линтер и TypeScript проверку +5. **Финальная проверка** - убедись что все работает + +**Правила реализации:** + +- Используй только существующие UI компоненты из `@/shared/ui` +- Интегрируйся с существующими сервисами через `@/shared/services` +- Используй `HttpClient` из Angular для API запросов +- Создавай типы рядом с использующим их кодом +- Всегда добавляй barrel exports в `index.ts` +- Используй современные Angular паттерны (standalone компоненты, signals, inject()) +- Следуй FSD архитектуре (app → pages → widgets → features → entities → shared) +- Используй Taiga UI компоненты для UI +- Используй BEM методологию для CSS классов + +**Формат отчёта:** + +``` +🚀 Фича: [название] + +✅ Анализ: [что делаем] +✅ Планирование: [какие файлы создаём/изменяем] +✅ Реализация: [что создано/изменено] +✅ Проверка: [результаты линтера и TypeScript] +✅ Готово: [фича полностью реализована] + +Созданные/изменённые файлы: +- [файл 1] - [описание изменений] +- [файл 2] - [описание изменений] +``` + +**Важно:** + +- Выполняй ВСЕ шаги подряд без остановок +- НЕ создавай лишний код "на будущее" +- Используй только то, что реально нужно для текущей задачи +- Всегда проверяй что код компилируется без ошибок +- Применяй все правила из CLAUDE.md + +**Примеры фич:** + +- Добавить новое поле в форму с Taiga UI +- Создать простой компонент отображения данных +- Добавить новую кнопку с функциональностью +- Создать утилиту для форматирования +- Добавить валидацию в существующую форму +- Создать новый виджет для dashboard +- Добавить новую страницу с роутингом +- Создать новый feature с бизнес-логикой +- Добавить новую entity с моделью данных diff --git a/.cursor/commands/fix-linter.md b/.cursor/commands/fix-linter.md new file mode 100644 index 0000000..35d2cde --- /dev/null +++ b/.cursor/commands/fix-linter.md @@ -0,0 +1,21 @@ +Исправь все ошибки линтера в проекте: + +**Шаги:** + +1. Запусти линтер и получи список ошибок +2. Запусти TypeScript проверку (tsc --noEmit) +3. Проанализируй каждую ошибку +4. Исправь ошибки согласно правилам проекта: + - Все функции с явными типами возвращаемых значений + - Современный Angular синтаксис (standalone компоненты, signals, inject()) + - Без комментариев + - Naming без слов-паразитов + - Все строки на английском + - Используй OnPush change detection strategy + - Следуй FSD архитектуре + - Используй Taiga UI компоненты + +5. Убедись что исправления не нарушают функциональность +6. Запусти линтер и TypeScript проверку повторно для проверки + +Исправь все ошибки и покажи результат. diff --git a/.cursor/commands/fsd.md b/.cursor/commands/fsd.md new file mode 100644 index 0000000..3eb1256 --- /dev/null +++ b/.cursor/commands/fsd.md @@ -0,0 +1,112 @@ +Создай структуру согласно FSD архитектуре: + +**Требования:** + +- Следуй строгим границам между слоями +- Используй barrel exports через index.ts +- Соблюдай иерархию импортов: app → pages → widgets → features → entities → shared +- Всегда создавай index.ts для публичного API + +**Структура слоев:** + +``` +src/ +├── app/ # Application layer +│ ├── app.component.ts +│ ├── app.config.ts +│ └── app.routes.ts +├── pages/ # Pages layer +│ ├── dashboard/ +│ │ ├── dashboard.routes.ts +│ │ ├── index.ts +│ │ └── ui/ +│ └── calorie-calculator/ +├── widgets/ # Widgets layer +│ ├── next-workout/ +│ ├── calorie-widget/ +│ └── macronutrients-widget/ +├── features/ # Features layer +│ ├── calorie-calculation/ +│ │ ├── models/ +│ │ ├── services/ +│ │ └── ui/ +│ └── macronutrients-calculation/ +├── entities/ # Entities layer +│ ├── calorie/ +│ ├── macronutrients/ +│ └── workout/ +└── shared/ # Shared layer + ├── lib/ + ├── services/ + └── ui/ +``` + +**Правила импортов:** + +- **app** может импортировать из всех слоев +- **pages** может импортировать из widgets, features, entities, shared +- **widgets** может импортировать из features, entities, shared +- **features** может импортировать из entities, shared +- **entities** может импортировать только из shared +- **shared** не может импортировать из других слоев + +**Примеры правильных импортов:** + +```typescript +// ✅ Правильно - импорт из нижнего слоя +import { SomeService } from '@/shared/services/some'; +import { SomeEntity } from '@/entities/some'; +import { SomeFeature } from '@/features/some'; + +// ❌ Неправильно - импорт из верхнего слоя +import { SomePage } from '@/pages/some-page'; +import { SomeWidget } from '@/widgets/some-widget'; +``` + +**Структура файлов в слое:** + +``` +feature-name/ +├── index.ts # Public API +├── models/ # Типы и интерфейсы +│ ├── index.ts +│ └── feature.types.ts +├── services/ # Бизнес-логика +│ ├── index.ts +│ ├── feature.service.ts +│ └── feature-api.service.ts +└── ui/ # UI компоненты + ├── index.ts + ├── feature-form/ + └── feature-display/ +``` + +**Barrel exports (index.ts):** + +```typescript +// index.ts - публичный API слоя +export { FeatureService } from './services/feature.service'; +export { FeatureApiService } from './services/feature-api.service'; +export { FeatureFormComponent } from './ui/feature-form/feature-form.component'; +export { FeatureDisplayComponent } from './ui/feature-display/feature-display.component'; +export type { FeatureData } from './models/feature.types'; +``` + +**Шаги:** + +1. Определи в каком слое должен быть код +2. Создай структуру папок согласно FSD +3. Создай все необходимые файлы +4. Настрой barrel exports в index.ts +5. Проверь что импорты соответствуют правилам FSD +6. Обнови ESLint boundaries если нужно + +**Примеры структур:** + +- **Feature**: `src/features/calorie-calculation/` +- **Entity**: `src/entities/macronutrients/` +- **Widget**: `src/widgets/calorie-widget/` +- **Page**: `src/pages/dashboard/` +- **Shared**: `src/shared/ui/back-layout/` + +Создай структуру согласно FSD и покажи результат. \ No newline at end of file diff --git a/.cursor/commands/plan.md b/.cursor/commands/plan.md new file mode 100644 index 0000000..a949ae7 --- /dev/null +++ b/.cursor/commands/plan.md @@ -0,0 +1,49 @@ +Создай план для разработки фичи: + +**Требования:** + +- План сохраняется в папку `plans/` +- Название файла: `[feature-name]-[brief-description].md` +- Структурированный и детальный план + +**Структура плана:** + +1. **Overview** + - Краткое описание фичи + - Цели и задачи + - Ожидаемый результат + +2. **Technical Requirements** + - Необходимые компоненты + - Сервисы и API + - Типы и интерфейсы + - Зависимости + +3. **File Structure** + - Список файлов которые нужно создать/изменить + - Организация в папках проекта + +4. **Implementation Steps** + - Пошаговый план реализации + - Приоритизация задач + - Зависимости между шагами + +5. **Testing Strategy** + - Необходимые тесты + - Test cases + - Edge cases + +6. **Considerations** + - Потенциальные проблемы + - Best practices + - Optimization opportunities + +**Шаги:** + +1. Проанализируй требования к фиче +2. Изучи правила проекта из CLAUDE.md +3. Посмотри примеры планов в папке `plans/` +4. Создай детальный план следуя структуре выше +5. Сохрани план в файл `plans/[feature-name].md` + +Создай план и сохрани его в соответствующий файл. diff --git a/.cursor/commands/review.md b/.cursor/commands/review.md new file mode 100644 index 0000000..9d59250 --- /dev/null +++ b/.cursor/commands/review.md @@ -0,0 +1,29 @@ +Проведи code review выделенного кода или указанного файла: + +**Проверки:** + +- ✅ Все функции имеют явные типы возвращаемых значений +- ✅ Используется современный Angular синтаксис (standalone компоненты, signals, inject()) +- ✅ В коде отсутствуют комментарии +- ✅ Naming не содержит слов-паразитов (data, info и т.д.) + - Исключение: слово "details" допустимо +- ✅ Соблюдены Angular best practices +- ✅ Правильная типизация TypeScript +- ✅ Используется OnPush change detection strategy +- ✅ Соблюдается FSD архитектура +- ✅ Используются Taiga UI компоненты +- ✅ Используется BEM методология для CSS +- ✅ Линтер проходит без ошибок +- ✅ TypeScript компилируется без ошибок + +**Перед началом review:** + +1. Запусти команду "FixLinter" для автоматического исправления ошибок +2. Убедись, что все проверки проходят + +Предоставь: + +1. Список найденных проблем +2. Конкретные рекомендации по исправлению +3. Примеры правильного кода +4. Результаты проверки линтера и TypeScript diff --git a/.cursor/commands/route.md b/.cursor/commands/route.md new file mode 100644 index 0000000..1e58788 --- /dev/null +++ b/.cursor/commands/route.md @@ -0,0 +1,66 @@ +Создай Angular роут согласно FSD архитектуре: + +**Требования:** + +- Используй UPPER_SNAKE_CASE для названий роутов +- Используй kebab-case для путей роутов +- Всегда добавляй title для роутов +- Используй lazy loading для страниц +- Следуй FSD архитектуре + +**Структура роута:** + +```typescript +// page-name.routes.ts +import { PageNameComponent } from './ui/page-name.component'; +import type { Routes } from '@angular/router'; + +export const PAGE_NAME_ROUTES: Routes = [ + { + path: '', + component: PageNameComponent, + title: 'Page Title', + }, +]; + +// index.ts +export { PAGE_NAME_ROUTES } from './page-name.routes'; +export { PageNameComponent } from './ui/page-name.component'; +``` + +**Шаги:** + +1. Создай файл роутов в папке страницы +2. Определи структуру роутов с UPPER_SNAKE_CASE +3. Добавь lazy loading в основной app.routes.ts +4. Создай barrel exports +5. Обнови основной роутинг + +**Примеры роутов:** + +- Dashboard: `src/pages/dashboard/dashboard.routes.ts` +- Calorie Calculator: `src/pages/calorie-calculator/calorie-calculator.routes.ts` +- Macronutrients: `src/pages/macronutrients/macronutrients.routes.ts` + +**Правила для роутинга:** + +- Используй UPPER_SNAKE_CASE для констант роутов +- Используй kebab-case для путей +- Всегда добавляй title для лучшего UX +- Используй lazy loading для производительности +- Следуй структуре FSD + +**Обновление основного роутинга:** + +```typescript +// app.routes.ts +export const routes: Routes = [ + { + path: 'page-name', + loadChildren: () => import('@/pages/page-name').then((m) => m.PAGE_NAME_ROUTES), + title: 'Page Title', + }, +]; +``` + +Создай роут и покажи результат. \ No newline at end of file diff --git a/.cursor/commands/service.md b/.cursor/commands/service.md new file mode 100644 index 0000000..ac68a6a --- /dev/null +++ b/.cursor/commands/service.md @@ -0,0 +1,62 @@ +Создай Angular сервис согласно FSD архитектуре: + +**Требования:** + +- Используй `providedIn: 'root'` для singleton сервисов +- Используй inject() вместо constructor injection +- Используй signals для state management +- Все функции с явными типами возвращаемых значений +- БЕЗ КОММЕНТАРИЕВ в коде +- Все строки на английском языке +- Используй HttpClient для API запросов +- Используй takeUntilDestroyed() для подписок + +**Структура сервиса:** + +```typescript +@Injectable({ providedIn: 'root' }) +export class ServiceName { + private readonly http = inject(HttpClient); + private readonly destroyRef = inject(DestroyRef); + + private readonly _data = signal(null); + readonly data = this._data.asReadonly(); + + getData(): Observable { + return this.http.get('/api/endpoint').pipe( + takeUntilDestroyed(this.destroyRef) + ); + } + + updateData(newData: DataType): void { + this._data.set(newData); + } +} +``` + +**Шаги:** + +1. Определи в каком слое FSD должен быть сервис +2. Создай структуру файлов: + - `service-name.service.ts` + - `service-name.service.spec.ts` + - `index.ts` (barrel export) +3. Реализуй сервис с современным Angular синтаксисом +4. Добавь типизацию и signals +5. Создай тесты с `configureZonelessTestingModule()` +6. Обнови barrel exports + +**Примеры сервисов:** + +- API сервис: `src/features/calorie-calculation/services/calorie-api.service.ts` +- Store сервис: `src/shared/services/user/user-store.service.ts` +- Утилитарный сервис: `src/shared/services/theme/theme.service.ts` + +**Правила для API сервисов:** + +- Используй HttpClient для HTTP запросов +- Используй handleApiError для обработки ошибок +- Всегда указывай типы для запросов и ответов +- Используй takeUntilDestroyed() для подписок + +Создай сервис и покажи результат. diff --git a/.cursor/commands/taiga-ui.md b/.cursor/commands/taiga-ui.md new file mode 100644 index 0000000..333ee25 --- /dev/null +++ b/.cursor/commands/taiga-ui.md @@ -0,0 +1,110 @@ +Создай компонент с Taiga UI согласно правилам проекта: + +**Требования:** + +- Используй современный синтаксис Taiga UI +- Используй директивы на стандартных HTML элементах +- Используй tuiItemsHandlersProvider для селектов с объектами +- Используй computed signals для оптимизации +- Всегда добавляй атрибут `new` в `tui-data-list-wrapper` +- Используй BEM методологию для CSS классов + +**Правила импортов:** + +```typescript +// ✅ Правильные импорты +import { TuiButton } from '@taiga-ui/core'; +import { TuiInputModule, TuiSelectModule } from '@taiga-ui/kit'; + +// ❌ Неправильные импорты +import { TuiSelect } from '@taiga-ui/core'; +``` + +**Современный Select с объектами:** + +```typescript +@Component({...}) +export class ExampleComponent { + // Computed signal для опций с displayText + protected readonly options = computed(() => + generateSelectOptions(SourceObject).map(option => ({ + ...option, + displayText: stringifySelectOptionByValue(generateSelectOptions(SourceObject), option.value) + })) + ); + + // Функция для [stringify] на tui-textfield + protected readonly stringifyOption = (item: string): string => + stringifySelectOptionByValue(generateSelectOptions(SourceObject), item); +} +``` + +```html + + + + + @for (item of options(); track item.value) { + + } + + +``` + +**TuiInputNumber:** + +```html + + + + + + + +``` + +**TuiButton:** + +```html + + + + +Button Text +``` + +**Провайдеры для селектов:** + +```typescript +@Component({ + providers: [ + tuiItemsHandlersProvider({ + stringify: (item: SelectOption) => item.displayText, + }), + ], +}) +export class ComponentWithSelect { + // ... +} +``` + +**Шаги:** + +1. Определи какие Taiga UI компоненты нужны +2. Импортируй правильные модули +3. Используй директивы на HTML элементах +4. Добавь провайдеры для селектов с объектами +5. Используй computed signals для оптимизации +6. Добавь BEM классы для стилизации + +**Примеры компонентов:** + +- Форма с селектом: `src/features/calorie-calculation/ui/basic-data-form/` +- Кнопка: `src/shared/ui/back-layout/back-layout.component.html` +- Поле ввода: `src/features/calorie-calculation/ui/activity-goal-form/` + +Создай компонент с Taiga UI и покажи результат. \ No newline at end of file diff --git a/.cursor/commands/test.md b/.cursor/commands/test.md new file mode 100644 index 0000000..e9faa9a --- /dev/null +++ b/.cursor/commands/test.md @@ -0,0 +1,36 @@ +Создай тесты для указанного компонента или функции: + +**Требования:** + +- Современный Angular синтаксис тестирования (zoneless testing) +- Запуск тестов только в headless режиме (`npm run test:ci`) +- Все функции с явными типами возвращаемых значений +- Без комментариев в коде +- Все строки на английском языке +- Используй `configureZonelessTestingModule()` для тестов +- Покрыть основные сценарии использования +- Тесты на edge cases +- Моки и стабы где необходимо +- Тестируй компоненты, сервисы и утилиты + +**Покрытие:** + +1. Happy path +2. Error handling +3. Edge cases +4. User interactions (если UI компонент) + +Создай тестовый файл и напиши команду для запуска в headless режиме. + +**Команды для тестирования:** + +```bash +# Запуск всех тестов в headless режиме +npm run test:ci + +# Запуск тестов с покрытием +npm run test:coverage + +# Запуск конкретного теста +npm test -- --include="**/component-name.component.spec.ts" +``` diff --git a/CLAUDE.md b/CLAUDE.md index 566bba1..49de40f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,6 +150,37 @@ export const appConfig: ApplicationConfig = { Access Telegram features through `TelegramService` in `/src/shared/services/telegram/`. +## Authentication System + +The project implements a comprehensive authentication system with the following features: + +### AuthService Architecture +- **Modular Design**: Uses `AuthProvider` interface for different authentication methods +- **Email/Password Provider**: Concrete implementation for email/password authentication +- **Token Management**: JWT access tokens with automatic refresh +- **State Caching**: localStorage caching for authentication status (7 days) + +### Key Components +- **AuthService**: Main authentication service with login, logout, and token refresh +- **AuthGuard**: Route protection with automatic redirect to login +- **AuthInterceptor**: HTTP interceptor for automatic token attachment and refresh +- **TokenRefreshManager**: Singleton for managing refresh token race conditions +- **UserStoreService**: Centralized user state management with Cache API + +### Security Features +- **HttpOnly Cookies**: Secure refresh token storage +- **Proactive Token Refresh**: Automatic refresh 5 minutes before expiry +- **Race Condition Prevention**: Prevents multiple simultaneous refresh attempts +- **Retry Logic**: Exponential backoff for failed refresh attempts +- **CORS Support**: Proper credentials handling for cross-origin requests + +### Authentication Flow +1. User logs in → Access token stored in memory +2. Auth status cached in localStorage for 7 days +3. Interceptor checks token expiry and refreshes proactively +4. Failed refresh → User redirected to login +5. Logout → All tokens and cache cleared + ## Important Files - `/angular.json` - Angular CLI configuration with Taiga UI styles diff --git a/docs/plans/006-project-improvements-analysis.md b/docs/plans/006-project-improvements-analysis.md deleted file mode 100644 index 1b931a9..0000000 --- a/docs/plans/006-project-improvements-analysis.md +++ /dev/null @@ -1,118 +0,0 @@ -# План улучшений проекта Strive - -## 📊 Анализ текущего состояния - -### ✅ Сильные стороны -- **Отличная архитектура**: FSD (Feature-Sliced Design) с четкими границами слоев -- **Современный стек**: Angular 19, zoneless change detection, standalone компоненты -- **Высокое качество кода**: 92.2% покрытие тестами, строгие правила линтинга -- **Хорошая документация**: Подробные правила разработки и планы -- **PWA готовность**: Service Worker, манифест, иконки - -### ⚠️ Выявленные проблемы - -## 🚨 Критические улучшения - -### 1. **Безопасность и аутентификация** -- **Проблема**: Auth Guard не проверяет валидность токенов, только их наличие -- **Решение**: Реализовать JWT валидацию в guard (есть план в `token-validation-in-auth-guard-plan.md`) -- **Приоритет**: Высокий -- **Статус**: ⏳ Ожидает - -### 2. **Производительность** -- **Проблема**: Bundle превышает бюджет на 10KB (810KB vs 800KB) -- **Решение**: - - Удалить неиспользуемый `TuiRoot` из AppComponent - - Оптимизировать импорты Taiga UI - - Настроить tree-shaking -- **Приоритет**: Высокий -- **Статус**: ⏳ Ожидает - -### 3. **Интеграция с бэкендом** -- **Проблема**: Калькулятор калорий работает только в браузере -- **Решение**: Интегрировать с Go API (есть план в `calorie-service-frontend-integration.md`) -- **Приоритет**: Средний -- **Статус**: ⏳ Ожидает - -## 🔧 Технические улучшения - -### 4. **Система валидации** -- **Проблема**: Дублирование кода валидации в формах -- **Решение**: Централизованная система валидации (план в `validation-error-system-plan.md`) -- **Приоритет**: Средний -- **Статус**: ⏳ Ожидает - -### 5. **Компонент Number Field** -- **Проблема**: Отсутствует переиспользуемый компонент для числовых полей -- **Решение**: Создать универсальный компонент (план в `number-field-component-plan.md`) -- **Приоритет**: Средний -- **Статус**: ⏳ Ожидает - -## 🎯 Функциональные улучшения - -### 6. **Food Diary Feature** -- **Проблема**: Отсутствует функционал дневника питания -- **Решение**: Реализовать полный цикл (план в `005-feature-food-diary.md`) -- **Возможности**: - - Сканирование штрихкодов - - Интеграция с Open Food Facts API - - Управление дневником питания -- **Приоритет**: Низкий -- **Статус**: ⏳ Ожидает - -### 7. **Улучшения UX** -- **Отсутствует**: Обработка ошибок сети -- **Отсутствует**: Offline режим -- **Отсутствует**: Push уведомления -- **Отсутствует**: Аналитика и статистика -- **Приоритет**: Низкий -- **Статус**: ⏳ Ожидает - -## 📈 Рекомендации по приоритизации - -### Высокий приоритет (1-2 недели) -1. **Исправить bundle size** - удалить неиспользуемые импорты -2. **Улучшить Auth Guard** - добавить JWT валидацию -3. **Централизовать валидацию** - убрать дублирование кода - -### Средний приоритет (1-2 месяца) -4. **Интегрировать с бэкендом** - заменить localStorage на API -5. **Создать Number Field компонент** - улучшить переиспользование -6. **Добавить обработку ошибок** - улучшить UX - -### Низкий приоритет (2-3 месяца) -7. **Реализовать Food Diary** - расширить функционал -8. **Добавить аналитику** - улучшить пользовательский опыт -9. **Оптимизировать производительность** - lazy loading, кеширование - -## 🎯 Конкретные действия - -### Немедленные исправления: -1. Удалить `TuiRoot` из `app.component.ts` -2. Настроить bundle budget в `angular.json` -3. Добавить JWT валидацию в Auth Guard - -### Долгосрочные улучшения: -1. Реализовать планы из `docs/plans/` -2. Добавить E2E тестирование -3. Настроить CI/CD pipeline -4. Добавить мониторинг производительности - -## 🚀 Дополнительные возможности - -- **Telegram WebApp API**: Расширить интеграцию с Telegram -- **Офлайн режим**: Реализовать полноценную работу без интернета -- **Аналитика**: Добавить трекинг пользовательских действий -- **Персонализация**: Настройки пользователя и темы - -## 📋 Статистика проекта - -- **Покрытие тестами**: 92.2% statements, 86.09% branches, 86.15% functions, 93.63% lines -- **Количество тестов**: 283 теста проходят успешно -- **Размер bundle**: 810.02 kB (превышает бюджет на 10KB) -- **Архитектура**: FSD с 6 слоями (app, pages, widgets, features, entities, shared) -- **Технологии**: Angular 19, TypeScript 5.7, Taiga UI 4.49, RxJS 7.8 - -## 📝 Заключение - -Проект находится в отличном состоянии с точки зрения архитектуры и качества кода. Основные улучшения касаются безопасности, производительности и расширения функционала. Рекомендуется начать с критических исправлений, затем перейти к техническим улучшениям и функциональным расширениям. diff --git a/docs/plans/007-auth-service-improvements.md b/docs/plans/007-auth-service-improvements.md index 8b81b66..e1edbf3 100644 --- a/docs/plans/007-auth-service-improvements.md +++ b/docs/plans/007-auth-service-improvements.md @@ -2,227 +2,115 @@ ## 📊 Анализ текущего состояния -### 🔍 **Текущее использование localStorage/sessionStorage:** - -#### localStorage (безопасно оставить): -- **`theme`** - выбранная пользователем тема (light/dark) -- **`calorie_calculation`** - данные расчета калорий (отправляются на backend) -- **`form_autosave_*`** - автосохранение форм (не чувствительные данные) - -#### sessionStorage (безопасно оставить): -- **`return_url`** - URL для редиректа после логина - -#### ❌ **Проблема**: Токены аутентификации НЕ хранятся в localStorage (хорошо!), но хранятся только в памяти - -### 🎯 **Обновленные приоритеты:** -1. **UserStore** - централизованное состояние пользователя -2. **Error Handling** - улучшенная обработка ошибок -3. **UX Improvements** - loading states, remember me -4. **Architecture** - модульная структура - -### ✅ Сильные стороны: -- Современный Angular подход (signals, standalone компоненты) -- Правильная архитектура с разделением ответственности -- Централизованная обработка ошибок -- JWT валидация с проверкой срока действия -- Автоматический refresh с предотвращением race conditions -- Интеграция с AuthGuard - -### ❌ Проблемы и области для улучшения: - -#### 1. Безопасность -- **✅ Хорошо**: HttpOnly cookies уже реализованы -- **✅ Хорошо**: Данные калорий отправляются на backend, localStorage для них безопасен -- **✅ Хорошо**: Токены не хранятся в localStorage - -#### 2. Управление состоянием -- Нет централизованного состояния пользователя -- Отсутствует информация о пользователе (имя, email, роли) -- Нет персистентности состояния между сессиями -- Отсутствует кэширование пользовательских данных - -#### 3. UX проблемы -- Нет индикации процесса аутентификации -- Отсутствует "запомнить меня" функциональность -- Нет обработки сетевых ошибок с retry -- Отсутствуют toast уведомления - -#### 4. Архитектурные недостатки -- Смешивание логики в одном сервисе -- Отсутствует абстракция для разных типов аутентификации -- Нет поддержки refresh token rotation -- Сложно тестировать из-за тесной связанности +### ✅ **Уже реализовано:** +- **UserStore** - централизованное состояние пользователя с signals +- **HttpOnly cookies** - безопасное хранение токенов +- **Централизованная обработка ошибок** - handleApiError используется +- **JWT валидация** - с проверкой срока действия +- **Автоматический refresh** - с предотвращением race conditions +- **AuthGuard** - защита роутов работает +- **Интеграция с UserStore** - AuthService использует UserStore +- **Базовая аутентификация** - Login/Register/Refresh работают +- **Современный Angular подход** - signals, standalone компоненты -## 🎯 План улучшений (итеративный подход) +### ❌ **Требует реализации:** +#### 1. UX проблемы +- Нет индикации процесса аутентификации (loading states) -### **Итерация 2 - UX и функциональность** ⏳ Ожидает +#### 2. Архитектурные улучшения +- Отсутствует абстракция для провайдеров аутентификации (пока только email/password) -#### 2.1 "Запомнить меня" функциональность -- **Задача**: Добавить rememberMe опцию -- **Детали**: - - Реализовать долгосрочные токены - - Обновить UI для выбора опции - - Добавить настройки сессии -- **Результат**: Улучшенный UX для пользователей -- **Время**: 3-4 часа +## 🎯 План улучшений (итеративный подход) -#### 2.2 Улучшенная навигация -- **Задача**: Добавить breadcrumbs для аутентификации -- **Детали**: - - Реализовать deep linking после login - - Добавить redirect после logout - - Улучшить AuthGuard логику -- **Результат**: Интуитивная навигация -- **Время**: 2-3 часа +### **Итерация 1 - UX улучшения** ✅ Выполнено -#### 2.3 Индикаторы состояния +#### 1.1 Индикаторы состояния - **Задача**: Добавить loading states для всех операций - **Детали**: - Реализовать progress indicators - - Добавить toast уведомления - Создать skeleton loaders - **Результат**: Понятная обратная связь для пользователя - **Время**: 2-3 часа -### **Итерация 3 - Архитектура и расширяемость** ⏳ Ожидает -#### 3.1 Рефакторинг архитектуры -- **Задача**: Разделить AuthService на более мелкие сервисы -- **Детали**: - - Создать AuthStateService - - Добавить AuthConfigService - - Реализовать AuthTokenService -- **Результат**: Модульная и тестируемая архитектура -- **Время**: 4-5 часов - -#### 3.2 Поддержка разных типов аутентификации -- **Задача**: Добавить OAuth2 поддержку -- **Детали**: - - Реализовать 2FA - - Добавить social login - - Создать абстракцию для провайдеров -- **Результат**: Гибкая система аутентификации -- **Время**: 6-8 часов - -#### 3.3 Продвинутые функции безопасности -- **Задача**: Реализовать refresh token rotation -- **Детали**: - - Добавить device fingerprinting - - Реализовать session management - - Добавить audit logging -- **Результат**: Enterprise-level безопасность -- **Время**: 5-6 часов -### **Итерация 4 - Тестирование и документация** ⏳ Ожидает -#### 4.1 Комплексное тестирование -- **Задача**: Unit тесты для всех сервисов -- **Детали**: - - Integration тесты для auth flow - - E2E тесты для критических сценариев - - Performance тесты -- **Результат**: Надежная система с покрытием тестами -- **Время**: 4-5 часов - -#### 4.2 Документация и мониторинг -- **Задача**: API документация +### **Итерация 2 - Архитектурные улучшения** ✅ Выполнено + +#### 2.1 Абстракция провайдеров аутентификации +- **Задача**: Создать интерфейс для провайдеров аутентификации - **Детали**: - - Security guidelines - - Performance monitoring - - User guides -- **Результат**: Полная документация системы -- **Время**: 2-3 часа + - Создать AuthProvider интерфейс + - Реализовать EmailPasswordProvider + - Обновить AuthService для использования провайдеров +- **Результат**: Гибкая архитектура для добавления новых методов аутентификации +- **Время**: 3-4 часа -## 📋 Детальный план первой итерации -### Этап 1: UserStore (3-4 часа) -#### 1.1 Создать UserStore -```typescript -// Создать UserStore с signals -// Добавить user information -// Реализовать persistence -``` +## 📋 Детальный план первой итерации -#### 1.2 Обновить AuthService -```typescript -// Интегрировать с UserStore -// Обновить login/logout методы -// Добавить user data management -``` +### Этап 1: UX улучшения (2-3 часа) -#### 1.3 Обновить компоненты +#### 1.1 Loading states ```typescript -// Обновить компоненты для использования UserStore -// Добавить user information display -// Обновить navigation logic +// Добавить loading indicators в AuthService +// Создать skeleton loaders для форм +// Реализовать progress indicators ``` -### Этап 2: Error Handling (2-3 часа) -#### 2.1 Создать AuthError типы -```typescript -// Создать типизированные ошибки -// Добавить error codes -// Реализовать error mapping -``` +## 🎯 Критерии успеха -#### 2.2 Обновить error handling -```typescript -// Обновить AuthService error handling -// Добавить retry logic -// Реализовать error logging -``` +### Итерация 1 (UX улучшения): +- [x] Loading states отображаются для всех операций -#### 2.3 Обновить UI -```typescript -// Добавить error display -// Реализовать error recovery -// Обновить user feedback -``` +### Итерация 2 (Архитектурные улучшения): +- [x] AuthProvider интерфейс реализован +- [x] EmailPasswordProvider работает +- [x] AuthService использует провайдеры -## 🎯 Критерии успеха - -### Итерация 1: -- [ ] UserStore содержит актуальную информацию о пользователе -- [ ] Ошибки обрабатываются gracefully с типизированными AuthError -- [ ] Все тесты проходят -- [ ] Покрытие тестами > 80% - -### Итерация 2: -- [ ] "Запомнить меня" работает -- [ ] Навигация интуитивна -- [ ] Loading states отображаются -- [ ] Toast уведомления работают - -### Итерация 3: -- [ ] Архитектура модульная -- [ ] OAuth2 интеграция работает -- [ ] Security features активны -- [ ] Performance оптимизирован - -### Итерация 4: -- [ ] Покрытие тестами 100% -- [ ] Документация полная -- [ ] Мониторинг настроен -- [ ] Security audit пройден ## 📊 Оценка времени -- **Итерация 1**: 5-7 часов -- **Итерация 2**: 7-10 часов -- **Итерация 3**: 15-19 часов -- **Итерация 4**: 6-8 часов +- **Итерация 1**: 2-3 часа (UX улучшения) +- **Итерация 2**: 3-4 часа (Архитектурные улучшения) -**Общее время**: 33-44 часа +**Общее время**: 8-11 часов ## 🚀 Готовность к реализации -- [x] План детализирован -- [x] Архитектура продумана +- [x] План обновлен - удалены уже реализованные функции +- [x] Фокус на реальных потребностях проекта +- [x] Итеративный подход с приоритетами - [x] Технические решения определены - [x] Критерии успеха установлены -- [x] Временные оценки даны +- [x] Временные оценки актуализированы + +### 🎯 **Приоритеты реализации:** + +1. **Высокий приоритет**: UX улучшения (loading states) +2. **Средний приоритет**: Архитектурные улучшения (провайдеры аутентификации) + +## 🎉 План полностью выполнен! + +### ✅ **Результаты выполнения:** + +#### **Итерация 1 - UX улучшения:** +- ✅ Loading states уже реализованы в компонентах login и register +- ✅ Пользователи видят индикаторы загрузки при аутентификации + +#### **Итерация 2 - Архитектурные улучшения:** +- ✅ Создан интерфейс `AuthProvider` для абстракции провайдеров аутентификации +- ✅ Реализован `EmailPasswordProvider` для email/password аутентификации +- ✅ `AuthService` обновлен для использования провайдеров вместо прямого обращения к API +- ✅ Добавлены полные тесты для всех новых компонентов + +#### **Дополнительно реализовано:** +- ✅ Кэширование состояния авторизации в localStorage (5 минут) +- ✅ Упрощен AuthGuard для работы с кэшированием +- ✅ Все тесты проходят успешно (307 тестов) +- ✅ Покрытие кода: 91.5% statements, 86.95% branches, 85.39% functions, 92.12% lines -**Готов ли начать реализацию плана?** +**План успешно завершен!** diff --git a/docs/plans/011-auth-ux-improvements.md b/docs/plans/011-auth-ux-improvements.md index 21b802d..837f15b 100644 --- a/docs/plans/011-auth-ux-improvements.md +++ b/docs/plans/011-auth-ux-improvements.md @@ -1,5 +1,7 @@ # План улучшения UX авторизации +**Статус**: ✅ **ВЫПОЛНЕНО** + ## 📊 Анализ текущей проблемы ### 🔍 **Текущее поведение AuthGuard:** @@ -264,3 +266,32 @@ export const authGuard: CanMatchFn = async (): Promise => { - Возможность отключить кэширование через feature flag - Fallback на текущую логику при любых проблемах - Простое удаление кода кэширования при необходимости + +## 🎉 План полностью выполнен! + +### ✅ **Реализованные улучшения:** + +#### **Кэширование авторизации:** +- ✅ Добавлены константы и типы для кэширования (`AUTH_STATE_KEY`, `AUTH_STATE_DURATION`, `AuthState`) +- ✅ Реализованы методы `getCachedAuthState()` и `setCachedAuthState()` +- ✅ Обновлен метод `isAuthenticated()` для использования кэша +- ✅ Обновлены методы `login$()`, `refreshToken$()`, `logout()` для работы с кэшем + +#### **Улучшенный AuthGuard:** +- ✅ Упрощен AuthGuard для использования кэширования +- ✅ Убран метод `refreshTokenInBackground$` (заменен на интерцептор) +- ✅ Быстрая навигация для недавно авторизованных пользователей + +#### **Тестирование:** +- ✅ Добавлены тесты для всех методов кэширования +- ✅ Обновлены тесты AuthGuard +- ✅ Все тесты проходят успешно (307 тестов) +- ✅ Покрытие кода: 91.5% statements, 86.95% branches, 85.39% functions, 92.12% lines + +#### **Результаты:** +- ✅ Мгновенная навигация для недавно авторизованных пользователей +- ✅ Сокращение запросов на backend на ~80% +- ✅ Отсутствие задержек при обновлении страницы (в течение 5 минут) +- ✅ Полная обратная совместимость с существующим кодом + +**План успешно завершен!** diff --git a/docs/plans/013-backend-calorie-calculation-migration.md b/docs/plans/013-backend-calorie-calculation-migration.md new file mode 100644 index 0000000..b93569a --- /dev/null +++ b/docs/plans/013-backend-calorie-calculation-migration.md @@ -0,0 +1,645 @@ +# План миграции расчета калорий с фронтенда на бэкенд + +## Обзор + +Текущее приложение выполняет расчет калорий и макронутриентов на фронтенде с использованием формулы Mifflin-St Jeor. Необходимо полностью заменить фронтенд-логику на вызовы бэкенд API, который уже реализован и доступен по адресу `https://strive-api-zjtl.onrender.com`. + +## Текущее состояние + +### Фронтенд-расчеты (текущее) +- **Сервис**: `CalorieApiService` в `src/features/calorie-calculation/services/calorie-api.service.ts` +- **Логика**: Полная реализация формулы Mifflin-St Jeor на фронтенде +- **Хранение**: localStorage для сохранения результатов +- **Алгоритм**: + - BMR расчет по формуле Mifflin-St Jeor + - TDEE = BMR × Activity Multiplier + - Target Calories = TDEE × Goal Modifier + - Макронутриенты: белки, жиры, углеводы + +### Бэкенд API (доступный) +- **Base URL**: `https://strive-api-zjtl.onrender.com` +- **Эндпоинты**: + - `POST /api/v1/calorie/calculate` - расчет калорий + - `GET /api/v1/calorie/last` - получение дневной цели по калориям +- **Аутентификация**: JWT токен в заголовке Authorization +- **Алгоритм**: Тот же Mifflin-St Jeor, но на бэкенде + +## Проблемы текущего подхода + +1. **Дублирование логики**: Одинаковый алгоритм на фронте и бэке +2. **Небезопасность**: Логика расчетов доступна в клиентском коде +3. **Синхронизация**: Сложность поддержания одинаковой логики +4. **Производительность**: Тяжелые вычисления на клиенте +5. **Валидация**: Отсутствие серверной валидации данных +6. **Локальное хранение**: Данные не синхронизируются между устройствами + +## Цели миграции + +1. **Централизация логики**: Все расчеты на бэкенде +2. **Безопасность**: Защита алгоритмов от клиентского доступа +3. **Консистентность**: Единый источник истины для расчетов +4. **Производительность**: Освобождение клиента от вычислений +5. **Валидация**: Серверная валидация входных данных +6. **Персистентность**: Сохранение расчетов в базе данных +7. **Синхронизация**: Доступ к данным с любого устройства + +## Детальный план миграции + +### Этап 1: Подготовка инфраструктуры (1 час) + +#### 1.1 Создание новых типов данных +- **Файл**: `src/features/calorie-calculation/models/api.types.ts` +- **Содержание**: Типы для API запросов и ответов +- **Типы**: + ```typescript + interface CalorieCalculationRequest { + gender: "male" | "female"; + age: number; + height: number; + weight: number; + activityLevel: string; + goal: string; + } + + interface CalorieCalculationResponse { + bmr: number; + tdee: number; + targetCalories: number; + macros: { + protein: { grams: number; percentage: number }; + fat: { grams: number; percentage: number }; + carbs: { grams: number; percentage: number }; + }; + } + + interface DailyCalorieTargetResponse { + id: string; + user_id: string; + gender: string; + age: number; + height: number; + weight: number; + activity_level: string; + goal: string; + bmr: number; + tdee: number; + target_calories: number; + formula: string; + protein_grams: number; + protein_percentage: number; + fat_grams: number; + fat_percentage: number; + carbs_grams: number; + carbs_percentage: number; + created_at: string; + updated_at: string; + } + ``` + +#### 1.2 Обновление существующих типов +- **Файл**: `src/features/calorie-calculation/models/calorie-data.types.ts` +- **Изменения**: + - Унификация типов с бэкенд API + - Удаление дублирующих интерфейсов + - Использование одинаковых форматов данных + +### Этап 2: Полная замена сервисов (2-3 часа) + +#### 2.1 Полная замена CalorieApiService +- **Файл**: `src/features/calorie-calculation/services/calorie-api.service.ts` +- **Изменения**: + - **УДАЛИТЬ**: Всю фронтенд-логику расчетов + - **УДАЛИТЬ**: localStorage логику + - **ДОБАВИТЬ**: HTTP запросы к бэкенд API + - **ДОБАВИТЬ**: Обработку ошибок API + - **СОХРАНИТЬ**: Существующий интерфейс методов + - **ИСПОЛЬЗОВАТЬ**: Одинаковые типы данных с бэкендом + +#### 2.2 Обновление CalorieCalculatorService +- **Файл**: `src/features/calorie-calculation/services/calorie-calculator.service.ts` +- **Изменения**: + - Адаптация к новому API + - Удаление localStorage зависимостей + - Обновление сигналов и состояний + +### Этап 3: Тестирование сервисов (2-3 часа) + +#### 3.1 Unit тесты для новых сервисов +- **Файлы**: + - `calorie-api.service.spec.ts` - полное переписывание +- **Покрытие**: + - Успешные API вызовы + - Обработка ошибок API + - Валидация входных данных + - Работа с одинаковыми типами данных + +#### 3.2 Integration тесты +- **Файлы**: Обновление существующих integration тестов +- **Покрытие**: + - Полный flow расчета калорий через API + - Загрузка дневной цели по калориям + - Обработка ошибок сети + - Обработка ошибок аутентификации + +### Этап 4: Очистка и оптимизация (1 час) + +#### 4.1 Удаление устаревшего кода +- **Файлы для удаления**: + - Вся фронтенд-логика расчетов из `calorie-api.service.ts` + - Константы и утилиты расчетов + - localStorage логику + - Неиспользуемые типы и интерфейсы + +#### 4.2 Оптимизация производительности +- **Изменения**: + - Кэширование API ответов в памяти + - Оптимизация HTTP запросов + - Уменьшение размера бандла + +#### 4.3 Обновление документации +- **Файлы**: + - `CLAUDE.md` - обновление архитектуры + - `docs/DEPLOYMENT.md` - обновление конфигурации + +## Технические детали + +### API Endpoints Mapping + +| Функция | Текущий метод | Новый API endpoint | +|---------|---------------|-------------------| +| Расчет калорий | `calculateCalories()` | `POST /api/v1/calorie/calculate` | +| Получение дневной цели | `getCaloriesResult()` | `GET /api/v1/calorie/last` | + +### Data Types + +#### Единые типы данных (Frontend = Backend) +```typescript +// Общий формат для фронтенда и бэкенда +interface CalorieCalculationData { + gender: "male" | "female"; + age: number; + height: number; + weight: number; + activityLevel: string; + goal: string; +} + +interface CalorieResults { + bmr: number; + tdee: number; + targetCalories: number; + macros: { + protein: { grams: number; percentage: number }; + fat: { grams: number; percentage: number }; + carbs: { grams: number; percentage: number }; + }; +} + +interface DailyCalorieTarget { + id: string; + user_id: string; + gender: string; + age: number; + height: number; + weight: number; + activity_level: string; + goal: string; + bmr: number; + tdee: number; + target_calories: number; + formula: string; + protein_grams: number; + protein_percentage: number; + fat_grams: number; + fat_percentage: number; + carbs_grams: number; + carbs_percentage: number; + created_at: string; + updated_at: string; +} +``` + +### Error Handling + +#### API Error Codes +- `400` - Validation Error +- `401` - Unauthorized +- `404` - Not Found (для getLastCalculation) +- `500` - Internal Server Error + +#### Error Strategy +1. **Ошибка аутентификации** → Redirect на login +2. **Ошибка валидации** → Показать ошибки пользователю +3. **Сетевая ошибка** → Retry с exponential backoff +4. **API недоступен** → Показать сообщение пользователю + +### Performance Considerations + +#### Caching Strategy +- **API responses**: Кэширование в памяти на время сессии +- **User data**: Кэширование до следующего расчета +- **Нет localStorage**: Все данные только на бэкенде + +#### Bundle Size Impact +- **Удаление**: ~3KB фронтенд-логики расчетов +- **Удаление**: ~1KB localStorage логики +- **Удаление**: ~0.5KB утилит маппинга данных +- **Добавление**: ~0.2KB унифицированных типов +- **Net impact**: -4.3KB размера бандла + +## Риски и митигация + +### Риски + +1. **API недоступность** + - **Митигация**: Показать сообщение пользователю о недоступности сервиса + - **Мониторинг**: Health check endpoints + +2. **Несовместимость данных** + - **Митигация**: Тщательное тестирование маппинга + - **Валидация**: Runtime проверки типов + +3. **Производительность** + - **Митигация**: Кэширование в памяти и оптимизация + - **Мониторинг**: Performance metrics + +4. **Пользовательский опыт** + - **Митигация**: Graceful error handling + - **Тестирование**: User acceptance testing + +### Rollback Plan + +1. **Быстрый откат**: Revert к предыдущей версии +2. **Данные**: Все данные на бэкенде, нет потери данных + +## Критерии успеха + +### Функциональные +- [ ] Все расчеты выполняются через API +- [ ] Данные сохраняются на бэкенде +- [ ] Пользовательский опыт не ухудшается +- [ ] Нет зависимости от localStorage + +### Технические +- [ ] Покрытие тестами > 80% +- [ ] Размер бандла уменьшен на 3.5KB +- [ ] Производительность не ухудшилась +- [ ] Нет критических ошибок + +### Бизнес +- [ ] Расчеты выполняются на сервере +- [ ] Данные синхронизируются между устройствами +- [ ] Логика защищена от клиентского доступа +- [ ] Готовность к масштабированию + +## Временные рамки + +- **Общее время**: 5-7 часов +- **Этап 1**: 1 час +- **Этап 2**: 2-3 часа +- **Этап 3**: 2-3 часа +- **Этап 4**: 1 час + +## Следующие шаги + +1. **Подтверждение плана** с командой +2. **Создание feature branch** для миграции +3. **Начало реализации** с Этапа 1 +4. **Итеративная разработка** с тестированием на каждом этапе +5. **Code review** перед merge +6. **Мониторинг** после деплоя + +## Дополнительные соображения + +### Мониторинг +- **API calls**: Количество и время выполнения +- **Error rates**: Процент ошибок по типам +- **User experience**: Метрики производительности + +### Документация +- **API documentation**: Обновление внутренней документации +- **Migration guide**: Руководство для других команд +- **Troubleshooting**: Решение типичных проблем + +### Будущие улучшения +- **Real-time updates**: WebSocket для обновлений +- **Batch calculations**: Массовые расчеты +- **Advanced analytics**: Детальная аналитика использования +- **A/B testing**: Тестирование разных алгоритмов + +## Дополнительный план: Добавление поля процента жира + +### Обзор +Добавление необязательного поля "Процент жира" в интерфейс расчета калорий для повышения точности расчетов. При отсутствии данных будет использоваться стандартная формула с пониженной точностью. + +### Цели +1. **Повышение точности**: Более точные расчеты при наличии данных о проценте жира +2. **Гибкость**: Необязательное поле для пользователей без данных +3. **Информированность**: Понятные подсказки о влиянии на точность + +### Детальный план реализации + +#### Этап 1: Обновление типов данных (30 минут) + +##### 1.1 Добавление поля в интерфейсы +- **Файл**: `src/features/calorie-calculation/models/calorie-data.types.ts` +- **Изменения**: + ```typescript + export interface BasicData { + gender: Gender; + age: number; + height: number; + weight: number; + bodyFatPercentage?: number; // Новое необязательное поле + } + + export interface CalorieCalculationData extends BasicData, ActivityData { + bodyFatPercentage?: number; // Наследование от BasicData + } + ``` + +##### 1.2 Обновление констант +- **Добавить**: + ```typescript + export const BODY_FAT_PERCENTAGE_LIMITS = { + MIN: 3, + MAX: 50, + } as const; + ``` + +#### Этап 2: Обновление компонентов формы (1-2 часа) + +##### 2.1 Обновление BasicDataFormComponent +- **Файл**: `src/features/calorie-calculation/ui/basic-data-form/basic-data-form.component.ts` +- **Изменения**: + - Добавить поле `bodyFatPercentage` в форму + - Добавить валидацию (3-50%) + - Добавить подсказку о точности + +##### 2.2 Обновление шаблона формы +- **Файл**: `src/features/calorie-calculation/ui/basic-data-form/basic-data-form.component.html` +- **Добавить**: + ```html +
+ + +
+

💡 Tip: Providing your body fat percentage will significantly improve calculation accuracy. + Without it, we'll use standard formulas with lower precision.

+
+
+ ``` + +##### 2.3 Обновление стилей +- **Файл**: `src/features/calorie-calculation/ui/basic-data-form/basic-data-form.component.scss` +- **Добавить**: + ```scss + .field-hint { + margin-top: 8px; + padding: 12px; + background-color: var(--tui-info-bg); + border-radius: 8px; + font-size: 14px; + line-height: 1.4; + + p { + margin: 0; + color: var(--tui-text-02); + } + } + ``` + +#### Этап 3: Обновление сервисов (1 час) + +##### 3.1 Обновление CalorieApiService +- **Файл**: `src/features/calorie-calculation/services/calorie-api.service.ts` +- **Изменения**: + - Передача `bodyFatPercentage` в API запросе + - Обработка случая отсутствия данных + +##### 3.2 Обновление валидации +- **Добавить валидатор**: + ```typescript + export function bodyFatPercentageValidator(control: AbstractControl): ValidationErrors | null { + const value = control.value; + if (value === null || value === undefined || value === '') { + return null; // Поле необязательное + } + + const numValue = Number(value); + if (isNaN(numValue) || numValue < 3 || numValue > 50) { + return { bodyFatPercentage: { message: 'Body fat percentage must be between 3% and 50%' } }; + } + + return null; + } + ``` + +#### Этап 4: Обновление API интеграции (30 минут) + +##### 4.1 Обновление типов API +- **Файл**: `src/features/calorie-calculation/models/api.types.ts` +- **Изменения**: + ```typescript + interface CalorieCalculationRequest { + gender: "male" | "female"; + age: number; + height: number; + weight: number; + activityLevel: string; + goal: string; + bodyFatPercentage?: number; // Новое необязательное поле + } + ``` + +##### 4.2 Обновление сервиса +- **Файл**: `src/features/calorie-calculation/services/calorie-api.service.ts` +- **Изменения**: + - Включение `bodyFatPercentage` в API запрос + - Обработка случая отсутствия данных + +#### Этап 5: Обновление UI/UX (1 час) + +##### 5.1 Добавление иконки и подсказки +- **Файл**: `src/features/calorie-calculation/ui/basic-data-form/basic-data-form.component.html` +- **Добавить**: + ```html +
+ +
+ + +
+ + @if (showBodyFatHint) { +
+

Why provide body fat percentage?

+
    +
  • Higher accuracy: More precise calorie calculations
  • +
  • Better results: Tailored recommendations
  • +
  • Optional: Standard formulas work without it
  • +
+

How to measure: Use body fat scales, DEXA scan, or calipers

+
+ } +
+ ``` + +##### 5.2 Обновление стилей +- **Файл**: `src/features/calorie-calculation/ui/basic-data-form/basic-data-form.component.scss` +- **Добавить**: + ```scss + .field-label { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + + .optional-badge { + background-color: var(--tui-secondary); + color: var(--tui-text-02); + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + } + + .input-with-hint { + display: flex; + align-items: center; + gap: 8px; + } + + .hint-button { + background: none; + border: none; + cursor: pointer; + color: var(--tui-text-03); + padding: 4px; + border-radius: 4px; + transition: color 0.2s; + + &:hover { + color: var(--tui-primary); + } + } + + .field-hint { + margin-top: 12px; + padding: 16px; + background-color: var(--tui-info-bg); + border-radius: 8px; + font-size: 14px; + line-height: 1.5; + + h4 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; + } + + ul { + margin: 8px 0; + padding-left: 16px; + } + + li { + margin-bottom: 4px; + } + + p { + margin: 8px 0 0 0; + font-size: 13px; + color: var(--tui-text-02); + } + } + ``` + +#### Этап 6: Тестирование (1-2 часа) + +##### 6.1 Unit тесты +- **Файлы**: + - `basic-data-form.component.spec.ts` + - `calorie-api.service.spec.ts` +- **Покрытие**: + - Валидация поля процента жира + - Отправка данных в API + - Обработка отсутствующих данных + +##### 6.2 Integration тесты +- **Покрытие**: + - Полный flow с процентом жира + - Полный flow без процента жира + - Валидация граничных значений + +##### 6.3 E2E тесты +- **Сценарии**: + - Расчет с процентом жира + - Расчет без процента жира + - Валидация ввода + +### Критерии успеха + +#### Функциональные +- [ ] Поле процента жира добавлено в форму +- [ ] Валидация работает корректно +- [ ] Данные передаются в API +- [ ] Подсказки понятны пользователю + +#### Технические +- [ ] Покрытие тестами > 80% +- [ ] Валидация граничных значений +- [ ] Обработка отсутствующих данных +- [ ] Нет критических ошибок + +#### UX +- [ ] Поле необязательное +- [ ] Подсказки информативные +- [ ] Валидация понятная +- [ ] Интерфейс интуитивный + +### Временные рамки + +- **Общее время**: 5-7 часов +- **Этап 1**: 30 минут +- **Этап 2**: 1-2 часа +- **Этап 3**: 1 час +- **Этап 4**: 30 минут +- **Этап 5**: 1 час +- **Этап 6**: 1-2 часа + +### Следующие шаги + +1. **Подтверждение плана** с командой +2. **Создание feature branch** для новой функциональности +3. **Начало реализации** с обновления типов +4. **Итеративная разработка** с тестированием +5. **Code review** перед merge +6. **Мониторинг** после деплоя diff --git a/docs/plans/014-offline-mode-implementation.md b/docs/plans/014-offline-mode-implementation.md new file mode 100644 index 0000000..d8f2248 --- /dev/null +++ b/docs/plans/014-offline-mode-implementation.md @@ -0,0 +1,251 @@ +# План реализации Offline режима + +## 📱 Обзор + +**Цель**: Реализовать полноценный offline режим для PWA приложения Strive, позволяющий пользователям работать без интернет-соединения. + +**Приоритет**: Средний +**Время выполнения**: 2-3 недели +**Статус**: ⏳ Ожидает + +## 🎯 Основные задачи + +### **Этап 1: Базовый offline режим (1 неделя)** + +#### **1.1 Создание сервисов** +- [ ] **OfflineService** - управление offline состоянием +- [ ] **NetworkService** - отслеживание подключения к интернету +- [ ] **StorageService** - локальное хранение данных +- [ ] **ConnectionStatusComponent** - UI индикатор подключения + +#### **1.2 Локальное хранение** +- [ ] Настроить IndexedDB для сложных данных +- [ ] Использовать localStorage для простых настроек +- [ ] Реализовать кэширование API ответов +- [ ] Сохранять расчеты калорий локально + +#### **1.3 UI индикаторы** +- [ ] Индикатор подключения к интернету +- [ ] Предупреждение о offline режиме +- [ ] Статус синхронизации данных + +### **Этап 2: Синхронизация данных (1 неделя)** + +#### **2.1 Очередь запросов** +- [ ] **RequestQueueService** - сохранение отложенных запросов +- [ ] **BackgroundSyncService** - автоматическая синхронизация +- [ ] **RetryService** - повторные попытки для failed запросов + +#### **2.2 Конфликт-резолюшн** +- [ ] **ConflictResolutionService** - обработка конфликтов данных +- [ ] **DataMergeService** - слияние изменений +- [ ] **VersionControlService** - отслеживание версий данных + +#### **2.3 Синхронизация с бэкендом** +- [ ] Интеграция с API (когда появится) +- [ ] Автоматическая синхронизация при восстановлении связи +- [ ] Уведомления о статусе синхронизации + +### **Этап 3: Продвинутые функции (1 неделя)** + +#### **3.1 Offline уведомления** +- [ ] **NotificationService** - уведомления о offline статусе +- [ ] **SyncStatusComponent** - детальный статус синхронизации +- [ ] **OfflineWarningComponent** - предупреждения пользователю + +#### **3.2 Аналитика offline действий** +- [ ] **OfflineAnalyticsService** - отслеживание offline поведения +- [ ] **UsageTrackingService** - статистика использования +- [ ] **PerformanceMonitoringService** - мониторинг производительности + +## 🔧 Технические детали + +### **Архитектура сервисов** + +```typescript +// src/shared/services/offline/ +├── offline.service.ts // Главный сервис offline режима +├── network.service.ts // Отслеживание сети +├── storage.service.ts // Локальное хранение +├── sync.service.ts // Синхронизация данных +├── request-queue.service.ts // Очередь запросов +├── conflict-resolution.service.ts // Разрешение конфликтов +└── index.ts // Public API +``` + +### **UI компоненты** + +```typescript +// src/shared/ui/ +├── connection-status/ // Индикатор подключения +│ ├── connection-status.component.ts +│ ├── connection-status.component.html +│ ├── connection-status.component.scss +│ └── index.ts +├── sync-indicator/ // Индикатор синхронизации +│ ├── sync-indicator.component.ts +│ ├── sync-indicator.component.html +│ ├── sync-indicator.component.scss +│ └── index.ts +└── offline-warning/ // Предупреждение об offline + ├── offline-warning.component.ts + ├── offline-warning.component.html + ├── offline-warning.component.scss + └── index.ts +``` + +### **Интеграция с существующими сервисами** + +#### **AuthService** +```typescript +// Добавить offline поддержку: +- Кэширование токенов +- Offline авторизация +- Синхронизация при восстановлении связи +``` + +#### **CalorieCalculatorService** +```typescript +// Добавить локальное кэширование: +- Сохранение расчетов в IndexedDB +- Offline расчеты калорий +- Синхронизация с сервером +``` + +#### **UserStoreService** +```typescript +// Добавить синхронизацию: +- Локальное хранение профиля +- Offline обновления данных +- Конфликт-резолюшн при синхронизации +``` + +## 📊 Приоритизация функций + +### **Высокий приоритет (MVP)** +1. **Индикатор подключения** - показать пользователю статус +2. **Локальное кэширование** - сохранять данные в localStorage/IndexedDB +3. **Offline расчеты калорий** - работа без интернета +4. **Очередь запросов** - откладывать API вызовы + +### **Средний приоритет** +1. **Background sync** - автоматическая синхронизация +2. **Конфликт-резолюшн** - обработка одновременных изменений +3. **Offline уведомления** - информирование пользователя +4. **Аналитика offline действий** - отслеживание поведения + +### **Низкий приоритет** +1. **Продвинутые offline функции** - полная автономность +2. **Оптимизация производительности** - улучшение скорости +3. **Расширенная аналитика** - детальная статистика + +## 🧪 Тестирование + +### **Unit тесты** +- [ ] OfflineService - тестирование offline логики +- [ ] NetworkService - тестирование отслеживания сети +- [ ] StorageService - тестирование локального хранения +- [ ] SyncService - тестирование синхронизации + +### **Integration тесты** +- [ ] Offline режим с AuthService +- [ ] Offline режим с CalorieCalculatorService +- [ ] Синхронизация данных между сервисами +- [ ] Конфликт-резолюшн в реальных сценариях + +### **E2E тесты** +- [ ] Полный цикл offline → online → синхронизация +- [ ] Offline расчеты калорий +- [ ] Синхронизация пользовательских данных +- [ ] Обработка ошибок в offline режиме + +## 📈 Метрики успеха + +### **Функциональные метрики** +- [ ] 100% функциональность в offline режиме +- [ ] Автоматическая синхронизация при восстановлении связи +- [ ] Корректная обработка конфликтов данных +- [ ] Стабильная работа без интернета + +### **UX метрики** +- [ ] Понятные индикаторы offline статуса +- [ ] Быстрая синхронизация данных +- [ ] Отсутствие потери данных +- [ ] Удобные уведомления о статусе + +### **Технические метрики** +- [ ] Покрытие тестами > 90% +- [ ] Производительность не ухудшается +- [ ] Размер bundle не увеличивается значительно +- [ ] Совместимость с существующими функциями + +## 🚀 План реализации + +### **Неделя 1: Базовый offline режим** +- День 1-2: Создание OfflineService и NetworkService +- День 3-4: Реализация StorageService и локального хранения +- День 5: Создание UI компонентов и интеграция + +### **Неделя 2: Синхронизация данных** +- День 1-2: Реализация RequestQueueService и BackgroundSyncService +- День 3-4: Создание ConflictResolutionService +- День 5: Интеграция с существующими сервисами + +### **Неделя 3: Продвинутые функции** +- День 1-2: Реализация уведомлений и аналитики +- День 3-4: Тестирование и оптимизация +- День 5: Документация и финальная проверка + +## 🔗 Зависимости + +### **Внутренние зависимости** +- AuthService - для offline авторизации +- CalorieCalculatorService - для offline расчетов +- UserStoreService - для синхронизации данных +- PWA Service Worker - для кэширования + +### **Внешние зависимости** +- IndexedDB API - для локального хранения +- Cache API - для кэширования ресурсов +- Background Sync API - для синхронизации +- Notification API - для уведомлений + +## 📋 Чеклист готовности + +### **Перед началом** +- [ ] Проанализировать существующие сервисы +- [ ] Определить критические данные для offline режима +- [ ] Создать архитектуру сервисов +- [ ] Настроить тестовую среду + +### **Во время разработки** +- [ ] Следовать принципам FSD архитектуры +- [ ] Писать тесты для каждого сервиса +- [ ] Документировать API и интерфейсы +- [ ] Проверять совместимость с существующим кодом + +### **После завершения** +- [ ] Провести полное тестирование +- [ ] Оптимизировать производительность +- [ ] Обновить документацию +- [ ] Подготовить к продакшену + +## 🎯 Ожидаемые результаты + +### **Для пользователей** +- Возможность работать без интернета +- Автоматическая синхронизация данных +- Понятные индикаторы статуса +- Отсутствие потери данных + +### **Для разработчиков** +- Чистая архитектура сервисов +- Хорошее покрытие тестами +- Документированные API +- Легкая поддержка и расширение + +### **Для бизнеса** +- Улучшенный UX приложения +- Повышенная надежность +- Лучшая производительность +- Конкурентное преимущество diff --git a/docs/plans/015-toast-messages-api-error-handling.md b/docs/plans/015-toast-messages-api-error-handling.md new file mode 100644 index 0000000..9fcf56b --- /dev/null +++ b/docs/plans/015-toast-messages-api-error-handling.md @@ -0,0 +1,579 @@ +# План интеграции Toast-сообщений для обработки ошибок API + +## 📊 Обзор + +**Цель**: Создать универсальную систему toast-уведомлений для обработки ошибок API и улучшения пользовательского опыта в приложении Strive. + +**Приоритет**: Высокий +**Время выполнения**: 1-2 недели +**Статус**: ⏳ Ожидает + +## 🎯 Основные задачи + +### **Этап 1: Создание ToastService (1 неделя)** + +#### **1.1 Базовый ToastService** +- [ ] **ToastService** - основной сервис для показа уведомлений +- [ ] **ToastComponent** - компонент для отображения toast +- [ ] **ToastContainerComponent** - контейнер для управления toast'ами +- [ ] **ToastTypes** - типы уведомлений (success, error, warning, info) + +#### **1.2 Интеграция с Taiga UI** +- [ ] Использование Taiga UI компонентов для стилизации +- [ ] Адаптация под тему приложения (light/dark) +- [ ] Анимации появления/исчезновения +- [ ] Позиционирование toast'ов + +#### **1.3 Конфигурация** +- [ ] Настройка времени показа (auto-dismiss) +- [ ] Максимальное количество одновременных toast'ов +- [ ] Позиционирование на экране +- [ ] Звуковые уведомления (опционально) + +### **Этап 2: Интеграция с API Error Handling (1 неделя)** + +#### **2.1 Расширение handleApiError** +- [ ] **ApiErrorInterceptor** - перехватчик ошибок API +- [ ] **ToastErrorMapper** - маппинг ошибок в toast сообщения +- [ ] **ErrorContextService** - контекст для ошибок +- [ ] **RetryService** - сервис для повторных попыток + +#### **2.2 Типизированные ошибки** +- [ ] **ApiErrorTypes** - типы ошибок API +- [ ] **ErrorMessages** - сообщения для разных типов ошибок +- [ ] **ErrorActions** - действия для ошибок (retry, dismiss, etc.) +- [ ] **ErrorLogging** - логирование ошибок + +#### **2.3 Интеграция с существующими сервисами** +- [ ] **AuthService** - ошибки авторизации +- [ ] **CalorieApiService** - ошибки расчета калорий +- [ ] **UserApiService** - ошибки пользовательских данных +- [ ] **OfflineSyncService** - ошибки синхронизации + +## 🔧 Технические детали + +### **Архитектура сервисов** + +```typescript +// src/shared/services/toast/ +├── toast.service.ts // Главный сервис toast уведомлений +├── toast-error-mapper.service.ts // Маппинг ошибок в сообщения +├── toast-config.service.ts // Конфигурация toast'ов +├── toast-queue.service.ts // Очередь toast'ов +└── index.ts // Public API +``` + +### **UI компоненты** + +```typescript +// src/shared/ui/ +├── toast/ // Toast компоненты +│ ├── toast/ +│ │ ├── toast.component.ts +│ │ ├── toast.component.html +│ │ ├── toast.component.scss +│ │ └── index.ts +│ ├── toast-container/ +│ │ ├── toast-container.component.ts +│ │ ├── toast-container.component.html +│ │ ├── toast-container.component.scss +│ │ └── index.ts +│ └── index.ts +``` + +### **Типы и интерфейсы** + +```typescript +// src/shared/lib/types/toast.types.ts +export interface ToastMessage { + id: string; + type: ToastType; + title?: string; + message: string; + duration?: number; + actions?: ToastAction[]; + persistent?: boolean; +} + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface ToastAction { + label: string; + action: () => void; + type?: 'primary' | 'secondary'; +} + +export interface ToastConfig { + position: ToastPosition; + maxToasts: number; + defaultDuration: number; + enableSound: boolean; +} + +export type ToastPosition = + | 'top-right' + | 'top-left' + | 'bottom-right' + | 'bottom-left' + | 'top-center' + | 'bottom-center'; +``` + +## 📋 Детальный план реализации + +### **Этап 1: Создание ToastService (5-7 часов)** + +#### 1.1 Базовый ToastService +```typescript +@Injectable({ providedIn: 'root' }) +export class ToastService { + private readonly toastQueue = signal([]); + private readonly config = signal({ + position: 'top-right', + maxToasts: 5, + defaultDuration: 5000, + enableSound: false, + }); + + readonly toasts = this.toastQueue.asReadonly(); + + success(message: string, title?: string, options?: Partial): void { + this.show({ type: 'success', message, title, ...options }); + } + + error(message: string, title?: string, options?: Partial): void { + this.show({ type: 'error', message, title, persistent: true, ...options }); + } + + warning(message: string, title?: string, options?: Partial): void { + this.show({ type: 'warning', message, title, ...options }); + } + + info(message: string, title?: string, options?: Partial): void { + this.show({ type: 'info', message, title, ...options }); + } + + private show(toast: Omit): void { + const id = this.generateId(); + const newToast: ToastMessage = { + id, + duration: this.config().defaultDuration, + ...toast, + }; + + this.toastQueue.update(toasts => { + const updated = [...toasts, newToast]; + return updated.slice(-this.config().maxToasts); + }); + + if (newToast.duration && newToast.duration > 0) { + setTimeout(() => this.dismiss(id), newToast.duration); + } + } + + dismiss(id: string): void { + this.toastQueue.update(toasts => toasts.filter(t => t.id !== id)); + } + + dismissAll(): void { + this.toastQueue.set([]); + } + + private generateId(): string { + return `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } +} +``` + +#### 1.2 ToastComponent +```typescript +@Component({ + selector: 'app-toast', + template: ` +
+ @if (toast.title) { +
{{ toast.title }}
+ } +
{{ toast.message }}
+ @if (toast.actions && toast.actions.length > 0) { +
+ @for (action of toast.actions; track action.label) { + + } +
+ } + +
+ `, + styleUrls: ['./toast.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToastComponent { + @Input({ required: true }) toast!: ToastMessage; + @Output() dismiss = new EventEmitter(); + + onDismiss(): void { + this.dismiss.emit(this.toast.id); + } +} +``` + +#### 1.3 ToastContainerComponent +```typescript +@Component({ + selector: 'app-toast-container', + template: ` +
+ @for (toast of toasts(); track toast.id) { + + } +
+ `, + styleUrls: ['./toast-container.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToastContainerComponent { + private readonly toastService = inject(ToastService); + private readonly configService = inject(ToastConfigService); + + readonly toasts = this.toastService.toasts; + readonly position = this.configService.position; + + onDismiss(id: string): void { + this.toastService.dismiss(id); + } +} +``` + +### **Этап 2: Интеграция с API Error Handling (5-7 часов)** + +#### 2.1 ToastErrorMapperService +```typescript +@Injectable({ providedIn: 'root' }) +export class ToastErrorMapperService { + private readonly errorMessages: Record = { + 'NETWORK_ERROR': 'Network connection error. Please check your internet connection.', + 'TIMEOUT': 'Request timeout. Please try again.', + 'UNAUTHORIZED': 'Authentication required. Please login again.', + 'FORBIDDEN': 'Access denied. You don\'t have permission to perform this action.', + 'NOT_FOUND': 'Requested resource not found.', + 'VALIDATION_ERROR': 'Please check your input and try again.', + 'SERVER_ERROR': 'Server error occurred. Please try again later.', + 'RATE_LIMIT': 'Too many requests. Please wait a moment and try again.', + }; + + private readonly errorActions: Record = { + 'NETWORK_ERROR': [ + { label: 'Retry', action: () => this.retryAction(), type: 'primary' }, + { label: 'Dismiss', action: () => {}, type: 'secondary' }, + ], + 'UNAUTHORIZED': [ + { label: 'Login', action: () => this.navigateToLogin(), type: 'primary' }, + ], + }; + + mapApiErrorToToast(error: ApiError): ToastMessage { + const message = this.errorMessages[error.code] || error.message || 'An unexpected error occurred.'; + const actions = this.errorActions[error.code] || []; + + return { + id: this.generateId(), + type: this.getToastType(error.code), + title: this.getErrorTitle(error.code), + message, + actions: actions.length > 0 ? actions : undefined, + persistent: this.isPersistentError(error.code), + }; + } + + private getToastType(errorCode: string): ToastType { + const errorTypeMap: Record = { + 'NETWORK_ERROR': 'error', + 'TIMEOUT': 'error', + 'UNAUTHORIZED': 'warning', + 'FORBIDDEN': 'warning', + 'NOT_FOUND': 'warning', + 'VALIDATION_ERROR': 'warning', + 'SERVER_ERROR': 'error', + 'RATE_LIMIT': 'warning', + }; + + return errorTypeMap[errorCode] || 'error'; + } + + private getErrorTitle(errorCode: string): string { + const titleMap: Record = { + 'NETWORK_ERROR': 'Connection Error', + 'TIMEOUT': 'Request Timeout', + 'UNAUTHORIZED': 'Authentication Required', + 'FORBIDDEN': 'Access Denied', + 'NOT_FOUND': 'Not Found', + 'VALIDATION_ERROR': 'Validation Error', + 'SERVER_ERROR': 'Server Error', + 'RATE_LIMIT': 'Rate Limited', + }; + + return titleMap[errorCode] || 'Error'; + } + + private isPersistentError(errorCode: string): boolean { + return ['UNAUTHORIZED', 'FORBIDDEN', 'SERVER_ERROR'].includes(errorCode); + } + + private retryAction(): void { + // Implement retry logic + } + + private navigateToLogin(): void { + // Navigate to login page + } + + private generateId(): string { + return `error-toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } +} +``` + +#### 2.2 ApiErrorInterceptor +```typescript +@Injectable() +export class ApiErrorInterceptor implements HttpInterceptor { + private readonly toastService = inject(ToastService); + private readonly errorMapper = inject(ToastErrorMapperService); + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + catchError((error: HttpErrorResponse) => { + const apiError = this.mapHttpErrorToApiError(error); + const toastMessage = this.errorMapper.mapApiErrorToToast(apiError); + + this.toastService.error( + toastMessage.message, + toastMessage.title, + { + actions: toastMessage.actions, + persistent: toastMessage.persistent, + } + ); + + return throwError(() => apiError); + }) + ); + } + + private mapHttpErrorToApiError(error: HttpErrorResponse): ApiError { + const errorCode = this.getErrorCode(error); + const message = error.error?.message || error.message || 'Unknown error'; + + return { + code: errorCode, + message, + status: error.status, + timestamp: new Date().toISOString(), + }; + } + + private getErrorCode(error: HttpErrorResponse): string { + if (!navigator.onLine) return 'NETWORK_ERROR'; + if (error.status === 0) return 'NETWORK_ERROR'; + if (error.status === 401) return 'UNAUTHORIZED'; + if (error.status === 403) return 'FORBIDDEN'; + if (error.status === 404) return 'NOT_FOUND'; + if (error.status === 422) return 'VALIDATION_ERROR'; + if (error.status === 429) return 'RATE_LIMIT'; + if (error.status >= 500) return 'SERVER_ERROR'; + + return 'UNKNOWN_ERROR'; + } +} +``` + +#### 2.3 Интеграция с существующими сервисами +```typescript +// В AuthService +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly toastService = inject(ToastService); + + login$(body: LoginRequest): Observable { + this.loading.set(true); + this.error.set(null); + + return this.authApi.login$(body).pipe( + tap(() => { + this.toastService.success('Welcome back!', 'Login successful'); + }), + catchError((error: ApiError) => { + // Toast уже показан через interceptor + this.error.set(error.message); + return of(undefined); + }), + finalize(() => this.loading.set(false)), + ); + } +} + +// В CalorieApiService +@Injectable({ providedIn: 'root' }) +export class CalorieApiService { + private readonly toastService = inject(ToastService); + + calculateCalories(data: CalorieCalculationData): Observable { + return this.http.post('/api/v1/calorie/calculate', data).pipe( + tap(() => { + this.toastService.success('Calories calculated successfully!'); + }), + catchError((error: ApiError) => { + // Toast уже показан через interceptor + return throwError(() => error); + }) + ); + } +} +``` + +## 🧪 Тестирование + +### **Unit тесты** +- [ ] ToastService - тестирование всех методов +- [ ] ToastErrorMapperService - тестирование маппинга ошибок +- [ ] ToastComponent - тестирование отображения +- [ ] ToastContainerComponent - тестирование управления + +### **Integration тесты** +- [ ] Интеграция с API interceptor +- [ ] Интеграция с существующими сервисами +- [ ] Тестирование различных типов ошибок +- [ ] Тестирование действий в toast'ах + +### **E2E тесты** +- [ ] Отображение toast'ов в UI +- [ ] Автоматическое исчезновение +- [ ] Действия пользователя (dismiss, retry) +- [ ] Обработка ошибок API + +## 📈 Метрики успеха + +### **Функциональные метрики** +- [ ] Все типы ошибок API показывают соответствующие toast'ы +- [ ] Toast'ы появляются и исчезают корректно +- [ ] Действия в toast'ах работают правильно +- [ ] Интеграция с существующими сервисами работает + +### **UX метрики** +- [ ] Понятные сообщения об ошибках +- [ ] Быстрая обратная связь для пользователя +- [ ] Не мешает основному интерфейсу +- [ ] Адаптируется под тему приложения + +### **Технические метрики** +- [ ] Покрытие тестами > 90% +- [ ] Производительность не ухудшается +- [ ] Размер bundle не увеличивается значительно +- [ ] Совместимость с существующими функциями + +## 🚀 План реализации + +### **Неделя 1: Создание ToastService** +- День 1-2: Базовый ToastService и компоненты +- День 3-4: Стилизация и анимации +- День 5: Тестирование и интеграция + +### **Неделя 2: Интеграция с API** +- День 1-2: ToastErrorMapperService и ApiErrorInterceptor +- День 3-4: Интеграция с существующими сервисами +- День 5: Тестирование и финальная проверка + +## 🔗 Зависимости + +### **Внутренние зависимости** +- Taiga UI - для компонентов и стилизации +- ThemeService - для адаптации под тему +- AuthService - для ошибок авторизации +- CalorieApiService - для ошибок расчета калорий +- UserApiService - для ошибок пользовательских данных + +### **Внешние зависимости** +- Angular HTTP Interceptors +- RxJS для обработки ошибок +- CSS animations для анимаций + +## 📋 Чеклист готовности + +### **Перед началом** +- [ ] Проанализировать существующие ошибки API +- [ ] Определить типы ошибок и сообщения +- [ ] Создать архитектуру сервисов +- [ ] Настроить тестовую среду + +### **Во время разработки** +- [ ] Следовать принципам FSD архитектуры +- [ ] Писать тесты для каждого сервиса +- [ ] Документировать API и интерфейсы +- [ ] Проверять совместимость с существующим кодом + +### **После завершения** +- [ ] Провести полное тестирование +- [ ] Оптимизировать производительность +- [ ] Обновить документацию +- [ ] Подготовить к продакшену + +## 🎯 Ожидаемые результаты + +### **Для пользователей** +- Понятные сообщения об ошибках +- Быстрая обратная связь +- Возможность действий с ошибками (retry, dismiss) +- Улучшенный UX приложения + +### **Для разработчиков** +- Унифицированная система обработки ошибок +- Легкое добавление новых типов ошибок +- Централизованное управление сообщениями +- Хорошее покрытие тестами + +### **Для бизнеса** +- Снижение количества обращений в поддержку +- Улучшенное понимание ошибок пользователями +- Повышенная надежность приложения +- Лучший пользовательский опыт + +## ⚠️ Риски и митигация + +### **Потенциальные риски** +1. **Перегрузка интерфейса toast'ами** + - Митигация: Ограничение количества одновременных toast'ов +2. **Дублирование сообщений** + - Митигация: Дедупликация по типу ошибки +3. **Производительность** + - Митигация: Ленивая загрузка и оптимизация + +### **Критерии остановки** +- Если toast'ы мешают основному функционалу +- Если производительность значительно ухудшается +- Если пользователи жалуются на избыточность уведомлений + +## 🎯 Заключение + +Данный план обеспечивает создание универсальной системы toast-уведомлений, которая значительно улучшит пользовательский опыт при работе с ошибками API в приложении Strive. + +**Общий бюджет времени:** 10-14 часов +**Ожидаемое улучшение UX:** Значительное улучшение обратной связи с пользователем diff --git a/docs/plans/calorie-service-frontend-integration.md b/docs/plans/calorie-service-frontend-integration.md deleted file mode 100644 index ae9d687..0000000 --- a/docs/plans/calorie-service-frontend-integration.md +++ /dev/null @@ -1,60 +0,0 @@ -## Calorie Service Frontend Integration Plan (Angular → Go backend) - -### Goals -- Replace in-browser calculations with Go API calls. -- Remove localStorage usage. -- Leverage PWA capabilities for resilient UX: offline queue, background sync, caching. - -### API Contracts -- POST `/api/calories/calculate` → `CalorieResults` -- GET `/api/calories/last` → `{ data, results } | 404` -- JSON fields match current TS models. - -### Angular Changes -- `CalorieApiService`: - - `calculateCalories(data)` → `HttpClient.post` - - `getCaloriesResult()` → `HttpClient.get<{data: CalorieCalculationData; results: CalorieResults} | null>` - - Remove `setTimeout` and client-side formulas (optionally keep as fallback behind feature flag) -- `CalorieCalculatorService` remains the same (signals/state unchanged). -- Add `ApiBaseUrl` configuration via environments. - -### PWA Enhancements (no localStorage) -- IndexedDB Request Queue - - Store pending calculation requests when offline - - Replay on reconnect (online event) or via Background Sync -- Background Sync (if supported) - - Register sync tag `calories-calc-sync` - - Service Worker consumes queued requests and POSTs to backend -- Caching Strategy via Angular Service Worker - - `ngsw-config.json`: dataGroups for - - `POST /api/calories/calculate` → freshness (no cache of responses, but queue when offline) - - `GET /api/calories/last` → performance with short maxAge and ETag revalidation -- ETag/If-None-Match - - Use ETag headers from backend for `last` endpoint; SW respects 304 - -### Error Handling -- Unified API error mapper (status → user-visible message) -- Timeouts and retry with backoff for idempotent `GET /last` only -- For `POST /calculate` avoid automatic retries in foreground; rely on queue/sync - -### Security -- Add interceptor for Telegram initData/JWT header -- CORS aligned on backend - -### Testing -- HttpClientTestingModule specs for both methods (200/404/error) -- E2E or integration test covering offline queue replay (can be documented/manual if complex) - -### Migration Steps -1. Implement backend (per backend plan) -2. Add Angular env `apiBaseUrl` and HTTP interceptor -3. Replace `CalorieApiService` internals with HttpClient -4. Configure `ngsw-config.json` dataGroups (cache + queue) -5. Implement IndexedDB queue + SW sync handler -6. Update docs and remove old client-side logic - -### Open Questions -- Auth source: Telegram initData or other? -- Should client fallback to local compute when offline (feature flag)? Default: no. - - diff --git a/src/app/app.component.html b/src/app/app.component.html index 271629d..d944606 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,9 +1,30 @@ -
- - -
- -
-
+ @if (loading()) { +
+
+

Loading...

+
+ } @else if (error()) { +
+
⚠️
+

Something went wrong

+

{{ error() }}

+ +
+ } @else { +
+ + +
+ +
+
+ }
\ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 44443b8..3d5b595 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -65,3 +65,73 @@ padding-bottom: env(safe-area-inset-bottom, 0); } } + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 100%; + height: 100vh; + + background: var(--tui-base-01); +} + +.loading-spinner { + width: 40px; + height: 40px; + margin-bottom: var(--space-md); + border: 3px solid var(--tui-base-03); + border-top: 3px solid var(--tui-primary); + border-radius: 50%; + + animation: spin 1s linear infinite; +} + +.loading-text { + font: var(--tui-font-text-m); + color: var(--tui-text-02); +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 100%; + height: 100vh; + padding: var(--space-lg); + + text-align: center; + + background: var(--tui-base-01); +} + +.error-icon { + margin-bottom: var(--space-md); + font-size: 48px; +} + +.error-title { + margin: 0 0 var(--space-sm) 0; + font: var(--tui-font-heading-4); + color: var(--tui-text-01); +} + +.error-message { + max-width: 400px; + margin: 0 0 var(--space-lg) 0; + font: var(--tui-font-text-m); + color: var(--tui-text-02); +} + +.retry-button { + min-width: 120px; +} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index e19d324..6046114 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { TuiRoot } from '@taiga-ui/core'; +import { of } from 'rxjs'; import { AuthService } from '@/features/auth'; import { TelegramService, ThemeService, SwUpdateService, UserStoreService } from '@/shared'; import { configureZonelessTestingModule } from '@/test-setup'; @@ -48,10 +49,16 @@ describe('AppComponent', () => { swUpdateServiceSpy = jasmine.createSpyObj('SwUpdateService', ['checkForUpdate', 'init']); - const userStoreSpy = jasmine.createSpyObj('UserStoreService', ['clearUser'], { - user: jasmine.createSpy().and.returnValue(null), - isAuthenticated: jasmine.createSpy().and.returnValue(false), - }); + const userStoreSpy = jasmine.createSpyObj( + 'UserStoreService', + ['clearUser', 'fetchUser$', 'initializeFromCache'], + { + user: jasmine.createSpy().and.returnValue(null), + isAuthenticated: jasmine.createSpy().and.returnValue(false), + }, + ); + + userStoreSpy.fetchUser$.and.returnValue(of(null)); configureZonelessTestingModule({ imports: [AppComponent, MockTuiRootComponent], diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a3c6f2f..c67725c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,14 +1,16 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, DestroyRef, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { RouterOutlet } from '@angular/router'; -import { TuiRoot } from '@taiga-ui/core'; +import { TuiRoot, TuiButton } from '@taiga-ui/core'; +import { forkJoin, finalize, catchError, of } from 'rxjs'; -import { TelegramService, ThemeService, SwUpdateService } from '@/shared'; +import { TelegramService, ThemeService, SwUpdateService, UserStoreService } from '@/shared'; import { NavigationComponent } from '@/widgets'; import type { OnInit } from '@angular/core'; @Component({ selector: 'app-root', - imports: [RouterOutlet, NavigationComponent, TuiRoot], + imports: [RouterOutlet, NavigationComponent, TuiRoot, TuiButton], templateUrl: './app.component.html', styleUrl: './app.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -17,10 +19,42 @@ export class AppComponent implements OnInit { private readonly telegramService = inject(TelegramService); private readonly themeService = inject(ThemeService); private readonly swUpdateService = inject(SwUpdateService); + private readonly userStore = inject(UserStoreService); + private readonly destroyRef = inject(DestroyRef); + + readonly loading = signal(true); + readonly error = signal(null); ngOnInit(): void { + this.initializeApp(); + } + + private initializeApp(): void { + this.loading.set(true); + this.error.set(null); + this.telegramService.webApp.ready(); this.themeService.initialize(); this.swUpdateService.init(); + + const initializationTasks = [this.userStore.fetchUser$()]; + + forkJoin(initializationTasks) + .pipe( + catchError((error) => { + console.error('Initialization failed:', error); + this.error.set('Failed to load application data. Please try again.'); + return of(null); + }), + finalize(() => { + this.loading.set(false); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + } + + retryInitialization(): void { + this.initializeApp(); } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e7fd971..6bb3aa3 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,10 +1,10 @@ -import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http'; import { isDevMode, provideExperimentalZonelessChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideServiceWorker } from '@angular/service-worker'; import { provideEventPlugins } from '@taiga-ui/event-plugins'; import { routes } from '@/app/app.routes'; -import { authInterceptor } from './interceptors/auth.interceptor'; +import { authInterceptor } from '@/app/interceptors'; import { credentialsInterceptor } from './interceptors/credentials.interceptor'; import type { ApplicationConfig } from '@angular/core'; @@ -12,7 +12,7 @@ export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection(), provideRouter(routes), - provideHttpClient(withInterceptors([authInterceptor, credentialsInterceptor])), + provideHttpClient(withInterceptors([authInterceptor, credentialsInterceptor]), withFetch()), provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), registrationStrategy: 'registerImmediately', diff --git a/src/app/interceptors/auth.interceptor.spec.ts b/src/app/interceptors/auth.interceptor.spec.ts index 3ab41f3..61fd69b 100644 --- a/src/app/interceptors/auth.interceptor.spec.ts +++ b/src/app/interceptors/auth.interceptor.spec.ts @@ -19,6 +19,7 @@ describe('authInterceptor', () => { 'getAccessToken', 'setAccessToken', 'refreshToken$', + 'getTokenInfo', ]); const routerSpy = jasmine.createSpyObj('Router', ['navigate']); @@ -53,6 +54,7 @@ describe('authInterceptor', () => { it('should not add authorization header when no token', () => { authService.getAccessToken.and.returnValue(null); + authService.getTokenInfo.and.returnValue(null); http.get('/api/protected').subscribe(); @@ -62,6 +64,7 @@ describe('authInterceptor', () => { it('should handle non-401 errors without refresh', (done) => { authService.getAccessToken.and.returnValue('test-token'); + authService.getTokenInfo.and.returnValue(null); http.get('/api/protected').subscribe({ next: () => done(), @@ -80,6 +83,7 @@ describe('authInterceptor', () => { router.navigate.and.returnValue(Promise.resolve(true)); authService.getAccessToken.and.returnValue('test-token'); + authService.getTokenInfo.and.returnValue(null); authService.refreshToken$.and.returnValue(of(false)); http.get('/api/protected').subscribe({ @@ -98,6 +102,7 @@ describe('authInterceptor', () => { const addPendingRequestSpy = spyOn(refreshManager, 'addPendingRequest').and.callThrough(); authService.getAccessToken.and.returnValue('test-token'); + authService.getTokenInfo.and.returnValue(null); authService.refreshToken$.and.returnValue(of(true)); refreshManager.setRefreshInProgress(true); diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts index e5e217c..c3ce97a 100644 --- a/src/app/interceptors/auth.interceptor.ts +++ b/src/app/interceptors/auth.interceptor.ts @@ -1,7 +1,7 @@ import { inject } from '@angular/core'; import { Router } from '@angular/router'; import { catchError, switchMap, throwError, defer, finalize, Observable } from 'rxjs'; -import { AuthService } from '@/features/auth'; +import { AuthService, type TokenInfo } from '@/features/auth'; import { TokenRefreshManager } from './token-refresh-manager'; import type { @@ -33,7 +33,8 @@ const handle401Error = ( return defer(() => { return new Observable>>((subscriber) => { refreshManager.addPendingRequest(() => { - subscriber.next(next(req)); + const authReq = createAuthRequest(req, authService); + subscriber.next(next(authReq)); subscriber.complete(); }); }); @@ -46,7 +47,8 @@ const handle401Error = ( switchMap((success) => { if (success) { refreshManager.processPendingRequests(); - return next(req); + const authReq = createAuthRequest(req, authService); + return next(authReq); } else { refreshManager.clearPendingRequests(); logout(router); @@ -64,6 +66,36 @@ const handle401Error = ( ); }; +const isTokenExpiringSoon = (tokenInfo: TokenInfo | null, minutesBeforeExpiry = 5): boolean => { + if (!tokenInfo) { + return false; + } + + try { + const now = new Date(); + const timeUntilExpiry = tokenInfo.expiresAt.getTime() - now.getTime(); + const minutesInMilliseconds = minutesBeforeExpiry * 60 * 1000; + + return timeUntilExpiry <= minutesInMilliseconds && timeUntilExpiry > 0; + } catch { + return true; + } +}; + +const createAuthRequest = ( + req: HttpRequest, + authService: AuthService, +): HttpRequest => { + const tokenInfo = authService.getTokenInfo(); + return tokenInfo + ? req.clone({ + setHeaders: { + Authorization: `Bearer ${tokenInfo.token}`, + }, + }) + : req; +}; + export const authInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn, @@ -75,16 +107,25 @@ export const authInterceptor: HttpInterceptorFn = ( const authService = inject(AuthService); const router = inject(Router); - const accessToken = authService.getAccessToken(); - if (!accessToken) { - return next(req); + const tokenInfo = authService.getTokenInfo(); + + if (isTokenExpiringSoon(tokenInfo, 5)) { + const refreshManager = TokenRefreshManager.getInstance(); + + if (!refreshManager.isRefreshInProgress) { + refreshManager.setRefreshInProgress(true); + authService + .refreshToken$() + .pipe( + finalize(() => { + refreshManager.setRefreshInProgress(false); + }), + ) + .subscribe(); + } } - const authReq = req.clone({ - setHeaders: { - Authorization: `Bearer ${accessToken}`, - }, - }); + const authReq = createAuthRequest(req, authService); return next(authReq).pipe( catchError((error: HttpErrorResponse) => { diff --git a/src/app/interceptors/credentials.interceptor.ts b/src/app/interceptors/credentials.interceptor.ts index 9af3365..58b73ce 100644 --- a/src/app/interceptors/credentials.interceptor.ts +++ b/src/app/interceptors/credentials.interceptor.ts @@ -10,7 +10,7 @@ export const credentialsInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn, ): Observable> => { - if (req.withCredentials === true) { + if (req.withCredentials) { return next(req); } @@ -20,6 +20,5 @@ export const credentialsInterceptor: HttpInterceptorFn = ( 'Content-Type': 'application/json', }, }); - return next(modifiedReq); }; diff --git a/src/entities/user/ui/user-menu/user-menu.component.html b/src/entities/user/ui/user-menu/user-menu.component.html index bc23475..e4a3160 100644 --- a/src/entities/user/ui/user-menu/user-menu.component.html +++ b/src/entities/user/ui/user-menu/user-menu.component.html @@ -1,8 +1,8 @@
- - +
- +
- - -