real back
This commit is contained in:
497
docs/compose/plans/2026-06-22-backend-stabilization.md
Normal file
497
docs/compose/plans/2026-06-22-backend-stabilization.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# Backend Stabilization Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Turn the current scaffold into a repeatable backend-first almost-production demo: real PostgreSQL persistence, real auth, real gateway-backed API responses, and passing root verification.
|
||||
|
||||
**Architecture:** Keep the current three-layer shape (`apps/web` -> `apps/gateway` -> `services/cmd/*`) and replace the shared Go `demoStore` with a PostgreSQL-backed store built on `GORM`. Preserve existing HTTP contracts where possible, add only the minimal new plumbing needed for config, auth, health checks, and smoke verification.
|
||||
|
||||
**Tech Stack:** npm workspaces, React 19, Vite, Axios, Zustand, Node.js + Elysia gateway, Go HTTP services, GORM, PostgreSQL 17, SQL migrations, Docker Compose.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- `package.json`: root verification commands and workspace entrypoints.
|
||||
- `README.md`: authoritative local run and acceptance instructions.
|
||||
- `.env.example`: example backend DSN, token secret, token TTL and API base URL.
|
||||
- `docker-compose.yml`: consistent local runtime for postgres, gateway, web and services.
|
||||
- `scripts/dev-backend.sh`: local backend boot with PostgreSQL-aware env.
|
||||
- `scripts/smoke-backend.sh`: curl-based critical-path smoke verification.
|
||||
- `apps/gateway/src/index.ts`: gateway proxying, auth fallback removal, unified error/health behavior.
|
||||
- `apps/web/src/shared/api/client.js`: correct default API base URL and error normalization.
|
||||
- `apps/web/src/shared/api/endpoints.js`: API contract used by the UI.
|
||||
- `apps/web/src/app/store/session.js`: login payload compatibility, `me`, logout, session reset.
|
||||
- `apps/web/src/pages/*.jsx`: key pages that currently assume mock-only data shapes.
|
||||
- `apps/web/src/**/*.test.*`: frontend regression tests for login and key data pages.
|
||||
- `services/go.mod`: PostgreSQL driver and password-hash dependency declarations.
|
||||
- `services/internal/service/types.go`: backend DTOs and enum/value mapping.
|
||||
- `services/internal/service/service.go`: service bootstrap, DB-aware health/readiness.
|
||||
- `services/internal/service/api.go`: HTTP handlers for auth, content, media, search, comments, subscriptions, notifications, admin and analytics.
|
||||
- `services/internal/service/store.go`: store interface and bootstrap hook.
|
||||
- `services/internal/service/db.go`: PostgreSQL connection, GORM bootstrap and migration helpers.
|
||||
- `services/internal/service/store_gorm.go`: GORM-backed implementation of the store.
|
||||
- `services/internal/service/auth.go`: password hashing and signed token helpers.
|
||||
- `services/internal/service/seed.go`: idempotent bootstrap data for roles, demo users and demo content.
|
||||
- `services/internal/service/*_test.go`: Go unit/integration coverage for critical backend flows.
|
||||
- `database/migrations/001_init.sql`: schema source of truth; must match current Go enum values and required tables.
|
||||
|
||||
### Task 1: Reproduce and lock the runtime baseline
|
||||
|
||||
**Covers:** [S1, S2, S7]
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `.env.example`
|
||||
- Modify: `package.json`
|
||||
- Modify: `scripts/dev-backend.sh`
|
||||
- Create: `scripts/smoke-backend.sh`
|
||||
|
||||
- [ ] **Step 1: Capture the failing baseline in notes and a smoke script stub**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run check
|
||||
```
|
||||
|
||||
Expected: failure is reproduced before code changes. Right now the first expected failure is the missing frontend toolchain (`vite: command not found`) or another dependency/runtime error from a clean install.
|
||||
|
||||
- [ ] **Step 2: Write the failing smoke script skeleton**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
API_BASE_URL="${API_BASE_URL:-http://127.0.0.1:3000/api}"
|
||||
|
||||
echo "[1/4] login"
|
||||
echo "[2/4] fetch current user"
|
||||
echo "[3/4] create draft content"
|
||||
echo "[4/4] read admin dashboard"
|
||||
|
||||
exit 1
|
||||
```
|
||||
|
||||
Save as `scripts/smoke-backend.sh` and mark it executable after the rest of the plan fills the real requests.
|
||||
|
||||
- [ ] **Step 3: Make the local runtime explicit and consistent**
|
||||
|
||||
Add backend env examples to `.env.example`:
|
||||
|
||||
```dotenv
|
||||
DATABASE_URL=postgres://fable:fable_dev_password@127.0.0.1:5432/fable?sslmode=disable
|
||||
TOKEN_SECRET=local-dev-secret-change-me
|
||||
TOKEN_TTL=24h
|
||||
VITE_API_URL=http://127.0.0.1:3000/api
|
||||
```
|
||||
|
||||
Update `scripts/dev-backend.sh` so every Go service receives `DATABASE_URL`, `TOKEN_SECRET` and `TOKEN_TTL` instead of relying on demo-only state.
|
||||
|
||||
- [ ] **Step 4: Update root docs and verification contract**
|
||||
|
||||
Document the intended acceptance flow in `README.md`:
|
||||
|
||||
```markdown
|
||||
1. `npm install`
|
||||
2. `docker compose up -d postgres`
|
||||
3. `npm run dev:backend`
|
||||
4. `npm run dev:web`
|
||||
5. `npm run check`
|
||||
6. `bash scripts/smoke-backend.sh`
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Re-run the root verification command**
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
|
||||
Expected: it may still fail, but only on real application gaps rather than missing setup instructions.
|
||||
|
||||
### Task 2: Replace the shared demo store with PostgreSQL-backed GORM bootstrap
|
||||
|
||||
**Covers:** [S2, S4, S5, S6]
|
||||
|
||||
**Files:**
|
||||
- Modify: `services/go.mod`
|
||||
- Modify: `services/internal/service/store.go`
|
||||
- Modify: `services/internal/service/service.go`
|
||||
- Create: `services/internal/service/db.go`
|
||||
- Create: `services/internal/service/store_gorm.go`
|
||||
- Create: `services/internal/service/seed.go`
|
||||
- Create: `services/internal/service/store_gorm_test.go`
|
||||
- Modify: `database/migrations/001_init.sql`
|
||||
|
||||
- [ ] **Step 1: Write the failing DB bootstrap test**
|
||||
|
||||
```go
|
||||
func TestBootstrapStoreLoadsSeededUsersAndContent(t *testing.T) {
|
||||
store := newTestGORMStore(t)
|
||||
|
||||
user, ok := store.UserByLogin(context.Background(), "demo_admin")
|
||||
if !ok {
|
||||
t.Fatal("expected seeded admin user")
|
||||
}
|
||||
|
||||
items, err := store.ListContent(context.Background(), ContentFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("list content: %v", err)
|
||||
}
|
||||
if len(items) == 0 {
|
||||
t.Fatal("expected seeded content")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing Go test**
|
||||
|
||||
```bash
|
||||
cd services && go test ./internal/service -run TestBootstrapStoreLoadsSeededUsersAndContent -v
|
||||
```
|
||||
|
||||
Expected: FAIL because there is no GORM bootstrap/store yet.
|
||||
|
||||
- [ ] **Step 3: Introduce DB config, connection and store interface**
|
||||
|
||||
In `services/internal/service/store.go`, replace the global concrete store with an interface:
|
||||
|
||||
```go
|
||||
type Store interface {
|
||||
UserByLogin(ctx context.Context, login string) (UserProfile, bool, error)
|
||||
ListContent(ctx context.Context, filter ContentFilter) ([]ContentItem, error)
|
||||
// add the remaining methods currently hanging off demoStore
|
||||
}
|
||||
```
|
||||
|
||||
In `services/internal/service/db.go`, add a PostgreSQL opener using `DATABASE_URL` and return a shared `*gorm.DB`.
|
||||
|
||||
- [ ] **Step 4: Implement GORM store and idempotent seed bootstrap**
|
||||
|
||||
Key shape for `services/internal/service/store_gorm.go`:
|
||||
|
||||
```go
|
||||
type gormStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (s *gormStore) UserByLogin(ctx context.Context, login string) (UserProfile, bool, error) {
|
||||
var user userRecord
|
||||
err := s.db.WithContext(ctx).Preload("Roles").Where("login = ?", login).First(&user).Error
|
||||
// map record -> API DTO
|
||||
}
|
||||
```
|
||||
|
||||
In `seed.go`, upsert roles, demo users, speakers, categories, tags, content, notifications and comments.
|
||||
|
||||
- [ ] **Step 5: Align the migration with Go values before wiring queries**
|
||||
|
||||
Fix the enum mismatch in `database/migrations/001_init.sql` so the stored content type matches Go (`event`, not `event_announcement`) or update Go and frontend in one consistent direction. Pick one representation and use it everywhere.
|
||||
|
||||
- [ ] **Step 6: Run the new store test and the existing Go service test**
|
||||
|
||||
```bash
|
||||
cd services && go test ./internal/service -run 'TestBootstrapStoreLoadsSeededUsersAndContent|TestHealthEndpoint' -v
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Implement real auth, session lookup and readiness behavior
|
||||
|
||||
**Covers:** [S2, S3, S5, S6, S7]
|
||||
|
||||
**Files:**
|
||||
- Modify: `services/internal/service/api.go`
|
||||
- Modify: `services/internal/service/types.go`
|
||||
- Create: `services/internal/service/auth.go`
|
||||
- Create: `services/internal/service/auth_test.go`
|
||||
- Modify: `services/internal/service/service.go`
|
||||
|
||||
- [ ] **Step 1: Write failing auth tests for register, login and me**
|
||||
|
||||
```go
|
||||
func TestAuthFlowRegisterLoginAndMe(t *testing.T) {
|
||||
h := newTestHandler(t, Config{Name: "auth", Domain: "auth"})
|
||||
|
||||
registerBody := `{"login":"new_user","password":"verysecret","name":"Новый пользователь"}`
|
||||
registerRec := performJSONRequest(t, h, http.MethodPost, "/api/auth/register", registerBody, "")
|
||||
if registerRec.Code != http.StatusCreated {
|
||||
t.Fatalf("register status = %d", registerRec.Code)
|
||||
}
|
||||
|
||||
token := readTokenFromResponse(t, registerRec.Body.Bytes())
|
||||
meRec := performJSONRequest(t, h, http.MethodGet, "/api/auth/me", "", token)
|
||||
if meRec.Code != http.StatusOK {
|
||||
t.Fatalf("me status = %d", meRec.Code)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing auth test**
|
||||
|
||||
```bash
|
||||
cd services && go test ./internal/service -run TestAuthFlowRegisterLoginAndMe -v
|
||||
```
|
||||
|
||||
Expected: FAIL because login currently accepts any password and returns a demo token.
|
||||
|
||||
- [ ] **Step 3: Add password hashing and signed tokens**
|
||||
|
||||
In `services/internal/service/auth.go` add minimal helpers:
|
||||
|
||||
```go
|
||||
func HashPassword(password string) (string, error) { /* bcrypt */ }
|
||||
func ComparePassword(hash, password string) error { /* bcrypt */ }
|
||||
func SignToken(secret string, claims TokenClaims, ttl time.Duration) (string, error) { /* HMAC-signed token */ }
|
||||
func ParseToken(secret, token string) (TokenClaims, error) { /* validate signature + expiry */ }
|
||||
```
|
||||
|
||||
Keep the token format simple and self-contained; do not preserve `demo-token-*` compatibility.
|
||||
|
||||
- [ ] **Step 4: Update auth handlers to use the SQL store**
|
||||
|
||||
Replace the login fallback in `api.go` with explicit credential checks:
|
||||
|
||||
```go
|
||||
user, ok, err := backendStore.UserByLogin(ctx, payload.Login)
|
||||
if err != nil { /* 500 */ }
|
||||
if !ok || ComparePassword(user.PasswordHash, payload.Password) != nil {
|
||||
writeAPIError(w, http.StatusUnauthorized, "INVALID_CREDENTIALS", "Неверный логин или пароль")
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
Also make `/auth/change-password` update the stored hash and make `/ready` fail when DB connectivity is broken.
|
||||
|
||||
- [ ] **Step 5: Run focused auth/readiness verification**
|
||||
|
||||
```bash
|
||||
cd services && go test ./internal/service -run 'TestAuthFlowRegisterLoginAndMe|TestHealthEndpoint' -v
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 4: Persist content, taxonomy, subscriptions, comments, admin and analytics endpoints
|
||||
|
||||
**Covers:** [S3, S4, S5, S6, S7]
|
||||
|
||||
**Files:**
|
||||
- Modify: `services/internal/service/api.go`
|
||||
- Modify: `services/internal/service/types.go`
|
||||
- Modify: `services/internal/service/store_gorm.go`
|
||||
- Create: `services/internal/service/content_api_test.go`
|
||||
- Create: `services/internal/service/admin_api_test.go`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for the critical domain path**
|
||||
|
||||
```go
|
||||
func TestContentCommentSubscriptionAndAdminFlow(t *testing.T) {
|
||||
h := newTestHandlerAsAdmin(t)
|
||||
|
||||
created := performJSONRequest(t, h, http.MethodPost, "/api/content", `{"title":"Новый материал","type":"article","category":"Статьи"}`, adminToken(t, h))
|
||||
if created.Code != http.StatusCreated {
|
||||
t.Fatalf("create content status = %d", created.Code)
|
||||
}
|
||||
|
||||
contentID := readItemID(t, created.Body.Bytes())
|
||||
comment := performJSONRequest(t, h, http.MethodPost, "/api/comments/"+contentID, `{"text":"Полезный материал"}`, userToken(t, h))
|
||||
if comment.Code != http.StatusCreated {
|
||||
t.Fatalf("create comment status = %d", comment.Code)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing domain test**
|
||||
|
||||
```bash
|
||||
cd services && go test ./internal/service -run TestContentCommentSubscriptionAndAdminFlow -v
|
||||
```
|
||||
|
||||
Expected: FAIL because handlers still depend on the in-memory methods and non-persistent side effects.
|
||||
|
||||
- [ ] **Step 3: Port each handler branch to GORM-backed methods without changing the external contract**
|
||||
|
||||
Implement GORM methods for:
|
||||
- content list/detail/create/update/delete;
|
||||
- events/media derived listings;
|
||||
- category/tag reads;
|
||||
- speaker reads;
|
||||
- comment list/create;
|
||||
- subscription list/create;
|
||||
- notification list/mark-read;
|
||||
- admin users/roles/audit/dashboard;
|
||||
- analytics summary;
|
||||
- search using PostgreSQL text search.
|
||||
|
||||
Represent the filter in Go explicitly:
|
||||
|
||||
```go
|
||||
type ContentFilter struct {
|
||||
Query string
|
||||
Category string
|
||||
Type string
|
||||
Sort string
|
||||
Limit int
|
||||
Exclude string
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Keep API payloads stable for the current frontend**
|
||||
|
||||
Return the same envelope keys the UI already expects:
|
||||
|
||||
```json
|
||||
{ "user": { ... } }
|
||||
{ "items": [ ... ] }
|
||||
{ "item": { ... } }
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
Do not introduce a second incompatible schema while stabilizing the backend.
|
||||
|
||||
- [ ] **Step 5: Run the focused Go verification for the critical path**
|
||||
|
||||
```bash
|
||||
cd services && go test ./internal/service -run 'TestContentCommentSubscriptionAndAdminFlow|TestAuthFlowRegisterLoginAndMe' -v
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 5: Remove gateway/frontend mock-only assumptions and align contracts
|
||||
|
||||
**Covers:** [S2, S3, S5, S7, S8]
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/gateway/src/index.ts`
|
||||
- Modify: `apps/web/src/shared/api/client.js`
|
||||
- Modify: `apps/web/src/shared/api/endpoints.js`
|
||||
- Modify: `apps/web/src/app/store/session.js`
|
||||
- Modify: `apps/web/src/pages/LoginPage.test.jsx`
|
||||
- Modify: `apps/web/src/pages/MaterialsPage.test.jsx`
|
||||
- Create: `apps/web/src/app/store/session.test.js`
|
||||
|
||||
- [ ] **Step 1: Write the failing frontend session test**
|
||||
|
||||
```js
|
||||
it("stores token after login and loads current user from /auth/me", async () => {
|
||||
authApi.login = vi.fn().mockResolvedValue({ accessToken: "real-token" });
|
||||
authApi.me = vi.fn().mockResolvedValue({ id: "u1", login: "demo_admin", roles: ["администратор"] });
|
||||
|
||||
const user = await useSession.getState().login({ email: "demo_admin", password: "secret123" });
|
||||
|
||||
expect(tokenStorage.get()).toBe("real-token");
|
||||
expect(user.login).toBe("demo_admin");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing frontend tests**
|
||||
|
||||
```bash
|
||||
npm --workspace @fable/web run test -- --runInBand
|
||||
```
|
||||
|
||||
Expected: FAIL because the current store sends `{ email, password }` while the backend expects `login`, and the default API URL points to `:8000/api` instead of the gateway on `:3000/api`.
|
||||
|
||||
- [ ] **Step 3: Fix the frontend contract with minimal surface area**
|
||||
|
||||
Make these concrete adjustments:
|
||||
- `client.js`: default `VITE_API_URL` to `http://localhost:3000/api`;
|
||||
- `client.js`: normalize backend errors from `{ error: { message } }` as well as `{ message }`;
|
||||
- `session.js`: submit `login` instead of `email`, while still reading the same form field from UI;
|
||||
- `session.js`: persist the `me` response shape without assuming mock-only fields.
|
||||
|
||||
Target login change:
|
||||
|
||||
```js
|
||||
login: async ({ email, password }) => {
|
||||
const response = await authApi.login({ login: email, password });
|
||||
const token = getToken(response);
|
||||
tokenStorage.set(token);
|
||||
const user = response.user ?? (await authApi.me());
|
||||
set({ user });
|
||||
persistUser(user);
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Remove gateway-local demo fallback behavior**
|
||||
|
||||
In `apps/gateway/src/index.ts`, delete or bypass the hardcoded `demoUser`, `tokens` and local content arrays once real service URLs are configured. Gateway behavior should become: proxy or fail explicitly, but never silently serve a shadow backend.
|
||||
|
||||
- [ ] **Step 5: Re-run frontend verification**
|
||||
|
||||
```bash
|
||||
npm --workspace @fable/web run test
|
||||
npm --workspace @fable/web run build
|
||||
npm --workspace @fable/gateway run check
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 6: Final smoke verification for local and Docker flows
|
||||
|
||||
**Covers:** [S2, S6, S7]
|
||||
|
||||
**Files:**
|
||||
- Modify: `docker-compose.yml`
|
||||
- Modify: `scripts/smoke-backend.sh`
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Fill the smoke script with real requests**
|
||||
|
||||
```bash
|
||||
TOKEN="$({
|
||||
curl -sS -X POST "$API_BASE_URL/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"login":"demo_admin","password":"demo_password"}'
|
||||
} | jq -r '.token // .accessToken')"
|
||||
|
||||
curl -sS "$API_BASE_URL/auth/me" -H "Authorization: Bearer $TOKEN"
|
||||
curl -sS -X POST "$API_BASE_URL/content" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"title":"Smoke draft","type":"article","category":"Статьи"}'
|
||||
curl -sS "$API_BASE_URL/admin/users" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the non-Docker local path**
|
||||
|
||||
Run in separate shells:
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
npm run dev:backend
|
||||
npm run dev:web
|
||||
bash scripts/smoke-backend.sh
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Verify the Docker path**
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
bash scripts/smoke-backend.sh
|
||||
```
|
||||
|
||||
Expected: PASS against the same API base URL.
|
||||
|
||||
- [ ] **Step 4: Run the final root verification command**
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Update the README acceptance section with the final proven commands**
|
||||
|
||||
```markdown
|
||||
Проверка готовности:
|
||||
- `npm run check`
|
||||
- `bash scripts/smoke-backend.sh`
|
||||
- `docker compose up --build`
|
||||
```
|
||||
|
||||
## Self-Review
|
||||
|
||||
- [x] Every spec section `[S1]`..`[S8]` is covered by at least one task.
|
||||
- [x] The migration/schema mismatch (`event_announcement` vs `event`) is explicitly addressed.
|
||||
- [x] No `TODO`/`TBD` placeholders remain in tasks.
|
||||
- [x] Frontend/gateway/backend contract mismatches are called out explicitly (`VITE_API_URL`, `login` payload, demo fallback removal).
|
||||
@@ -0,0 +1,95 @@
|
||||
# Backend Stabilization Design
|
||||
|
||||
## [S1] Контекст
|
||||
|
||||
Репозиторий уже содержит три основных слоя: `apps/web`, `apps/gateway` и Go-сервисы в `services/`. Сейчас проект не доведен до почти production-состояния по трем причинам: установка и сборка из корня не подтверждены, frontend частично опирается на mock-данные, а backend хранит состояние в `demo`/in-memory store.
|
||||
|
||||
Цель этой спецификации: определить первый под-проект, который переведет приложение из состояния scaffold/demo в состояние почти production-демо с реальным backend-контуром.
|
||||
|
||||
## [S2] Цель первого под-проекта
|
||||
|
||||
Первый под-проект называется `backend stabilization`.
|
||||
|
||||
Его результат:
|
||||
- проект воспроизводимо устанавливается и собирается из корня репозитория;
|
||||
- gateway и Go-сервисы работают на PostgreSQL, а не на in-memory store;
|
||||
- frontend перестает зависеть от mock-only ядра в основных сценариях;
|
||||
- аутентификация, авторизация и сохранение данных становятся реальными и повторяемыми;
|
||||
- локальный запуск и Docker-окружение дают одинаково рабочий backend-контур.
|
||||
|
||||
## [S3] Объем и границы
|
||||
|
||||
В объем первой фазы входят:
|
||||
- реальные данные для `auth`, `users/me`, `content`, `categories`, `tags`, `speakers`, `subscriptions`, `notifications`, `comments`, `search`, `admin`, `analytics summary`, `media metadata/upload`;
|
||||
- password hashing, access token, проверка текущего пользователя, logout, защита маршрутов;
|
||||
- миграции и seed-данные для стартовых ролей, аккаунтов и базовых сущностей;
|
||||
- smoke/integration verification для критического пути.
|
||||
|
||||
В первую фазу не входят:
|
||||
- внешняя production-инфраструктура вне репозитория;
|
||||
- CDN или объектное хранилище;
|
||||
- полный UI-polish всех экранов;
|
||||
- расширенная аналитика сверх уже существующих экранов и summary-метрик.
|
||||
|
||||
## [S4] Архитектурный подход
|
||||
|
||||
Архитектура сохраняется существующая:
|
||||
- `apps/web` остается клиентом;
|
||||
- `apps/gateway` остается единой внешней точкой входа;
|
||||
- `services/cmd/*` остаются отдельными Go-процессами;
|
||||
- `database/` и миграции становятся источником истины для схемы данных.
|
||||
|
||||
Ключевой принцип этой фазы: не переписывать систему заново и не вводить тяжелую новую абстракцию без необходимости. Минимальный путь — заменить текущее общее `demoStore` на PostgreSQL-backed хранилище, сохранив текущие HTTP-контракты везде, где это возможно.
|
||||
|
||||
Новые или уточненные backend-компоненты:
|
||||
- конфигурация для DSN, токен-секретов, TTL и внутренних URL;
|
||||
- GORM-backed persistence слой поверх PostgreSQL;
|
||||
- auth-логика для хэша паролей и проверки токенов;
|
||||
- seed/bootstrap слой для стартовых данных;
|
||||
- реальные health checks для gateway и сервисов.
|
||||
|
||||
## [S5] Потоки данных
|
||||
|
||||
Целевой поток запроса:
|
||||
1. frontend отправляет запрос только в gateway;
|
||||
2. gateway валидирует запрос, извлекает токен, определяет пользователя и применяет базовые RBAC-проверки;
|
||||
3. gateway проксирует запрос в нужный Go-сервис;
|
||||
4. Go-сервис читает и записывает состояние в PostgreSQL;
|
||||
5. ответ возвращается через gateway в единообразном JSON-формате.
|
||||
|
||||
Скрытый local fallback и `demo-token` перестают быть основным режимом работы. Для разработки допускаются только явные seed-данные, а не подмена настоящих ответов моками при ошибках backend-контура.
|
||||
|
||||
## [S6] Ошибки, безопасность и наблюдаемость
|
||||
|
||||
Требования этой фазы:
|
||||
- единый error response через gateway: код, сообщение, request ID;
|
||||
- корректные `401` и `403` для защищенных endpoint'ов;
|
||||
- health endpoints должны отражать реальное состояние зависимостей, включая БД;
|
||||
- request ID должен проходить через gateway и сервисы для трассировки;
|
||||
- пароли хранятся только в хэшированном виде;
|
||||
- защищенные маршруты не должны обходиться demo-механикой.
|
||||
|
||||
## [S7] Проверка готовности
|
||||
|
||||
Работа считается завершенной, когда подтверждены все пункты:
|
||||
- чистая установка зависимостей и сборка проекта из корня;
|
||||
- успешный `npm run check`;
|
||||
- успешный локальный запуск backend и frontend;
|
||||
- критические API-сценарии проходят в smoke/integration verification;
|
||||
- данные сохраняются в PostgreSQL и переживают перезапуск сервисов.
|
||||
|
||||
Минимальный критический путь для проверки:
|
||||
- login;
|
||||
- `GET /api/auth/me`;
|
||||
- список и изменение контента;
|
||||
- создание комментария;
|
||||
- создание подписки;
|
||||
- admin users;
|
||||
- audit log;
|
||||
- analytics summary.
|
||||
|
||||
## [S8] Решения по объему
|
||||
|
||||
Путь выполнения для этой спецификации: `backend-first`.
|
||||
|
||||
Это означает, что в первой фазе приоритет отдается реальности backend-контура, API, БД, auth и repeatable verification. Frontend должен быть совместим с результатом, но крупные визуальные доработки не входят в критический путь этой спецификации.
|
||||
Reference in New Issue
Block a user