feat: auth/stars API, collections and publish routes, star/author migrations
This commit is contained in:
parent
43cf0e277d
commit
65820d4ef9
43 changed files with 2008 additions and 408 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# docs — Индекс документации Indexium
|
||||
# docs - Индекс документации Indexium
|
||||
|
||||
| Документ | Описание |
|
||||
|----------|----------|
|
||||
|
|
@ -12,12 +12,12 @@
|
|||
| [analytics.md](analytics.md) | bStats-аналог: SDK, ingestion, daily агрегаты, приватность |
|
||||
| [adr/003-search-engine.md](adr/003-search-engine.md) | ADR-003: Postgres FTS + pg_trgm вместо Meilisearch |
|
||||
| [adr/004-auth-strategy.md](adr/004-auth-strategy.md) | ADR-004: GitHub-only + PAT (+ Device Flow Phase 2) |
|
||||
| [adr/005-analytics.md](adr/005-analytics.md) | ADR-005: bStats аналог — Postgres + daily_salt |
|
||||
| [adr/005-analytics.md](adr/005-analytics.md) | ADR-005: bStats аналог - Postgres + daily_salt |
|
||||
|
||||
## Как добавлять доки
|
||||
|
||||
- Новые ADR: `docs/adr/NNN-kebab-title.md` по шаблону ниже.
|
||||
- Диаграммы — Mermaid внутри markdown (рендерится в GitHub).
|
||||
- Диаграммы - Mermaid внутри markdown (рендерится в GitHub).
|
||||
|
||||
### Шаблон ADR
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
В корне два пакета: `indexium-backend` (Rust/Axum) и `indexium-frontend` (SvelteKit). Нужно решить как организовать git: один репозиторий на всё или два отдельных. Backend уже имел пустой `.git` без коммитов, фронт без гита.
|
||||
|
||||
## Рассмотренные варианты
|
||||
1. **Монорепо** — один `.git` в корне.
|
||||
2. **Полирепо** — два независимых репозитория.
|
||||
3. **Submodules** — корневой репо + сабмодули.
|
||||
1. **Монорепо** - один `.git` в корне.
|
||||
2. **Полирепо** - два независимых репозитория.
|
||||
3. **Submodules** - корневой репо + сабмодули.
|
||||
|
||||
## Решение
|
||||
Выбрать **монорепо**. Удалить `indexium-backend/.git`, инициализировать `Indexium/.git` в корне. См. `docs/git-strategy.md`.
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
Статус: Принято
|
||||
|
||||
## Контекст
|
||||
Нужен дешёвый и устойчивый к лимитам GitHub способ индексировать моды. Хранить `.jar` у себя дорого, проксировать трафик — упрёмся в bandwidth и rate limits.
|
||||
Нужен дешёвый и устойчивый к лимитам GitHub способ индексировать моды. Хранить `.jar` у себя дорого, проксировать трафик - упрёмся в bandwidth и rate limits.
|
||||
|
||||
## Решение
|
||||
Бэкенд не хранит артефакты. GitHub Releases CDN — источник правды для файлов. Мы только:
|
||||
Бэкенд не хранит артефакты. GitHub Releases CDN - источник правды для файлов. Мы только:
|
||||
- принимаем webhook `release.published` (HMAC + queue + 202),
|
||||
- воркер читает zip central directory через Range Request,
|
||||
- парсит манифест (`fabric.mod.json` и т.д.),
|
||||
|
|
@ -18,9 +18,9 @@
|
|||
|
||||
## Последствия
|
||||
- Плюс: минимальный storage, нет egress costs, +5k–12.5k RPH через GitHub App.
|
||||
- Минус: зависимость от доступности GitHub CDN (приемлемо — моды и так там).
|
||||
- Минус: зависимость от доступности GitHub CDN (приемлемо - моды и так там).
|
||||
- Вынесен malware-скан и SHA-256 сверка как обязательные.
|
||||
|
||||
## Альтернативы
|
||||
- Хранить файлы у себя (S3) — отклонено: дорого, дублирование.
|
||||
- Полный pull `.jar` на каждый релиз — отклонено: трафик, медленно.
|
||||
- Хранить файлы у себя (S3) - отклонено: дорого, дублирование.
|
||||
- Полный pull `.jar` на каждый релиз - отклонено: трафик, медленно.
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@
|
|||
|
||||
## Альтернативы (отклонены на MVP)
|
||||
|
||||
- **Meilisearch** — отличный typo-tolerance, но +1 сервис, нужен отдельный деплой и синк.
|
||||
- **Elasticsearch / OpenSearch** — оверхед по RAM/диску, нужен кластер даже для малого объёма.
|
||||
- **SQLite FTS** — не подходит, уже Postgres как основной.
|
||||
- **Meilisearch** - отличный typo-tolerance, но +1 сервис, нужен отдельный деплой и синк.
|
||||
- **Elasticsearch / OpenSearch** - оверхед по RAM/диску, нужен кластер даже для малого объёма.
|
||||
- **SQLite FTS** - не подходит, уже Postgres как основной.
|
||||
|
||||
## Ссылки
|
||||
- `docs/database-schema.md` — секция 2 (триггер `mods_search_vector_update`), секция 3 (примеры FTS + fuzzy).
|
||||
- `docs/architecture.md` — секция 4 (Поиск без Elasticsearch).
|
||||
- `docs/database-schema.md` - секция 2 (триггер `mods_search_vector_update`), секция 3 (примеры FTS + fuzzy).
|
||||
- `docs/architecture.md` - секция 4 (Поиск без Elasticsearch).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# ADR-004: Стратегия авторизации и профилей — GitHub-only + PAT (+ Device Flow позже)
|
||||
# ADR-004: Стратегия авторизации и профилей - GitHub-only + PAT (+ Device Flow позже)
|
||||
|
||||
Дата: 2026-09-06
|
||||
Статус: Принято (MVP) / Запроектировано (Phase 2)
|
||||
|
|
@ -9,22 +9,22 @@
|
|||
## Решение
|
||||
|
||||
**MVP:**
|
||||
- Вход только GitHub OAuth (`read:user`, `user:email`), JWT httpOnly cookie, без паролей. Читатели — anonymous.
|
||||
- Вход только GitHub OAuth (`read:user`, `user:email`), JWT httpOnly cookie, без паролей. Читатели - anonymous.
|
||||
- PAT с `SHA256` хранением и скоупами `read:mods|write:mods|webhooks:manage` для CI/лаунчеров.
|
||||
- `verified` бейдж если репо публичное + лицензия + GitHub App установлен.
|
||||
- Sponsors (GitHub/Patreon/Ko-fi) + star/follow (in-app) + SVG badges (`/v1/badges/:slug/downloads.svg`).
|
||||
|
||||
**Phase 2 (спроектировано, не кодим сейчас):**
|
||||
- Device Code Flow (RFC 8628) — Indexium как OAuth2 Provider для лаунчеров (`/oauth/device/code` → `/activate`).
|
||||
- Device Code Flow (RFC 8628) - Indexium как OAuth2 Provider для лаунчеров (`/oauth/device/code` → `/activate`).
|
||||
- Discord linked_account + бот роли `Verified Modder`.
|
||||
- Collections/Modlists с экспортом Prism/packwiz, Activity Feed, аналитика по версиям/лоадерам, PGP проверка.
|
||||
|
||||
**Backlog:** краш-логи, лидерборды, Profile README.
|
||||
|
||||
## Альтернативы
|
||||
- Google/email логин — отклонён для MVP (публикация всё равно требует GitHub, лишняя сложность).
|
||||
- Сразу Device Flow — отклонён ( +2 недели, PAT покрывает 80% кейсов).
|
||||
- Discord как логин — отклонён (только linked).
|
||||
- Google/email логин - отклонён для MVP (публикация всё равно требует GitHub, лишняя сложность).
|
||||
- Сразу Device Flow - отклонён ( +2 недели, PAT покрывает 80% кейсов).
|
||||
- Discord как логин - отклонён (только linked).
|
||||
|
||||
## Последствия
|
||||
- Плюс: минимум GDPR, нет паролей, доказуемое владение репо, CLI готов через PAT.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
Статус: Принято (дизайн) / К реализации в Phase 3
|
||||
|
||||
## Контекст
|
||||
Скачивания накручиваются CI, нужна честная метрика популярности — активные установки в рантайме. bStats де-факто стандарт для Minecraft модов: lightweight SDK → POST gzip JSON → агрегация. Пользователь предложил полный дизайн с `server_uuid`, daily_salt, `mod_telemetry_pings` + `mod_daily_stats`, opt-out и сортировкой `active_servers`.
|
||||
Скачивания накручиваются CI, нужна честная метрика популярности - активные установки в рантайме. bStats де-факто стандарт для Minecraft модов: lightweight SDK → POST gzip JSON → агрегация. Пользователь предложил полный дизайн с `server_uuid`, daily_salt, `mod_telemetry_pings` + `mod_daily_stats`, opt-out и сортировкой `active_servers`.
|
||||
|
||||
## Решение
|
||||
- **SDK:** MIT Java/Kotlin модуль `dev.indexium:analytics` ~15KB, `IndexiumMetrics(slug)` + `SimplePie`, уважает `-Dindexium.analytics.disable=true` и `config/indexium.json`.
|
||||
|
|
@ -14,14 +14,14 @@
|
|||
- **Приватность:** не храним IP, daily_salt ротация (не трекать сквозь дни), `custom_charts` ≤5 ключей, opt-out на клиенте.
|
||||
|
||||
## Альтернативы
|
||||
- Сторонний bStats.org — отклонён (внешняя зависимость, нет контроля, нет breakdown по нашим лоадерам).
|
||||
- ClickHouse сразу — отклонён (оверхед для MVP, Postgres хватает).
|
||||
- Хранить сырые пинги навсегда — отклонён (раздувание, достаточно daily агрегата).
|
||||
- Сторонний bStats.org - отклонён (внешняя зависимость, нет контроля, нет breakdown по нашим лоадерам).
|
||||
- ClickHouse сразу - отклонён (оверхед для MVP, Postgres хватает).
|
||||
- Хранить сырые пинги навсегда - отклонён (раздувание, достаточно daily агрегата).
|
||||
|
||||
## Последствия
|
||||
- Плюс: честная сортировка `active_servers`, графики для авторов, бейджи, без сторонних сервисов.
|
||||
- Минус: +2 таблицы, крон-агрегация, SDK нужно публиковать в Maven Central.
|
||||
- План: сначала Axum handler + агрегация, потом SDK (или наоборот — можно параллельно).
|
||||
- План: сначала Axum handler + агрегация, потом SDK (или наоборот - можно параллельно).
|
||||
|
||||
## Ссылки
|
||||
- `docs/analytics.md`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Indexium Analytics — собственный аналог bStats
|
||||
# Indexium Analytics - собственный аналог bStats
|
||||
|
||||
> Даём мододелам встроенную аналитику рантайма (активные серверы/клиенты, MC/Java/OS) без сторонних сервисов. Indexium получает честную метрику популярности — не по скачиваниям (накручиваются CI), а по реальным установкам.
|
||||
> Даём мододелам встроенную аналитику рантайма (активные серверы/клиенты, MC/Java/OS) без сторонних сервисов. Indexium получает честную метрику популярности - не по скачиваниям (накручиваются CI), а по реальным установкам.
|
||||
|
||||
Основано на твоей схеме + правки под KISS/SOLID/приватность.
|
||||
|
||||
|
|
@ -8,15 +8,15 @@
|
|||
|
||||
## 1. Как работает bStats (база)
|
||||
|
||||
1. **SDK в моде** — фоновый таймер каждые 30–60 мин собирает `mc_version, loader, java_version, os, player_count, server_uuid, custom_charts` → `POST` gzip JSON асинхронно, не блокируя главный поток.
|
||||
2. **Ingestion** — бэкенд валидирует, rate-limit по `server_hash`, анонимизирует `server_uuid`.
|
||||
3. **Aggregation** — сырые пинги → часовые/суточные агрегаты (Time Series), сырые удаляются по TTL.
|
||||
1. **SDK в моде** - фоновый таймер каждые 30–60 мин собирает `mc_version, loader, java_version, os, player_count, server_uuid, custom_charts` → `POST` gzip JSON асинхронно, не блокируя главный поток.
|
||||
2. **Ingestion** - бэкенд валидирует, rate-limit по `server_hash`, анонимизирует `server_uuid`.
|
||||
3. **Aggregation** - сырые пинги → часовые/суточные агрегаты (Time Series), сырые удаляются по TTL.
|
||||
|
||||
---
|
||||
|
||||
## 2. Indexium реализация
|
||||
|
||||
### 2.1 Клиент — Lightweight Java/Kotlin модуль
|
||||
### 2.1 Клиент - Lightweight Java/Kotlin модуль
|
||||
|
||||
```java
|
||||
// Fabric/NeoForge initialize()
|
||||
|
|
@ -27,7 +27,7 @@ metrics.addCustomChart(new SimplePie("config_type", () -> config.getType()));
|
|||
|
||||
**Что собираем (allow-list, ничего лишнего):**
|
||||
- `mc_version` (1.20.1), `loader` (fabric/neoforge/forge/quilt), `loader_version`
|
||||
- `java_version` (21.0.2), `os` (linux/windows/macos — без детальной версии), `arch` (x64/arm64)
|
||||
- `java_version` (21.0.2), `os` (linux/windows/macos - без детальной версии), `arch` (x64/arm64)
|
||||
- `player_count` (0 на клиенте, N на сервере), `server_uuid` (генерим раз, храним в `config/indexium-uuid.txt`)
|
||||
- `mod_version` (из `fabric.mod.json`), `custom_charts` (String→String, до 5 ключей, до 32 символов)
|
||||
|
||||
|
|
@ -37,9 +37,9 @@ SDK: ~15KB, без зависимостей, `CompletableFuture` + `HttpURLConne
|
|||
|
||||
### 2.2 API
|
||||
|
||||
- `POST /api/v1/analytics/submit` — пинг от мода (gzip JSON, `Content-Encoding: gzip` опционально)
|
||||
- `GET /api/v1/mods/:slug/analytics?range=7d|30d|90d` — графики для SvelteKit
|
||||
- `GET /api/v1/badges/:slug/servers.svg` — бейдж активных серверов
|
||||
- `POST /api/v1/analytics/submit` - пинг от мода (gzip JSON, `Content-Encoding: gzip` опционально)
|
||||
- `GET /api/v1/mods/:slug/analytics?range=7d|30d|90d` - графики для SvelteKit
|
||||
- `GET /api/v1/badges/:slug/servers.svg` - бейдж активных серверов
|
||||
|
||||
Пример payload (как в твоём ТЗ):
|
||||
```json
|
||||
|
|
@ -57,9 +57,9 @@ SDK: ~15KB, без зависимостей, `CompletableFuture` + `HttpURLConne
|
|||
}
|
||||
```
|
||||
|
||||
### 2.3 Хранение — PostgreSQL (MVP) → TimescaleDB/ClickHouse при росте
|
||||
### 2.3 Хранение - PostgreSQL (MVP) → TimescaleDB/ClickHouse при росте
|
||||
|
||||
На MVP хватает Postgres + daily агрегат (как ты предложил). Сырые пинги храним 30 дней, агрегаты — навсегда.
|
||||
На MVP хватает Postgres + daily агрегат (как ты предложил). Сырые пинги храним 30 дней, агрегаты - навсегда.
|
||||
|
||||
```sql
|
||||
-- Полуагрегат: один пинг = одна строка, TTL 30 дней через cron
|
||||
|
|
@ -90,14 +90,14 @@ CREATE TABLE mod_daily_stats (
|
|||
|
||||
**Агрегация:** воркер-кроном раз в час: `INSERT INTO mod_daily_stats ... ON CONFLICT DO UPDATE` группировкой по `server_hash` (последний пинг сервера за день). Через `pg_cron` или tokio `interval` в бэкенде.
|
||||
|
||||
**Масштаб:** при >10M пингов/мес — мигрируем на TimescaleDB hypertable (`create_hypertable('mod_telemetry_pings','pinged_at')`) или ClickHouse. Схема не меняется.
|
||||
**Масштаб:** при >10M пингов/мес - мигрируем на TimescaleDB hypertable (`create_hypertable('mod_telemetry_pings','pinged_at')`) или ClickHouse. Схема не меняется.
|
||||
|
||||
### 2.4 Защита и анонимность (критично)
|
||||
|
||||
- **Хеш + daily_salt:** `server_hash = sha256(server_uuid + salt_for_today)`. Соль ротируется в `analytics_salts(date, salt)`, храним 2 дня. Нельзя трекать сервер сквозь дни, но можно считать уникальные за день.
|
||||
- **Не храним IP:** `tower_http::TraceLayer` без IP, `X-Forwarded-For` игнорируем, в логах — `/analytics/submit 200` без IP.
|
||||
- **Opt-Out:** SDK проверяет в порядке: JVM флаг `-Dindexium.analytics.disable=true` → `global_privacy.json` (`.minecraft/config/indexium.json { enabled:false }`) → `config/<modid>/indexium.json`. Если любой `false` — не шлём.
|
||||
- **Rate limit:** Redis `SET server_hash:mod_slug NX EX 900` — 1 пинг / 15 мин. Ответ `429` с `Retry-After`, SDK бэкофф 30 мин.
|
||||
- **Не храним IP:** `tower_http::TraceLayer` без IP, `X-Forwarded-For` игнорируем, в логах - `/analytics/submit 200` без IP.
|
||||
- **Opt-Out:** SDK проверяет в порядке: JVM флаг `-Dindexium.analytics.disable=true` → `global_privacy.json` (`.minecraft/config/indexium.json { enabled:false }`) → `config/<modid>/indexium.json`. Если любой `false` - не шлём.
|
||||
- **Rate limit:** Redis `SET server_hash:mod_slug NX EX 900` - 1 пинг / 15 мин. Ответ `429` с `Retry-After`, SDK бэкофф 30 мин.
|
||||
- **Валидация:** `mod_slug` должен существовать, `mc_version`/`loader` из allow-list, `custom_charts` ≤5 ключей, `player_count` 0–10000. Иначе `400`.
|
||||
|
||||
---
|
||||
|
|
@ -106,14 +106,14 @@ CREATE TABLE mod_daily_stats (
|
|||
|
||||
1. **Live Charts (SvelteKit + LayerChart/Chart.js):** `GET /mods/:slug/analytics?range=30d` → `{ daily: [{date, active_servers, active_players}], breakdown: {mc_versions, loaders, os} }`. Графики: активные серверы (линия), разбивка по MC (пончик), лоадерам (бар).
|
||||
2. **Badge:** `https://api.indexium.example.com/v1/badges/sodium-extra/servers.svg` → `Active Servers: 1.2k` (из `mod_daily_stats` за вчера, кэш 1h).
|
||||
3. **Сортировка "Real-world Usage":** `GET /mods?sort=active_servers` — `ORDER BY (SELECT active_servers FROM mod_daily_stats WHERE date = CURRENT_DATE -1)`, а не по скачиваниям. Фильтр против накрутки CI.
|
||||
3. **Сортировка "Real-world Usage":** `GET /mods?sort=active_servers` - `ORDER BY (SELECT active_servers FROM mod_daily_stats WHERE date = CURRENT_DATE -1)`, а не по скачиваниям. Фильтр против накрутки CI.
|
||||
|
||||
---
|
||||
|
||||
## 4. Что не делаем (чтобы не стать spyware)
|
||||
|
||||
- Не собираем ник, чат, координаты, список всех модов без явного согласия (если включим — отдельный `custom_charts` с opt-in).
|
||||
- Не собираем ник, чат, координаты, список всех модов без явного согласия (если включим - отдельный `custom_charts` с opt-in).
|
||||
- Не fingerprint'им по железу.
|
||||
- SDK открыт (MIT) — любой может проверить что шлём (как bStats — код на GitHub).
|
||||
- SDK открыт (MIT) - любой может проверить что шлём (как bStats - код на GitHub).
|
||||
|
||||
См. `adr/005-analytics.md`, `api-spec.md` §Analytics, `database-schema.md` §telemetry.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Public REST API — Indexium v1
|
||||
# Public REST API - Indexium v1
|
||||
|
||||
Base URL: `https://api.indexium.example.com/api/v1` (локально `http://localhost:3000/api/v1`)
|
||||
|
||||
Все ответы — `application/json`. Пагинация — `page`/`limit` (MVP) → cursor позже. Кэш — `Cache-Control: public, max-age=60`, `ETag`.
|
||||
Все ответы - `application/json`. Пагинация - `page`/`limit` (MVP) → cursor позже. Кэш - `Cache-Control: public, max-age=60`, `ETag`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -30,12 +30,12 @@ Headers:
|
|||
|
||||
Body: raw JSON от GitHub.
|
||||
|
||||
**202** — принято в очередь
|
||||
**202** - принято в очередь
|
||||
```json
|
||||
{ "status": "accepted", "delivery_id": "..." }
|
||||
```
|
||||
**401** — неверная подпись
|
||||
**409** — уже обработано (идемпотентность)
|
||||
**401** - неверная подпись
|
||||
**409** - уже обработано (идемпотентность)
|
||||
|
||||
Логика: HMAC проверка → дедуп по `delivery_id` → push в Redis Streams → 202.
|
||||
|
||||
|
|
@ -105,18 +105,18 @@ Query params:
|
|||
]
|
||||
}
|
||||
```
|
||||
**404** — `{"error":"mod_not_found"}`
|
||||
**404** - `{"error":"mod_not_found"}`
|
||||
|
||||
### `GET /mods/:slug/icon`
|
||||
|
||||
Отдаёт иконку мода. Воркер при индексации извлекает `assets/<modid>/icon.png` (или `icon` из `fabric.mod.json` → путь внутри jar) → сохраняет в кэш/проксирует.
|
||||
|
||||
- **200** — `image/png` / `image/webp` с `Cache-Control: public, max-age=86400`, `ETag`. Если иконки нет → `302` на `raw.githubusercontent.com` fallback или дефолтная заглушка.
|
||||
- **404** — мод не найден.
|
||||
- **200** - `image/png` / `image/webp` с `Cache-Control: public, max-age=86400`, `ETag`. Если иконки нет → `302` на `raw.githubusercontent.com` fallback или дефолтная заглушка.
|
||||
- **404** - мод не найден.
|
||||
|
||||
> Альтернатива на MVP: не хранить иконку у себя, а отдавать `icon_url` как прямую ссылку `https://raw.githubusercontent.com/<owner>/<repo>/<branch>/src/main/resources/assets/...`. Эндпоинт `/icon` тогда — 302 редирект + кэш заголовков.
|
||||
> Альтернатива на MVP: не хранить иконку у себя, а отдавать `icon_url` как прямую ссылку `https://raw.githubusercontent.com/<owner>/<repo>/<branch>/src/main/resources/assets/...`. Эндпоинт `/icon` тогда - 302 редирект + кэш заголовков.
|
||||
|
||||
### `POST /mods/resolve` — пакетный резолв для лаунчеров
|
||||
### `POST /mods/resolve` - пакетный резолв для лаунчеров
|
||||
|
||||
Принимает список модов + окружение, возвращает дерево прямых скачиваний и зависимостей (для Prism / Modrinth-compatible клиентов).
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ Query params:
|
|||
|
||||
- `version: "latest"` → резолвит последнюю совместимую с `game_version` + `loader`.
|
||||
- Транзитивные зависимости резолвятся рекурсивно (BFS, max depth 20, защита от циклов).
|
||||
- **422** — несовместимая комбинация `game_version`/`loader`.
|
||||
- **422** - несовместимая комбинация `game_version`/`loader`.
|
||||
- Кэшируется по ключу `resolve:gv:loader:hash(mods)` TTL 60s.
|
||||
|
||||
### `GET /mods/:slug/versions/:version`
|
||||
|
|
@ -193,9 +193,9 @@ Body:
|
|||
|
||||
Логика: проверить что токен имеет доступ к репо → fetch `fabric.mod.json` из default branch → создать запись `mods` → повесить webhook.
|
||||
|
||||
**201** — создан
|
||||
**409** — уже импортирован
|
||||
**422** — манифест не найден
|
||||
**201** - создан
|
||||
**409** - уже импортирован
|
||||
**422** - манифест не найден
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -204,14 +204,14 @@ Body:
|
|||
### `GET /auth/github` → 302 redirect на GitHub OAuth
|
||||
### `GET /auth/github/callback?code=...` → обмен code→token, установка httpOnly cookie / JWT
|
||||
|
||||
### `POST /auth/tokens` (auth) — PAT creation
|
||||
### `POST /auth/tokens` (auth) - PAT creation
|
||||
Body: `{ "name": "ci-token", "scopes": ["read:mods","write:mods"], "expires_in_days": 30 }` → `201 { token: "idx_...", id, expires_at }` (токен показывается 1 раз, храним hash). `Authorization: Bearer idx_...` для API.
|
||||
|
||||
### `GET /auth/tokens` / `DELETE /auth/tokens/:id` — список/отзыв.
|
||||
### `GET /auth/tokens` / `DELETE /auth/tokens/:id` - список/отзыв.
|
||||
|
||||
### Device Flow (Phase 2, RFC 8628)
|
||||
- `POST /oauth/device/code` → `{ device_code, user_code: "ABCD-1234", verification_uri: "https://indexium.example.com/activate", expires_in: 600 }`
|
||||
- `GET /activate` (frontend) — ввод `user_code` → consent → `POST /oauth/device/verify { user_code }`
|
||||
- `GET /activate` (frontend) - ввод `user_code` → consent → `POST /oauth/device/verify { user_code }`
|
||||
- `POST /oauth/token` grant_type=`urn:ietf:params:oauth:grant-type:device_code` → `{ access_token, refresh_token }`
|
||||
- Лаунчер поллит `/oauth/token` до получения токена.
|
||||
|
||||
|
|
@ -219,16 +219,16 @@ Body: `{ "name": "ci-token", "scopes": ["read:mods","write:mods"], "expires_in_d
|
|||
|
||||
## Profiles & Social
|
||||
|
||||
### `GET /u/:login` / `GET /org/:login` — публичный профиль (кэш 60s, ISR)
|
||||
### `POST /mods/:slug/star` / `DELETE /mods/:slug/star` — звезда (auth)
|
||||
### `POST /u/:login/follow` / `DELETE /u/:login/follow` — подписка на автора с опционально `?game_version=1.20.1&loader=fabric`
|
||||
### `GET /u/:login` / `GET /org/:login` - публичный профиль (кэш 60s, ISR)
|
||||
### `POST /mods/:slug/star` / `DELETE /mods/:slug/star` - звезда (auth)
|
||||
### `POST /u/:login/follow` / `DELETE /u/:login/follow` - подписка на автора с опционально `?game_version=1.20.1&loader=fabric`
|
||||
### `GET /collections` / `POST /collections` (auth, body: `{ title, description, mods: [{slug, version}] }`)
|
||||
### `GET /collections/:slug` / `GET /collections/:slug/export?format=prism|packwiz`
|
||||
### `GET /v1/badges/:slug/downloads.svg` / `GET /v1/badges/:slug/version.svg` — SVG виджет для README (public, кэш 1h)
|
||||
### `GET /v1/badges/:slug/downloads.svg` / `GET /v1/badges/:slug/version.svg` - SVG виджет для README (public, кэш 1h)
|
||||
|
||||
## Analytics (bStats аналог, см. docs/analytics.md)
|
||||
|
||||
### `POST /api/v1/analytics/submit` — пинг от мода (gzip опционально)
|
||||
### `POST /api/v1/analytics/submit` - пинг от мода (gzip опционально)
|
||||
Headers: `Content-Type: application/json`, `Content-Encoding: gzip` (optional)
|
||||
Body:
|
||||
```json
|
||||
|
|
@ -246,7 +246,7 @@ Body:
|
|||
}
|
||||
```
|
||||
- Валидация: `mod_slug` exists, allow-list версий/лоадеров, `custom_charts` ≤5 ключей.
|
||||
- Анонимизация: `server_hash = sha256(server_uuid + daily_salt)` — IP не храним.
|
||||
- Анонимизация: `server_hash = sha256(server_uuid + daily_salt)` - IP не храним.
|
||||
- Rate limit: 1 пинг / 15 мин per `server_hash+mod_slug` (Redis `SET NX EX 900`) → `429`.
|
||||
- Opt-Out: respect `-Dindexium.analytics.disable=true` на клиенте.
|
||||
- **200** `{ "status": "ok" }` **400** validation **429** rate_limited
|
||||
|
|
@ -272,13 +272,13 @@ Public, кэш `public, max-age=300`.
|
|||
```
|
||||
**404** mod_not_found. Источник: `mod_daily_stats`.
|
||||
|
||||
### `GET /api/v1/badges/:slug/servers.svg` — бейдж активных серверов (как downloads.svg, кэш 1h)
|
||||
### `GET /api/v1/badges/:slug/servers.svg` - бейдж активных серверов (как downloads.svg, кэш 1h)
|
||||
SVG `Active Servers: 1.2k` из `mod_daily_stats` за вчера.
|
||||
|
||||
## Sponsors & Badges
|
||||
|
||||
- `GET /u/:login` отдаёт `sponsors: { github, patreon, kofi, bmc }` и `badges: ["early_adopter","verified"]`
|
||||
- Бейджи выдаются воркером (`badges` таблица), SVG — динамически.
|
||||
- Бейджи выдаются воркером (`badges` таблица), SVG - динамически.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -315,4 +315,4 @@ SVG `Active Servers: 1.2k` из `mod_daily_stats` за вчера.
|
|||
|
||||
## OpenAPI
|
||||
|
||||
Спека будет жить в `indexium-backend/openapi.yaml` (генерировать из Axum через `utoipa` когда созреет). На MVP — этот markdown как источник правды.
|
||||
Спека будет жить в `indexium-backend/openapi.yaml` (генерировать из Axum через `utoipa` когда созреет). На MVP - этот markdown как источник правды.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Архитектура Indexium — Асинхронный событийный индексатор
|
||||
# Архитектура Indexium - Асинхронный событийный индексатор
|
||||
|
||||
> Цель: сделать сервис максимально лёгким, дешёвым в обслуживании и устойчивым к ограничениям GitHub.
|
||||
> Принцип: бэкенд **не хранит** тяжёлые файлы — артефакты отдаются с GitHub Releases CDN, мы валидируем, индексируем метаданные и выдаём быстрые JSON-ответы.
|
||||
> Принцип: бэкенд **не хранит** тяжёлые файлы - артефакты отдаются с GitHub Releases CDN, мы валидируем, индексируем метаданные и выдаём быстрые JSON-ответы.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -37,9 +37,9 @@
|
|||
```
|
||||
|
||||
### Потоки данных
|
||||
1. **Ingestion** — GitHub шлёт webhook → Ingestion API валидирует HMAC → кладёт job в очередь → отвечает `202`.
|
||||
2. **Indexing** — Worker читает job → делает `Range Request` к `.jar` → парсит манифест → пишет в Postgres → инвалидирует кэш.
|
||||
3. **Serving** — Лаунчер / Web UI дергает `GET /api/v1/mods` → читаем Redis → miss → Postgres FTS → кэшируем 60s → отдаём JSON.
|
||||
1. **Ingestion** - GitHub шлёт webhook → Ingestion API валидирует HMAC → кладёт job в очередь → отвечает `202`.
|
||||
2. **Indexing** - Worker читает job → делает `Range Request` к `.jar` → парсит манифест → пишет в Postgres → инвалидирует кэш.
|
||||
3. **Serving** - Лаунчер / Web UI дергает `GET /api/v1/mods` → читаем Redis → miss → Postgres FTS → кэшируем 60s → отдаём JSON.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -47,14 +47,14 @@
|
|||
|
||||
| Слой | Технология | Почему именно это? |
|
||||
| --- | --- | --- |
|
||||
| **Backend Core** | **Rust (Axum)** | Минимальный memory footprint, высокий throughput, быстрый async I/O при парсинге. Альтернатива Go (Fiber) — допустима на Phase 2. |
|
||||
| **Backend Core** | **Rust (Axum)** | Минимальный memory footprint, высокий throughput, быстрый async I/O при парсинге. Альтернатива Go (Fiber) - допустима на Phase 2. |
|
||||
| **Main Database** | **PostgreSQL** | Нативный FTS, `JSONB` для зависимостей, `pg_trgm` для fuzzy, `pgvector` опционально для семантики. |
|
||||
| **Cache & Queue** | **Redis / Valkey** | Одновременно брокер очередей (Streams/PubSub) и L2-кэш популярных эндпоинтов. |
|
||||
| **Worker Engine** | **Rust Background Worker (tokio)** | Потребляет webhook-события, скачивает только zip-header stream, валидирует байткод. |
|
||||
| **Auth & Security** | **GitHub App / OAuth 2.0** | Вход только через GitHub, без локальных паролей. |
|
||||
| **Frontend** | **SvelteKit + TypeScript** | SSR, маленький бандл, быстрый dev. |
|
||||
|
||||
> Обоснование против Elasticsearch/Meilisearch на старте: `to_tsvector` + `pg_trgm` переваривают десятки тысяч модов с <5ms без отдельного кластера. Миграция на внешний поиск — только если FTS упрётся.
|
||||
> Обоснование против Elasticsearch/Meilisearch на старте: `to_tsvector` + `pg_trgm` переваривают десятки тысяч модов с <5ms без отдельного кластера. Миграция на внешний поиск - только если FTS упрётся.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -68,9 +68,9 @@
|
|||
### 3.2 Обработка webhook (Ingestion & Job Dispatch)
|
||||
1. GitHub шлёт `release.published`.
|
||||
2. **Ingestion API** валидирует `X-Hub-Signature-256` (HMAC SHA-256 с `WEBHOOK_SECRET`), проверяет идемпотентность по `X-GitHub-Delivery`.
|
||||
3. Кладёт задачу в Redis Queue, отвечает `202 Accepted` за <50ms (чтобы не висеть по таймауту GitHub — 10s).
|
||||
3. Кладёт задачу в Redis Queue, отвечает `202 Accepted` за <50ms (чтобы не висеть по таймауту GitHub - 10s).
|
||||
|
||||
### 3.3 Работа воркера-индексатора (Worker Execution) — детальный алгоритм `jar_parser.rs`
|
||||
### 3.3 Работа воркера-индексатора (Worker Execution) - детальный алгоритм `jar_parser.rs`
|
||||
|
||||
> Цель: не скачивать весь `.jar` (может быть 20–50MB), а прочитать только нужный манифест через 2–3 Range-запроса.
|
||||
|
||||
|
|
@ -102,15 +102,15 @@ download_url = "https://github.com/owner/repo/releases/download/v1.2.3/mod-1.2.3
|
|||
```
|
||||
|
||||
**Детали реализации:**
|
||||
1. `HEAD` — обязателен, чтобы получить `Content-Length` и убедиться `Accept-Ranges: bytes`. Таймаут 5s, retry 2.
|
||||
2. Последние 64KB достаточно для EOCD даже для jar с 10k файлов (EOCD в конце). Если не найден — fallback к последним 128KB.
|
||||
1. `HEAD` - обязателен, чтобы получить `Content-Length` и убедиться `Accept-Ranges: bytes`. Таймаут 5s, retry 2.
|
||||
2. Последние 64KB достаточно для EOCD даже для jar с 10k файлов (EOCD в конце). Если не найден - fallback к последним 128KB.
|
||||
3. Central Directory читается одним запросом (обычно 5–30KB). Парсим `central_dir_offset/size` из EOCD.
|
||||
4. Манифест — 4-й запрос только если нужен (часто 1–3KB). Иконка — опционально 5-й запрос, кэшируется и отдаётся через `GET /mods/:slug/icon`.
|
||||
5. Все `GET` — `reqwest` с `header("Range", ...)`, проверка `206 Partial Content`, иначе fallback к полному скачиванию с лимитом 10MB.
|
||||
4. Манифест - 4-й запрос только если нужен (часто 1–3KB). Иконка - опционально 5-й запрос, кэшируется и отдаётся через `GET /mods/:slug/icon`.
|
||||
5. Все `GET` - `reqwest` с `header("Range", ...)`, проверка `206 Partial Content`, иначе fallback к полному скачиванию с лимитом 10MB.
|
||||
|
||||
**Ошибки:** `412` если `Accept-Ranges` != bytes → full download; `404` на Range → retry без Range; повреждённый ZIP → помечаем `suspicious` и DLQ.
|
||||
|
||||
Код: `indexium-backend/src/worker/jar_parser.rs` — чистые функции `find_eocd()`, `parse_central_dir()`, `fetch_manifest()` без I/O в тестах.
|
||||
Код: `indexium-backend/src/worker/jar_parser.rs` - чистые функции `find_eocd()`, `parse_central_dir()`, `fetch_manifest()` без I/O в тестах.
|
||||
|
||||
### 3.4 Агрегация и индексация (Storage & Cache Invalidation)
|
||||
1. Сохраняет версию в `mod_versions` (см. `database-schema.md`).
|
||||
|
|
@ -122,8 +122,8 @@ download_url = "https://github.com/owner/repo/releases/download/v1.2.3/mod-1.2.3
|
|||
## 4. Обход ключевых ограничений (Edge Cases)
|
||||
|
||||
### GitHub Rate Limits
|
||||
- Запросы воркеров — от имени **GitHub App Installs** (5k–12.5k RPH на инсталл vs 60 RPH анонимных).
|
||||
- Скачивание не проксируем — выдаём клиентам прямые CDN-ссылки, трафик не идёт через нас.
|
||||
- Запросы воркеров - от имени **GitHub App Installs** (5k–12.5k RPH на инсталл vs 60 RPH анонимных).
|
||||
- Скачивание не проксируем - выдаём клиентам прямые CDN-ссылки, трафик не идёт через нас.
|
||||
|
||||
### Безопасность (Malware Protection)
|
||||
- Сверка `SHA-256` ассета с `.sha256` если есть.
|
||||
|
|
@ -216,4 +216,4 @@ CREATE INDEX idx_versions_lookup ON mod_versions USING GIN (game_versions, loade
|
|||
--> [SvelteKit :5173] (SSR, fetch API)
|
||||
```
|
||||
|
||||
Все сервисы — `docker compose` локально, один VPS в проде.
|
||||
Все сервисы - `docker compose` локально, один VPS в проде.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Профили, аккаунты и авторизация — дизайн Indexium
|
||||
# Профили, аккаунты и авторизация - дизайн Indexium
|
||||
|
||||
> Цель: максимально лёгкая, но крутая система профилей без паролей, где GitHub — источник правды.
|
||||
> Цель: максимально лёгкая, но крутая система профилей без паролей, где GitHub - источник правды.
|
||||
|
||||
---
|
||||
|
||||
## 1. TL;DR — рекомендуем для MVP
|
||||
## 1. TL;DR - рекомендуем для MVP
|
||||
|
||||
**Авторизация: только GitHub OAuth / GitHub App.** Никаких паролей, email+пароль, Google и т.д. на старте.
|
||||
|
||||
|
|
@ -15,10 +15,10 @@
|
|||
| 1 клик, нет форм регистрации | Отсекаем тех у кого нет GitHub (но они и моды не публикуют) |
|
||||
| Доказуемое владение репозиторием (`GET /repos` с токеном) | Зависимость от GitHub OAuth (но у нас и так всё на GitHub) |
|
||||
| Аватар, ник, био подтягиваются автоматически | Нет anon-публикаций (и это хорошо для open source) |
|
||||
| Один токен — и публикация, и вебхуки, и профиль | Если GitHub лежит — логин не работает (редкость) |
|
||||
| Один токен - и публикация, и вебхуки, и профиль | Если GitHub лежит - логин не работает (редкость) |
|
||||
| Нет хранения паролей, нет утечек | |
|
||||
|
||||
> **Вывод:** для каталога где `1 мод = 1 GitHub репо` — GitHub-only это не ограничение, а фича. Пользователи-читатели (игроки) могут смотреть каталог **без логина вообще**. Логин нужен только авторам.
|
||||
> **Вывод:** для каталога где `1 мод = 1 GitHub репо` - GitHub-only это не ограничение, а фича. Пользователи-читатели (игроки) могут смотреть каталог **без логина вообще**. Логин нужен только авторам.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -26,10 +26,10 @@
|
|||
|
||||
### Роли
|
||||
|
||||
- **Reader (anonymous)** — ищет, качает по прямым ссылкам, смотрит профили. Без аккаунта.
|
||||
- **Author** — залогинен через GitHub, импортировал хотя бы один репо. Может публиковать релизы (через `git push` + webhook, без кнопки "загрузить jar").
|
||||
- **Contributor** — указан в `mod_authors` с `role=contributor`, не обязательно owner репо. Получает бейдж на карточке мода.
|
||||
- **Moderator / Admin** — ручная выдача, может ставить `verified` / `suspicious`, банить.
|
||||
- **Reader (anonymous)** - ищет, качает по прямым ссылкам, смотрит профили. Без аккаунта.
|
||||
- **Author** - залогинен через GitHub, импортировал хотя бы один репо. Может публиковать релизы (через `git push` + webhook, без кнопки "загрузить jar").
|
||||
- **Contributor** - указан в `mod_authors` с `role=contributor`, не обязательно owner репо. Получает бейдж на карточке мода.
|
||||
- **Moderator / Admin** - ручная выдача, может ставить `verified` / `suspicious`, банить.
|
||||
|
||||
### Что храним (минимум GDPR)
|
||||
|
||||
|
|
@ -56,12 +56,12 @@ CREATE TABLE mod_authors (
|
|||
);
|
||||
```
|
||||
|
||||
Никаких email в открытом виде (берём только для JWT, не показываем), никаких паролей. `github_id` — неизменяемый PK, `login` может смениться — обновляем по webhook `user.renamed` или при следующем логине.
|
||||
Никаких email в открытом виде (берём только для JWT, не показываем), никаких паролей. `github_id` - неизменяемый PK, `login` может смениться - обновляем по webhook `user.renamed` или при следующем логине.
|
||||
|
||||
### Сессии
|
||||
|
||||
- **JWT (httpOnly cookie)**: `sub: github_id`, `login`, `exp: 7d`. Подпись `HS256` с `JWT_SECRET` или `RS256` если хотим ротацию.
|
||||
- **Не храним сессии в Redis на MVP** — stateless JWT достаточно. Позже — refresh token в `author_sessions`.
|
||||
- **Не храним сессии в Redis на MVP** - stateless JWT достаточно. Позже - refresh token в `author_sessions`.
|
||||
- **CSRF**: `SameSite=Lax` + `Origin` check для `POST /mods/import`.
|
||||
|
||||
---
|
||||
|
|
@ -78,7 +78,7 @@ CREATE TABLE mod_authors (
|
|||
→ 302 /me или /?welcomed=1
|
||||
```
|
||||
|
||||
**Для публикации модов нужен `repo` scope** только если хотим ставить webhook автоматически. На MVP можно `read:user` + `public_repo` (только публичные). Токен GitHub не храним долго — меняем на JWT и забываем (или храним encrypted `github_access_token` для будущих API вызовов, с возможностью revoke).
|
||||
**Для публикации модов нужен `repo` scope** только если хотим ставить webhook автоматически. На MVP можно `read:user` + `public_repo` (только публичные). Токен GitHub не храним долго - меняем на JWT и забываем (или храним encrypted `github_access_token` для будущих API вызовов, с возможностью revoke).
|
||||
|
||||
**GitHub App (альтернатива OAuth):**
|
||||
- Плюс: `5k–12.5k RPH`, управление webhooks через App, `installation_id` per org.
|
||||
|
|
@ -87,26 +87,26 @@ CREATE TABLE mod_authors (
|
|||
|
||||
---
|
||||
|
||||
## 4. Профили — как сделать круто и по open source
|
||||
## 4. Профили - как сделать круто и по open source
|
||||
|
||||
### URL структура
|
||||
|
||||
- `/u/:login` — профиль пользователя (зеркало GitHub, но с модами)
|
||||
- `/org/:login` — профиль организации (если `type: Organization`)
|
||||
- `/mod/:slug` — карточка мода (показывает авторов с ролями)
|
||||
- `/u/:login` - профиль пользователя (зеркало GitHub, но с модами)
|
||||
- `/org/:login` - профиль организации (если `type: Organization`)
|
||||
- `/mod/:slug` - карточка мода (показывает авторов с ролями)
|
||||
|
||||
Все профили **публичны и кэшируются** (ISR в SvelteKit).
|
||||
|
||||
### Что показываем на `/u/:login`
|
||||
|
||||
```
|
||||
[avatar] flashy (@flashy) — "Minecraft modder"
|
||||
[avatar] flashy (@flashy) - "Minecraft modder"
|
||||
bio | 📍 Berlin | 🔗 flashy.dev | Joined 2024
|
||||
|
||||
Stats: 12 mods · 48 releases · 12k downloads (aggregated) · 342 stars (from GH)
|
||||
|
||||
Mods:
|
||||
[sodium-extra] 1.20.1 fabric — ★ 42 — MIT
|
||||
[sodium-extra] 1.20.1 fabric - ★ 42 - MIT
|
||||
[lithium-fork] ...
|
||||
|
||||
Contributions: контрибьютил в 5 чужих модов (через mod_authors)
|
||||
|
|
@ -124,22 +124,22 @@ Links: GitHub → github.com/flashy | Indexium RSS → /u/flashy/feed.xml
|
|||
|
||||
### Крутые идеи (backlog, но заложим)
|
||||
|
||||
- **Profile README** — рендерим `https://github.com/:login/:login/blob/main/README.md` если есть (как GitHub profile README).
|
||||
- **Profile README** - рендерим `https://github.com/:login/:login/blob/main/README.md` если есть (как GitHub profile README).
|
||||
- **Achievements:** `First Mod`, `10k Downloads`, `GPL Defender` (все моды GPL).
|
||||
- **Follow:** подписка на автора (email / webhook) — `POST /u/:login/follow` → уведомляем о новых релизах (через `author_follows` таблицу).
|
||||
- **Follow:** подписка на автора (email / webhook) - `POST /u/:login/follow` → уведомляем о новых релизах (через `author_follows` таблицу).
|
||||
- **Organizations:** группируем моды по `owner` (из `mods.owner`), страница `/org/:owner` агрегирует всех авторов организации.
|
||||
- **Sponsors:** кнопка `Sponsor` → ссылка на `github.com/sponsors/:login` если у автора включён Sponsors.
|
||||
|
||||
---
|
||||
|
||||
## 5. Альтернативы — когда добавлять второй провайдер
|
||||
## 5. Альтернативы - когда добавлять второй провайдер
|
||||
|
||||
| Провайдер | Когда добавлять | Как |
|
||||
|---|---|---|
|
||||
| **Discord OAuth** | Если заведём Discord сервер и хотим связать роли | `GET /auth/discord` → линк к `authors.discord_id`, не как замена GitHub, а как `linked_accounts` |
|
||||
| **Google / Email magic link** | Если появятся читатели-комментаторы без GitHub | Только для `Reader` роли, без права публикации. Публикация всё равно требует GitHub линк (`GET /link/github`) |
|
||||
| **Passkeys / WebAuthn** | Если хотим passwordless для модераторов | Избыточно на MVP |
|
||||
| **Gitea / Codeberg / GitLab** | Если хотим тру-децентрализацию | Добавляем `provider: github|gitlab|codeberg` в `authors`, но каждый — отдельный OAuth. На MVP — только GitHub |
|
||||
| **Gitea / Codeberg / GitLab** | Если хотим тру-децентрализацию | Добавляем `provider: github|gitlab|codeberg` в `authors`, но каждый - отдельный OAuth. На MVP - только GitHub |
|
||||
|
||||
**Архитектура на будущее (не делаем сейчас, но не блокируем):**
|
||||
|
||||
|
|
@ -153,66 +153,66 @@ CREATE TABLE linked_accounts (
|
|||
-- Публикация мода всё равно требует linked GitHub с доступом к репо
|
||||
```
|
||||
|
||||
**Рекомендация:** MVP — **только GitHub**. Второй провайдер — Discord линк **после** первых 500 пользователей, если попросят.
|
||||
**Рекомендация:** MVP - **только GitHub**. Второй провайдер - Discord линк **после** первых 500 пользователей, если попросят.
|
||||
|
||||
---
|
||||
|
||||
## 6. Безопасность и приватность
|
||||
|
||||
- Никаких паролей — нечего утекать.
|
||||
- Никаких паролей - нечего утекать.
|
||||
- `access_token` GitHub храним только в памяти/JWT, не в БД (или encrypted at rest).
|
||||
- Rate limit на `/auth/*` — 10 req/min per IP.
|
||||
- Rate limit на `/auth/*` - 10 req/min per IP.
|
||||
- Удаление аккаунта: `DELETE /me` → удаляем `authors` + `mod_authors`, но `mods` остаются ( orphan → показываем `by @deleted` ), т.к. код уже open source и на GitHub.
|
||||
- GDPR: `GET /me/export` → JSON со всеми данными, `DELETE` — право на забвение (кроме публичных модов).
|
||||
- GDPR: `GET /me/export` → JSON со всеми данными, `DELETE` - право на забвение (кроме публичных модов).
|
||||
|
||||
---
|
||||
|
||||
## 7. Расширенная авторизация — твои идеи (оценка)
|
||||
## 7. Расширенная авторизация - твои идеи (оценка)
|
||||
|
||||
### 7.1 API Keys / PAT — **да, делаем в Phase 1**
|
||||
### 7.1 API Keys / PAT - **да, делаем в Phase 1**
|
||||
Генерация в `/settings/tokens` с кастомными скоупами `read:mods`, `write:mods`, `webhooks:manage`.
|
||||
- Хранение: `personal_access_tokens (id, github_id, token_hash, scopes[], expires_at)` — храним только `SHA256(token)` как у GitHub.
|
||||
- Хранение: `personal_access_tokens (id, github_id, token_hash, scopes[], expires_at)` - храним только `SHA256(token)` как у GitHub.
|
||||
- Зачем: CI/CD (`github actions: indexium publish --token $INDEXIUM_TOKEN`), лаунчеры без браузера.
|
||||
- Риск: утечка → лимит скоупов + `expires_at` 30/90 дней + `last_used_at` + revoke.
|
||||
- **Вердикт:** берём в MVP — 1 таблица + 2 эндпоинта, без OAuth сервера.
|
||||
- **Вердикт:** берём в MVP - 1 таблица + 2 эндпоинта, без OAuth сервера.
|
||||
|
||||
### 7.2 OAuth2 Provider / Device Code Flow (RFC 8628) — **круто, но Phase 2**
|
||||
### 7.2 OAuth2 Provider / Device Code Flow (RFC 8628) - **круто, но Phase 2**
|
||||
Ты предлагаешь сделать Indexium IdP для лаунчеров: лаунчер показывает `ABCD-1234` → юзер на `indexium.example.com/activate` подтверждает.
|
||||
- Плюс: идеален для Prism на Linux/TV/без браузера, как у GitHub CLI (`gh auth login --web`).
|
||||
- Минус: нужно реализовать полноценный Authorization Server (`/oauth/authorize`, `/oauth/token`, `/oauth/device/code`, `/oauth/device/verify`) + consent screen + refresh tokens. Это +2-3 недели.
|
||||
- Альтернатива на MVP: **PAT** — лаунчер просит вставить токен вручную (как `gh` с PAT). UX хуже, но без IdP.
|
||||
- Альтернатива на MVP: **PAT** - лаунчер просит вставить токен вручную (как `gh` с PAT). UX хуже, но без IdP.
|
||||
- **Вердикт:** проектируем сейчас (закладываем `oauth_clients`, `device_codes`), реализуем после PAT когда попросят лаунчеры.
|
||||
|
||||
### 7.3 Discord линк — **да, но как linked_account, не как логин**
|
||||
### 7.3 Discord линк - **да, но как linked_account, не как логин**
|
||||
- Флоу: `GET /auth/discord` → `linked_accounts (github_id, provider='discord', provider_id)` → бот выдаёт `Verified Modder` на сервере Indexium, шлёт DM о релизах.
|
||||
- Не делаем Discord как замену GitHub — публикация всё равно требует GitHub. Это синк ролей, не вход.
|
||||
- Не делаем Discord как замену GitHub - публикация всё равно требует GitHub. Это синк ролей, не вход.
|
||||
- **Вердикт:** делаем после MVP, когда заведём Discord сервер.
|
||||
|
||||
---
|
||||
|
||||
## 8. Фичи профиля — разбор твоих идей
|
||||
## 8. Фичи профиля - разбор твоих идей
|
||||
|
||||
### 8.1 Для разработчиков (оценка)
|
||||
|
||||
| Идея | Оценка | Комментарий |
|
||||
|---|---|---|
|
||||
| **Дашборд аналитики** (скачивания по версиям/лоадерам/OS, краш-логи) | **Phase 2** | Скачивания считаем агрегатом `downloads_daily` (без IP), OS — из `User-Agent` лаунчера если пришлёт. Краш-логи — отдельный `POST /telemetry/crash` с анонимизацией, опционально. |
|
||||
| **Дашборд аналитики** (скачивания по версиям/лоадерам/OS, краш-логи) | **Phase 2** | Скачивания считаем агрегатом `downloads_daily` (без IP), OS - из `User-Agent` лаунчера если пришлёт. Краш-логи - отдельный `POST /telemetry/crash` с анонимизацией, опционально. |
|
||||
| **Организации/команды** (Team CoFH) | **MVP-лайт** | Уже есть `mod_authors` + `mods.owner` (org). Делаем `/org/:login` как агрегатор, `role=maintainer` для команды. Без отдельного `teams` на старте. |
|
||||
| **Спонсорство** (GitHub Sponsors, Patreon, Ko-fi) | **MVP** | Поле `authors.sponsors: JSONB { github, patreon, kofi, bmc }` + кнопки в шапке профиля/мода. Парсим из GitHub `sponsors` API или ручной ввод. |
|
||||
| **Verified + PGP/GPG подпись** | **MVP / Phase 2** | `verified` уже в `mods` — ставим если репо через GitHub App и `license` ok. PGP — показываем `gpg_keys` из GitHub API (`GET /users/:login/gpg_keys`), проверка `.asc` рядом с `.jar` — Phase 2. |
|
||||
| **Verified + PGP/GPG подпись** | **MVP / Phase 2** | `verified` уже в `mods` - ставим если репо через GitHub App и `license` ok. PGP - показываем `gpg_keys` из GitHub API (`GET /users/:login/gpg_keys`), проверка `.asc` рядом с `.jar` - Phase 2. |
|
||||
|
||||
### 8.2 Для игроков
|
||||
|
||||
| Идея | Оценка |
|
||||
|---|---|
|
||||
| **Коллекции / Модпаки** (`My OptiFine Alternatives`) с экспортом в Prism/CurseForge | **Phase 2, хит** | `collections (id, author_id, slug, title, mods[] JSONB, visibility)` + `collection_stars`. Экспорт — `GET /collections/:slug/export?format=prism|packwiz`. Виральная фича. |
|
||||
| **Star / Follow + подписки** (уведомления о релизе под `1.20.1+fabric`) | **MVP-лайт** | `stars (github_id, mod_id)`, `follows (github_id, author_id)` + фильтр `notify_game_version/loader`. Уведомления — сначала in-app + Discord DM, email позже. |
|
||||
| **Коллекции / Модпаки** (`My OptiFine Alternatives`) с экспортом в Prism/CurseForge | **Phase 2, хит** | `collections (id, author_id, slug, title, mods[] JSONB, visibility)` + `collection_stars`. Экспорт - `GET /collections/:slug/export?format=prism|packwiz`. Виральная фича. |
|
||||
| **Star / Follow + подписки** (уведомления о релизе под `1.20.1+fabric`) | **MVP-лайт** | `stars (github_id, mod_id)`, `follows (github_id, author_id)` + фильтр `notify_game_version/loader`. Уведомления - сначала in-app + Discord DM, email позже. |
|
||||
| **Activity Feed** | **Phase 2** | Лента из `webhook_deliveries` + `collections` + `stars` по подпискам. |
|
||||
|
||||
### 8.3 Геймификация и виджет
|
||||
|
||||
- **Бейджи:** `Early Adopter` (id <1000), `Bug Hunter` (репорты), `Top Contributor` (N релизов/мес), `Open Source Veteran` (GitHub age >5 лет через `created_at` из API). Храним `badges (github_id, badge_id)` — выдаём воркером раз в день. Показываем на `/u/:login`.
|
||||
- **Showcase Widget SVG:** `GET /v1/badges/:slug/downloads.svg` и `GET /v1/badges/:slug/version.svg` — генерируем SVG на лету (как `shields.io`), кэш 1h, без JS. Пример: `` — **делаем в MVP**, это маркетинг.
|
||||
- **Бейджи:** `Early Adopter` (id <1000), `Bug Hunter` (репорты), `Top Contributor` (N релизов/мес), `Open Source Veteran` (GitHub age >5 лет через `created_at` из API). Храним `badges (github_id, badge_id)` - выдаём воркером раз в день. Показываем на `/u/:login`.
|
||||
- **Showcase Widget SVG:** `GET /v1/badges/:slug/downloads.svg` и `GET /v1/badges/:slug/version.svg` - генерируем SVG на лету (как `shields.io`), кэш 1h, без JS. Пример: `` - **делаем в MVP**, это маркетинг.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -226,7 +226,7 @@ CREATE TABLE linked_accounts (
|
|||
|
||||
## 10. Что решить сейчас
|
||||
|
||||
1. Подтверди: **PAT в MVP — да?** (я заложил, это быстро).
|
||||
2. Device Flow — **проектируем сейчас, код позже** — ок?
|
||||
3. Коллекции — делать сразу после MVP или откладываем до 500 юзеров?
|
||||
1. Подтверди: **PAT в MVP - да?** (я заложил, это быстро).
|
||||
2. Device Flow - **проектируем сейчас, код позже** - ок?
|
||||
3. Коллекции - делать сразу после MVP или откладываем до 500 юзеров?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Философия каталога Indexium — Open Source Only, Zero Storage
|
||||
# Философия каталога Indexium - Open Source Only, Zero Storage
|
||||
|
||||
> **Тезис:** Indexium — не хостинг файлов. Ты даёшь свой GitHub, мы даём индексацию, поиск и доверие. Все моды в каталоге обязаны быть open source.
|
||||
> **Тезис:** Indexium - не хостинг файлов. Ты даёшь свой GitHub, мы даём индексацию, поиск и доверие. Все моды в каталоге обязаны быть open source.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -19,22 +19,22 @@
|
|||
|
||||
**Почему это круто:**
|
||||
- Дешёво: VPS $5 + managed Postgres, без S3.
|
||||
- Честно: автор контролирует файлы, может удалить релиз — он пропадёт и у нас (через webhook `release.deleted`).
|
||||
- Устойчиво к DMCA: мы — индексатор, а не дистрибьютор (как `crates.io` vs `GitHub`).
|
||||
- Честно: автор контролирует файлы, может удалить релиз - он пропадёт и у нас (через webhook `release.deleted`).
|
||||
- Устойчиво к DMCA: мы - индексатор, а не дистрибьютор (как `crates.io` vs `GitHub`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Open Source Only — честь и правило
|
||||
## 2. Open Source Only - честь и правило
|
||||
|
||||
### Что значит "обязан быть open source"
|
||||
|
||||
Мод принимается в каталог только если:
|
||||
|
||||
1. **Репозиторий публичный** (`private: false` через GitHub API).
|
||||
2. **Есть файл лицензии** в корне: `LICENSE` / `COPYING` / `LICENSE.md`. Проверяем через `GET /repos/{owner}/{repo}/license` — поле `license.spdx_id != null` и `license.spdx_id != "NOASSERTION"`.
|
||||
2. **Есть файл лицензии** в корне: `LICENSE` / `COPYING` / `LICENSE.md`. Проверяем через `GET /repos/{owner}/{repo}/license` - поле `license.spdx_id != null` и `license.spdx_id != "NOASSERTION"`.
|
||||
3. **Лицензия из allow-list OSI:** `MIT`, `Apache-2.0`, `GPL-2.0`, `GPL-3.0`, `LGPL-2.1`, `LGPL-3.0`, `MPL-2.0`, `BSD-2/3-Clause`, `CC0-1.0`, `Unlicense`, `EUPL-1.2`, `AGPL-3.0`. Список расширяется через ADR.
|
||||
4. **Исходники соответствуют артефакту** (best-effort): проверяем что в репо есть `fabric.mod.json` / `gradle.properties` с тем же `mod_id`/`version` что и в `.jar`. Полная reproducible-build проверка — в backlog.
|
||||
5. **Нет обфускации/шифрования** в релизе без исходников: если воркер находит `Runtime.exec` без открытого кода — флаг `suspicious`.
|
||||
4. **Исходники соответствуют артефакту** (best-effort): проверяем что в репо есть `fabric.mod.json` / `gradle.properties` с тем же `mod_id`/`version` что и в `.jar`. Полная reproducible-build проверка - в backlog.
|
||||
5. **Нет обфускации/шифрования** в релизе без исходников: если воркер находит `Runtime.exec` без открытого кода - флаг `suspicious`.
|
||||
|
||||
> **На MVP** достаточно п.1 + п.2 (любая распознанная лицензия GitHub). Строгий OSI allow-list включаем после первых 100 модов.
|
||||
|
||||
|
|
@ -43,23 +43,23 @@
|
|||
```
|
||||
POST /mods/import { repo: "owner/repo" }
|
||||
→ GitHub API: GET /repos/{repo} → private? reject 422
|
||||
→ GET /repos/{repo}/license → null? reject 422 "LICENSE required — open source only"
|
||||
→ GET /repos/{repo}/license → null? reject 422 "LICENSE required - open source only"
|
||||
→ GET /repos/{repo}/contents/fabric.mod.json?ref=main → not found? reject
|
||||
→ Создаём mods + ставим webhook
|
||||
```
|
||||
|
||||
При каждом `release.published` повторно проверяем лицензию — если автор сменил на `NOASSERTION`/сделал приватным → мод помечается `deprecated`, скрывается из поиска, но старые версии доступны (кэш).
|
||||
При каждом `release.published` повторно проверяем лицензию - если автор сменил на `NOASSERTION`/сделал приватным → мод помечается `deprecated`, скрывается из поиска, но старые версии доступны (кэш).
|
||||
|
||||
### Что показываем пользователю
|
||||
|
||||
- Бейдж `OSI: MIT` на карточке мода, ссылка на `LICENSE` на GitHub.
|
||||
- Фильтр `license:MIT` в поиске.
|
||||
- Страница `/manifesto` — манифест: "Почему только open source" (прозрачность, безопасность, форки, обучение).
|
||||
- Страница `/manifesto` - манифест: "Почему только open source" (прозрачность, безопасность, форки, обучение).
|
||||
|
||||
### Edge cases
|
||||
|
||||
- **Форки:** разрешены, но `slug` уникален, показываем `fork_of: owner/repo`. Оригинал помечается `upstream`.
|
||||
- **Мульти-мод репо (монорепо):** на MVP 1 репо = 1 мод. Позже — поддержка `mods.toml` с несколькими `modId`.
|
||||
- **Мульти-мод репо (монорепо):** на MVP 1 репо = 1 мод. Позже - поддержка `mods.toml` с несколькими `modId`.
|
||||
- **Организация vs личный акк:** оба ок, если репо публичное и лицензия есть.
|
||||
- **Что если автор закрыл репо?** Webhook `repository.privatized` → скрываем мод, чистим кэш, храним метаданные 30 дней для восстановления.
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ POST /mods/import { repo: "owner/repo" }
|
|||
|
||||
## 3. Что это даёт экосистеме
|
||||
|
||||
- **Доверие:** любой может `git clone`, проверить код, собрать самому — нет "левый jar с майнером".
|
||||
- **Доверие:** любой может `git clone`, проверить код, собрать самому - нет "левый jar с майнером".
|
||||
- **Долговечность:** даже если Indexium умрёт, моды живут на GitHub.
|
||||
- **Культура:** стимулируем PR'ы, а не "скачал и забыл". Профили показывают контрибьюторов, а не только owner.
|
||||
|
||||
|
|
@ -77,6 +77,6 @@ POST /mods/import { repo: "owner/repo" }
|
|||
|
||||
- Не принимаем бинарники без исходников (даже если автор "обещает" открыть позже).
|
||||
- Не зеркалируем закрытые репозитории, даже с токеном.
|
||||
- Не храним `.jar` у себя даже кэшем (кроме 64KB хвоста для парсинга — эфемерно).
|
||||
- Не храним `.jar` у себя даже кэшем (кроме 64KB хвоста для парсинга - эфемерно).
|
||||
|
||||
См. также: `docs/auth-profiles.md` — как профили усиливают open source (контрибьюторы, верификация), `docs/adr/004-open-source-only.md`.
|
||||
См. также: `docs/auth-profiles.md` - как профили усиливают open source (контрибьюторы, верификация), `docs/adr/004-open-source-only.md`.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy search
|
|||
|
||||
## 2. Таблицы
|
||||
|
||||
### `authors` — авторы (зеркало GitHub users)
|
||||
### `authors` - авторы (зеркало GitHub users)
|
||||
|
||||
```sql
|
||||
CREATE TABLE authors (
|
||||
|
|
@ -25,7 +25,7 @@ CREATE TABLE authors (
|
|||
);
|
||||
```
|
||||
|
||||
### `mods` — моды (один репозиторий = один мод на MVP)
|
||||
### `mods` - моды (один репозиторий = один мод на MVP)
|
||||
|
||||
```sql
|
||||
CREATE TABLE mods (
|
||||
|
|
@ -68,7 +68,7 @@ BEFORE INSERT OR UPDATE OF name, summary, description ON mods
|
|||
FOR EACH ROW EXECUTE FUNCTION mods_search_vector_update();
|
||||
```
|
||||
|
||||
### `mod_versions` — версии / релизы
|
||||
### `mod_versions` - версии / релизы
|
||||
|
||||
```sql
|
||||
CREATE TABLE mod_versions (
|
||||
|
|
@ -91,7 +91,7 @@ CREATE INDEX idx_versions_lookup ON mod_versions USING GIN (game_versions, loade
|
|||
CREATE INDEX idx_versions_sha ON mod_versions (file_sha256);
|
||||
```
|
||||
|
||||
### `webhook_deliveries` — идемпотентность webhook'ов (<50ms ответ)
|
||||
### `webhook_deliveries` - идемпотентность webhook'ов (<50ms ответ)
|
||||
|
||||
```sql
|
||||
CREATE TABLE webhook_deliveries (
|
||||
|
|
@ -107,11 +107,11 @@ CREATE TABLE webhook_deliveries (
|
|||
-- В handler: если affected_rows == 0 → 409 Already Processed, иначе push в Redis Streams.
|
||||
```
|
||||
|
||||
> **Почему так:** Ingestion API должен ответить `202` за <50ms. Сначала `INSERT ... ON CONFLICT DO NOTHING` в `webhook_deliveries`, только потом `XADD` в Redis. Если `delivery_id` уже есть — сразу `409` без очереди.
|
||||
> **Почему так:** Ingestion API должен ответить `202` за <50ms. Сначала `INSERT ... ON CONFLICT DO NOTHING` в `webhook_deliveries`, только потом `XADD` в Redis. Если `delivery_id` уже есть - сразу `409` без очереди.
|
||||
|
||||
### `mod_authors` — M2M авторы/контрибьюторы
|
||||
### `mod_authors` - M2M авторы/контрибьюторы
|
||||
|
||||
На MVP `mods.author_github_id` достаточно (1 репо = 1 owner). Для организаций и соавторов — нормализуем сразу, чтобы не мигрировать болезненно:
|
||||
На MVP `mods.author_github_id` достаточно (1 репо = 1 owner). Для организаций и соавторов - нормализуем сразу, чтобы не мигрировать болезненно:
|
||||
|
||||
```sql
|
||||
CREATE TABLE mod_authors (
|
||||
|
|
@ -129,20 +129,20 @@ CREATE INDEX idx_mod_authors_github ON mod_authors (github_id);
|
|||
|
||||
> **MVP стратегия:** оставляем `mods.author_github_id` (как сейчас в `migrations/20260906000000_init_schema.sql`) для простых запросов, но добавляем `mod_authors` когда появится первый кейс организации. В `GET /mods/:slug` отдаём `authors: [{login, role}]` вместо одиночного `author`.
|
||||
|
||||
### `mod_versions.file_size` — откуда берётся
|
||||
### `mod_versions.file_size` - откуда берётся
|
||||
|
||||
В `api-spec.md` поле `file_size` возвращается клиентам. Заполняется воркером из HTTP-заголовка:
|
||||
|
||||
```sql
|
||||
-- уже в mod_versions: file_size BIGINT — bytes из Content-Length
|
||||
-- уже в mod_versions: file_size BIGINT - bytes из Content-Length
|
||||
```
|
||||
|
||||
Алгоритм воркера (`jar_parser.rs`):
|
||||
1. `HEAD download_url` → `Content-Length` + `Accept-Ranges: bytes`.
|
||||
2. Если `Content-Length` отсутствует — fallback на `GET` с `Range: bytes=0-0` и парсинг `Content-Range`.
|
||||
2. Если `Content-Length` отсутствует - fallback на `GET` с `Range: bytes=0-0` и парсинг `Content-Range`.
|
||||
3. Значение пишется в `mod_versions.file_size` при `INSERT`.
|
||||
|
||||
> GitHub CDN (`objects.githubusercontent.com`) всегда отдаёт `Content-Length` и поддерживает `Range` для release assets — проверено для `.jar` до 50MB.
|
||||
> GitHub CDN (`objects.githubusercontent.com`) всегда отдаёт `Content-Length` и поддерживает `Range` для release assets - проверено для `.jar` до 50MB.
|
||||
|
||||
### `dependencies` (опционально, нормализованная)
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ CREATE TABLE mod_dependencies (
|
|||
);
|
||||
```
|
||||
|
||||
## 3. Телеметрия — аналог bStats (см. docs/analytics.md)
|
||||
## 3. Телеметрия - аналог bStats (см. docs/analytics.md)
|
||||
|
||||
```sql
|
||||
-- Полуагрегат: один пинг = одна строка, TTL 30 дней (DELETE via cron)
|
||||
|
|
@ -176,7 +176,7 @@ CREATE TABLE mod_telemetry_pings (
|
|||
CREATE INDEX idx_telemetry_lookup ON mod_telemetry_pings (mod_id, pinged_at DESC);
|
||||
CREATE INDEX idx_telemetry_hash ON mod_telemetry_pings (server_hash, pinged_at);
|
||||
|
||||
-- Суточный агрегат — хранится навсегда
|
||||
-- Суточный агрегат - хранится навсегда
|
||||
CREATE TABLE mod_daily_stats (
|
||||
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
|
|
@ -191,13 +191,13 @@ CREATE TABLE analytics_salts (
|
|||
date DATE PRIMARY KEY,
|
||||
salt CHAR(64) NOT NULL
|
||||
);
|
||||
-- Хеш: server_hash = sha256(server_uuid || salt_for_today) — позволяет считать уникальные за день, но не трекать сквозь дни.
|
||||
-- Хеш: server_hash = sha256(server_uuid || salt_for_today) - позволяет считать уникальные за день, но не трекать сквозь дни.
|
||||
-- Rate limit: Redis SET server_hash:mod_id NX EX 900 (1 пинг / 15 мин)
|
||||
-- TTL: DELETE FROM mod_telemetry_pings WHERE pinged_at < NOW() - INTERVAL '30 days' (cron hourly)
|
||||
-- Агрегация: кроном раз в час INSERT INTO mod_daily_stats ... ON CONFLICT DO UPDATE COUNT(DISTINCT server_hash)
|
||||
```
|
||||
|
||||
> Postgres хватает до ~10M пингов/мес. При росте — `SELECT create_hypertable('mod_telemetry_pings','pinged_at')` (TimescaleDB) или ClickHouse без смены схемы.
|
||||
> Postgres хватает до ~10M пингов/мес. При росте - `SELECT create_hypertable('mod_telemetry_pings','pinged_at')` (TimescaleDB) или ClickHouse без смены схемы.
|
||||
|
||||
## 4. Пример запросов
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ migrations/
|
|||
|
||||
## 6. Сиды
|
||||
|
||||
Для дев-окружения: `migrations/seeds/dev.sql` — 5 фейковых модов + версии, чтобы фронт сразу имел данные.
|
||||
Для дев-окружения: `migrations/seeds/dev.sql` - 5 фейковых модов + версии, чтобы фронт сразу имел данные.
|
||||
|
||||
## 7. Будущие расширения
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Git-стратегия — почему монорепо
|
||||
# Git-стратегия - почему монорепо
|
||||
|
||||
## Решение (ADR-001)
|
||||
|
||||
**Выбрано: монорепо в корне `/` с двумя пакетами `indexium-backend/` и `indexium-frontend/`.**
|
||||
|
||||
Альтернатива — полирепо (два отдельных git) — отклонена на старте.
|
||||
Альтернатива - полирепо (два отдельных git) - отклонена на старте.
|
||||
|
||||
## Почему монорепо
|
||||
|
||||
|
|
@ -16,11 +16,11 @@
|
|||
| Версионирование контрактов | Фронт всегда соответствует бэку в `main` | Нужен отдельный версионинг |
|
||||
| Стоимость поддержки | Минимальна для 1-3 человек | Оверхед: 2 набора настроек, 2 issue-треккера |
|
||||
|
||||
Монорепо оправдан пока команда <10 человек и релизный цикл единый. Если в будущем бэкенд и фронт разойдутся по командам/каденсу — легко разрезать через `git filter-repo` или `git subtree`.
|
||||
Монорепо оправдан пока команда <10 человек и релизный цикл единый. Если в будущем бэкенд и фронт разойдутся по командам/каденсу - легко разрезать через `git filter-repo` или `git subtree`.
|
||||
|
||||
## Что было сделано
|
||||
|
||||
1. Удалён пустой `.git` из `indexium-backend/` (коммитов не было — безопасно).
|
||||
1. Удалён пустой `.git` из `indexium-backend/` (коммитов не было - безопасно).
|
||||
2. `git init --initial-branch=main` в корне `Indexium/`.
|
||||
3. Корневой `.gitignore` + локальные.
|
||||
4. Весь код теперь трекается как:
|
||||
|
|
@ -38,8 +38,8 @@
|
|||
## Workflow
|
||||
|
||||
### Ветки
|
||||
- `main` — защищённая, только через PR.
|
||||
- `feat/<scope>-<short>` — фичи, напр. `feat/webhook-hmac`.
|
||||
- `main` - защищённая, только через PR.
|
||||
- `feat/<scope>-<short>` - фичи, напр. `feat/webhook-hmac`.
|
||||
- `fix/<scope>-<short>`.
|
||||
|
||||
### Коммиты (Conventional Commits)
|
||||
|
|
@ -52,7 +52,7 @@ chore(frontend): bump svelte 5.56 → 5.57
|
|||
|
||||
### PR
|
||||
- Один PR = одна фича/фикс.
|
||||
- Если меняется API — в том же PR обновляется `docs/api-spec.md` и фронт-клиент.
|
||||
- Если меняется API - в том же PR обновляется `docs/api-spec.md` и фронт-клиент.
|
||||
- CI должен пройти: `cargo fmt --check`, `cargo clippy`, `cargo test`, `svelte-check`.
|
||||
|
||||
### Локально
|
||||
|
|
@ -70,17 +70,17 @@ git push -u origin feat/my-feature
|
|||
|
||||
Сигналы что пора:
|
||||
- >10 активных контрибьюторов, частые конфликты в `main`.
|
||||
- Фронт деплоится 10× в день, бэк — 1× в неделю (разный каденс).
|
||||
- Фронт деплоится 10× в день, бэк - 1× в неделю (разный каденс).
|
||||
- Нужны разные права доступа (внешние контрибьюторы только к фронту).
|
||||
|
||||
Как резать: `git subtree split -P indexium-backend -b backend-only` и аналогично для фронта, либо `git filter-repo --path`.
|
||||
|
||||
## Альтернативы (для справки)
|
||||
|
||||
- **Git submodules** — не рекомендуется: сложны, легко сломать, плохой DX.
|
||||
- **Polyrepo + shared package** — имеет смысл если выносить `openapi`/`types` в отдельный npm/crate.
|
||||
- **Git submodules** - не рекомендуется: сложны, легко сломать, плохой DX.
|
||||
- **Polyrepo + shared package** - имеет смысл если выносить `openapi`/`types` в отдельный npm/crate.
|
||||
|
||||
## ADR
|
||||
|
||||
- ADR-001: Монорепо vs полирепо — принято монорепо (этот документ).
|
||||
- ADR-001: Монорепо vs полирепо - принято монорепо (этот документ).
|
||||
- Следующие ADR складывать в `docs/adr/NNN-title.md`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue