An open API service indexing awesome lists of open source software.

https://github.com/aventhis/practice_avito

Разбираю тестовые задания авито
https://github.com/aventhis/practice_avito

Last synced: about 1 year ago
JSON representation

Разбираю тестовые задания авито

Awesome Lists containing this project

README

          

# 🚀 Пошаговое README: Как я строила инфраструктуру проекта

Этот README не стандартный. Это **моя инженерная хроника** — чтобы я сама (и любой, кто будет смотреть проект) могла понять:

* как я пришла к текущему решению,
* что я сделала в какой момент,
* и почему это имеет смысл.

---

## ✅ Шаг 1. Старт проекта

* Прочитала задание от Авито.
* Поняла, что сервис будет состоять минимум из двух компонентов:

* backend-приложение на Go;
* база данных PostgreSQL.
* Приняла решение использовать Docker и Docker Compose.

---

## ✅ Шаг 2. Написание Dockerfile (multi-stage)

* Использовала **двухэтапную сборку** для оптимизации:

* Сначала сборка Go-приложения в `golang:1.21-alpine`;
* Затем перенос только бинарника в `alpine:latest`.
* Установила `CGO_ENABLED=0` и `-ldflags="-s -w"`, чтобы сделать бинарник лёгким и безопасным.
* Настроила `WORKDIR`, `COPY`, `RUN go build`, `CMD`, `EXPOSE`.
* Проверила, что Dockerfile рабочий и адекватно собирается.

---

## ✅ Шаг 3. Составление docker-compose.yml

* Указала версию схемы: `version: "3.9"`.
* Добавила два сервиса:

* `app`: билд из Dockerfile, порт 8080, `depends_on`, `restart`, `environment` с переменными из `.env`;
* `db`: образ `postgres:14`, переменные среды, порт 5432, volume, `restart`, `healthcheck`.

---

## ✅ Шаг 4. Обсуждение стратегии запуска

* Поняла, что обычный `depends_on` **не ждёт готовности базы**.
* Добавила `healthcheck` для `postgres`, чтобы Docker понимал, когда он готов.
* Изучила, что можно использовать `depends_on: condition: service_healthy`, и **осознанно приняла решение его добавить** — для надёжного запуска `app` после `postgres`.

---

## ✅ Шаг 5. Подключение .env файла

* Создала файл `.env`, чтобы не хранить чувствительные данные в `docker-compose.yml`.
* Вынесла туда:

* `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_PORT`, `DB_HOST`, `DB_SSL_MODE`
* `APP_ENV`, `APP_PORT`, `GRPC_PORT`, `METRICS_PORT`
* Убедилась, что Docker Compose подхватывает `.env` автоматически.
* Добавила `.env` в `.gitignore`.

---

## ✅ Шаг 6. Отказ от DB\_URL в пользу отдельных переменных

* Решила не использовать `DB_URL` напрямую, чтобы избежать дублирования и повысить гибкость.
* Использую отдельные переменные окружения и собираю строку подключения в Go-коде с помощью `os.Getenv()`.
* Это упрощает поддержку и переключение между окружениями.

---

## ✅ Шаг 7. Добавление APP\_ENV и системных переменных

* Добавила `APP_ENV`, чтобы в будущем различать `development`, `staging`, `production` режимы.
* Подготовила `APP_PORT`, `GRPC_PORT`, `METRICS_PORT` — на случай, если потребуется раздельная маршрутизация.
* В коде можно использовать `os.Getenv("APP_ENV")` для смены логики поведения.

---

## ✅ Шаг 8. Проброс портов через .env

* В секции `ports:` использую переменные `${APP_PORT}`, `${GRPC_PORT}`, `${METRICS_PORT}` вместо жёстко заданных чисел.
* Это позволяет быстро переключать порты через `.env`, без правок в `docker-compose.yml`.
* Переменные также передаются в `environment`, чтобы Go-приложение знало, на каких портах слушать.

---

## ✅ Шаг 9. Создание .env.example для проверяющего

* Чтобы проверяющий мог легко запустить проект, создала файл `.env.example` с безопасными значениями по умолчанию.
* Он лежит в репозитории, и его можно скопировать в `.env` с помощью команды:

```bash
cp .env.example .env
```
* Это позволяет запускать проект без утечки чувствительных данных и без лишних шагов.

---

## ✅ Шаг 10. Начало main.go — конфигурация

* Начала реализацию проекта с `main.go`, как с точки входа и дирижёра системы.
* Задала переменные окружения с помощью `os.Getenv` + функция `getEnv` с дефолтами.
* Включила: `APP_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_PORT`, `DB_HOST`, `DB_SSL_MODE`.
* Добавила `JWT_SECRET` — с защитой: если не задан, выводится предупреждение и используется `dev-secret-key`.
* Проверила и логично оформила преобразования (например, `strconv.Atoi` для портов).

---

## ✅ Шаг 11. Вынос конфигурации в отдельный пакет config

* Создала отдельный модуль `config`, который инкапсулирует работу с переменными окружения.
* Разделила структуру на два блока: `DatabaseConfig` и `ServerConfig`, вложенные в `Config`.
* Добавила генерацию строки подключения `DSN` на этапе загрузки конфига.
* Обработала `JWT_SECRET` с предупреждением, если он не задан.
* В `main.go` подключаю конфиг с помощью `config.LoadConfig()` и передаю `cfg.Database.DSN` в `storage.NewStorage`.
* Привела переменные к единому стилю и исправила возможные ошибки (русская `с`, `%v` в `log.Fatalf`).

---

## ✅ Шаг 12. Проверка готовности базы данных

* После создания `storage` вызываю `storage.Ping()` для проверки соединения с базой.
* Если база недоступна, приложение логирует ошибку и завершает работу.
* Затем вызываю `storage.InitDB()` для инициализации таблиц и подготовки схемы базы данных.
* Таким образом, на этапе запуска программы база всегда проверяется и готовится к работе.

---

## ✅ Шаг 13. Создание структуры Storage

* В `internal/storage/storage.go` создала структуру `Storage` с полем `db *sql.DB`.
* Реализовала конструктор `NewStorage(DSN string)`, который открывает соединение с базой и сразу проверяет его через `Ping()`.
* Подключение теперь полностью централизовано через `storage`.

---

## ✅ Шаг 14. Реализация метода InitDB

* Внутри `Storage` реализовала метод `InitDB()`.
* При старте приложения автоматически создаётся таблица `users`, если она отсутствует.
* Таблица включает в себя:

* UUID ID;
* Email;
* Хэш пароля;
* Роль (`employee`, `moderator`) с проверкой через `CHECK`;
* Время создания записи.
* Запрос максимально адаптирован для запуска без дополнительных расширений PostgreSQL.

---

## ✅ Шаг 15. Создание модели User

* В `internal/models/models.go` создала структуру `User`.
* В неё входят поля: `ID`, `Email`, `PasswordHash`, `Role`, `CreatedAt`.
* Используются только `db:"..."` теги для корректной работы с базой данных.
* Структура соответствует требованиям задания и правильной архитектуре: хранит только внутренние данные.

---

## ✅ Шаг 16. Реализация dummyLoginHandler

* Добавила в `internal/api/api.go` хендлер `dummyLoginHandler`.
* Создала вспомогательную структуру `DummyLoginRequest` в `models`, с единственным полем `Role`.
* В случае невалидного JSON или недопустимой роли — возвращается ошибка с соответствующим сообщением.
* Ответы стандартизированы: структура `ErrorResponse` в `models` и функция `respondWithError()` в `api`.
* Все тексты ошибок вынесены в отдельный файл `errors.go`, что упрощает поддержку и локализацию.
* Статус `200 OK` теперь устанавливается явно через `WriteHeader`.
* Возвращается структура `TokenResponse`, содержащая JWT-токен.

---

## ✅ Шаг 17. Реализация GenerateToken

* Внутри `auth.go` реализовала метод `GenerateToken(role Role)`.
* Создала структуру `CustomClaims`, в которую входят поле `Role` и встраиваются `jwt.RegisteredClaims`.
* В `RegisteredClaims` задала поля `IssuedAt` и `ExpiresAt` — токен действует 24 часа.
* Использовала `jwt.NewWithClaims(...)` и `token.SignedString(...)`, чтобы создать и подписать токен.
* Подпись выполняется с помощью `jwtSecret`, передаваемого в `AuthService`.
* В итоге возвращается строка-токен, которую отправляем клиенту в `dummyLoginHandler`.

---

## ✅ Шаг 18. Проверка dummyLogin через Postman

* Создала запрос POST `/dummyLogin` с телом `{ "role": "moderator" }`.
* Получила успешный ответ со статусом `200 OK` и валидным JWT токеном в поле `token`.
* Убедилась, что токен содержит корректные claims и работает как ожидается.
* Это значит, что связка `API → AuthService → JWT` функционирует полностью.

---

## 💬 Зачем всё это?

* Чтобы проект был:

* легко переносимым;
* быстро разворачиваемым одной командой;
* расширяемым (можно будет добавить Prometheus, pgAdmin и т.д.);
* и показывал мой уровень инженерного мышления.