- Introduced a new API endpoint `GET /stats/summary` to retrieve detailed smoking statistics for users, including daily and global averages. - Updated the API client to support fetching summary statistics. - Enhanced the statistics page with a new tab for summary statistics, featuring key metrics and visualizations for user comparison. - Implemented error handling and loading states for the summary statistics fetch operation. - Refactored the statistics page to separate daily and summary statistics into distinct components for improved organization and readability.
25 KiB
Smoke Tracker API — Документация для Frontend
Базовый URL
http://localhost:8044/smoke-tracker
В production окружении замените на соответствующий домен.
Оглавление
Авторизация
Все эндпоинты, кроме /auth/signup и /auth/signin, требуют JWT-токен в заголовке:
Authorization: Bearer <token>
Токен возвращается при успешном входе (/auth/signin) и действителен 12 часов.
POST /auth/signup
Описание: Регистрация нового пользователя
Требуется авторизация: ❌ Нет
Тело запроса (JSON):
{
"login": "string", // обязательно, уникальный логин
"password": "string" // обязательно
}
Пример запроса:
curl -X POST http://localhost:8044/smoke-tracker/auth/signup \
-H "Content-Type: application/json" \
-d '{
"login": "user123",
"password": "mySecurePassword"
}'
Ответ при успехе (200 OK):
{
"success": true,
"body": {
"ok": true
}
}
Возможные ошибки:
- 400 Bad Request:
"Не все поля заполнены: login, password"— не указаны обязательные поля - 500 Internal Server Error:
"Пользователь с таким логином уже существует"— логин занят
POST /auth/signin
Описание: Вход в систему (получение JWT-токена)
Требуется авторизация: ❌ Нет
Тело запроса (JSON):
{
"login": "string", // обязательно
"password": "string" // обязательно
}
Пример запроса:
curl -X POST http://localhost:8044/smoke-tracker/auth/signin \
-H "Content-Type: application/json" \
-d '{
"login": "user123",
"password": "mySecurePassword"
}'
Ответ при успехе (200 OK):
{
"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 и передавайте в заголовке всех последующих запросов:
// Пример для 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):
{
"smokedAt": "string (ISO 8601)", // необязательно, по умолчанию — текущее время
"note": "string" // необязательно, заметка/комментарий
}
Пример запроса:
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": "После обеда"
}'
Пример без указания времени (будет текущее время):
curl -X POST http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{}'
Ответ при успехе (200 OK):
{
"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 |
Пример запроса (все сигареты):
curl -X GET http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Пример запроса (с фильтрацией по датам):
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):
{
"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 дней):
curl -X GET http://localhost:8044/smoke-tracker/stats/daily \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Пример запроса (с указанием периода):
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):
{
"success": true,
"body": [
{
"date": "2024-01-15",
"count": 8
},
{
"date": "2024-01-16",
"count": 12
},
{
"date": "2024-01-17",
"count": 5
}
]
}
Поля ответа:
date— дата в форматеYYYY-MM-DDcount— количество сигарет, выкуренных в этот день
Особенности:
- Данные отсортированы по дате (от старых к новым)
- Дни без сигарет не включаются в ответ (фронтенду нужно самостоятельно заполнить пропуски нулями при построении графика)
- Агрегация происходит по дате из поля
smokedAt(неcreated)
Пример использования для графика (Chart.js):
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 |
Текущая дата и время |
Пример запроса:
curl -X GET http://localhost:8044/smoke-tracker/stats/summary \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Ответ при успехе (200 OK):
{
"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-DDcount— количество сигарет
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 - Глобальная статистика позволяет сравнить свои результаты с другими пользователями
Примеры использования:
// Получение сводной статистики
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} сигарет)`);
Визуализация данных по дням недели:
// Данные для круговой диаграммы (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)'
]
}]
};
Сравнение с глобальной статистикой:
// Сравнительный график (ваши данные 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 в следующем формате:
Успешный ответ:
{
"success": true,
"body": { /* данные */ }
}
Ответ с ошибкой:
{
"success": false,
"errors": "Описание ошибки"
}
или (при использовании глобального обработчика ошибок):
{
"message": "Описание ошибки"
}
Коды состояния HTTP
| Код | Описание |
|---|---|
| 200 OK | Запрос выполнен успешно |
| 400 Bad Request | Некорректные данные в запросе |
| 401 Unauthorized | Требуется авторизация или токен невалидный |
| 500 Internal Server Error | Внутренняя ошибка сервера |
Примеры интеграции
React + Axios
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
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');
}
Рекомендации по безопасности
-
Хранение токена:
- Для веб-приложений: используйте
httpOnlycookies илиsessionStorage - Избегайте
localStorageпри работе с чувствительными данными - Для мобильных приложений: используйте безопасное хранилище (Keychain/Keystore)
- Для веб-приложений: используйте
-
HTTPS: В production всегда используйте HTTPS для защиты токена при передаче
-
Обработка истечения токена:
- Токен действителен 12 часов
- При получении ошибки 401 перенаправляйте пользователя на страницу входа
- Реализуйте механизм refresh token для бесшовного обновления
-
Валидация на фронтенде:
- Проверяйте корректность email/логина перед отправкой
- Требуйте минимальную длину пароля (8+ символов)
- Показывайте индикатор силы пароля
Postman-коллекция
Готовая коллекция для тестирования доступна в файле:
server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json
Импортируйте её в Postman для быстрого тестирования всех эндпоинтов.
Поддержка
При возникновении вопросов или обнаружении проблем обращайтесь к разработчикам backend-команды.