533 lines
11 KiB
Markdown
533 lines
11 KiB
Markdown
# Multiberry Backend
|
||
|
||
Веб-приложение для хакатона ВТБ. Backend на Rust + Axum, БД — PostgreSQL.
|
||
|
||
## 🚀 Быстрый старт (macOS)
|
||
|
||
### 1. Установка зависимостей
|
||
|
||
при условии что docker, sops, age уже установлены
|
||
|
||
just - это Makefile 2.0
|
||
|
||
```zsh
|
||
brew install just
|
||
|
||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||
|
||
rustup --version
|
||
cargo --version
|
||
|
||
# если cargo ругается
|
||
rustup default stable
|
||
|
||
# если же нет, все прекрасно, curl и rustup уже сделали свое дело
|
||
```
|
||
|
||
### 2. Клонируй репозиторий
|
||
|
||
```bash
|
||
git clone ssh://git@gitverse.ru:2222/rorikstr/multiberry-backend.git
|
||
cd multiberry-backend
|
||
```
|
||
|
||
### 3. Запусти проект
|
||
|
||
Короч. Чтобы не запускать бесконечное число раз sops exec-env для вскрытия secrets.yaml
|
||
я придумал выполнять проект на Makefile 2.0: justfile
|
||
|
||
```bash
|
||
# Посмотри все доступные команды
|
||
just
|
||
|
||
# Запусти базу данных
|
||
just db-up
|
||
|
||
# В новом терминале запусти бэкенд
|
||
just run
|
||
|
||
# Проверь, что всё работает
|
||
just health
|
||
```
|
||
|
||
**Ожидаемый результат:**
|
||
```json
|
||
{"status":"Database connection is successful."}
|
||
```
|
||
|
||
✅ Готово! Бэкенд работает на `http://localhost:3000`
|
||
|
||
## 📋 Основные команды
|
||
|
||
| Команда | Описание |
|
||
|---------|---------|
|
||
| `just run` | Запустить бэкенд в dev режиме |
|
||
| `just db-up` | Поднять PostgreSQL |
|
||
| `just db-down` | Остановить PostgreSQL |
|
||
| `just db-reset` | Пересоздать БД (удалит все данные!) |
|
||
| `just build` | Собрать релизную версию |
|
||
| `just test` | Запустить тесты |
|
||
| `just health` | Проверить здоровье API |
|
||
| `just db-logs` | Показать логи БД |
|
||
| `just stop` | Остановить всё |
|
||
|
||
Больше информации: `just --list`
|
||
|
||
## 🔐 Секреты и переменные окружения
|
||
|
||
Все секреты (пароли, API ключи) хранятся в зашифрованном файле `secrets.yaml`.
|
||
|
||
**Как это работает:**
|
||
- `just` команды автоматически расшифровывают `secrets.yaml` перед запуском
|
||
|
||
Если нужно **отредактировать** секреты (добавить новый API ключ и т.д.):
|
||
```
|
||
sops secrets.yaml
|
||
```
|
||
|
||
## 🛠️ Разработка
|
||
|
||
### Структура проекта
|
||
|
||
```
|
||
.
|
||
├── src/
|
||
│ └── main.rs # Основной код приложения
|
||
├── Cargo.toml # Зависимости Rust
|
||
├── Cargo.lock # Зафиксированные версии зависимостей
|
||
├── compose.yaml # Описание Docker контейнера с PostgreSQL
|
||
├── secrets.yaml # Зашифрованные секреты (в Git, не редактируй вручную!)
|
||
├── .sops.yaml # Конфигурация sops (управление ключами)
|
||
├── justfile # Команды проекта
|
||
└── README.md # Этот файл
|
||
```
|
||
|
||
### Workflow разработки
|
||
|
||
1. **Создай фича-ветку:**
|
||
```
|
||
git checkout -b feature/my-feature
|
||
```
|
||
|
||
2. **Запусти проект:**
|
||
```
|
||
just db-up
|
||
just run
|
||
```
|
||
|
||
3. **Внеси изменения в `src/main.rs`**
|
||
|
||
4. **Тесты:**
|
||
```
|
||
just test
|
||
```
|
||
|
||
5. **Push и создай Pull Request**
|
||
|
||
## ❓ Troubleshooting
|
||
|
||
### "Connection reset by peer" при запуске
|
||
|
||
БД еще инициализируется. Подожди 5-10 секунд и попробуй снова:
|
||
```
|
||
just run
|
||
```
|
||
|
||
### "password authentication failed"
|
||
|
||
Вероятно, в `secrets.yaml` неверный пароль. Напиши Wave.
|
||
|
||
### Docker Desktop не запускается
|
||
|
||
Убедись, что Docker Desktop открыт:
|
||
```
|
||
open /Applications/Docker.app
|
||
```
|
||
|
||
```
|
||
just db-logs # Логи БД
|
||
docker ps # Список запущенных контейнеров
|
||
```
|
||
|
||
|
||
## API Documentation (для фронтендеров)
|
||
|
||
Вот **полная документация нашего API** на основе bash скрипта:
|
||
|
||
```markdown
|
||
# Multiberry Backend API Documentation
|
||
|
||
## Base URL
|
||
```
|
||
http://localhost:3000/api
|
||
```
|
||
|
||
## Authentication Endpoints
|
||
|
||
### 1. Register User
|
||
**POST** `/auth/register`
|
||
|
||
```
|
||
{
|
||
"bank_user_number": 6,
|
||
"password": "testpass123"
|
||
}
|
||
```
|
||
|
||
**Response (201 Created)**
|
||
```
|
||
{
|
||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||
"bank_user_id": "team275-6"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Login
|
||
**POST** `/auth/login`
|
||
|
||
```
|
||
{
|
||
"bank_user_id": "team275-6",
|
||
"password": "testpass123"
|
||
}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||
"bank_user_id": "team275-6"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Get Current User Info
|
||
**GET** `/auth/me`
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"user_id": 1,
|
||
"bank_user_id": "team275-6"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Consent Management (Open Banking Permission)
|
||
|
||
### 4. Request Consent from Bank
|
||
**POST** `/consent/{bank}/{bank_user_id}`
|
||
|
||
**Parameters**
|
||
- `bank`: `vbank`, `abank`, или `sbank`
|
||
- `bank_user_id`: например `team275-6`
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (201 Created)**
|
||
```
|
||
{
|
||
"consent_id": "consent-2c4ea1d99e5b",
|
||
"expires_at": "2026-11-07T16:03:13.562301022Z",
|
||
"status": "approved",
|
||
"message": "Согласие одобрено автоматически"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. Get Consent Status
|
||
**GET** `/consent/{bank}/{bank_user_id}`
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"consent_id": "consent-2c4ea1d99e5b",
|
||
"bank": "vbank",
|
||
"user_id": "team275-6"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 6. Revoke Consent
|
||
**DELETE** `/consent/{bank}/{bank_user_id}`
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"status": "deleted"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Account Aggregation
|
||
|
||
### 7. Get All Accounts (from Bank via Multiberry)
|
||
**GET** `/accounts/{bank}/{bank_user_id}`
|
||
|
||
**Parameters**
|
||
- `bank`: `vbank`, `abank`, или `sbank`
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"data": {
|
||
"account": [
|
||
{
|
||
"accountId": "acc-3848",
|
||
"status": "Enabled",
|
||
"currency": "RUB",
|
||
"accountType": "Personal",
|
||
"accountSubType": "Checking",
|
||
"nickname": "Checking счет",
|
||
"openingDate": "2024-10-30",
|
||
"description": null,
|
||
"account": [
|
||
{
|
||
"schemeName": "RU.CBR.PAN",
|
||
"identification": "4081781027508005359",
|
||
"name": "Кредитов Кредит Кредитович (team275)"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"links": {
|
||
"self": "/accounts"
|
||
},
|
||
"meta": {
|
||
"totalPages": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**Cached in DB** ✅ - следующий запрос вернёт из кэша
|
||
|
||
---
|
||
|
||
## Balance Retrieval
|
||
|
||
### 8. Get Account Balance (Cached)
|
||
**GET** `/balances/{bank}/{account_id}`
|
||
|
||
**Parameters**
|
||
- `bank`: `vbank`, `abank`, или `sbank`
|
||
- `account_id`: например `acc-3848`
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"data": {
|
||
"balance": [
|
||
{
|
||
"accountId": "acc-3848",
|
||
"type": "InterimAvailable",
|
||
"dateTime": "2025-11-07T18:14:52.937793Z",
|
||
"amount": {
|
||
"amount": "115879.84",
|
||
"currency": "RUB"
|
||
},
|
||
"creditDebitIndicator": "Credit"
|
||
},
|
||
{
|
||
"accountId": "acc-3848",
|
||
"type": "InterimBooked",
|
||
"dateTime": "2025-11-07T18:14:52.937809Z",
|
||
"amount": {
|
||
"amount": "115879.84",
|
||
"currency": "RUB"
|
||
},
|
||
"creditDebitIndicator": "Credit"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Cached in DB** ✅
|
||
|
||
---
|
||
|
||
## Transaction History (для generative API)
|
||
|
||
### 9. Get Transactions (Paginated + Cached)
|
||
**GET** `/transactions/{bank}/{bank_user_id}/{account_id}?page=1&limit=6`
|
||
|
||
**Parameters**
|
||
- `bank`: `vbank`, `abank`, или `sbank`
|
||
- `bank_user_id`: например `team275-6`
|
||
- `account_id`: например `acc-3848`
|
||
- `page`: номер страницы (default: 1)
|
||
- `limit`: количество записей (default: 6)
|
||
|
||
**Headers**
|
||
```
|
||
Authorization: Bearer {token}
|
||
```
|
||
|
||
**Response (200 OK)**
|
||
```
|
||
{
|
||
"data": {
|
||
"transaction": [
|
||
{
|
||
"accountId": "acc-3848",
|
||
"transactionId": "tx-team275-8-m0-1",
|
||
"amount": {
|
||
"amount": "80462.92",
|
||
"currency": "RUB"
|
||
},
|
||
"creditDebitIndicator": "Credit",
|
||
"status": "Booked",
|
||
"bookingDateTime": "2025-10-28T17:59:45.080562Z",
|
||
"valueDateTime": "2025-10-28T17:59:45.080562Z",
|
||
"transactionInformation": "💼 Зарплата",
|
||
"bankTransactionCode": {
|
||
"code": "ReceivedCreditTransfer"
|
||
}
|
||
}
|
||
]
|
||
},
|
||
"links": {
|
||
"self": "/accounts/acc-3848/transactions?page=1&limit=6",
|
||
"next": "/accounts/acc-3848/transactions?page=2&limit=6"
|
||
},
|
||
"meta": {
|
||
"totalPages": 12,
|
||
"totalRecords": 67,
|
||
"currentPage": 1,
|
||
"pageSize": 6
|
||
}
|
||
}
|
||
```
|
||
|
||
**Cached in DB** ✅ - 67 транзакций сохранены!
|
||
|
||
---
|
||
|
||
## Error Handling
|
||
|
||
All endpoints return errors in format:
|
||
```
|
||
{
|
||
"error": "Description",
|
||
"details": "Additional info if available"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Database Schema (что сохраняется)
|
||
|
||
```
|
||
-- Users: register/login
|
||
users (id, bank_user_id, password_hash, created_at)
|
||
|
||
-- Consents: permissions from banks
|
||
user_consents (user_id, bank_code, consent_id, status, expires_at)
|
||
|
||
-- Accounts: aggregated from all banks
|
||
accounts (account_id, user_id, bank_code, nickname, currency, status, etc)
|
||
|
||
-- Balances: current balances (cached)
|
||
balances (account_id, balance_type, amount, currency, date_time)
|
||
|
||
-- Transactions: all transactions (cached)
|
||
transactions (transaction_id, account_id, bank_code, amount, currency, etc)
|
||
```
|
||
|
||
---
|
||
|
||
## Frontend Integration (Dioxus)
|
||
|
||
```
|
||
// Example: Get accounts for multi-bank view
|
||
let accounts = fetch_accounts("vbank", "team275-6", token).await;
|
||
for account in accounts.data.account {
|
||
println!("{}: {}", account.nickname, account.currency);
|
||
}
|
||
|
||
// Get transactions for chart/visualization
|
||
let txs = fetch_transactions("vbank", "team275-6", "acc-3848", 1, 6, token).await;
|
||
for tx in txs.data.transaction {
|
||
// Pass to image generation API
|
||
generate_transaction_chart(&tx).await;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Multi-Bank Usage
|
||
|
||
```
|
||
# Get accounts from VBank
|
||
GET /accounts/vbank/team275-6
|
||
|
||
# Get accounts from ABank (same user, different bank)
|
||
GET /accounts/abank/team275-6
|
||
|
||
# Get accounts from SBank
|
||
GET /accounts/sbank/team275-6
|
||
|
||
# All cached! Switch between banks instantly ⚡
|
||
```
|
||
```
|
||
|
||
***
|
||
|
||
## Для Image Generation API
|
||
|
||
Структура данных транзакции для передачи в generative API:
|
||
|
||
```json
|
||
{
|
||
"transaction": {
|
||
"accountId": "acc-3848",
|
||
"transactionId": "tx-team275-8-m0-1",
|
||
"amount": "80462.92",
|
||
"currency": "RUB",
|
||
"creditDebitIndicator": "Credit",
|
||
"transactionInformation": "💼 Зарплата",
|
||
"bookingDateTime": "2025-10-28T17:59:45.080562Z",
|
||
"status": "Booked"
|
||
},
|
||
"prompt_template": "Transaction: {transactionInformation} for {amount} {currency} - {creditDebitIndicator}"
|
||
}
|
||
```
|
||
|
||
***
|
||
|