Skip to content

ce-fello/Projects-Service

Repository files navigation

Projects Service

Микросервис приёма и отбора заявок на проекты от внешних инициаторов.

Сервис реализован на Go с использованием:

  • net/http и стандартного ServeMux
  • oapi-codegen для generated роутинга и transport-типов
  • Postgres как основной БД
  • pgx/v5 для доступа к БД
  • sqlc для статических SQL-запросов
  • squirrel и pgx.CollectRows для динамического списка заявок
  • Taskfile для локальных задач и CI
  • JWT для аутентификации
  • Docker Compose для локального запуска
  • GitHub Actions для CI

API-контракт задания находится в api.json.

Что реализовано

  • Публичная подача внешней заявки на проект
  • Просмотр справочника типов проектов
  • Логин внутренних пользователей
  • Административный просмотр, принятие и отклонение заявок
  • Инварианты жизненного цикла заявки:
    • PENDING -> ACCEPTED
    • PENDING -> REJECTED
  • Миграции базы данных
  • Idempotent demo seed
  • Unit и integration tests

Бизнес-модель

Роли

  • USER может только логиниться
  • ADMIN может просматривать заявки, список заявок, принимать и отклонять их
  • Внешний инициатор подаёт заявку без авторизации

Статусы заявки

  • PENDING
  • ACCEPTED
  • REJECTED

Причина отказа существует только для REJECTED и дополнительно защищена constraint-ами в БД.

Архитектура

Проект разделён на несколько слоёв:

  • cmd/service

    • bootstrap приложения
    • создание HTTP-сервера
    • подключение к БД
    • запуск миграций и опционального demo seed
  • internal/domain

    • базовые сущности домена
    • роли, actor, статусы, ошибки
  • internal/application

    • use-cases сервиса
    • валидация входных данных
    • проверка прав доступа на application boundary
    • бизнес-правила переходов статусов
  • internal/platform/postgres

    • открытие pgxpool connection
    • транзакции на pgx.Tx
    • миграции
    • seed
    • sqlc-generated queries для статических запросов
    • squirrel-based dynamic query для списка заявок
  • internal/platform/auth

    • bcrypt password hashing
    • JWT generation/parsing
  • internal/transport/http

    • oapi-codegen generated std-http-server
    • HTTP handlers
    • middleware
    • strict JSON request parsing
    • auth revalidation
    • request id и logging

Ключевые инженерные решения

  • Admin-права защищены на двух уровнях:
    • transport-level middleware валидирует токен и перечитывает пользователя из БД
    • application-layer use cases дополнительно требуют domain.Actor и сами проверяют роль
  • Изменение статуса заявки выполняется транзакционно с SELECT ... FOR UPDATE
  • Раннер миграций использует Postgres advisory lock, чтобы не гоняться при конкурентном старте
  • Статические SQL-запросы генерируются через sqlc
  • Динамический список заявок строится через squirrel и читается через pgx.CollectRows
  • Routing и path/query binding берутся из oapi-codegen, а strict JSON decode и error mapping остаются hand-written
  • Demo seed включается только через ENABLE_DEMO_SEED=true
  • Для локального запуска, генерации и тестов используется Taskfile

Структура данных

Основные таблицы:

  • users
  • project_types
  • external_applications
  • schema_migrations

Ключевые ограничения:

  • role IN ('ADMIN', 'USER')
  • status IN ('PENDING', 'ACCEPTED', 'REJECTED')
  • согласованность status и rejection_reason
  • foreign key на project_types

Аутентификация и авторизация

Логин

POST /login принимает логин и пароль и возвращает JWT.

Передача токена

Токен передаётся в заголовке:

X-API-TOKEN: <jwt>

Актуальность прав

Даже после успешного логина admin-доступ не доверяет одному только JWT:

  • пользователь перечитывается из БД на защищённых admin-ручках
  • если пользователь удалён, сервис вернёт 401
  • если роль больше не ADMIN, сервис вернёт 403

