Приветствую друзья, таких гайдов множество, но я решил что лишним она не будет.
Очень важное предисловие, это только первая часть темы, их будет 2-3 в общей сумме, разделить я решил потому что очень много букв, следующая тема выйдет через пару дней буквально, долго ждать не придётся. В каждой статье я буду прикреплять итоговый архив этой части статьи.
А так же ещё один важный момент, я не фронтендер, и за это очень плохо шарю из за этого код фронтенда будет так себе, но рабочий
Но для тех кто всё ещё не понял из названия темы, что тут будет происходить:
Мы с вами в рамках этой темы (может нескольких тем, посмотрим) напишем простенькую админ-панель для ваших проектов
А конкретнее мы с вами разработаем:
- Простую систему авторизации в панели
- Системы генерации ключей
- Работа с базой данных
- АПИ для вашего лаунчера
- Настроим деплой, метрику и всё такое.
В этой же части из всего этого мы разберём:
1. Выбор технологий которые мы будем использовать
2. Создание проекта и описывание его структуры
3. Написание базы данных
4. Написание первых запросов на БД
5. Создание простой авторизации и нарисуем страницу авторизации
Но с чего же нам начать?
А начать надо с архитектуры проекта. Для тех кто не знает, архитектура проекта - это планирование структуры проекта, подбор технологий для использования в продукте и прочее.
Начнем то мы как раз таки с подбора технологий:
1. Язык - Из названия темы тут и так понятно, что мы будем писать на Golang, почему? Потому что этот язык простой в использовании, при этом быстрый при правильном использовании.
2. База данных - Тут я долго думать не стал, выбрал то что больше всего люблю и чаще всего использую, PostgreSQL - Достаточно быстрая и удобная СУБД.
3. Хранилище - Я долго думал, нужно ли тут это нам. И пришёл к выводу что если не объясню я, то ваи никто ещё долго не объяснит об этом ничего. По этому мы возьмём
4. Библиотеки - Так как у нашего проекта вряд ли будет тысячи, сотни тысяч RPS, в качестве веб фреймворка мы возьмём
5. Метрика - Для этого мы возьмём самое известное и удобное Prometheus и Grafana, о том как это настриоть я расскажу чуть попозже
6. Деплой - Ну тут всё просто, будем использовать Docker, а так же gchr.io (Github actions)
И так мы определились с технологиями что мы будем использовать в проекте. Теперь можно приступать к разработке
Я буду использовать Goland от Jetbrains для разработки, но вам буду писать команды для терминала которые будем использовать
Для начала нам надо создать проект в целом. Для этого надо использовать команду:
После того как создали проект, давайте сразу организуем его структуру. С непривычки будет выглядеть сложно и не понятно, но в процессе данной статьи я вам объясню что и как тут устроено. Чтобы не тянуть время, вот так должна выглядеть структура:
Тут всё мы распределили аккуратно и удобно, работать с этим. Теперь нам надо сделать базу данных
Для этого нам надо установить goose, делается это следующей командой:
После того как он установится проверим работоспособность
Вам должно вывести что то вроде этого:
Когда мы понимаем что goose установлен, создадим наши файлы для миграций
Я уточню, что я делаю всё на ОС Linux Mint и команды могут отличаться от ваших
Делаем по порядку, переходим в папку с нашими миграциями:
Дальше нам надо создать файлы миграций, делается это с помощью команды:
Но помимо этого нам нужно создать всё остальное, я лично разделяю файлы миграций на несколько разных, тут просто потому что мне так удобно, так что создадим остальные
В итоге у вас в папке migrations должны появится эти файлы:
Этих таблиц нам должно хватить для минимальной админ панели для вашего чита.
add_users_table.sql - Тут будет таблица с пользователями, которые имеют доступ в админ панель
add_keys_table.sql - Тут будет таблица с ключами, которые пользователи будут использовать для активации
add_product_table.sql - Тут будет таблица с нашими продуктами, для которых мы и будем генерировать ключи
add_ban_table.sql - Тут будет таблица с заблокированными пользователями, по hardware id
Ну а теперь нам надо написать сами таблицы, и начем мы с add_users_table, она будет минимальной
В данном коде мы создаем таблицу users в которой есть поля:
1. id - Уникальный айди пользователя, он будет заполняться самостоятельно при создании пользователя
2. username - Юзернейм пользователя по которому будет происходить логин
3. passsword_hash - Хешированый пароль, его мы будем хранить в bcrypt
Помимо этого мы добавили коммантарии к нашим полям username и password_hash, так будет легче для документации и понимания
Дальше мы создаем нашего первого юзера в с данными admin:admin, тут я захардкодил пароль, лучше так не делать
Всё тут помечено с помощью +goose Up и +goose Down чтобы goose мог отличать что выполнять при командах up и down соответственно
Теперь нам надо разработать таблицу продуктов
Тут думаю не надо объяснять что происходит в таблице, но есть интересный момент, а именно триггер и функция
Так конечно лучше не делать, ибо зачем выносить логику в БД если можно сделать её в коде, но всё же я показываю для вас что так можно
В данном случае я сделал простую функцию, которая будет сама обновлять updated_at при вызове триггера update_products_updated_at. Триггер же будет срабатывать автоматически при каком либо обновлении в нашей таблице. В основном это нужно при обновлении статуса.
Теперь мы можем заполнить нашу таблицу add_keys_table
В данном случае все точно так же понятно должно быть исходя из объяснений прошлых таблиц, но есть пара уточнений. Например тут мы создаем связь записи ключа с записями продуктов, сделан это для того, чтобы если мы удалим продукт, то все связанные с ним ключи так же удаляться. Это надо для того, чтобы не оставлять в БД лишних записей, ибо зачем они нам нужны будут если продукта уже не будет существовать? А так же я добавил тут индекс, который будет полезен при команде типа SELECT * FROM keys WHERE product_id = 1; С этим наш код будет работать чуть быстрее
И осталась нам миграция add_ban_table
Тут все так же просто по самой таблице, а так же есть функция и триггер которая при INSERT в таблицу бана поменяет статус у всех ключей с таким же hardware ID
На этом мы закончили с разработкой базы данных. Возможно в процессе разработки мы её доработаем, но пока что оставим так.
Теперь нам надо перейти к разработке самой админки. И что логично начнем мы с конфига
Для начала, нам нужно установить библиотеку что мы хотим использовать для конфига, о ней я писал в начале статьи:
Дальше мы создаём файл config.go по пути pkg/config/config.go
В нем мы должны написать такой код:
Что мы тут видим? Мы создаем 2 структуры, первая это общий Config оттуда мы получаем доступ к конфигурации БД. А так же создаём PostgresConfig где мы храним все данные о нашей БД, а так же мы создаем функцию относящуюся к PostgresConfig где заполняем строку для подключения к БД, она нам понадобиться в будущем, называется функция DSN или же Data Source Name. При вызове этой функции нам отдаст строку: postgres://postgres:postgress@localhost:5432/yougame-panel?sslmode=disable
Теперь нам надо накидать сам конфиг, пока что он довольно простой
Конфиг нам надо поместить по пути config/config.toml. А его содержимое:
Теперь когда мы сделали конфигурацию для нашей базы данных, давайте создадим подключение к ней в целом. Для этого сначала установим pgx, небольшое уточнение мы будем использовать в коде Connection Pool, это нужно чтобы не надо было закрывать и открывать подключение к БД при новвых запросах.
Переходим непосредственно к установке:
После того как установилось, нам надо по пути internal/infrastructure/db/ создать файл: postgres.go
В нем у нас будет всё для начала работы с нашей БД:
Тут у нас есть 3 метода:
1. Метод который можно назвать конструктором, он у нас собственно и создаёт подключение к базе данных
2. Метод который у нас закрывает подключение к БД, он нам нужен будет для корректного завершения нашего бэкенда
3. Метод Ping нужен для проверки соединения с БД, он нужен нам при инициализации и при каких нибудь сложных запросах чтобы убедиться что база данных работает нормально
Пока что нам хватит этого, запросы к БД мы будем писать в других файлах, но до этого нам ещё далеко
Теперь давайте напишем наш софт для того, чтобы сделать миграцию данных. Это конечно можно сделать с помощью самого goose, в целом то мы и не уходим от его использования, а просто оборачиваем в свой код. Мне лично так удобнее работать, потому что всегда можно продебажить что пошло не так.
По пути cmd/migration создаем файл main.go
Далее устанавливаем библиотеку goose:
Данный софт будет работать с тем же конфигом что и наш основной бэкенд, так как он находится в том же проекте.
Код будет выглядеть у нас так:
Тут всё просто, мы подключаемся к бд, получаем подключение в виде стандартной либы, устанавливаем диалект подключения, и в зависимости от команды (--up или --down) выполняем команду. Up у нас отвечает за поднятие миграций, а Down за то чтоб их удалить / откатить
выполнить команду можно с помощью
После успешного выполнения данной команды вам должно выдать:
Теперь установим gofiber, так как сейчас он нам нужен будет
Тут уже надо написать наш файл инициализации http handler
Будет он по пути internal/delivery/http/http.go
Тут мы просто передаем параметры в нашу структуру Handler. Функция Start которая отвечает за сам запуск HTTP сервера и функция Stop для более корректной остановки сервера. Так же в Register Routes мы добавили простой метод который при гет запросе вернёт нам JSON строку с ответом hello: world
Сейчас нам надо написать инициализацию нашего приложения, создаем файл по пути internal/app/app.go и пишем:
Тут мы инициализируем наше приложение, сначала парсим конфиг. Если всё успешно, то подключаемся к базе данных и делаем пинг для того, чтобы верифицировать что подключение действительно установилось, а после мы вызываем наш конструктор для HttpHandler и передаём ему созданные элементы, а так же регистрируем наши роуты
Функция Start отвечает за запуск нашего веб сервера, а Shutdown для адекватной остановки сервера.
Теперь мы можем написать наш main файл сервера. Создаем его по пути cmd/server/main.go и пишем:
Код мейна у нас вряд ли изменится когда нибудь, так что больше к нему мы не вернёмся, а вот к файлу app.go нам придётся вернуться ещё не пару раз точно
Теперь мы можем запустить наш сервер, напомню что перед тем как запускать вам надо установить PostgreSQL, создать там базу данных и запустить миграцию.
Запускаем сервер командой
При успешном запуске нам выдаст:
Теперь мы можем попробовать постучаться на наш сервер
И при успешном запросе мы сможем увидеть:
Это ответ от нашего роута который мы создали в http.go
Ну а теперь перейдём к финальному, для данной части статьи, моменту. Авторизация
Для этого нам сначала надо установить scany:
После установки начнем писать код, для начала создадим нашу модель, internal/domain/models/user.go:
Эта модель нужна нам, для того чтобы парсить данные из БД в структуру корректно, с помощью scany
После этого, нам надо файл internal/domain/repositories/user_repository.go:
Тут мы создаём интерфейс в котором определяет набор методов (в данный момент один метод), но без их реализации. Он нам нужен будет в наших ручках
Дальше нам надо сделать файл, в котором будет описана реализация данного интерфейса. internal/infrastructure/db/user_repository.go:
Мы создаём структуру UserRepository в которой хранится доступ к нашей БД. После чего передаём её через конструктор. А так же создаём реализацию нашего метода в интерфейсе GetUserByUsername. Мы составляем запрос который получит пользователя с юзернеймом что мы передаём в аргументы, далее мы создаём пустую переменную user которая относится к нашей модели, и вызываем pgxscan.Get который выполнит этот запрос и заполнит нашу структуру. Понятное дело мы проверяем на ошибку, мало ли. После чего возвращаем нашу модель с заполненым юзером который нам нужен.
Теперь нам нужно создать нам dto для юзера по пути: internal/delivery/dto/request/user_request.go:
Это нам нужно для того, чтобы обрабатывать запрос на логин
После того как мы это все сделали, можно написать обработчик запроса на логин. На данный момент он будет мега простой, если тема зайдёт то мы будем расширять весь этот код и делать его более безопасным, но да ладно, создаём файл: internal/delivery/http/user/handlers.go:
У нас есть структура Handler, которая хранит в себе наш репозиторий. Инициализируем всё в конструкторе, и делаем функцию которая будет хендлить наш логин. Мы выделяем память для нашей структуры который мы сделали в dto, после чего мы парсим наш Body и заполняем эту структуру, как только структура заполнена мы вызываем h.repo.GetUserByUsername для того чтобы получить нашего пользователя логин которого получили из Body. Если юзер корректно получается, мы должны сверить пароль который передали в Body с тем что есть у нас в БД в виде хеша. Для этого в Go предусмотрен пакет crypto и в нём bcrypt. Мы вызываем просто bcrypt.CompareHashAndPassword - он проверяет является ли данный пароль и хеш одним и тем же. Если всё хорошо, то мы возвращаем статус ок и наш юзернейм.
Осталось совсем чуть чуть. Возвращаемся к файлу internal/delivery/http/http.go.
В структуру Handler тут нам нужно добавить наш user Handler -
Дальше в конструкторе NewHttpHandler нам нужно добавить инициализацию репозитория и хендлера юзера, а так же заполнить структуру до конца:
После чего в RegisterRoutes мы должны зарегистрировать нашу ручку для того, чтобы была возможность войти в аккаунт:
Я группировал всё это в общую группу /api и подгруппу /user, что в итоге выдаст нам localhost:3000/api/user, дабы отделить апи от остального что нам надо будет.
Теперь мы можем запустить наш сервер. При успешном запуске нам выдаст:
Видим что есть у нас 1 хендлер, это наш /api/user/login. Теперь нам надо это протестить
Отправим через терминал простой запрос:
И в ответ мы должны получить:
Значит всё ок, и наш бэкенд и БД отрабатывают. Это успех. Для тестов вы можете поменять пароль в запросе или же юзернейм, он вам будет отдавать ошибки, которые мы описали в нашем user handler
На этом 1 часть статей завершена, завтра же приступлю к продолжению этой статьи, тут получилось много букв как вы можете заметить, но я очень старался описывать всё максимально подробно. Я так же прикрепил исходник этой части в файлах, так что вы можете просто скачать и потыкать это всё самостоятельно.
Очень важное предисловие, это только первая часть темы, их будет 2-3 в общей сумме, разделить я решил потому что очень много букв, следующая тема выйдет через пару дней буквально, долго ждать не придётся. В каждой статье я буду прикреплять итоговый архив этой части статьи.
А так же ещё один важный момент, я не фронтендер, и за это очень плохо шарю из за этого код фронтенда будет так себе, но рабочий
Но для тех кто всё ещё не понял из названия темы, что тут будет происходить:
Мы с вами в рамках этой темы (может нескольких тем, посмотрим) напишем простенькую админ-панель для ваших проектов
А конкретнее мы с вами разработаем:
- Простую систему авторизации в панели
- Системы генерации ключей
- Работа с базой данных
- АПИ для вашего лаунчера
- Настроим деплой, метрику и всё такое.
В этой же части из всего этого мы разберём:
1. Выбор технологий которые мы будем использовать
2. Создание проекта и описывание его структуры
3. Написание базы данных
4. Написание первых запросов на БД
5. Создание простой авторизации и нарисуем страницу авторизации
Но с чего же нам начать?
А начать надо с архитектуры проекта. Для тех кто не знает, архитектура проекта - это планирование структуры проекта, подбор технологий для использования в продукте и прочее.
Начнем то мы как раз таки с подбора технологий:
1. Язык - Из названия темы тут и так понятно, что мы будем писать на Golang, почему? Потому что этот язык простой в использовании, при этом быстрый при правильном использовании.
2. База данных - Тут я долго думать не стал, выбрал то что больше всего люблю и чаще всего использую, PostgreSQL - Достаточно быстрая и удобная СУБД.
3. Хранилище - Я долго думал, нужно ли тут это нам. И пришёл к выводу что если не объясню я, то ваи никто ещё долго не объяснит об этом ничего. По этому мы возьмём
Пожалуйста, авторизуйтесь для просмотра ссылки.
в качестве объектного хранилища, у него есть совместимость с S3 по этому будет удобно взаимодействовать с ним4. Библиотеки - Так как у нашего проекта вряд ли будет тысячи, сотни тысяч RPS, в качестве веб фреймворка мы возьмём
Пожалуйста, авторизуйтесь для просмотра ссылки.
, у него есть весь нужный нам набор функций, включая SSR. Для базы данных мы возьмём библиотеку
Пожалуйста, авторизуйтесь для просмотра ссылки.
и
Пожалуйста, авторизуйтесь для просмотра ссылки.
, pgx очень удобен для использования особенно вместе со scany, будет очень легко взаимодествовать с БД. Для работы с MinIO мы возьмём их же библиотеку
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Для миграций базы данных будем использовать
Пожалуйста, авторизуйтесь для просмотра ссылки.
, а для конфигурации мы возьмёмь библиотеку
Пожалуйста, авторизуйтесь для просмотра ссылки.
и будем использовать toml5. Метрика - Для этого мы возьмём самое известное и удобное Prometheus и Grafana, о том как это настриоть я расскажу чуть попозже
6. Деплой - Ну тут всё просто, будем использовать Docker, а так же gchr.io (Github actions)
И так мы определились с технологиями что мы будем использовать в проекте. Теперь можно приступать к разработке
Я буду использовать Goland от Jetbrains для разработки, но вам буду писать команды для терминала которые будем использовать
Для начала нам надо создать проект в целом. Для этого надо использовать команду:
go mod init yougame-backend
После того как создали проект, давайте сразу организуем его структуру. С непривычки будет выглядеть сложно и не понятно, но в процессе данной статьи я вам объясню что и как тут устроено. Чтобы не тянуть время, вот так должна выглядеть структура:
Структура проекта:
├── cmd
│ ├── migration
│ └── server
├── internal
│ ├── app
│ ├── delivery
│ │ ├── dto
│ │ └── http
│ ├── domain
│ │ ├── models
│ │ └── repositories
│ └── infrastructure
│ ├── db
│ └── storage
├── migrations
├── config
└── pkg
├── config
└── logger
Тут всё мы распределили аккуратно и удобно, работать с этим. Теперь нам надо сделать базу данных
Для этого нам надо установить goose, делается это следующей командой:
go install github.com/pressly/goose/v3/cmd/goose@latest
После того как он установится проверим работоспособность
goose -version
Вам должно вывести что то вроде этого:
goose version: v3.24.1
Когда мы понимаем что goose установлен, создадим наши файлы для миграций
Я уточню, что я делаю всё на ОС Linux Mint и команды могут отличаться от ваших
Делаем по порядку, переходим в папку с нашими миграциями:
cd migrations
Дальше нам надо создать файлы миграций, делается это с помощью команды:
goose create add_users_table sql
Но помимо этого нам нужно создать всё остальное, я лично разделяю файлы миграций на несколько разных, тут просто потому что мне так удобно, так что создадим остальные
goose create add_keys_table sql
goose create add_product_table sql
goose create add_ban_table sql
В итоге у вас в папке migrations должны появится эти файлы:
Этих таблиц нам должно хватить для минимальной админ панели для вашего чита.
add_users_table.sql - Тут будет таблица с пользователями, которые имеют доступ в админ панель
add_keys_table.sql - Тут будет таблица с ключами, которые пользователи будут использовать для активации
add_product_table.sql - Тут будет таблица с нашими продуктами, для которых мы и будем генерировать ключи
add_ban_table.sql - Тут будет таблица с заблокированными пользователями, по hardware id
Ну а теперь нам надо написать сами таблицы, и начем мы с add_users_table, она будет минимальной
add_users_table migration:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE users(
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password_hash VARCHAR(60) NOT NULL,
);
COMMENT ON COLUMN users.username IS 'Unique username of the user';
COMMENT ON COLUMN users.password_hash IS 'Hashed password using bcrypt';
INSERT INTO users (username, password_hash) VALUES ('admin', '$2a$12$eoTNJ6kFwfujsNTJvRNmvuAFoTZeKXc8m5y9UH1vE50wP2I.Z06pi');
CREATE UNIQUE INDEX CONCURRENTLY users_username_index ON users (username);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE users;
DROP INDEX users_username_index;
-- +goose StatementEnd
В данном коде мы создаем таблицу users в которой есть поля:
1. id - Уникальный айди пользователя, он будет заполняться самостоятельно при создании пользователя
2. username - Юзернейм пользователя по которому будет происходить логин
3. passsword_hash - Хешированый пароль, его мы будем хранить в bcrypt
Помимо этого мы добавили коммантарии к нашим полям username и password_hash, так будет легче для документации и понимания
Дальше мы создаем нашего первого юзера в с данными admin:admin, тут я захардкодил пароль, лучше так не делать
Всё тут помечено с помощью +goose Up и +goose Down чтобы goose мог отличать что выполнять при командах up и down соответственно
Теперь нам надо разработать таблицу продуктов
add_product_table migration:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
process_name VARCHAR(255) NOT NULL,
object_path VARCHAR(255) NOT NULL,
status SMALLINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_products_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TRIGGER IF EXISTS update_products_updated_at ON products;
DROP FUNCTION IF EXISTS update_updated_at_column;
DROP TABLE products;
-- +goose StatementEnd
Тут думаю не надо объяснять что происходит в таблице, но есть интересный момент, а именно триггер и функция
Так конечно лучше не делать, ибо зачем выносить логику в БД если можно сделать её в коде, но всё же я показываю для вас что так можно
В данном случае я сделал простую функцию, которая будет сама обновлять updated_at при вызове триггера update_products_updated_at. Триггер же будет срабатывать автоматически при каком либо обновлении в нашей таблице. В основном это нужно при обновлении статуса.
Теперь мы можем заполнить нашу таблицу add_keys_table
add_keys_table migration:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE keys (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
product_id INTEGER NOT NULL,
status SMALLINT NOT NULL,
hardware_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT keys_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE CASCADE
);
CREATE OR REPLACE FUNCTION update_keys_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_keys_updated_at
BEFORE UPDATE ON keys
FOR EACH ROW
EXECUTE FUNCTION update_keys_updated_at_column();
CREATE INDEX idx_keys_product_id ON keys(product_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS idx_keys_product_id;
DROP TRIGGER IF EXISTS update_keys_updated_at ON keys;
DROP FUNCTION IF EXISTS update_keys_updated_at_column;
DROP TABLE keys;
-- +goose StatementEnd
В данном случае все точно так же понятно должно быть исходя из объяснений прошлых таблиц, но есть пара уточнений. Например тут мы создаем связь записи ключа с записями продуктов, сделан это для того, чтобы если мы удалим продукт, то все связанные с ним ключи так же удаляться. Это надо для того, чтобы не оставлять в БД лишних записей, ибо зачем они нам нужны будут если продукта уже не будет существовать? А так же я добавил тут индекс, который будет полезен при команде типа SELECT * FROM keys WHERE product_id = 1; С этим наш код будет работать чуть быстрее
И осталась нам миграция add_ban_table
add_ban_table migration:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE ban (
id SERIAL PRIMARY KEY,
hardware_id TEXT NOT NULL UNIQUE,
reason TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE OR REPLACE FUNCTION update_keys_status_on_ban()
RETURNS TRIGGER AS $$
BEGIN
UPDATE keys
SET status = 3
WHERE hardware_id = NEW.hardware_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_keys_status_on_ban
AFTER INSERT ON ban
FOR EACH ROW
EXECUTE FUNCTION update_keys_status_on_ban();
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TRIGGER IF EXISTS trigger_update_keys_status_on_ban ON ban;
DROP FUNCTION IF EXISTS update_keys_status_on_ban;
DROP TABLE ban;
-- +goose StatementEnd
Тут все так же просто по самой таблице, а так же есть функция и триггер которая при INSERT в таблицу бана поменяет статус у всех ключей с таким же hardware ID
На этом мы закончили с разработкой базы данных. Возможно в процессе разработки мы её доработаем, но пока что оставим так.
Теперь нам надо перейти к разработке самой админки. И что логично начнем мы с конфига
Для начала, нам нужно установить библиотеку что мы хотим использовать для конфига, о ней я писал в начале статьи:
go get github.com/spf13/viper
Дальше мы создаём файл config.go по пути pkg/config/config.go
В нем мы должны написать такой код:
config.go:
package config
import (
"fmt"
"github.com/spf13/viper"
)
type Config struct {
Postgres PostgresConfig
}
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
}
func (cfg PostgresConfig) DSN() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
}
func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath("./config")
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, err
}
return &config, nil
}
Что мы тут видим? Мы создаем 2 структуры, первая это общий Config оттуда мы получаем доступ к конфигурации БД. А так же создаём PostgresConfig где мы храним все данные о нашей БД, а так же мы создаем функцию относящуюся к PostgresConfig где заполняем строку для подключения к БД, она нам понадобиться в будущем, называется функция DSN или же Data Source Name. При вызове этой функции нам отдаст строку: postgres://postgres:postgress@localhost:5432/yougame-panel?sslmode=disable
Теперь нам надо накидать сам конфиг, пока что он довольно простой
Конфиг нам надо поместить по пути config/config.toml. А его содержимое:
config.toml:
[postgres]
host = "localhost"
port = 5432
user = "postgres"
password = "postgress"
database = "yougame-panel"
Теперь когда мы сделали конфигурацию для нашей базы данных, давайте создадим подключение к ней в целом. Для этого сначала установим pgx, небольшое уточнение мы будем использовать в коде Connection Pool, это нужно чтобы не надо было закрывать и открывать подключение к БД при новвых запросах.
Переходим непосредственно к установке:
go get github.com/jackc/pgx/v5/pgxpool
После того как установилось, нам надо по пути internal/infrastructure/db/ создать файл: postgres.go
В нем у нас будет всё для начала работы с нашей БД:
postgres.go:
package db
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
"yougame-backend/pkg/config"
)
type Postgres struct {
conn *pgxpool.Pool
}
func NewPostgres(ctx context.Context, cfg *config.PostgresConfig) (*Postgres, error) {
poolConfig, err := pgxpool.ParseConfig(cfg.DSN())
if err != nil {
return nil, err
}
conn, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return nil, err
}
return &Postgres{conn: conn}, nil
}
func (p *Postgres) Close() {
p.conn.Close()
}
func (p *Postgres) Ping(ctx context.Context) error {
return p.conn.Ping(ctx)
}
Тут у нас есть 3 метода:
1. Метод который можно назвать конструктором, он у нас собственно и создаёт подключение к базе данных
2. Метод который у нас закрывает подключение к БД, он нам нужен будет для корректного завершения нашего бэкенда
3. Метод Ping нужен для проверки соединения с БД, он нужен нам при инициализации и при каких нибудь сложных запросах чтобы убедиться что база данных работает нормально
Пока что нам хватит этого, запросы к БД мы будем писать в других файлах, но до этого нам ещё далеко
Теперь давайте напишем наш софт для того, чтобы сделать миграцию данных. Это конечно можно сделать с помощью самого goose, в целом то мы и не уходим от его использования, а просто оборачиваем в свой код. Мне лично так удобнее работать, потому что всегда можно продебажить что пошло не так.
По пути cmd/migration создаем файл main.go
Далее устанавливаем библиотеку goose:
go get github.com/pressly/goose/v3
Данный софт будет работать с тем же конфигом что и наш основной бэкенд, так как он находится в том же проекте.
Код будет выглядеть у нас так:
migration/main.go:
package main
import (
"context"
"flag"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
"os"
"yougame-backend/pkg/config"
)
func main() {
up := flag.Bool("up", false, "Run migrations up")
down := flag.Bool("down", false, "Run migrations down")
flag.Parse()
cfg, err := config.Load()
if err != nil {
panic(err)
}
if *up && *down {
panic("cannot run both --up and --down migrations")
}
poolConfig, err := pgxpool.ParseConfig(cfg.Postgres.DSN())
pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig)
if err != nil {
panic("failed to connect to database: " + err.Error())
}
defer pool.Close()
db := stdlib.OpenDBFromPool(pool)
if err := goose.SetDialect("postgres"); err != nil {
panic("failed to set dialect: " + err.Error())
}
migrationsPath := "./migrations"
var migrateErr error
if *up {
migrateErr = goose.Up(db, migrationsPath)
} else if *down {
migrateErr = goose.Down(db, migrationsPath)
} else {
fmt.Println("migration failed: no command specified")
os.Exit(1)
}
if migrateErr != nil {
fmt.Println("Migration failed: ", migrateErr)
} else {
fmt.Println("Migrations applied successfully")
}
}
Тут всё просто, мы подключаемся к бд, получаем подключение в виде стандартной либы, устанавливаем диалект подключения, и в зависимости от команды (--up или --down) выполняем команду. Up у нас отвечает за поднятие миграций, а Down за то чтоб их удалить / откатить
выполнить команду можно с помощью
go run cmd/migration/main.go --up
После успешного выполнения данной команды вам должно выдать:
результат выполнения:
$ go run cmd/migration/main.go --up
2025/06/26 21:38:31 OK 20250626133026_add_users_table.sql (15.62ms)
2025/06/26 21:38:31 OK 20250626133208_add_product_table.sql (15.71ms)
2025/06/26 21:38:31 OK 20250626133209_add_keys_table.sql (25.21ms)
2025/06/26 21:38:31 OK 20250626133217_add_ban_table.sql (18.46ms)
2025/06/26 21:38:31 goose: successfully migrated database to version: 20250626133217
Migrations applied successfully
Теперь установим gofiber, так как сейчас он нам нужен будет
go get -u github.com/gofiber/fiber/v2
Тут уже надо написать наш файл инициализации http handler
Будет он по пути internal/delivery/http/http.go
http.go:
package http
import (
"github.com/gofiber/fiber/v2"
"yougame-backend/internal/infrastructure/db"
"yougame-backend/pkg/config"
)
type Handler struct {
Fiber *fiber.App
db *db.Postgres
cfg *config.Config
}
func NewHttpHandler(db *db.Postgres, cfg *config.Config) *Handler {
app := fiber.New(fiber.Config{})
return &Handler{
Fiber: app,
db: db,
cfg: cfg,
}
}
func (h *Handler) Start() error {
return h.Fiber.Listen(":3000")
}
func (h *Handler) Stop() error {
return h.Fiber.Shutdown()
}
func (h *Handler) RegisterRoutes() {
h.Fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"hello": "world"})
})
}
Тут мы просто передаем параметры в нашу структуру Handler. Функция Start которая отвечает за сам запуск HTTP сервера и функция Stop для более корректной остановки сервера. Так же в Register Routes мы добавили простой метод который при гет запросе вернёт нам JSON строку с ответом hello: world
Сейчас нам надо написать инициализацию нашего приложения, создаем файл по пути internal/app/app.go и пишем:
internal/app/app.go:
package app
import (
"context"
"yougame-backend/internal/delivery/http"
"yougame-backend/internal/infrastructure/db"
"yougame-backend/pkg/config"
)
type App struct {
HttpHandler *http.Handler
DB *db.Postgres
Cfg *config.Config
}
func New(ctx context.Context) (*App, error) {
cfg, err := config.Load()
if err != nil {
return nil, err
}
postgres, err := db.NewPostgres(ctx, &cfg.Postgres)
if err != nil {
return nil, err
}
err = postgres.Ping(ctx)
if err != nil {
return nil, err
}
httpHandler := http.NewHttpHandler(postgres, cfg)
httpHandler.RegisterRoutes()
return &App{
HttpHandler: httpHandler,
DB: postgres,
Cfg: cfg,
}, nil
}
func (app *App) Start() error {
return app.HttpHandler.Start()
}
func (app *App) Shutdown() error {
app.DB.Close()
return app.HttpHandler.Stop()
}
Тут мы инициализируем наше приложение, сначала парсим конфиг. Если всё успешно, то подключаемся к базе данных и делаем пинг для того, чтобы верифицировать что подключение действительно установилось, а после мы вызываем наш конструктор для HttpHandler и передаём ему созданные элементы, а так же регистрируем наши роуты
Функция Start отвечает за запуск нашего веб сервера, а Shutdown для адекватной остановки сервера.
Теперь мы можем написать наш main файл сервера. Создаем его по пути cmd/server/main.go и пишем:
cmd/server/main.go:
package main
import (
"context"
"fmt"
"yougame-backend/internal/app"
)
func main() {
application, err := app.New(context.Background())
if err != nil {
panic(err)
}
defer func(application *app.App) {
err := application.Shutdown()
if err != nil {
fmt.Println("Failed to shutdown application: ", err)
}
}(application)
if err := application.Start(); err != nil {
panic(err)
}
}
Код мейна у нас вряд ли изменится когда нибудь, так что больше к нему мы не вернёмся, а вот к файлу app.go нам придётся вернуться ещё не пару раз точно
Теперь мы можем запустить наш сервер, напомню что перед тем как запускать вам надо установить PostgreSQL, создать там базу данных и запустить миграцию.
Запускаем сервер командой
go run cmd/server/main.go
При успешном запуске нам выдаст:
Теперь мы можем попробовать постучаться на наш сервер
Пожалуйста, авторизуйтесь для просмотра ссылки.
И при успешном запросе мы сможем увидеть:
Это ответ от нашего роута который мы создали в http.go
Ну а теперь перейдём к финальному, для данной части статьи, моменту. Авторизация
Для этого нам сначала надо установить scany:
go get github.com/georgysavva/scany/v2/pgxscan
После установки начнем писать код, для начала создадим нашу модель, internal/domain/models/user.go:
internal/domain/models/user.go:
package models
type User struct {
ID int `db:"id"`
Username string `db:"username"`
PasswordHash string `db:"password_hash"`
}
После этого, нам надо файл internal/domain/repositories/user_repository.go:
Код:
package repositories
import (
"context"
"yougame-backend/internal/domain/models"
)
type UserRepository interface {
GetUserByUsername(ctx context.Context, username string) (*models.User, error)
}
Тут мы создаём интерфейс в котором определяет набор методов (в данный момент один метод), но без их реализации. Он нам нужен будет в наших ручках
Дальше нам надо сделать файл, в котором будет описана реализация данного интерфейса. internal/infrastructure/db/user_repository.go:
internal/infrastructure/db/user_repository.go:
package db
import (
"context"
"fmt"
"github.com/georgysavva/scany/v2/pgxscan"
"yougame-backend/internal/domain/models"
"yougame-backend/internal/domain/repositories"
)
type UserRepository struct {
postgres *Postgres
}
func NewUserRepository(postgres *Postgres) repositories.UserRepository {
return &UserRepository{postgres: postgres}
}
func (p *UserRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
query := `
SELECT * FROM users
WHERE username = $1
`
var user models.User
err := pgxscan.Get(ctx, p.postgres.Pgpool, &user, query, username)
if err != nil {
// TODO: Log error
fmt.Println("GetUserByUsername", err)
return nil, err
}
return &user, nil
}
Мы создаём структуру UserRepository в которой хранится доступ к нашей БД. После чего передаём её через конструктор. А так же создаём реализацию нашего метода в интерфейсе GetUserByUsername. Мы составляем запрос который получит пользователя с юзернеймом что мы передаём в аргументы, далее мы создаём пустую переменную user которая относится к нашей модели, и вызываем pgxscan.Get который выполнит этот запрос и заполнит нашу структуру. Понятное дело мы проверяем на ошибку, мало ли. После чего возвращаем нашу модель с заполненым юзером который нам нужен.
Теперь нам нужно создать нам dto для юзера по пути: internal/delivery/dto/request/user_request.go:
internal/delivery/dto/request/user_request.go:
package request
type LoginUserRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
Это нам нужно для того, чтобы обрабатывать запрос на логин
После того как мы это все сделали, можно написать обработчик запроса на логин. На данный момент он будет мега простой, если тема зайдёт то мы будем расширять весь этот код и делать его более безопасным, но да ладно, создаём файл: internal/delivery/http/user/handlers.go:
internal/delivery/http/user/handlers.go:
package user
import (
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"yougame-backend/internal/delivery/dto/request"
"yougame-backend/internal/domain/repositories"
)
type Handler struct {
repo repositories.UserRepository
}
func NewUserHandler(repo repositories.UserRepository) *Handler {
return &Handler{repo: repo}
}
func (h *Handler) LoginUser(c *fiber.Ctx) error {
req := new(request.LoginUserRequest)
if err := c.BodyParser(req); err != nil {
return c.Status(fiber.StatusBadRequest).
JSON(fiber.Map{
"error": "Invalid request body",
})
}
// Get user info from database by username
user, err := h.repo.GetUserByUsername(c.Context(), req.Username)
if err != nil {
return c.Status(fiber.StatusNotFound).
JSON(fiber.Map{
"error": "User not found",
})
}
// Compare user password with hash password in database
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil {
return c.Status(fiber.StatusBadRequest).
JSON(fiber.Map{
"error": "Invalid password",
})
}
return c.Status(fiber.StatusOK).
JSON(fiber.Map{
"status": "ok",
"user": user.Username,
})
}
У нас есть структура Handler, которая хранит в себе наш репозиторий. Инициализируем всё в конструкторе, и делаем функцию которая будет хендлить наш логин. Мы выделяем память для нашей структуры который мы сделали в dto, после чего мы парсим наш Body и заполняем эту структуру, как только структура заполнена мы вызываем h.repo.GetUserByUsername для того чтобы получить нашего пользователя логин которого получили из Body. Если юзер корректно получается, мы должны сверить пароль который передали в Body с тем что есть у нас в БД в виде хеша. Для этого в Go предусмотрен пакет crypto и в нём bcrypt. Мы вызываем просто bcrypt.CompareHashAndPassword - он проверяет является ли данный пароль и хеш одним и тем же. Если всё хорошо, то мы возвращаем статус ок и наш юзернейм.
Осталось совсем чуть чуть. Возвращаемся к файлу internal/delivery/http/http.go.
В структуру Handler тут нам нужно добавить наш user Handler -
internal/delivery/http/http.go Handler struct:
type Handler struct {
Fiber *fiber.App
db *db.Postgres
cfg *config.Config
// Handlers
userHandler *user.Handler
}
Дальше в конструкторе NewHttpHandler нам нужно добавить инициализацию репозитория и хендлера юзера, а так же заполнить структуру до конца:
internal/delivery/http/http.go NewHttpHandler:
func NewHttpHandler(postgres *db.Postgres, cfg *config.Config) *Handler {
app := fiber.New(fiber.Config{})
userRepo := db.NewUserRepository(postgres)
userHandler := user.NewUserHandler(userRepo)
return &Handler{
Fiber: app,
db: postgres,
cfg: cfg,
// Handlers
userHandler: userHandler,
}
}
После чего в RegisterRoutes мы должны зарегистрировать нашу ручку для того, чтобы была возможность войти в аккаунт:
internal/delivery/http/http.go:
func (h *Handler) RegisterRoutes() {
api := h.Fiber.Group("/api")
userApi := api.Group("/user")
userApi.Post("/login", h.userHandler.LoginUser)
}
Я группировал всё это в общую группу /api и подгруппу /user, что в итоге выдаст нам localhost:3000/api/user, дабы отделить апи от остального что нам надо будет.
Теперь мы можем запустить наш сервер. При успешном запуске нам выдаст:
Видим что есть у нас 1 хендлер, это наш /api/user/login. Теперь нам надо это протестить
Отправим через терминал простой запрос:
$ curl -X POST -H "Content-Type: application/json" http://localhost:3000/api/user/login -d '{"username": "admin", "password": "admin"}'
И в ответ мы должны получить:
{"status":"ok","user":"admin"}
Значит всё ок, и наш бэкенд и БД отрабатывают. Это успех. Для тестов вы можете поменять пароль в запросе или же юзернейм, он вам будет отдавать ошибки, которые мы описали в нашем user handler
На этом 1 часть статей завершена, завтра же приступлю к продолжению этой статьи, тут получилось много букв как вы можете заметить, но я очень старался описывать всё максимально подробно. Я так же прикрепил исходник этой части в файлах, так что вы можете просто скачать и потыкать это всё самостоятельно.