# Smoke Tracker API — Документация для Frontend ## Базовый URL ``` http://localhost:8044/smoke-tracker ``` В production окружении замените на соответствующий домен. --- ## Оглавление 1. [Авторизация](#авторизация) - [Регистрация](#post-authsignup) - [Вход](#post-authsignin) 2. [Логирование сигарет](#логирование-сигарет) - [Записать сигарету](#post-cigarettes) - [Получить список сигарет](#get-cigarettes) 3. [Статистика](#статистика) - [Дневная статистика](#get-statsdaily) - [Сводная статистика](#get-statssummary) --- ## Авторизация Все эндпоинты, кроме `/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**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен --- ### `GET /stats/summary` **Описание**: Получить расширенную статистику для текущего пользователя и общую по всем пользователям **Требуется авторизация**: ✅ Да (Bearer token) **Query-параметры** (все необязательные): | Параметр | Тип | Описание | Пример | По умолчанию | |----------|-----|----------|--------|--------------| | `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты | | `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время | **Пример запроса**: ```bash curl -X GET http://localhost:8044/smoke-tracker/stats/summary \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` **Ответ при успехе** (200 OK): ```json { "success": true, "body": { "user": { "daily": [ { "date": "2024-01-15", "count": 8 }, { "date": "2024-01-16", "count": 12 } ], "averagePerDay": 10.5, "weekday": [ { "dayOfWeek": 2, "dayName": "Понедельник", "count": 25, "average": "6.25" }, { "dayOfWeek": 3, "dayName": "Вторник", "count": 30, "average": "7.50" } ], "total": 315, "daysWithData": 30 }, "global": { "daily": [ { "date": "2024-01-15", "count": 45 }, { "date": "2024-01-16", "count": 52 } ], "averagePerDay": 48.5, "weekday": [ { "dayOfWeek": 2, "dayName": "Понедельник", "count": 120, "average": "30.00" }, { "dayOfWeek": 3, "dayName": "Вторник", "count": 135, "average": "33.75" } ], "total": 1455, "daysWithData": 30, "activeUsers": 5 }, "period": { "from": "2024-01-01T00:00:00.000Z", "to": "2024-01-31T23:59:59.999Z" } } } ``` **Структура ответа**: **`user`** — статистика текущего пользователя: - `daily` — массив с количеством сигарет по дням - `date` — дата в формате YYYY-MM-DD - `count` — количество сигарет - `averagePerDay` — среднее количество сигарет в день (число с плавающей точкой) - `weekday` — статистика по дням недели - `dayOfWeek` — номер дня недели (1 = воскресенье, 2 = понедельник, ..., 7 = суббота) - `dayName` — название дня недели - `count` — общее количество сигарет в этот день недели за весь период - `average` — среднее количество за один такой день недели (строка) - `total` — общее количество сигарет за период - `daysWithData` — количество дней, в которые были записи **`global`** — общая статистика по всем пользователям: - `daily` — массив с суммарным количеством сигарет всех пользователей по дням - `averagePerDay` — среднее количество сигарет в день (все пользователи) - `weekday` — статистика по дням недели (все пользователи) - `total` — общее количество сигарет всех пользователей за период - `daysWithData` — количество дней с записями - `activeUsers` — количество уникальных пользователей, записывавших сигареты в период **`period`** — информация о запрошенном периоде: - `from` — начало периода (ISO 8601) - `to` — конец периода (ISO 8601) **Особенности**: - Дни недели нумеруются по стандарту MongoDB: 1 = Воскресенье, 2 = Понедельник, ..., 7 = Суббота - `average` для дней недели рассчитывается делением общего количества на количество таких дней в периоде - Дни без записей **не включаются** в массив `daily` - Глобальная статистика позволяет сравнить свои результаты с другими пользователями **Примеры использования**: ```javascript // Получение сводной статистики const response = await fetch('http://localhost:8044/smoke-tracker/stats/summary', { headers: { 'Authorization': `Bearer ${token}` } }); const { body } = await response.json(); console.log(`Вы в среднем выкуриваете ${body.user.averagePerDay} сигарет в день`); console.log(`Общее среднее по всем пользователям: ${body.global.averagePerDay} сигарет в день`); console.log(`Активных пользователей в периоде: ${body.global.activeUsers}`); // Поиск самого "тяжёлого" дня недели const maxWeekday = body.user.weekday.reduce((max, day) => parseFloat(day.average) > parseFloat(max.average) ? day : max ); console.log(`Больше всего вы курите в ${maxWeekday.dayName} (в среднем ${maxWeekday.average} сигарет)`); ``` **Визуализация данных по дням недели**: ```javascript // Данные для круговой диаграммы (Chart.js) const weekdayChartData = { labels: body.user.weekday.map(d => d.dayName), datasets: [{ label: 'Сигарет в день недели', data: body.user.weekday.map(d => d.count), backgroundColor: [ 'rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)', 'rgba(75, 192, 192, 0.6)', 'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)', 'rgba(199, 199, 199, 0.6)' ] }] }; ``` **Сравнение с глобальной статистикой**: ```javascript // Сравнительный график (ваши данные vs общие данные) const comparisonData = { labels: body.user.weekday.map(d => d.dayName), datasets: [ { label: 'Вы', data: body.user.weekday.map(d => parseFloat(d.average)), borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.2)', }, { label: 'Среднее по пользователям', data: body.global.weekday.map(d => parseFloat(d.average)), borderColor: 'rgb(54, 162, 235)', backgroundColor: 'rgba(54, 162, 235, 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-команды.