HTTP API

Основные ручки:

  • POST /login
  • GET /project/type
  • POST /project/application/external
  • GET /project/application/external/list
  • GET /project/application/external/{applicationId}
  • POST /project/application/external/{applicationId}/accept
  • POST /project/application/external/{applicationId}/reject

Важные правила API

  • POST /project/application/external не принимает rejectedReason
  • причина отказа передаётся только в POST /project/application/external/{applicationId}/reject
  • запросы ограничены по размеру body
  • сервис возвращает X-Request-ID в response headers

Локальный запуск

Рекомендуемый способ

docker compose up -d

После старта сервис доступен на:

http://localhost:8000

Compose поднимает:

  • приложение
  • Postgres

В compose.yaml уже включён demo seed.

Тестовые пользователи

  • admin / admin123
  • user / user123

Это demo-учётки для локальной проверки тестового задания.

Локальный запуск без Docker

Нужен доступный Postgres и переменные окружения:

export DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/projects_service?sslmode=disable'
export JWT_SECRET='projects-service-secret'
export ENABLE_DEMO_SEED='true'
go run ./cmd/service

Переменные окружения

  • HTTP_PORT

    • порт HTTP-сервера
    • default: 8000
  • DATABASE_URL

    • строка подключения к Postgres
    • обязательна
  • JWT_SECRET

    • секрет подписи JWT
    • обязательна
  • ENABLE_DEMO_SEED

    • включает demo seed пользователей и типов проектов
    • default: false

Тестирование

Генерация кода

task generate

Полный локальный прогон

task test

Что делает команда:

  • поднимает postgres через Docker Compose
  • запускает unit + integration tests
  • прогоняет suite последовательно (go test -p 1 ./...), чтобы избежать гонок за общей тестовой БД
  • останавливает контейнер после завершения

Быстрый прогон без Docker

task test-unit

Только integration tests

task test-integration

При необходимости можно переопределить БД:

task test TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/projects_service?sslmode=disable'

CI

В репозитории настроен GitHub Actions pipeline:

  • workflow запускается на push
  • поднимает Postgres service container
  • выполняет task ci

Файл workflow: .github/workflows/ci.yaml

Наблюдаемость и hardening

Что уже есть:

  • request logging через slog
  • X-Request-ID
  • логирование status, response_bytes, duration, user_id, error_category
  • ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout
  • ограничение размера request body

Проверка сценария руками

  1. Запустить сервис:
docker compose up -d
  1. Получить токен администратора:
curl -sS -X POST http://localhost:8000/login \
  -H 'Content-Type: application/json' \
  -d '{"login":"admin","password":"admin123"}'
  1. Получить типы проектов:
curl -sS http://localhost:8000/project/type
  1. Подать заявку:
curl -sS -X POST http://localhost:8000/project/application/external \
  -H 'Content-Type: application/json' \
  -d '{
    "fullName":"Ivan Ivanov",
    "email":"ivan@example.com",
    "phone":"+7 (999) 123-45-67",
    "organisationName":"ACME",
    "organisationUrl":"https://acme.test",
    "projectName":"New Venture",
    "typeId":1,
    "expectedResults":"Launch MVP",
    "isPayed":true,
    "additionalInformation":"Important details"
  }'
  1. Посмотреть список заявок:
curl -sS 'http://localhost:8000/project/application/external/list?active=true' \
  -H 'X-API-TOKEN: <token>'
  1. Принять или отклонить заявку:
curl -sS -X POST http://localhost:8000/project/application/external/1/accept \
  -H 'X-API-TOKEN: <token>'

или

curl -sS -X POST http://localhost:8000/project/application/external/1/reject \
  -H 'Content-Type: application/json' \
  -H 'X-API-TOKEN: <token>' \
  -d '{"reason":"Out of scope"}'

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors