# 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 `, проверяет `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 ` или `/done_` | Отметить задачу выполненной | | `/check ` или `/check_` | Отметить привычку | | `/help` | Справка | **Callback-кнопки:** - `donetask_` — выполнить задачу - `deltask_` — удалить задачу - `checkhabit_` — отметить привычку сегодня - `checkhabit__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/.go` + роут в `cmd/api/main.go` | | Изменить модель/таблицу | `model/.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` |