initial commit
This commit is contained in:
commit
65de1782c9
43 files changed
+9164
No files matched your search
+151
@@ -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 текущей реализации имеют приоритет над этой общей рекомендацией.
|
||||
@@ -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 совпадал с клиентской логикой.
|
||||
@@ -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 без периода совместимости.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
Любое изменение протокола требует проверки обеих реализаций.
|
||||
@@ -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
@@ -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` — идентификатор устройства, а не механизм авторизации.
|
||||
Reference in new issue
Block a user