- archive local-fs-storage-backend, sqlite-metadata-backend, storage-orphan-reconciliation, metadata-managed-jwt-secret, default-local-backends - sync specs: storage-backends, metadata-backends, storage-reconciliation, app-secrets; update photos/share-links - defaults reflected in specs: SQLite and local FS
5.0 KiB
Design
Context
См. proposal.md — Why. Текущее состояние:
JWT_SECRETобязателен в env (config.Load), используется вauth.NewTokenManager(HS256-подпись access-токенов) и как fallback дляURL_SECRETвstorage.Open.- Refresh-токены не подписываются секретом (случайные, хранятся хэшем).
- Есть два бэкенда метаданных (
postgres,sqlite) за интерфейсомRepositoryи фабрикойstore.NewPostgres/NewSQLite; миграции раздельные (pg-набор иmigrations/sqlite). - Порядок старта в
cmd/server: миграции → метаданные → storage → app.
Goals / Non-Goals
Goals:
- Хранить секрет приложения в слое метаданных; генерировать при первом использовании.
- Убрать обязательный
JWT_SECRETиз окружения. - Единый стабильный секрет на инсталляцию; корректное поведение при конкурентном старте.
Non-Goals:
- Шифрование секрета в БД (БД доверенная).
- UI/команда ротации секрета.
- Миграция существующих env-секретов в БД.
- Изменение формата токенов или наблюдаемого поведения аутентификации для клиента.
Decisions
D1. Таблица app_settings
app_settings(key TEXT PRIMARY KEY, value TEXT NOT NULL, created_at, updated_at) в обоих
бэкендах. Ключ jwt_secret. Отдельные миграции: PostgreSQL 000010_app_settings,
SQLite 000002_app_settings (консолидированная 000001 уже применена на существующих БД).
D2. Репозиторий internal/settings
Интерфейс Repository { Get(ctx,key) (value string, ok bool, err error); Set(ctx,key,value) error; Ensure(ctx,key, generate func() (string,error)) (string, error) }.
Реализации pg и sqlite. Ensure: SELECT → если есть, вернуть; иначе сгенерировать,
INSERT ... ON CONFLICT(key) DO NOTHING, затем SELECT (единый секрет при гонке).
Добавляется в store.Repos.
D3. Генерация секрета
crypto/rand 32 байта → base64url (без padding). Значение хранится как строка; в
TokenManager и Signer передаётся как есть.
D4. Порядок старта и проводка
cmd/server: миграции → метаданные → secret := repos.Settings.Ensure("jwt_secret", generate)
→ tm := auth.NewTokenManager(secret, accessTTL, refreshTTL) → storage.Open(ctx, cfg, secret)
(где URL_SECRET, если задан, имеет приоритет, иначе используется secret) → app.New.
app.New перестаёт строить TokenManager из конфига и принимает готовый *auth.TokenManager
(или секрет).
D5. Конфиг
Поле Auth.JWTSecret и проверка обязательности удаляются; JWT_ACCESS_TTL/JWT_REFRESH_TTL
остаются. URL_SECRET остаётся необязательным. storage.Open получает секрет параметром
вместо чтения из конфига.
Risks / Trade-offs
- [Смена источника секрета у существующих деплоев] → access-токены инвалидируются, но refresh-токены остаются валидными (не подписаны секретом), клиент молча обновляет access; в худшем случае — перелогин. Отмечаем в доках.
- [Конкурентный первый старт на общей БД] →
INSERT ... ON CONFLICT DO NOTHING+ повторныйSELECT(D2). - [Секрет в открытом виде в БД] → БД доверенная; шифрование — вне рамок.
- [Тихая смена поведения] → при
URL_SECRETне задан подпись URL зависит от секрета из БД; явныйURL_SECRETснимает зависимость.
Migration Plan
- Добавить таблицу и репозиторий секретов; сид отсутствует — генерируется при старте.
- Переключить
cmd/server/app/storageна секрет из метаданных; убратьJWT_SECRET. - Обновить доки и
.env.example. - Откат: вернуть чтение
JWT_SECRETиз env (код), данные не меняются.
Open Questions
- Нужна ли операция ротации секрета (сброс значения и инвалидация access-токенов).