From da9e17f45f7f6659542b41352dd7668bc7c0bb57 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Tue, 8 Sep 2026 13:36:43 +0700 Subject: [PATCH] added AGENTS.md and docs in root dir --- AGENTS.md | 167 ++++++++++++++++++++++++++++++++++++++++++ README.md | 86 ++++++++++++++++++++-- docs/ARCHITECTURE.md | 85 +++++++++++++++++++++ docs/DATA_MODEL.md | 108 +++++++++++++++++++++++++++ docs/DEVELOPMENT.md | 106 +++++++++++++++++++++++++++ docs/SECURITY.md | 95 ++++++++++++++++++++++++ docs/SYNC_PROTOCOL.md | 88 ++++++++++++++++++++++ 7 files changed, 728 insertions(+), 7 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DATA_MODEL.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/SECURITY.md create mode 100644 docs/SYNC_PROTOCOL.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d8d80d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,167 @@ +# AGENTS.md — корень монорепозитория Balance + +Этот файл — точка входа для AI-агентов и разработчиков, работающих в +репозитории Balance. Он описывает, как устроены три подпроекта, как они +связаны друг с другом, и куда идти за подробностями. **Каждый подпроект +имеет собственный `AGENTS.md` с детальными правилами — читай его перед +любым изменением внутри соответствующей папки.** + +## Что это за проект + +Balance — приложение для учёта личных финансов, доступное на пяти +платформах, которые используют одну и ту же доменную модель и один и +тот же протокол синхронизации: + +| Платформа | Папка | Стек | +| --- | --- | --- | +| iOS 17+ | `Balance/` | SwiftUI + SwiftData | +| macOS 14+ | `Balance/` (`BalanceMac/`) | SwiftUI + SwiftData | +| watchOS 10+ | `Balance/` (`BalanceWatch/`) | SwiftUI + SwiftData | +| Android 8.0+ | `BalanceAndroid/` | Kotlin + Jetpack Compose + Room | +| Web (встроен в сервер) | `BalanceServer/` | Go + embedded static client | +| Сервер синхронизации | `BalanceServer/` | Go + SQLite | + +Все клиенты — iOS, macOS, watchOS, Android, web — синхронизируются через +один и тот же self-hosted сервер (`BalanceServer`) по единому HTTP(S) +API: регистрация, вход, refresh-токены, push/pull изменений с курсором, +tombstone-удаления, пользовательские категории и месячные бюджеты. + +## Структура репозитория + +```text +Balance/ # монорепозиторий +├── AGENTS.md # этот файл — общие правила для агентов +├── README.md # общий обзор и точки входа +├── docs/ # сводная документация по всему проекту +│ ├── ARCHITECTURE.md # как связаны клиенты и сервер +│ ├── SYNC_PROTOCOL.md # единый протокол синхронизации +│ ├── DATA_MODEL.md # сопоставление сущностей между платформами +│ ├── SECURITY.md # сквозные требования безопасности +│ └── DEVELOPMENT.md # как собрать и проверить все три проекта +│ +├── Balance/ # Xcode-проект: iOS + macOS + watchOS +│ ├── AGENTS.md # правила для Apple-клиента +│ ├── README.md +│ └── docs/ # ARCHITECTURE, DATA_MODEL, SYNC, DEVELOPMENT, PRODUCT +│ +├── BalanceAndroid/ # Android-проект (Gradle/Kotlin) +│ ├── AGENTS.md # правила для Android-клиента +│ ├── README.md +│ └── docs/ # ARCHITECTURE, DATA_MODEL, SYNC, DEVELOPMENT, PRODUCT +│ +└── BalanceServer/ # Go-сервер синхронизации + web-клиент + ├── AGENTS.md # правила для сервера + ├── README.md + └── docs/ # ARCHITECTURE, API, DATA_MODEL, SYNC, SECURITY, DEVELOPMENT, DEPLOYMENT, PRODUCT +``` + +## Как работать в этом репозитории + +### 1. Сначала пойми, в каком подпроекте изменение + +Каждый подпроект — независимый билд с собственными зависимостями, +тестами и CI-шагами. Изменения в одной папке не должны требовать +правок в другой, **кроме** случаев, когда меняется общий контракт: + +- HTTP API / формат sync-запросов и ответов (`BalanceServer`); +- набор синхронизируемых полей сущности (транзакция, категория, бюджет); +- правила финансовых расчётов (баланс, корректировки, аналитика). + +Если меняется общий контракт — обнови все три подпроекта согласованно +и синхронизируй документацию (`docs/SYNC_PROTOCOL.md`, а также +`docs/SYNC.md`/`docs/API.md`/`docs/DATA_MODEL.md` внутри каждой папки). + +### 2. Единая доменная модель + +Три клиента независимо реализуют одну и ту же модель на своём стеке +(SwiftData / Room), но со семантически одинаковыми сущностями: + +- **Транзакция** (доход/расход, сумма, дата, заметка, категория — + хранится snapshot'ом имени/иконки/цвета категории на момент операции); +- **Пользовательская категория** (имя, иконка/emoji, цвет); +- **Месячный бюджет** (категория, месяц, лимит); +- **Tombstone/deletion journal** для распространения удалений между + устройствами. + +Общие правила расчётов (баланс = доходы − расходы; корректировки +баланса учитываются в общем балансе, но не в периодической аналитике; +расходы группируются по имени категории) продублированы в: + +- `Balance/Balance/Utilities/FinanceCalculations.swift` (Apple); +- `BalanceAndroid/.../data/FinanceMath.kt` (Android). + +При изменении этих правил на одной платформе — переноси изменение на +другую и обновляй оба набора тестов +(`BalanceTests/FinanceCalculationsTests.swift`, `FinanceMathTest`). + +### 3. Синхронизация — общий контракт + +Единый протокол описан в `docs/SYNC_PROTOCOL.md` (сводно) и подробно — +в `BalanceServer/docs/API.md` и `BalanceServer/docs/SYNC.md`. Клиентские +реализации: + +- Apple: `Balance/Balance/Shared/ServerSync.swift`; +- Android: `BalanceAndroid/.../sync/SyncRepository.kt`. + +Изменение cursor/sequence semantics, JSON-полей или auth-заголовков — +breaking change для всех клиентов сразу. Не меняй его в одном +подпроекте, не проверив остальные два. + +### 4. Безопасность (сквозные требования) + +- Секреты (пароли, access/refresh токены) никогда не хранятся в + открытом виде: Keychain (Apple), Android Keystore + AES/GCM + (Android), Argon2id-хеши + SHA-256 refresh-token хеши (сервер). +- HTTPS обязателен для удалённого сервера на всех клиентах; HTTP + разрешён только для `localhost`/`127.0.0.1`/`::1`. +- Не логируй пароли, токены или заголовок `Authorization` ни в одном + из подпроектов. +- Не коммить секреты, production-URL или credentials. + +Подробности — `docs/SECURITY.md` и `BalanceServer/docs/SECURITY.md`. + +### 5. Тестирование перед PR + +Запусти проверки для всех подпроектов, которые ты менял: + +```bash +# Apple (из Balance/) +xcodebuild -project Balance.xcodeproj -scheme Balance \ + -destination 'platform=iOS Simulator,name=' test + +# Android (из BalanceAndroid/) +./gradlew clean test assembleDebug + +# Server (из BalanceServer/) +go test ./... +go vet ./... +go build ./... +``` + +Если менялся общий контракт (API/sync/data model) — прогони проверки +во всех трёх, а не только в изменённой папке. + +### 6. Документация + +- Меняешь контракт синхронизации → обнови `docs/SYNC_PROTOCOL.md` в + корне **и** `BalanceServer/docs/SYNC.md`/`API.md`, а также + `docs/SYNC.md` в `Balance/` и `BalanceAndroid/`. +- Меняешь модель данных на любой платформе → обнови `docs/DATA_MODEL.md` + в корне и в затронутом подпроекте. +- Меняешь способ сборки/запуска → обнови `docs/DEVELOPMENT.md` в + корне и соответствующий `README.md`/`docs/DEVELOPMENT.md` подпроекта. +- Каждый подпроект содержит собственный подробный `AGENTS.md` — этот + файл не заменяет их, а даёт общую картину и правила координации между + платформами. + +## PR checklist (для изменений, затрагивающих несколько платформ) + +- [ ] Определены все подпроекты, которые затрагивает изменение. +- [ ] Общий контракт (API/sync/data model) обновлён согласованно во + всех платформах, а не только в одной. +- [ ] Финансовые расчёты остаются идентичными на Apple и Android. +- [ ] Обновлены `docs/SYNC_PROTOCOL.md`, `docs/DATA_MODEL.md` в корне, + если менялась схема/протокол. +- [ ] Секреты и токены не добавлены в репозиторий ни в одном подпроекте. +- [ ] Тесты пройдены в каждом изменённом подпроекте. +- [ ] Локальный `AGENTS.md` соответствующего подпроекта тоже соблюдён. diff --git a/README.md b/README.md index 74684ab..dc18c1c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,85 @@ # Balance — все платформы -В архиве находятся нативные приложения личных финансов и общий self-hosted сервер синхронизации. +Balance — приложение для учёта личных финансов с нативными клиентами +для iOS, macOS, watchOS, Android и встроенным web-интерфейсом, а также +self-hosted сервер синхронизации на Go. В архиве находится +монорепозиторий из трёх независимо собираемых проектов. -| Папка | Назначение | +| Папка | Назначение | Стек | +| --- | --- | --- | +| [`Balance/`](Balance/README.md) | iOS 17+, macOS 14+ и watchOS 10+ | SwiftUI + SwiftData | +| [`BalanceAndroid/`](BalanceAndroid/README.md) | Android 8.0+ | Kotlin + Jetpack Compose + Room | +| [`BalanceServer/`](BalanceServer/README.md) | Go-сервер авторизации, синхронизации и встроенный web-клиент | Go + SQLite | + +Все пять клиентов — iOS, macOS, watchOS, Android и web — используют +одну модель данных и один протокол сервера: операции, пользовательские +категории, бюджеты, удаления, регистрация, вход, refresh-токены и +постраничная двусторонняя синхронизация. Web-интерфейс открывается по +адресу запущенного сервера и не требует отдельной установки. + +## Быстрый старт + +1. **Поднять сервер** — см. [`BalanceServer/README.md`](BalanceServer/README.md#quick-start-with-docker) + (Docker или локальная сборка Go-бинарника). +2. **Открыть Apple-проект** — `open Balance/Balance.xcodeproj`, см. + [`Balance/README.md`](Balance/README.md). +3. **Собрать Android-приложение** — `cd BalanceAndroid && ./gradlew assembleDebug`, + см. [`BalanceAndroid/README.md`](BalanceAndroid/README.md). +4. В любом клиенте укажите адрес запущенного сервера в настройках + аккаунта, чтобы включить синхронизацию между устройствами. + +## Документация + +Документация организована на двух уровнях: + +- **Корневой уровень** (`docs/`) — то, что относится ко всем платформам + сразу: как устроена система в целом, единый протокол синхронизации, + сквозная модель данных и требования безопасности. +- **Уровень подпроекта** (`Balance/docs/`, `BalanceAndroid/docs/`, + `BalanceServer/docs/`) — детали конкретной платформы: код, схемы, + сборка, деплой. + +| Документ | Описание | | --- | --- | -| `Balance` | iOS 17+, macOS 14+ и watchOS 10+, SwiftUI + SwiftData | -| `BalanceAndroid` | Android 8.0+, Kotlin + Jetpack Compose + Room | -| `BalanceServer` | Go-сервер авторизации и синхронизации со встроенным web-приложением | +| [`AGENTS.md`](AGENTS.md) | Правила для AI-агентов и разработчиков, работающих во всём репозитории | +| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Как связаны клиенты и сервер, общая схема системы | +| [`docs/SYNC_PROTOCOL.md`](docs/SYNC_PROTOCOL.md) | Единый протокол синхронизации между всеми клиентами и сервером | +| [`docs/DATA_MODEL.md`](docs/DATA_MODEL.md) | Сопоставление доменных сущностей между платформами | +| [`docs/SECURITY.md`](docs/SECURITY.md) | Сквозные требования безопасности (токены, HTTPS, хранение секретов) | +| [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | Как собрать, запустить и протестировать все три проекта | -Все пять клиентов — iOS, macOS, watchOS, Android и web — используют одну модель данных и один протокол сервера: операции, пользовательские категории, бюджеты, удаления, регистрация, вход, refresh-токены и постраничная двусторонняя синхронизация. Web-интерфейс открывается по адресу запущенного сервера и не требует отдельной установки. +### Документация по подпроектам -Инструкции по запуску находятся в `Balance/README.md`, `BalanceAndroid/README.md` и `BalanceServer/README.md`. +- **Apple (`Balance/`):** [`AGENTS.md`](Balance/AGENTS.md) · + [`docs/ARCHITECTURE.md`](Balance/docs/ARCHITECTURE.md) · + [`docs/DATA_MODEL.md`](Balance/docs/DATA_MODEL.md) · + [`docs/SYNC.md`](Balance/docs/SYNC.md) · + [`docs/DEVELOPMENT.md`](Balance/docs/DEVELOPMENT.md) · + [`docs/PRODUCT.md`](Balance/docs/PRODUCT.md) +- **Android (`BalanceAndroid/`):** [`AGENTS.md`](BalanceAndroid/AGENTS.md) · + [`docs/ARCHITECTURE.md`](BalanceAndroid/docs/ARCHITECTURE.md) · + [`docs/DATA_MODEL.md`](BalanceAndroid/docs/DATA_MODEL.md) · + [`docs/SYNC.md`](BalanceAndroid/docs/SYNC.md) · + [`docs/DEVELOPMENT.md`](BalanceAndroid/docs/DEVELOPMENT.md) · + [`docs/PRODUCT.md`](BalanceAndroid/docs/PRODUCT.md) +- **Server (`BalanceServer/`):** [`AGENTS.md`](BalanceServer/AGENTS.md) · + [`docs/ARCHITECTURE.md`](BalanceServer/docs/ARCHITECTURE.md) · + [`docs/API.md`](BalanceServer/docs/API.md) · + [`docs/DATA_MODEL.md`](BalanceServer/docs/DATA_MODEL.md) · + [`docs/SYNC.md`](BalanceServer/docs/SYNC.md) · + [`docs/SECURITY.md`](BalanceServer/docs/SECURITY.md) · + [`docs/DEVELOPMENT.md`](BalanceServer/docs/DEVELOPMENT.md) · + [`docs/DEPLOYMENT.md`](BalanceServer/docs/DEPLOYMENT.md) · + [`docs/PRODUCT.md`](BalanceServer/docs/PRODUCT.md) + +## Структура репозитория + +```text +Balance/ +├── AGENTS.md # общие правила для агентов по всему репо +├── README.md # этот файл +├── docs/ # сводная документация по всем платформам +├── Balance/ # Xcode-проект: iOS + macOS + watchOS +├── BalanceAndroid/ # Android (Gradle/Kotlin) +└── BalanceServer/ # Go-сервер + встроенный web-клиент +``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..ca7ddc0 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,85 @@ +# Архитектура Balance (обзор всей системы) + +Этот документ описывает, как связаны между собой три подпроекта. +Детальная архитектура каждой платформы — в `docs/ARCHITECTURE.md` +соответствующего подпроекта. + +## Общая схема + +```text +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ iOS клиент │ │ macOS клиент│ │watchOS клиент│ Apple: SwiftUI + SwiftData +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ (Balance/) + │ │ │ + └────────┬────────┴────────┬────────┘ + │ │ + HTTPS (или HTTP только для localhost) + │ │ + ┌────────┴────────┐ │ + │ Android клиент │ │ Kotlin + Jetpack Compose + Room + │ (BalanceAndroid)│ │ (BalanceAndroid/) + └────────┬────────┘ │ + │ │ + ▼ ▼ + ┌───────────────────────────────┐ + │ BalanceServer │ Go 1.25 + SQLite + │ ┌───────────┬───────────────┐ │ (BalanceServer/) + │ │ HTTP API │ Web-клиент │ │ + │ │ /v1/* │ (embedded) │ │ + │ └───────────┴───────────────┘ │ + └───────────────────────────────┘ +``` + +Каждый нативный клиент (iOS/macOS/watchOS/Android) хранит полную копию +данных локально (SwiftData или Room) и работает офлайн. Сервер — +единственная точка правды при конфликте между устройствами одного +аккаунта: он хранит append-only лог изменений с курсором и применяет +last-write-wins по времени изменения записи (`updatedAt`/`syncUpdatedAt`). + +Web-клиент не является отдельным подпроектом — это статические файлы, +встроенные (embed) в Go-бинарник сервера, использующие тот же HTTP API, +что и нативные клиенты, но с cookie-based сессией вместо Bearer-токена. + +## Компоненты сервера + +- **HTTP API** (`internal/httpapi/`) — auth-эндпоинты и `/v1/sync`. +- **auth** (`internal/auth/`) — пароли (Argon2id), JWT access/refresh + токены. +- **database** (`internal/database/`) — SQLite-хранилище, схема, + индексы. +- **config** (`internal/config/`) — переменные окружения. +- **web** (`internal/httpapi/web.go`) — раздача встроенного + web-приложения. + +Подробнее — [`BalanceServer/docs/ARCHITECTURE.md`](../BalanceServer/docs/ARCHITECTURE.md). + +## Компоненты Apple-клиента + +- **Models** — SwiftData `@Model` сущности. +- **Utilities/FinanceCalculations** — единый источник правил расчёта + баланса и аналитики. +- **Shared/ServerSync** — HTTP-клиент, авторизация, sync orchestration, + Keychain. +- **Views** — общий SwiftUI UI; `BalanceMac/` и `BalanceWatch/` — + платформенно-специфичные точки входа и экраны. + +Подробнее — [`Balance/docs/ARCHITECTURE.md`](../Balance/docs/ARCHITECTURE.md). + +## Компоненты Android-клиента + +- **data/** — Room entities, DAO, `FinanceMath` (та же роль, что и + `FinanceCalculations` в Apple-клиенте). +- **sync/** — `ServerApi` (HTTP/auth), `ServerPreferences` (URL/cursor), + `SecureSessionStore` (Android Keystore), `SyncRepository` + (orchestration), `SyncWorker` (периодический фон). +- **ui/** — Jetpack Compose экраны, состояние из `MainViewModel`. + +Подробнее — [`BalanceAndroid/docs/ARCHITECTURE.md`](../BalanceAndroid/docs/ARCHITECTURE.md). + +## Принцип согласованности между платформами + +Три клиента реализуют одну и ту же доменную модель и один и тот же +sync-протокол независимо друг от друга, на разных стеках. Это +осознанный компромисс (нативный UX на каждой платформе) с обратной +стороной: любое изменение общего контракта нужно вносить согласованно +в трёх местах. См. `docs/SYNC_PROTOCOL.md` и `docs/DATA_MODEL.md`. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md new file mode 100644 index 0000000..239e7c6 --- /dev/null +++ b/docs/DATA_MODEL.md @@ -0,0 +1,108 @@ +# Сквозная модель данных + +Три платформы реализуют одну и ту же доменную модель независимо, на +своём стеке хранения. Этот документ сопоставляет поля между ними. +Полные описания — [`Balance/docs/DATA_MODEL.md`](../Balance/docs/DATA_MODEL.md), +[`BalanceAndroid/docs/DATA_MODEL.md`](../BalanceAndroid/docs/DATA_MODEL.md), +[`BalanceServer/docs/DATA_MODEL.md`](../BalanceServer/docs/DATA_MODEL.md). + +## Транзакция + +| Логическое поле | Apple (`FinanceTransaction`) | Android (`TransactionEntity`) | Сервер | +| --- | --- | --- | --- | +| Идентификатор | `id: UUID` | `id: String` (UUID) | stable ID | +| Сумма | `amount: Double` | `amount: Double` | amount | +| Дата операции | `date: Date` | `date: Long` (epoch millis) | date | +| Заметка | `note: String` | `note: String` | note | +| Имя категории (snapshot) | `categoryName: String` | `categoryName: String` | category presentation data | +| Иконка категории | `categoryIcon: String` (SF Symbol) | `categoryIcon: String` | — | +| Emoji категории | `categoryEmoji: String` | `categoryEmoji: String` | — | +| Цвет категории | `categoryColorName: String` | `categoryColorName: String` | — | +| Корректировка баланса | `isBalanceAdjustment: Bool` | `isBalanceAdjustment: Boolean` | balance-adjustment flag | +| Тип операции | `kindRawValue: String` (`income`/`expense`) | `kindRawValue: String` | transaction kind | +| Версия/время изменения | `syncUpdatedAt: Date` | `updatedAt: Long` | `updatedAt` | +| Владелец | сессия аккаунта | сессия аккаунта | принадлежность пользователю | + +**Категория хранится snapshot'ом**, а не ссылкой, на всех платформах: +это позволяет не терять отображение старой операции при удалении +категории и синхронизировать запись без relationship. Из-за этого +переименование пользовательской категории должно явно обновлять уже +существующие транзакции и бюджеты — и Apple-, и Android-клиент это +делают. + +## Пользовательская категория + +| Логическое поле | Apple (`CustomCategory`) | Android (`categories`) | Сервер | +| --- | --- | --- | --- | +| Идентификатор | `id: UUID` | `id` | stable ID | +| Имя | `name: String` | `name` | name | +| Иконка | `icon: String` | `icon` | icon/emoji | +| Emoji | `emoji: String` | `emoji` | — | +| Цвет | `colorName: String` | `colorName` | color | +| Тип | `kindRawValue: String` | `kindRawValue` | operation kind | +| Создание | `createdAt: Date` | `createdAt` | creation metadata | +| Версия | `syncUpdatedAt: Date` | `updatedAt` | update metadata | + +Имя пользовательской категории уникально без учёта регистра/диакритики +относительно системных и других пользовательских категорий (проверяется +на клиентах). Встроенные системные категории (Продукты, Транспорт, +Зарплата и т.д.) не персистятся как отдельные записи ни на одной +платформе — они закодированы в клиентском коде. + +## Месячный бюджет + +| Логическое поле | Apple (`MonthlyBudget`) | Android (`budgets`) | Сервер | +| --- | --- | --- | --- | +| Идентификатор | `id: UUID` | `id` | stable ID | +| Имя категории | `categoryName: String` | `categoryName` | category information | +| Иконка/emoji категории | `categoryIcon`, `categoryEmoji` | `categoryIcon`, `categoryEmoji` | — | +| Лимит | `limit: Double` | `limitAmount: Double` | monthly limit | +| Начало месяца | `monthStart: Date` | `monthStart: Long` | month start | +| Версия | `syncUpdatedAt: Date` | `updatedAt: Long` | `updatedAt` | + +Бюджет идентифицируется собственным UUID, но связывается с категорией +по сохранённому имени (`categoryName`), а не по ссылке. + +## Tombstone / deletion journal + +| Логическое поле | Apple (`SyncTombstone`) | Android (`deletion_journal`) | Сервер (change record) | +| --- | --- | --- | --- | +| Идентификатор записи | `id: UUID` | `id = entity:recordId` | record ID | +| Тип сущности | `entity: String` | `entity` | entity | +| ID удалённой записи | `recordID: String` | `recordId` | id | +| Время | `updatedAt: Date` | `updatedAt: Long` | `updatedAt` | +| Флаг удаления | подразумевается (`deleted: true` в payload) | подразумевается | `deleted: true` | + +Поддерживаемые значения `entity` одинаковы на всех платформах: +`transaction`, `category`, `budget`. + +## Финансовые инварианты (одинаковы на Apple и Android) + +1. `amount` не может быть отрицательным через обычный UI. +2. Общий баланс = `sum(income.amount) − sum(expense.amount)`; записи с + `isBalanceAdjustment == true` **учитываются** в этой сумме. +3. Периодическая аналитика **исключает** balance adjustments и записи + вне выбранного периода. +4. Расходы группируются по `categoryName` (не по ID категории). +5. `syncUpdatedAt`/`updatedAt` обязано меняться при каждом изменении + синхронизируемой сущности. +6. Удаление синхронизируемой сущности обязано создать tombstone — + прямой `DELETE`/`delete()` без tombstone запрещён. + +Источники правды по расчётам: +`Balance/Balance/Utilities/FinanceCalculations.swift` (Apple) и +`BalanceAndroid/.../data/FinanceMath.kt` (Android). При изменении +правил на одной платформе перенеси изменение на другую. + +## Инварианты на стороне сервера + +1. Record ID стабилен между синхронизациями. +2. Запись принадлежит ровно одному пользователю; клиент не может + читать/изменять чужие записи. +3. `sequence` (серверный порядок изменений) не должен уменьшаться. +4. Повторная доставка одного и того же изменения не должна повреждать + данные (идемпотентность). +5. Удаление обязано быть видимым клиенту через следующий pull. +6. Все timestamps хранятся в UTC. +7. Enum/строковые значения (`kindRawValue` и т.п.) совместимы между + iOS и Android — не переименовывать без периода совместимости. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..aee4842 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,106 @@ +# Разработка (все платформы) + +Сводная шпаргалка по сборке, запуску и проверке всех трёх подпроектов. +Подробности — в `docs/DEVELOPMENT.md` каждого подпроекта. + +## Требования по платформам + +| Подпроект | Инструменты | Минимальные версии | +| --- | --- | --- | +| `Balance/` | Xcode, Swift | Xcode 16+, iOS 17+, macOS 14+, watchOS 10+ | +| `BalanceAndroid/` | Android Studio / Gradle | AGP 8.13, Kotlin 2.3.21, compileSdk 36, minSdk 26, Java 17 | +| `BalanceServer/` | Go | Go 1.25.0+ | + +## Локальный запуск всей системы + +Порядок, удобный для end-to-end проверки sync между платформами: + +```bash +# 1. Поднять сервер +cd BalanceServer +cp .env.example .env +# задать BALANCE_JWT_SECRET (см. README.md) +go mod tidy +go run ./cmd/balance-server +# сервер слушает http://localhost:8080 + +# 2. Открыть Apple-проект и указать http://localhost:8080 в Settings → Server account +cd ../Balance +open Balance.xcodeproj + +# 3. Собрать и запустить Android-приложение +cd ../BalanceAndroid +./gradlew assembleDebug +# на эмуляторе сервер локального хоста доступен как http://10.0.2.2:8080 +``` + +Для физического iPhone/Android-устройства `localhost` указывает на само +устройство, а не на машину разработчика — используйте сетевой адрес +или HTTPS reverse proxy (см. `docs/SECURITY.md`). + +## Сборка и тесты по отдельности + +### Apple (`Balance/`) + +```bash +xcodebuild -project Balance.xcodeproj -list + +xcodebuild -project Balance.xcodeproj -scheme Balance \ + -destination 'generic/platform=iOS' build + +xcodebuild -project Balance.xcodeproj -scheme Balance \ + -destination 'platform=iOS Simulator,name=' test +``` + +Схемы: `Balance` (iOS), `BalanceMac`, `BalanceWatch`. Без +`BalanceCloudKitEnabled=YES` приложение работает в local-mode +(SwiftData без CloudKit) — рекомендуемый режим для разработки. + +### Android (`BalanceAndroid/`) + +```bash +./gradlew clean test assembleDebug +``` + +Тесты — `app/src/test/FinanceMathTest`. `local.properties` с `sdk.dir` +не должен попадать в Git. + +### Server (`BalanceServer/`) + +```bash +go mod download +go test ./... +go test -race ./... +go vet ./... +go build ./... +``` + +Docker-путь — `docker compose up -d --build` (см. +[`BalanceServer/README.md`](../BalanceServer/README.md)). + +## Что проверять при изменении общего контракта + +Если PR меняет sync API, модель данных или финансовые расчёты — прогони +проверки во всех трёх подпроектах, а не только в изменённом: + +```text +old iOS client + new server +new iOS client + server +Android client + server +concurrent sync с двух устройств одного аккаунта +login/refresh после истечения access token +delete → pull на другом устройстве +``` + +## Типовые проблемы синхронизации (одинаковый чек-лист для обеих платформ) + +1. Проверь server URL в настройках клиента. +2. Проверь правило HTTPS/localhost (см. `docs/SECURITY.md`). +3. Проверь авторизацию/сессию (login, не истёк ли refresh). +4. Проверь дату последней синхронизации и сохранённый cursor. +5. Проверь совместимость версии клиента и `BalanceServer`. +6. Если данные не удаляются на другом устройстве — убедись, что + удаление прошло через tombstone-путь + (`modelContext.deleteForSync` на Apple, + `FinanceDao.deleteTransaction/deleteCategory/deleteBudget` на + Android), а не через прямой `delete()`. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..feebc43 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,95 @@ +# Безопасность (сквозные требования) + +Этот документ собирает требования безопасности, которые применяются +ко **всем** подпроектам одновременно. Платформенно-специфичные детали — +в [`BalanceServer/docs/SECURITY.md`](../BalanceServer/docs/SECURITY.md), +`Balance/AGENTS.md` (раздел «Безопасность») и +`BalanceAndroid/AGENTS.md` (раздел «Security»). + +## Хранение секретов на клиентах + +| | Apple | Android | +| --- | --- | --- | +| Access/refresh токены | Keychain (service `BalanceServer`) | зашифрованное хранилище через Android Keystore (AES/GCM), `SecureSessionStore` | +| Несекретные настройки (URL сервера, cursor, sync metadata) | `UserDefaults` | `ServerPreferences` | +| Запрещено | пароль в `UserDefaults`, SwiftData или файлах | токены в обычных `SharedPreferences` | + +Ни один клиент не должен хранить пароль пользователя после +аутентификации — только access/refresh токены. + +## Хранение секретов на сервере + +- Пароли — только Argon2id-хеши, plaintext никогда не сохраняется. +- Refresh-токены хранятся как SHA-256 хеши, не в открытом виде. +- `BALANCE_JWT_SECRET` должен быть случайной строкой не короче 32 + символов; его смена инвалидирует access-токены (refresh-сессии + остаются валидны и получают новый access-токен при следующем + refresh). + +## Транспорт + +- Для удалённого (не-localhost) сервера все клиенты обязаны + использовать HTTPS. +- HTTP разрешён клиентам **только** для `localhost`, `127.0.0.1` и + `::1` — это относится и к Apple-, и к Android-клиенту. +- Production-деплой сервера должен работать через HTTPS/TLS + (например, через reverse proxy — Caddy/nginx), см. + [`BalanceServer/README.md#https`](../BalanceServer/README.md#https). + +## Логирование + +Запрещено логировать на любой из трёх платформ: + +- пароли; +- access/refresh токены; +- заголовок `Authorization` целиком; +- полные request bodies с финансовыми данными без явной необходимости. + +Безопасно логировать (на сервере): request ID, endpoint, status code, +latency, размер batch, неконфиденциальные идентификаторы записей. + +## Авторизация на сервере + +Каждая sync-мутация обязана проверять ownership по authenticated user +из токена/сессии. Никогда не доверяй `userId`, пришедшему в теле +запроса от клиента, если identity уже известна из +Authorization-заголовка или cookie-сессии: + +```text +Authorization header / session cookie + ↓ + authenticated user + ↓ + server-side ownership check + ↓ + requested entity +``` + +## Валидация входных данных (сервер) + +Проверяются: UUID/ID, enum-значения, `amount`, timestamps, длины строк, +размер batch, размер JSON, границы cursor/sequence. + +## Защита от злоупотреблений (production-деплой сервера) + +- rate limiting для auth-эндпоинтов (встроенный лимитер — 30 + auth-запросов на IP в минуту; для более сильного публичного + rate limiting используйте reverse proxy); +- разумные ограничения на размер тела запроса; +- таймауты и graceful shutdown; +- ограничение подключений к БД; +- мониторинг. + +## Секреты в репозитории + +Ни в одном из трёх подпроектов не должны появляться: + +- production URL серверов; +- реальные `BALANCE_JWT_SECRET` или иные секреты; +- закоммиченные `.env`, keystore-файлы, сертификаты; +- захардкоженные тестовые credentials, которые могут быть спутаны с + боевыми. + +`.env.example` в `BalanceServer/` и `local.properties.example` в +`BalanceAndroid/` — единственные допустимые «примерные» файлы с +конфигурацией; реальные значения в них не хранятся. diff --git a/docs/SYNC_PROTOCOL.md b/docs/SYNC_PROTOCOL.md new file mode 100644 index 0000000..5ee94f5 --- /dev/null +++ b/docs/SYNC_PROTOCOL.md @@ -0,0 +1,88 @@ +# Единый протокол синхронизации + +Все клиенты (iOS, macOS, watchOS, Android, web) синхронизируются с +`BalanceServer` через один и тот же HTTP(S) API и один и тот же набор +инвариантов. Это сводка контракта; детальные описания на стороне +сервера — [`BalanceServer/docs/API.md`](../BalanceServer/docs/API.md) и +[`BalanceServer/docs/SYNC.md`](../BalanceServer/docs/SYNC.md). +Клиентские реализации — [`Balance/docs/SYNC.md`](../Balance/docs/SYNC.md) +и [`BalanceAndroid/docs/SYNC.md`](../BalanceAndroid/docs/SYNC.md). + +## Эндпоинты + +| Метод | Путь | Назначение | +| --- | --- | --- | +| `GET` | `/health` | Health check | +| `POST` | `/v1/auth/register` | Создание аккаунта | +| `POST` | `/v1/auth/login` | Вход, выдача access/refresh токенов | +| `POST` | `/v1/auth/refresh` | Ротация токенов | +| `POST` | `/v1/auth/logout` | Отзыв refresh-токена | +| `GET` | `/v1/me` | Текущий аккаунт | +| `POST` | `/v1/sync` | Push и pull изменений | + +Нативные клиенты используют `Authorization: Bearer `. +Web-клиент использует `HttpOnly`/`SameSite=Strict` cookie-сессию — +это единственное отличие транспорта, сама sync-семантика идентична. + +## Основные инварианты + +1. **Cursor** — каждый клиент хранит курсор последнего успешно + применённого изменения. Курсор сохраняется только после успешного + применения соответствующего batch, не раньше. +2. **Sequence** — сервер поддерживает глобальный монотонный порядок + изменений в change stream; порядок не должен уменьшаться и должен + быть однозначным при конкурентных запросах. +3. **Push** — клиент отправляет свои изменения (`POST /v1/sync`); + сервер валидирует, проверяет владение записью (ownership) и + добавляет изменение в change stream. Повторный push (retry) не + должен создавать вторую логическую запись из одного и того же + client-change (идемпотентность). +4. **Pull** — клиент запрашивает изменения после своего курсора; + сервер никогда не возвращает чужие данные (изоляция по аккаунту) и + поддерживает постраничный pull с продолжением от нового курсора. +5. **Conflict resolution** — last-write-wins по `updatedAt` + (`syncUpdatedAt` на Apple, `updatedAt` на Android/сервере). Сервер — + единственный источник правды в этом сравнении, поэтому не должен + произвольно перезаписывать `updatedAt` при каждом чтении. +6. **Удаления (tombstones)** — удаление сущности не является простым + `DELETE`; оно создаёт запись изменения вида + `{ "entity": ..., "id": ..., "deleted": true, "updatedAt": ... }`, + которая проходит через тот же change stream, что и обычные + изменения. На Apple это `SyncTombstone`/`modelContext.deleteForSync`, + на Android — `deletion_journal` через DAO-методы `deleteTransaction` / + `deleteCategory` / `deleteBudget`. +7. **Batch** — Apple-клиент отправляет батчи до 100 изменений и может + делить крупные payload'ы; сервер не должен предполагать, что все + изменения аккаунта приходят одним запросом. Android должен + оставаться совместимым с этим же батчингом. +8. **Reset/full sync** — если клиент теряет курсор, сервер должен + поддерживать предусмотренный API механизм полного pull или новый + baseline-курсор. Неизвестный/просроченный курсор нельзя молча + трактовать как актуальный. + +## Синхронизируемые сущности + +- **Транзакция** (доход/расход, сумма, дата, заметка, snapshot + категории); +- **Пользовательская категория** (имя, иконка/emoji, цвет); +- **Месячный бюджет** (категория, месяц, лимит); +- **Tombstone / deletion journal** запись для удалений. + +Подробное пополевое сопоставление — [`docs/DATA_MODEL.md`](DATA_MODEL.md). + +## Что нельзя менять без координации + +Любое из следующих изменений — breaking change для всех клиентов +одновременно, даже если каждый подпроект по отдельности компилируется +и проходит собственные тесты: + +- имена/типы JSON-полей в payload'ах `/v1/sync` и auth-эндпоинтов; +- HTTP-методы или пути эндпоинтов; +- семантика курсора/sequence; +- правило разрешения конфликтов (last-write-wins по `updatedAt`); +- формат tombstone-записи. + +При таком изменении обнови одновременно: `BalanceServer` (API + +хендлеры + `docs/API.md`/`docs/SYNC.md`), `Balance/Shared/ServerSync.swift` +(+ `Balance/docs/SYNC.md`), `BalanceAndroid/.../sync/*` +(+ `BalanceAndroid/docs/SYNC.md`) и этот файл.