initial commit

This commit is contained in:
wt committed 2026-10-29 18:43:28 +07:00
commit 65de1782c9
43 files changed
+9164

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
# HTTP API
## Общие правила
API предназначен для iOS/macOS/watchOS и Android Balance.
Базовый URL задаётся клиентом и зависит от deployment environment.
JSON:
```http
Content-Type: application/json
```
Защищённые endpoints используют:
```http
Authorization: Bearer <access-token>
```
## Authentication
Основные операции:
```text
POST /v1/auth/register
POST /v1/auth/login
POST /v1/auth/logout
POST /v1/auth/refresh
```
### Register
Создаёт пользовательский аккаунт.
Input должен валидироваться сервером. Password никогда не хранится plaintext.
### Login
Проверяет credentials и создаёт authenticated session.
### Refresh
Принимает refresh credential и выдаёт новый access credential согласно текущей server-side session policy.
### Logout
Инвалидирует текущую session/refresh state согласно текущей реализации.
## Synchronization
Основной endpoint:
```text
POST /v1/sync
```
Логический request:
```json
{
"cursor": 123,
"deviceId": "UUID",
"changes": [],
"pull": true
}
```
Точная JSON schema должна оставаться совместимой с `ServerSync.swift` и Android `SyncRepository`.
## Change
Логическая форма:
```json
{
"entity": "transaction",
"id": "UUID",
"deleted": false,
"updatedAt": "2026-09-08T10:00:00Z",
"payload": {}
}
```
Поддерживаемые entity:
```text
transaction
category
budget
```
## Push
Client отправляет локальные изменения.
Server должен:
1. аутентифицировать пользователя;
2. проверить ownership/ID;
3. валидировать payload;
4. применить изменения;
5. создать server change entries;
6. вернуть результат и актуальную sync metadata.
Операция должна быть безопасна при retry.
## Pull
Client передаёт cursor.
Server возвращает изменения после этого cursor, включая deletions.
Если существует pagination, ответ должен содержать понятный признак продолжения и новый cursor/sequence.
## Ошибки
Рекомендуемый контракт:
```json
{
"error": {
"code": "invalid_request",
"message": "Human-readable safe message"
}
}
```
Не возвращать:
- SQL error;
- stack trace;
- password/token;
- filesystem paths;
- внутренние identifiers инфраструктуры.
## HTTP status semantics
Рекомендуемая семантика:
```text
200 OK — успешный request
201 Created — создание ресурса
400 Bad Request — malformed/invalid input
401 Unauthorized — missing/expired auth
403 Forbidden — resource belongs to another user
404 Not Found — resource/endpoint unavailable
409 Conflict — semantic conflict, если endpoint использует такую семантику
429 Too Many Requests — rate limit
500 Internal Server Error — internal failure
```
Конкретные status codes текущей реализации имеют приоритет над этой общей рекомендацией.
+119
View File
@@ -0,0 +1,119 @@
# Архитектура BalanceServer
## Роль сервера
BalanceServer — backend для синхронизации личных финансов между устройствами.
Поток данных:
```text
iOS/macOS/watchOS ─┐
├── HTTPS/JSON API ──> BalanceServer ──> Database
Android ───────────┘
```
Сервер отвечает за:
- регистрацию и авторизацию;
- пользовательские сессии;
- хранение финансовых изменений;
- push/pull synchronization;
- глобальную последовательность изменений;
- удаление через tombstones;
- выдачу изменений начиная с cursor.
## Исходный код
Production runtime — один Rust-процесс: `src/main.rs`, `runtime.rs`, `config.rs`, `database.rs`, `auth.rs`, `sync.rs`, `server.rs`, `go_json.rs`. SQLite bundled через rusqlite; HTTP обслуживается axum/hyper. `runtime.rs` сохраняет socket deadlines Go (header 5s, read 15s, write 30s, idle 60s) и ограничивает graceful shutdown десятью секундами. `go_json.rs` сохраняет особенности encoding/json и исходные payload bytes. Статические файлы из `internal/httpapi/web` встроены при компиляции. Go-код сохранён как эталон совместимости и вариант rollback.
Пути сохранённой Go-реализации:
```text
- internal/auth
- internal/config
- internal/database
- internal/httpapi
```
Конкретные границы package/handler/service/repository нужно сохранять при рефакторинге: перенос кода между слоями не должен менять внешний API.
## HTTP layer
HTTP layer принимает JSON requests от мобильных клиентов.
Каждый handler должен:
1. проверить method/content type;
2. аутентифицировать пользователя, если endpoint защищён;
3. валидировать input;
4. вызвать domain/storage logic;
5. вернуть стабильный JSON response.
## Domain / sync layer
Sync layer является наиболее критичной частью backend.
Он должен разделять:
- изменения, созданные текущим клиентом;
- изменения, которые нужно вернуть клиенту;
- удалённые записи;
- cursor/sequence.
Ключевая модель:
```text
client cursor
│
▼
server change log
│
├── sequence 101
├── sequence 102
├── sequence 103
└── ...
```
Клиент может запросить изменения после своего cursor и затем сохранить новый cursor.
## Persistence
Хранилище должно сохранять как минимум:
- user/account identity;
- credentials/session metadata;
- financial records;
- change metadata;
- deletion/tombstone metadata;
- monotonically increasing sequence, если оно является частью текущего sync protocol.
Конкретные entities и schema документированы в `docs/DATA_MODEL.md`.
## Security boundaries
```text
Internet
│
▼
HTTP server
│
├── authentication
├── authorization
├── input validation
│
▼
domain/sync
│
▼
database
```
Нельзя позволять клиенту выбирать произвольный `user_id` для чужих records.
## Совместимость клиентов
Apple и Android реализации должны видеть одну и ту же семантику данных.
Особенно важно, чтобы:
- enum values были стабильными;
- даты имели единый формат;
- UUID/string IDs не менялись;
- `deleted` semantics были одинаковыми;
- cursor/sequence были монотонными;
- conflict resolution совпадал с клиентской логикой.
+119
View File
@@ -0,0 +1,119 @@
# Модель данных BalanceServer
Ниже описана серверная модель на основании исходного кода проекта. При изменении schema обновляйте этот документ.
## Найденные определения моделей/schema
- internal/database/database.go
- internal/database/database.go
- internal/database/database.go
- internal/config/config.go
- internal/auth/tokens.go
- internal/auth/tokens.go
- internal/httpapi/auth_handlers.go
- internal/httpapi/auth_handlers.go
- internal/httpapi/auth_handlers.go
- internal/httpapi/server.go
- internal/httpapi/server.go
- internal/httpapi/server.go
- internal/httpapi/sync_handler.go
- internal/httpapi/sync_handler.go
- internal/httpapi/sync_handler.go
- internal/httpapi/web.go
- internal/httpapi/server_test.go
- internal/httpapi/server_test.go
- internal/httpapi/server_test.go
## Общие сущности
Сервер должен различать как минимум следующие логические типы синхронизируемых данных, совместимые с клиентами Balance:
```text
transaction
category
budget
```
### Transaction
Логически содержит:
- стабильный ID;
- amount;
- date;
- note;
- category presentation data;
- transaction kind;
- balance-adjustment flag;
- update timestamp;
- принадлежность пользователю.
### Category
Логически содержит:
- стабильный ID;
- name;
- icon/emoji;
- color;
- operation kind;
- creation/update metadata;
- принадлежность пользователю.
### Budget
Логически содержит:
- стабильный ID;
- category information;
- monthly limit;
- month start;
- update timestamp;
- принадлежность пользователю.
## Change metadata
Для sync необходима метаинформация:
```text
user
entity
record ID
deleted
updatedAt
sequence
payload
```
`sequence` используется как серверный порядок изменений, а `updatedAt` — как timestamp версии самой записи.
## Tombstones
Удаление нельзя реализовывать только физическим `DELETE`, если клиентам нужно узнать о нём при следующем pull.
Используется логическое изменение:
```json
{
"entity": "transaction",
"id": "…",
"deleted": true,
"updatedAt": "…"
}
```
После того как tombstone перестанет быть нужен для поддерживаемого sync history window, сервер может применять retention policy. Такая политика должна быть согласована с cursor semantics.
## Invariants
1. Record ID стабилен между sync.
2. Record принадлежит одному пользователю.
3. Клиент не может читать/изменять чужую запись.
4. `sequence` не должен уменьшаться.
5. Повторная доставка одного change не должна повреждать данные.
6. Удаление должно быть видимо клиенту через sync.
7. Timestamps должны храниться в UTC.
8. Enum/string values должны быть совместимы с iOS и Android.
## Миграции
Schema changes должны быть backward-compatible с активными версиями клиентов либо сопровождаться versioned API/миграцией.
Не переименовывайте JSON field без периода совместимости.
+85
View File
@@ -0,0 +1,85 @@
# Deployment
## Runtime and configuration
The production executable is the Rust server, built with `cargo build --locked
--release`. SQLite is bundled into the executable. The Go implementation remains
in `cmd/balance-server` and `internal/` as a compatibility reference and rollback
option; build it with `go build -o balance-server-go ./cmd/balance-server`.
The Dockerfile uses a Rust 1.90 Debian Bookworm builder and a Debian Bookworm slim
runtime, running as the unprivileged `balance` user. The image embeds the existing
web client and uses no separate frontend process. Build dependencies and SQLite
are compiled at image build time.
The numeric identity remains UID 100 / GID 101, matching the original Alpine Go
image, so existing volume permissions are preserved across the switch.
`docker compose up -d --build` publishes **http://localhost:8123** (host 8123,
container 8080). `GET /health` is the liveness endpoint. Direct binary execution
uses `:8080` by default. Place a TLS reverse proxy in front of the published port
for remote access; pass `X-Forwarded-Proto: https` to enable Secure session cookies.
| Variable | Default | Purpose |
| --- | --- | --- |
| `BALANCE_ADDR` | `:8080` | HTTP listen address |
| `BALANCE_DB_PATH` | `./data/balance.db` | SQLite file; Docker uses `/data/balance.db` |
| `BALANCE_JWT_SECRET` | required | At least 32 characters; retain across migration |
| `BALANCE_ACCESS_TTL` | `15m` | Go-style duration for access tokens |
| `BALANCE_REFRESH_TTL` | `720h` | Go-style duration for refresh tokens |
| `BALANCE_ALLOW_REGISTRATION` | `true` | Enable creation of accounts |
| `BALANCE_CORS_ORIGINS` | empty | Comma-separated allowed browser origins |
Inject secrets through the deployment environment or `.env`; never put them in
images or source control. Compose preserves the existing named `balance-data`
volume mounted at `/data`. Keep that volume and its project name when upgrading.
## Go to Rust migration
1. Build and validate the Rust binary/image before stopping production. Retain the
previous Go executable/image and exact environment for rollback.
2. Stop the Go service and confirm no other process writes its SQLite database.
3. Back up the entire database directory, including `balance.db` and any
`balance.db-wal`/`balance.db-shm` files. Protect the backup as financial data.
Record the existing environment securely, especially `BALANCE_JWT_SECRET`.
4. Start Rust with the **same database path/volume and JWT secret**. The schema is
unchanged: startup creates missing tables and indexes without rewriting
existing accounts, records, change sequences or refresh-token hashes.
5. Check `/health`, login with an existing account, refresh an existing session,
and sync a device with its saved cursor. Verify another device receives edits
and tombstones. Inspect error rates without logging credentials or tokens.
The existing Argon2id password hashes, HS256 JWT claims, hashed refresh tokens,
RFC3339 timestamps, entity versions and global change sequences are shared with
Go. No export/import, cursor reset, account recreation or forced sign-out is
required. Do not start both implementations against the same database at once.
## Rollback
Stop Rust first. Start the retained Go executable/image with the same current
database directory and secret. Rust-written accounts, password hashes, refresh
sessions, records, tombstones and cursors remain readable by Go. Check health,
login, refresh and sync again. The compatibility suite exercises both directions,
including a second takeover back to the original implementation.
For database corruption or an unrelated failed migration, stop all writers before
restoring the complete backup directory. Restoring an older backup discards
changes after that backup and may require clients to resynchronize; prefer normal
binary rollback against the current database when it is healthy. Never overwrite
an active database or delete a Compose volume as part of an upgrade.
## Validation and operations
Before release run `cargo test --locked`, `cargo build --locked --release`,
`python3 tools/compatibility.py --rust-binary target/release/balance-server`, and
the retained reference checks `go test ./...`, `go vet ./...`, `go build ./...`.
The differential suite uses temporary databases and loopback listeners, compares
API/errors/security headers/assets, tests browser cookies, pagination, LWW,
tombstones, concurrent pushes, token expiration, refresh rotation, rate limiting,
and shared-database takeover/rollback in both directions.
Keep regular encrypted backups with retention and restore drills. Monitor HTTP
5xx, sync errors, authentication failures, latency, database/change-log growth
and free disk space. The in-memory limiter allows 30 registration/login/refresh
attempts per remote IP per minute and resets on restart; use proxy controls when
operating a public service. Application logs must never include passwords,
Authorization headers or tokens.
+120
View File
@@ -0,0 +1,120 @@
# Разработка и запуск
## Требования
Production-сервер использует Rust 1.90+ и bundled SQLite. Go 1.25+ нужен для сохранённой эталонной реализации и differential-проверок. Python 3 используется без сторонних пакетов.
Проверить локальную версию:
```bash
rustc --version
cargo --version
go version
```
## Установка зависимостей
```bash
cargo fetch --locked
go mod download
```
## Сборка
```bash
cargo build --locked
go build ./...
```
## Тесты
```bash
cargo test --locked
cargo build --locked
python3 tools/compatibility.py
go test ./...
```
С race detector:
```bash
go test -race ./...
```
## Static checks
```bash
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
go vet ./...
```
Если в проекте есть Makefile/CI, его команды имеют приоритет как canonical build pipeline.
## Локальный запуск
Перед запуском изучите конфигурацию в коде и `.env`/deployment files.
Не коммитьте реальные credentials.
Собрать и запустить Rust-сервер:
```bash
cargo build --locked --release
export BALANCE_JWT_SECRET="replace-with-a-long-random-secret"
export BALANCE_DB_PATH="./data/balance.db"
./target/release/balance-server
```
По умолчанию сервер слушает `:8080`. Файл `.env` читает Docker Compose; при
прямом запуске переменные нужно передать процессу через окружение.
## Database
SQLite встроен в executable. При первом запуске сервер создаёт каталог,
таблицы и индексы автоматически. Отдельный процесс БД не нужен. При переходе
с Go сохраните существующий файл БД и `BALANCE_JWT_SECRET`; подробная
процедура и откат описаны в [DEPLOYMENT.md](DEPLOYMENT.md).
## Production
Рекомендуемый pipeline:
```text
source
↓
cargo fmt --check && cargo test --locked
↓
cargo clippy --locked --all-targets -- -D warnings
↓
cargo build --locked --release
↓
python3 tools/compatibility.py --rust-binary target/release/balance-server
↓
container/package
↓
deploy
↓
health check
```
Для production нужны:
- TLS termination;
- persistent database;
- backups;
- monitoring;
- log rotation;
- secrets management;
- migration procedure.
## Совместимость
При релизе server проверяйте минимум:
- старый iOS client + новый server;
- новый iOS client + server;
- Android client + server;
- concurrent sync с двух устройств;
- login/refresh после истечения access token;
- delete → pull на другом устройстве.
Production build: `cargo build --locked --release`. Прямой запуск: `BALANCE_JWT_SECRET=<secret> target/release/balance-server`, порт 8080. Docker Compose публикует порт 8123 и сохраняет прежний volume `/data`. Процедура миграции Go → Rust и rollback: [DEPLOYMENT.md](DEPLOYMENT.md).
+62
View File
@@ -0,0 +1,62 @@
# Функциональная документация BalanceServer
## Назначение
BalanceServer является backend для приложения Balance — персонального финансового учёта.
Он не реализует пользовательский UI. Его задача — безопасно хранить и синхронизировать данные между устройствами.
## Пользовательские данные
Синхронизируются:
- финансовые операции;
- пользовательские категории;
- месячные бюджеты;
- удаления этих сущностей.
## Multi-device
Один пользователь может работать с несколькими устройствами.
Пример:
```text
iPhone добавил расход
↓
Server
↓
Android получил расход
↓
Mac получил расход
```
## Offline-first
Мобильные приложения сохраняют изменения локально и затем отправляют их серверу.
Поэтому сервер должен корректно принимать изменения спустя некоторое время после их создания.
## Delete
Удаление является syncable operation, а не только локальным DB delete.
Другие устройства должны получить tombstone и удалить локальную запись.
## Privacy
Сервер хранит персональные финансовые данные. Поэтому production deployment должен обеспечивать:
- TLS;
- authentication;
- authorization;
- encrypted backups;
- controlled database access;
- минимальное логирование финансовых payloads.
## Клиентская совместимость
Backend должен рассматриваться как общий контракт для:
- Apple Balance;
- Android Balance.
Любое изменение протокола требует проверки обеих реализаций.
+79
View File
@@ -0,0 +1,79 @@
# Безопасность
## Credentials
Пароли пользователей должны храниться только в виде slow password hash с современным password hashing алгоритмом.
Не хранить plaintext password.
## Tokens
Access/refresh tokens не должны попадать в:
- application logs;
- panic messages;
- error responses;
- analytics;
- database debug dumps.
## Authorization
Каждый sync mutation должен проверять ownership по authenticated user.
Нельзя доверять `userId` из client payload, если authenticated identity уже известна из token.
Правильная модель:
```text
Authorization header
↓
authenticated user
↓
server-side ownership
↓
requested entity
```
## Transport
Production API должен работать через HTTPS/TLS.
HTTP следует оставлять только для controlled local development.
## Input validation
Проверяйте:
- UUID/ID;
- enum values;
- amount;
- timestamps;
- string lengths;
- batch size;
- JSON size;
- cursor/sequence bounds.
## Logging
Безопасно логировать:
- request ID;
- endpoint;
- status code;
- latency;
- batch size;
- non-sensitive user/record identifiers в допустимой для проекта форме.
Нельзя логировать:
- passwords;
- Authorization headers;
- access tokens;
- refresh tokens;
- полные request bodies финансовых данных без явной необходимости.
## Abuse protection
Production deployment должен иметь:
- rate limiting для auth endpoints;
- reasonable request body limits;
- timeouts;
- graceful shutdown;
- database connection limits;
- monitoring.
+138
View File
@@ -0,0 +1,138 @@
# Синхронизация
## Цель
Один аккаунт Balance может использоваться на нескольких устройствах:
```text
iPhone
│
├── transaction A
├── category B
└── budget C
│
▼
BalanceServer
│
┌────┴────┐
▼ ▼
Android Mac
```
## Cursor
Каждый клиент хранит cursor последнего успешно применённого server change.
Принцип:
```text
cursor = 100
server:
101 A
102 B
103 C
pull(cursor=100)
→ A, B, C
→ new cursor = 103
```
Клиент должен сохранять cursor только после успешного применения соответствующего batch.
## Sequence
Server sequence задаёт глобальный порядок изменений в sync stream.
Требования:
- монотонность;
- отсутствие уменьшения;
- однозначность порядка;
- корректность при concurrent requests.
## Push
Push передаёт изменения клиента:
```text
local mutations
↓
POST /v1/sync
↓
validation
↓
ownership check
↓
persist
↓
append to change stream
```
Retry не должен создавать две разные логические записи из одного client change.
## Pull
Pull получает изменения после cursor.
Сервер не должен возвращать изменения другого пользователя.
При pagination клиент должен иметь возможность продолжить с нового cursor.
## Conflict resolution
Клиенты Balance используют timestamp-based versioning.
Типовая проверка:
```text
local.updatedAt < remote.updatedAt
```
Следовательно, server должен сохранять `updatedAt` достаточно точно и не подменять его произвольным временем каждого чтения.
При конкурентных writes сервер должен иметь однозначное правило победителя.
## Deletions
Удаление создаёт tombstone/change:
```json
{
"entity": "transaction",
"id": "...",
"deleted": true,
"updatedAt": "..."
}
```
Tombstone должен попасть в change stream так же, как обычное изменение.
## Idempotency
Push может повториться из-за:
- timeout;
- network failure;
- app restart;
- retry after 401/token refresh.
Поэтому сервер должен обрабатывать повторную отправку безопасно.
## Batch
Apple-клиент отправляет batches до 100 изменений и может делить крупные payloads.
Android должен использовать совместимый protocol.
Server не должен предполагать, что все изменения приходят одним запросом.
## Reset/full sync
Если client потерял cursor или требует reset, сервер должен поддерживать предусмотренный текущим API механизм полного pull либо новый cursor baseline.
Нельзя молча трактовать неизвестный/просроченный cursor как актуальный.
## Security
Sync endpoint всегда ограничен текущим authenticated user.
`deviceId` — идентификатор устройства, а не механизм авторизации.