Files
obsidian/Инфраструктура/pulse-api.md

338 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pulse-api — Архитектура
**Репозиторий:** `https://git.digital-home.site/daniil/pulse-api`
**Go module:** `github.com/daniil/homelab-api`
**URL:** `https://api.digital-home.site`
**Dev:** `http://192.168.31.60:8081`
## Общая архитектура
Классический Go REST API с разделением на слои:
```
cmd/api/main.go ← точка входа, роутер, инициализация
internal/
config/ ← загрузка env-переменных
repository/ ← работа с БД (SQL-запросы)
service/ ← бизнес-логика
handler/ ← HTTP-хендлеры (request/response)
model/ ← структуры данных (Go structs + JSON)
middleware/ ← JWT-аутентификация
bot/ ← Telegram бот
scheduler/ ← cron-задачи (напоминания)
```
**Стек:**
- Router: `go-chi/chi v5`
- ORM: `jmoiron/sqlx` (raw SQL + named queries)
- БД: PostgreSQL 16
- JWT: `golang-jwt/jwt v5`
- Telegram: `go-telegram-bot-api v5`
- Cron: `robfig/cron v3`
- Email: Resend API (`service/email.go`)
- Крипто: `golang.org/x/crypto` (bcrypt для паролей)
## Структура папок
```
pulse-api/
├── cmd/api/main.go # Точка входа: роутер, DI, запуск
├── internal/
│ ├── bot/
│ │ ├── bot.go # Инициализация, Start(), SendMessage()
│ │ └── handlers.go # Команды и callback кнопки
│ ├── config/
│ │ └── config.go # Config struct + Load() из env
│ ├── handler/
│ │ ├── auth.go # Register, Login, Refresh, Me, VerifyEmail...
│ │ ├── tasks.go # CRUD задач + complete/uncomplete
│ │ ├── habits.go # CRUD привычек + логи + статистика
│ │ ├── habit_freeze.go # Заморозка привычек
│ │ ├── finance.go # Категории и транзакции финансов
│ │ ├── savings.go # Накопления (категории, участники, планы)
│ │ ├── interest.go # Начисление процентов
│ │ ├── profile.go # Профиль пользователя
│ │ └── health.go # GET /health
│ ├── middleware/
│ │ └── auth.go # JWT Bearer middleware
│ ├── model/
│ │ ├── user.go # User, RegisterRequest, LoginRequest...
│ │ ├── task.go # Task, CreateTaskRequest...
│ │ ├── habit.go # Habit, HabitLog, HabitStats...
│ │ ├── habit_freeze.go # HabitFreeze
│ │ ├── finance.go # FinanceCategory, FinanceTransaction...
│ │ ├── savings.go # SavingsCategory, SavingsTransaction...
│ │ └── email.go # Email templates
│ ├── repository/
│ │ ├── db.go # NewDB() + RunMigrations()
│ │ ├── user.go # UserRepository
│ │ ├── task.go # TaskRepository
│ │ ├── habit.go # HabitRepository
│ │ ├── habit_freeze.go # HabitFreezeRepository
│ │ ├── finance.go # FinanceRepository
│ │ ├── savings.go # SavingsRepository
│ │ └── email_token.go # EmailTokenRepository
│ ├── service/
│ │ ├── auth.go # AuthService: register/login/JWT
│ │ ├── habit.go # HabitService
│ │ ├── task.go # TaskService
│ │ ├── finance.go # FinanceService
│ │ ├── interest.go # Начисление процентов
│ │ └── email.go # EmailService (Resend)
│ └── scheduler/
│ └── scheduler.go # Cron-напоминания через Telegram
├── docs/
│ └── SAVINGS.md
├── go.mod
├── Dockerfile
└── docker-compose.yml
```
## API Эндпоинты
### Публичные (без авторизации)
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/health` | Health check |
| POST | `/auth/register` | Регистрация: `{email, username, password}` |
| POST | `/auth/login` | Логин: `{email, password}``{access_token, refresh_token, user}` |
| POST | `/auth/refresh` | Обновление токена: `{refresh_token}` |
| POST | `/auth/verify-email` | Подтверждение email: `{token}` |
| POST | `/auth/resend-verification` | Повторная отправка: `{email}` |
| POST | `/auth/forgot-password` | Сброс пароля: `{email}` |
| POST | `/auth/reset-password` | Новый пароль: `{token, new_password}` |
### Авторизованные (Bearer JWT)
#### Аутентификация/Профиль
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/auth/me` | Текущий пользователь |
| PUT | `/auth/me` | Обновить профиль |
| PUT | `/auth/password` | Сменить пароль: `{old_password, new_password}` |
| GET | `/profile` | Профиль |
| PUT | `/profile` | Обновить профиль |
#### Задачи
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/tasks` | Список задач. Query: `?completed=true/false` |
| GET | `/tasks/today` | Задачи на сегодня |
| POST | `/tasks` | Создать: `{title, description?, icon?, color?, due_date?, priority?, reminder_time?, is_recurring?, recurrence_type?, recurrence_interval?, recurrence_end_date?}` |
| GET | `/tasks/{id}` | Получить задачу |
| PUT | `/tasks/{id}` | Обновить задачу |
| DELETE | `/tasks/{id}` | Удалить задачу |
| POST | `/tasks/{id}/complete` | Отметить выполненной |
| POST | `/tasks/{id}/uncomplete` | Снять отметку |
#### Привычки
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/habits` | Список привычек |
| POST | `/habits` | Создать: `{name, description?, color?, icon?, frequency, target_days?, target_count?, reminder_time?, start_date?}` |
| GET | `/habits/{id}` | Получить привычку |
| PUT | `/habits/{id}` | Обновить |
| DELETE | `/habits/{id}` | Удалить |
| POST | `/habits/{id}/log` | Отметить: `{date?, count?, note?}` |
| GET | `/habits/{id}/logs` | История отметок |
| DELETE | `/habits/{id}/logs/{logId}` | Удалить отметку |
| GET | `/habits/stats` | Общая статистика |
| GET | `/habits/{id}/stats` | Статистика привычки |
| GET | `/habits/{id}/freezes` | Заморозки привычки |
| POST | `/habits/{id}/freezes` | Создать заморозку |
| DELETE | `/habits/{id}/freezes/{freezeId}` | Удалить заморозку |
#### Финансы
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/finance/categories` | Категории расходов/доходов |
| POST | `/finance/categories` | Создать: `{name, emoji?, type, budget?, color?, sort_order?}` |
| PUT | `/finance/categories/{id}` | Обновить |
| DELETE | `/finance/categories/{id}` | Удалить |
| GET | `/finance/transactions` | Транзакции. Query: `?month=&year=` |
| POST | `/finance/transactions` | Создать: `{category_id, type, amount, description?, date}` |
| PUT | `/finance/transactions/{id}` | Обновить |
| DELETE | `/finance/transactions/{id}` | Удалить |
| GET | `/finance/summary` | Сводка: баланс, доходы, расходы, по категориям |
| GET | `/finance/analytics` | Аналитика: тренды по месяцам |
#### Накопления
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/savings/categories` | Категории накоплений |
| POST | `/savings/categories` | Создать категорию |
| GET | `/savings/categories/{id}` | Получить |
| PUT | `/savings/categories/{id}` | Обновить |
| DELETE | `/savings/categories/{id}` | Удалить |
| GET | `/savings/categories/{id}/members` | Участники |
| POST | `/savings/categories/{id}/members` | Добавить участника |
| DELETE | `/savings/categories/{id}/members/{userId}` | Удалить участника |
| GET | `/savings/categories/{id}/recurring-plans` | Регулярные планы |
| POST | `/savings/categories/{id}/recurring-plans` | Создать план |
| PUT | `/savings/recurring-plans/{planId}` | Обновить план |
| DELETE | `/savings/recurring-plans/{planId}` | Удалить план |
| GET | `/savings/transactions` | Транзакции накоплений |
| POST | `/savings/transactions` | Создать транзакцию |
| GET | `/savings/transactions/{id}` | Получить |
| PUT | `/savings/transactions/{id}` | Обновить |
| DELETE | `/savings/transactions/{id}` | Удалить |
| GET | `/savings/stats` | Статистика |
## Модели данных
### User
```go
type User struct {
ID int64
Email string
Username string
PasswordHash string // bcrypt, в JSON скрыто
EmailVerified bool
TelegramChatID *int64 // nullable
NotificationsEnabled bool
Timezone string
MorningReminderTime string // "09:00"
EveningReminderTime string // "21:00"
CreatedAt, UpdatedAt time.Time
}
```
### Task
```go
type Task struct {
ID, UserID int64
Title, Description string
Icon, Color string
DueDate *string // "2026-01-01"
Priority int // 1=низкий, 2=средний, 3=высокий
ReminderTime *string // "19:00"
Completed bool // производное от CompletedAt
// Повторяющиеся задачи:
IsRecurring bool
RecurrenceType *string // "daily", "weekly", "monthly"
RecurrenceInterval int
RecurrenceEndDate *string
ParentTaskID *int64
CreatedAt, UpdatedAt time.Time
}
```
### Habit
```go
type Habit struct {
ID, UserID int64
Name, Description string
Color, Icon string
Frequency string // "daily", "weekly", "custom"
TargetDays []int // дни недели (0=вс...6=сб)
TargetCount int
ReminderTime *string // "19:00"
StartDate *string
IsArchived bool
CreatedAt, UpdatedAt time.Time
}
type HabitLog struct {
ID, HabitID, UserID int64
Date time.Time
Count int
Note string
CreatedAt time.Time
}
```
### FinanceCategory / FinanceTransaction
```go
type FinanceCategory struct {
ID, UserID int64
Name, Emoji string
Type string // "income" | "expense"
Budget *float64
Color string
SortOrder int
CreatedAt time.Time
}
type FinanceTransaction struct {
ID, UserID, CategoryID int64
Type string // "income" | "expense"
Amount float64
Description string
Date time.Time
CreatedAt time.Time
CategoryName, CategoryEmoji string // из JOIN
}
```
## Аутентификация
- **JWT HS256** с двумя типами токенов: `access` (короткий) и `refresh` (длинный)
- Middleware `Authenticate` парсит `Authorization: Bearer <token>`, проверяет `type == "access"`
- UserID извлекается из claims и помещается в context: `GetUserID(ctx)`
- Пароли хешируются bcrypt
- Email-верификация при регистрации через Resend API
- Password reset — одноразовые токены в таблице `email_tokens`
## Telegram Бот
**Команды:**
| Команда | Действие |
|---------|---------|
| `/start` | Показать Chat ID (для привязки к аккаунту Pulse) |
| `/tasks` | Список задач на сегодня с inline-кнопками |
| `/habits` | Привычки на сегодня с inline-кнопками |
| `/done <id>` или `/done_<id>` | Отметить задачу выполненной |
| `/check <id>` или `/check_<id>` | Отметить привычку |
| `/help` | Справка |
**Callback-кнопки:**
- `donetask_<id>` — выполнить задачу
- `deltask_<id>` — удалить задачу
- `checkhabit_<id>` — отметить привычку сегодня
- `checkhabit_<id>_yesterday` — отметить привычку за вчера
**Привязка:** пользователь вводит `/start`, получает Chat ID, вставляет его в настройки Pulse → `PUT /profile {telegram_chat_id: ...}`
## Scheduler (cron-напоминания)
`internal/scheduler/scheduler.go` — использует `robfig/cron`:
- Утренние напоминания (время из настроек пользователя)
- Вечерние напоминания
- Напоминания о задачах по `reminder_time`
- Напоминания о привычках по `reminder_time`
## Конфигурация (env переменные)
| Переменная | Описание | Default |
|-----------|----------|---------|
| `DATABASE_URL` | PostgreSQL DSN | `postgres://homelab:homelab@db:5432/homelab` |
| `JWT_SECRET` | Секрет для JWT | `change-me-in-production` |
| `PORT` | Порт сервера | `8080` |
| `RESEND_API_KEY` | API ключ Resend (email) | — |
| `FROM_EMAIL` | Email отправителя | `noreply@digital-home.site` |
| `FROM_NAME` | Имя отправителя | `Homelab` |
| `APP_URL` | Публичный URL приложения | `https://api.digital-home.site` |
| `TELEGRAM_BOT_TOKEN` | Токен Telegram бота | — |
## Где искать что
| Задача | Файл |
|--------|------|
| Добавить новый эндпоинт | `handler/<domain>.go` + роут в `cmd/api/main.go` |
| Изменить модель/таблицу | `model/<domain>.go` + `repository/db.go` (migrations) |
| Логика уведомлений | `internal/scheduler/scheduler.go` |
| Telegram команды | `internal/bot/handlers.go` |
| Финансы (категории/транзакции) | `handler/finance.go`, `service/finance.go`, `repository/finance.go` |
| Привычки | `handler/habits.go`, `service/habit.go`, `repository/habit.go` |
| Задачи | `handler/tasks.go`, `service/task.go`, `repository/task.go` |
| Накопления | `handler/savings.go`, `repository/savings.go` |
| Email отправка | `service/email.go` |
| JWT/Auth | `service/auth.go`, `middleware/auth.go` |
| Конфиг | `config/config.go` |