# Smoke Tracker API — Документация для Frontend ## Базовый URL ``` http://localhost:8044/smoke-tracker ``` В production окружении замените на соответствующий домен. --- ## Оглавление 1. [Авторизация](#авторизация) - [Регистрация](#post-authsignup) - [Вход](#post-authsignin) 2. [Логирование сигарет](#логирование-сигарет) - [Записать сигарету](#post-cigarettes) - [Получить список сигарет](#get-cigarettes) 3. [Статистика](#статистика) - [Дневная статистика](#get-statsdaily) --- ## Авторизация Все эндпоинты, кроме `/auth/signup` и `/auth/signin`, требуют JWT-токен в заголовке: ``` Authorization: Bearer ``` Токен возвращается при успешном входе (`/auth/signin`) и действителен **12 часов**. --- ### `POST /auth/signup` **Описание**: Регистрация нового пользователя **Требуется авторизация**: ❌ Нет **Тело запроса** (JSON): ```json { "login": "string", // обязательно, уникальный логин "password": "string" // обязательно } ``` **Пример запроса**: ```bash curl -X POST http://localhost:8044/smoke-tracker/auth/signup \ -H "Content-Type: application/json" \ -d '{ "login": "user123", "password": "mySecurePassword" }' ``` **Ответ при успехе** (200 OK): ```json { "success": true, "body": { "ok": true } } ``` **Возможные ошибки**: - **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля - **500 Internal Server Error**: `"Пользователь с таким логином уже существует"` — логин занят --- ### `POST /auth/signin` **Описание**: Вход в систему (получение JWT-токена) **Требуется авторизация**: ❌ Нет **Тело запроса** (JSON): ```json { "login": "string", // обязательно "password": "string" // обязательно } ``` **Пример запроса**: ```bash curl -X POST http://localhost:8044/smoke-tracker/auth/signin \ -H "Content-Type: application/json" \ -d '{ "login": "user123", "password": "mySecurePassword" }' ``` **Ответ при успехе** (200 OK): ```json { "success": true, "body": { "user": { "id": "507f1f77bcf86cd799439011", "login": "user123", "created": "2024-01-15T10:30:00.000Z" }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } ``` **Поля ответа**: - `user.id` — уникальный идентификатор пользователя - `user.login` — логин пользователя - `user.created` — дата создания аккаунта (ISO 8601) - `token` — JWT-токен для авторизации (действителен 12 часов) **Возможные ошибки**: - **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля - **500 Internal Server Error**: `"Неверный логин или пароль"` — неправильные учётные данные **Использование токена**: Сохраните токен в localStorage/sessionStorage/cookie и передавайте в заголовке всех последующих запросов: ```javascript // Пример для fetch API const token = localStorage.getItem('smokeToken'); fetch('http://localhost:8044/smoke-tracker/cigarettes', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); ``` --- ## Логирование сигарет ### `POST /cigarettes` **Описание**: Записать факт выкуренной сигареты **Требуется авторизация**: ✅ Да (Bearer token) **Тело запроса** (JSON): ```json { "smokedAt": "string (ISO 8601)", // необязательно, по умолчанию — текущее время "note": "string" // необязательно, заметка/комментарий } ``` **Пример запроса**: ```bash curl -X POST http://localhost:8044/smoke-tracker/cigarettes \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{ "smokedAt": "2024-01-15T14:30:00.000Z", "note": "После обеда" }' ``` **Пример без указания времени** (будет текущее время): ```bash curl -X POST http://localhost:8044/smoke-tracker/cigarettes \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{}' ``` **Ответ при успехе** (200 OK): ```json { "success": true, "body": { "id": "507f1f77bcf86cd799439012", "userId": "507f1f77bcf86cd799439011", "smokedAt": "2024-01-15T14:30:00.000Z", "note": "После обеда", "created": "2024-01-15T14:30:05.123Z" } } ``` **Поля ответа**: - `id` — уникальный идентификатор записи - `userId` — ID пользователя - `smokedAt` — дата и время курения (ISO 8601) - `note` — заметка (если была указана) - `created` — дата создания записи в БД **Возможные ошибки**: - **401 Unauthorized**: `"Требуется авторизация"` — не передан токен - **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен - **400 Bad Request**: `"Некорректный формат даты smokedAt"` — неверный формат даты --- ### `GET /cigarettes` **Описание**: Получить список всех выкуренных сигарет текущего пользователя **Требуется авторизация**: ✅ Да (Bearer token) **Query-параметры** (все необязательные): | Параметр | Тип | Описание | Пример | |----------|-----|----------|--------| | `from` | string (ISO 8601) | Начало периода (включительно) | `2024-01-01T00:00:00.000Z` | | `to` | string (ISO 8601) | Конец периода (включительно) | `2024-01-31T23:59:59.999Z` | **Пример запроса** (все сигареты): ```bash curl -X GET http://localhost:8044/smoke-tracker/cigarettes \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` **Пример запроса** (с фильтрацией по датам): ```bash curl -X GET "http://localhost:8044/smoke-tracker/cigarettes?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` **Ответ при успехе** (200 OK): ```json { "success": true, "body": [ { "id": "507f1f77bcf86cd799439012", "userId": "507f1f77bcf86cd799439011", "smokedAt": "2024-01-15T10:30:00.000Z", "note": "Утренняя", "created": "2024-01-15T10:30:05.123Z" }, { "id": "507f1f77bcf86cd799439013", "userId": "507f1f77bcf86cd799439011", "smokedAt": "2024-01-15T14:30:00.000Z", "note": "После обеда", "created": "2024-01-15T14:30:05.456Z" } ] } ``` **Особенности**: - Записи отсортированы по `smokedAt` (от старых к новым) - Если указаны `from` и/или `to`, будет применена фильтрация - Пустой массив возвращается, если сигарет в периоде нет **Возможные ошибки**: - **401 Unauthorized**: `"Требуется авторизация"` — не передан токен - **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен --- ## Статистика ### `GET /stats/daily` **Описание**: Получить дневную статистику по количеству сигарет для построения графика **Требуется авторизация**: ✅ Да (Bearer token) **Query-параметры** (все необязательные): | Параметр | Тип | Описание | Пример | По умолчанию | |----------|-----|----------|--------|--------------| | `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты | | `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время | **Пример запроса** (последние 30 дней): ```bash curl -X GET http://localhost:8044/smoke-tracker/stats/daily \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` **Пример запроса** (с указанием периода): ```bash curl -X GET "http://localhost:8044/smoke-tracker/stats/daily?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` **Ответ при успехе** (200 OK): ```json { "success": true, "body": [ { "date": "2024-01-15", "count": 8 }, { "date": "2024-01-16", "count": 12 }, { "date": "2024-01-17", "count": 5 } ] } ``` **Поля ответа**: - `date` — дата в формате `YYYY-MM-DD` - `count` — количество сигарет, выкуренных в этот день **Особенности**: - Данные отсортированы по дате (от старых к новым) - Дни без сигарет **не включаются** в ответ (фронтенду нужно самостоятельно заполнить пропуски нулями при построении графика) - Агрегация происходит по дате из поля `smokedAt` (не `created`) **Пример использования для графика** (Chart.js): ```javascript const response = await fetch('http://localhost:8044/smoke-tracker/stats/daily', { headers: { 'Authorization': `Bearer ${token}` } }); const { body } = await response.json(); // Заполнение пропущенных дней нулями const fillMissingDates = (data, from, to) => { const result = []; const current = new Date(from); const end = new Date(to); while (current <= end) { const dateStr = current.toISOString().split('T')[0]; const existing = data.find(d => d.date === dateStr); result.push({ date: dateStr, count: existing ? existing.count : 0 }); current.setDate(current.getDate() + 1); } return result; }; const filledData = fillMissingDates(body, '2024-01-01', '2024-01-31'); // Данные для графика const chartData = { labels: filledData.map(d => d.date), datasets: [{ label: 'Количество сигарет', data: filledData.map(d => d.count), borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.2)', }] }; ``` **Возможные ошибки**: - **401 Unauthorized**: `"Требуется авторизация"` — не передан токен - **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен --- ## Общая структура ответов Все эндпоинты возвращают JSON в следующем формате: **Успешный ответ**: ```json { "success": true, "body": { /* данные */ } } ``` **Ответ с ошибкой**: ```json { "success": false, "errors": "Описание ошибки" } ``` или (при использовании глобального обработчика ошибок): ```json { "message": "Описание ошибки" } ``` --- ## Коды состояния HTTP | Код | Описание | |-----|----------| | **200 OK** | Запрос выполнен успешно | | **400 Bad Request** | Некорректные данные в запросе | | **401 Unauthorized** | Требуется авторизация или токен невалидный | | **500 Internal Server Error** | Внутренняя ошибка сервера | --- ## Примеры интеграции ### React + Axios ```javascript import axios from 'axios'; const API_BASE_URL = 'http://localhost:8044/smoke-tracker'; // Создание экземпляра axios с базовыми настройками const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' } }); // Интерцептор для добавления токена api.interceptors.request.use(config => { const token = localStorage.getItem('smokeToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Регистрация export const signup = async (login, password) => { const { data } = await api.post('/auth/signup', { login, password }); return data; }; // Вход export const signin = async (login, password) => { const { data } = await api.post('/auth/signin', { login, password }); if (data.success) { localStorage.setItem('smokeToken', data.body.token); } return data; }; // Выход export const signout = () => { localStorage.removeItem('smokeToken'); }; // Записать сигарету export const logCigarette = async (smokedAt = null, note = '') => { const { data } = await api.post('/cigarettes', { smokedAt, note }); return data; }; // Получить список сигарет export const getCigarettes = async (from = null, to = null) => { const params = {}; if (from) params.from = from; if (to) params.to = to; const { data } = await api.get('/cigarettes', { params }); return data; }; // Получить дневную статистику export const getDailyStats = async (from = null, to = null) => { const params = {}; if (from) params.from = from; if (to) params.to = to; const { data } = await api.get('/stats/daily', { params }); return data; }; ``` ### Vanilla JavaScript + Fetch ```javascript const API_BASE_URL = 'http://localhost:8044/smoke-tracker'; // Получение токена const getToken = () => localStorage.getItem('smokeToken'); // Базовый запрос const apiRequest = async (endpoint, options = {}) => { const token = getToken(); const headers = { 'Content-Type': 'application/json', ...options.headers }; if (token) { headers.Authorization = `Bearer ${token}`; } const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || error.errors || 'Ошибка запроса'); } return response.json(); }; // Регистрация async function signup(login, password) { return apiRequest('/auth/signup', { method: 'POST', body: JSON.stringify({ login, password }) }); } // Вход async function signin(login, password) { const data = await apiRequest('/auth/signin', { method: 'POST', body: JSON.stringify({ login, password }) }); if (data.success) { localStorage.setItem('smokeToken', data.body.token); } return data; } // Записать сигарету async function logCigarette(note = '') { return apiRequest('/cigarettes', { method: 'POST', body: JSON.stringify({ note }) }); } // Получить дневную статистику async function getDailyStats() { return apiRequest('/stats/daily'); } ``` --- ## Рекомендации по безопасности 1. **Хранение токена**: - Для веб-приложений: используйте `httpOnly` cookies или `sessionStorage` - Избегайте `localStorage` при работе с чувствительными данными - Для мобильных приложений: используйте безопасное хранилище (Keychain/Keystore) 2. **HTTPS**: В production всегда используйте HTTPS для защиты токена при передаче 3. **Обработка истечения токена**: - Токен действителен 12 часов - При получении ошибки 401 перенаправляйте пользователя на страницу входа - Реализуйте механизм refresh token для бесшовного обновления 4. **Валидация на фронтенде**: - Проверяйте корректность email/логина перед отправкой - Требуйте минимальную длину пароля (8+ символов) - Показывайте индикатор силы пароля --- ## Postman-коллекция Готовая коллекция для тестирования доступна в файле: ``` server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json ``` Импортируйте её в Postman для быстрого тестирования всех эндпоинтов. --- ## Поддержка При возникновении вопросов или обнаружении проблем обращайтесь к разработчикам backend-команды.