Сервис предоставляет:
- регистрацию пользователя;
- логин с выдачей
accessиrefreshтокенов; - проверку токена через
/validate; - обновление access токена через
/refresh; - CRUD для задач текущего авторизованного пользователя;
- внутренние метрики через
/metrics.
Изначально проект работал только на in-memory хранилищах, что давало типичные ограничения:
- данные терялись при рестарте;
- между инстансами не было общего состояния;
- нельзя было нормально масштабировать сервис;
- решение не подходило даже для базового production-like сценария.
Сейчас проект поддерживает две реализации хранения:
memorypostgres
Сервисный слой работает через интерфейсы, поэтому бизнес-логика не зависит от конкретного storage backend.
Ключевые интерфейсы:
UserStoreTaskStore
Файлы:
internal/storage/interfaces.gointernal/storage/storage.gointernal/storage/task.gointernal/storage/postgres/user_store.gointernal/storage/postgres/task_store.go
В Config сейчас используются:
PortJWTSecretLogLevelStorageAccessTokenTTLRefreshTokenTTLDatabaseURL
Что важно:
JWT_SECRETобязателен;DATABASE_URLобязателен, еслиSTORAGE=postgres;- TTL токенов вынесены в конфиг;
accessиrefreshтокены имеют разные сроки жизни.
Файл:
internal/config/config.go
Пример .env:
PORT=8081
JWT_SECRET=secret
STORAGE=postgres
DATABASE_URL=postgres://postgres:postgres@localhost:5432/task_tracker?sslmode=disable
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=168h
LOG_LEVEL=info- регистрация нового пользователя;
- логин с проверкой пароля через
bcrypt; - выдача двух токенов:
accessrefresh
- обновление access токена через
/refresh; - различение типа токена через
token_typeв claims; - в JWT claims хранятся:
loginuser_idtoken_type
Файлы:
internal/service/auth.gointernal/transport/http/handlers/auth.gointernal/dto/auth.go
- TTL токенов больше не захардкожен;
- появился
refreshflow; - после перехода на нормализованную модель задач в claims добавлен
user_id.
AuthMiddleware:
- читает
Authorization: Bearer <token>; - валидирует access token;
- кладет в request context:
loginuser_id
Файлы:
internal/transport/http/middleware/auth.gointernal/transport/http/requestctx/context.go
Ошибки и JSON-ответы унифицированы через helper:
DecodeBody(...)WriteJSON(...)WriteError(...)
Что дополнительно есть:
DisallowUnknownFields();- ограничение размера request body;
- проверка, что body содержит только один JSON объект.
Файл:
internal/transport/http/response/json.go
Для каждого запроса логируются:
- HTTP method;
- path;
- status code;
- duration.
Файл:
internal/transport/http/middleware/logging.go
In-memory storage теперь привязан не к login, а к user_id.
Текущая модель Task:
type Task struct {
ID string
Title string
Description string
Status TaskStatus
UserID int64
}В памяти задачи сейчас хранятся как:
map[int64][]TaskТо есть группировка идет по user_id.
Файл:
internal/storage/task.go
В Postgres-реализации есть все CRUD операции:
CreateTaskGetTasksUpdateTaskDeleteTask
Особенности:
- выборка задач идет по
user_id; - update собирается динамически;
- обновленная задача возвращается через
RETURNING; - delete проверяет
RowsAffected.
Файл:
internal/storage/postgres/task_store.go
Пользователь теперь имеет ID:
type User struct {
ID int64
Login string
Password string
}В in-memory user storage добавлен автоинкрементный nextID, чтобы auth service мог работать с user_id единообразно и для memory, и для postgres.
Файл:
internal/storage/storage.go
GetUser(...) теперь возвращает:
idloginpassword
Файл:
internal/storage/postgres/user_store.go
migrations/001_init.sql уже создает нормализованную схему:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
login TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tasks (
id UUID PRIMARY KEY,
user_id BIGINT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'todo',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fk_tasks_user_id
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT chk_tasks_status
CHECK (status IN ('todo', 'in_progress', 'done'))
);Также создаются индексы:
idx_tasks_user_ididx_tasks_user_id_status
Добавлена отдельная миграция:
migrations/002_migrate_tasks_user_login_to_user_id.sql
Ее задача:
- добавить
user_idв старую таблицуtasks; - перенести данные из
user_login; - перестроить FK и индексы;
- удалить старый
user_login.
Для логина реализован in-memory rate limiter:
- ключ:
login + ip; - при превышении лимита возвращается
429 Too Many Requests; - при успешном логине счетчик сбрасывается.
Файлы:
internal/security/ratelimiter.gointernal/transport/http/handlers/auth.go
Сейчас в проекте есть простые in-memory counters:
- успешные логины;
- неуспешные логины;
- количество запросов к tasks;
- количество
4xx; - количество
5xx.
Endpoint:
/metrics
Он защищен LocalOnlyMiddleware, то есть доступен только с localhost.
Файлы:
internal/metrics/metrics.gointernal/transport/http/handlers/metrics.gointernal/transport/http/middleware/local_only.go
Добавлены integration tests для Postgres storage:
internal/storage/postgres/user_store_integration_test.gointernal/storage/postgres/task_store_integration_test.go
Важный момент:
- обычный
go test ./...теперь не должен падать без локального Postgres; - integration tests запускаются только при явном
RUN_POSTGRES_INTEGRATION_TESTS=1; - URL можно переопределить через
TEST_DATABASE_URL; - если БД недоступна, тесты корректно
Skip, а не ломают весь suite.
Файл инфраструктуры тестов:
internal/storage/postgres/postgres_test.go
Пример запуска:
RUN_POSTGRES_INTEGRATION_TESTS=1 \
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5433/task_tracker_test?sslmode=disable \
GOCACHE=$(pwd)/.gocache \
go test ./internal/storage/postgres -vПример через Docker:
docker run -d \
--name task-tracker-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=task_tracker \
-p 5432:5432 \
postgres:16Применение init-схемы:
docker exec -i task-tracker-postgres \
psql -U postgres -d task_tracker < migrations/001_init.sqlЕсли поднимается legacy-схема со старым user_login, после этого потребуется отдельно применить:
docker exec -i task-tracker-postgres \
psql -U postgres -d task_tracker < migrations/002_migrate_tasks_user_login_to_user_id.sqlcurl -X POST http://localhost:8081/register \
-H "Content-Type: application/json" \
-d '{"login":"alice","password":"password123"}'TOKENS=$(curl -s -X POST http://localhost:8081/login \
-H "Content-Type: application/json" \
-d '{"login":"alice","password":"password123"}')
ACCESS_TOKEN=$(echo "$TOKENS" | jq -r .access_token)
REFRESH_TOKEN=$(echo "$TOKENS" | jq -r .refresh_token)curl -X POST http://localhost:8081/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$REFRESH_TOKEN\"}"CREATE_RESPONSE=$(curl -s -X POST http://localhost:8081/tasks \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"first task","description":"postgres works"}')
TASK_ID=$(echo "$CREATE_RESPONSE" | jq -r .id)curl -X GET http://localhost:8081/tasks \
-H "Authorization: Bearer $ACCESS_TOKEN"curl -X PUT http://localhost:8081/tasks/$TASK_ID \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"done"}'curl -X DELETE http://localhost:8081/tasks/$TASK_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"На текущем этапе логично обсуждать в review:
- миграционную стратегию между
001и002, чтобы цепочка была безопасной; - проверку обязательности
user_idв JWT claims после перехода на новую модель; - connection pool настройки для Postgres;
- использование настоящего migration tool вместо ручного SQL применения;
- возможный переход с in-memory metrics на Prometheus/OpenTelemetry;
- дальнейшую чистку API-контрактов и структуры ошибок.