Categorygithub.com/Country7/backend-webwizards
modulepackage
0.0.0-20240414175155-e50f9becba1d
Repository: https://github.com/country7/backend-webwizards.git
Documentation: pkg.go.dev

# README

I. Работа с базой данных

1. Cхема БД и SQL-код

diagram.io

->  Go to App
    Export PostgreSQL

doc/db.dbml doc/schema.sql



2. Docker Postgres

$ docker ps    // список всех запущенных контейнеров
$ docker images   // список всех имеющихся образов

Удалить установленный Postgres

Uninstall the PostgreSQL application
$ sudo apt-get --purge remove postgresql
Remove PostgreSQL packages
$ dpkg -l | grep postgres
To uninstall PostgreSQL completely, you need to remove all of these packages using the following command:
$ sudo apt-get --purge remove <package_name>
Remove PostgreSQL directories
$ sudo rm -rf /var/lib/postgresql/
$ sudo rm -rf /var/log/postgresql/
$ sudo rm -rf /etc/postgresql/
Remove the postgres user
$ sudo deluser postgres
Verify uninstallation
$ psql --version

Скачать образ

hub.docker.com
поиск postgres
https://hub.docker.com/_/postgres

docker pull <image>:<tag>
$ docker pull postgres:16-alpine

Запуск контейнера из образа

docker run --name <container_name> -e <environment_variable> -d <image>:<tag>

Environment Variables:

  • POSTGRES_PASSWORD
  • POSTGRES_USER
  • POSTGRES_DB
  • POSTGRES_INITDB_ARGS
  • POSTGRES_INITDB_WALDIR
  • POSTGRES_HOST_AUTH_METHOD
  • PGDATA

For example:

$ docker run -d \
    --name some-postgres \
    -e POSTGRES_PASSWORD=mysecretpassword \
    -e PGDATA=/var/lib/postgresql/data/pgdata \
    -v /custom/mount:/var/lib/postgresql/data \
    postgres

пароли можно подгружать из файла:

$ docker run --name some-postgres -e POSTGRES_PASSWORD_FILE=/run/secrets/postgres-passwd -d postgres

Port mapping

docker run --name ‹container_name> -e ‹environment_variable> -p ‹host_ports:container_ports> -d ‹image>:<tag>

$ docker run --name postgres16 -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:16-alpine
$ docker ps

$ docker stop postgres16
$ docker ps -a   // все контейнеры вне зависимости запущены или нет
$ docker start postgres16   // снова запустить имеющийся контейнер
$ docker rm postgres16   // удалить полностью имеющийся контейнер

Запуск команды в контейнере

docker exec -it ‹container _name_or_id> ‹command> [args]

$ docker exec -it postgres16 psql -U root
    select now();
    \q    - выход

$ docker exec -it postgres16 /bin/sh     // запускаем оболочку в контейнере

Просмотр логов контейнера

docker logs <container_name_or_id>
$ docker logs postgres16



TablePlus

Для kubuntu лучше либо pgAdmin4 либо DBeaver

Для mac - tableplus.com

basename: root
user: root
password: secret
url: localhost:5432



3. Миграции

github.com/golang-migrate/migrate

$ brew install golang-migrate
$ migrate -version
    v4.17.0
$ migrate -help

$ migrate create -ext sql -dir db/migration -seq init schema


$ docker exec -it postgres16 /bin/sh
    # createdb -username=root -owner=root main_db
    # psql main_db
    # dropdb main_db
    # exit

$ docker exec postgres16 createdb --username=root --owner=root main_db
$ docker exec -it postgres12 psql -U root main_db
    \q

Миграция в проекте

$ set -e
$ source ./app.env
// если нет базы  $ docker exec postgres16 createdb --username=root --owner=root main_db
$ migrate -path ./db/migration -database "$DB_SOURCE" -verbose up

Cоздаем Makefile

run-postgres: ## Start postgresql database docker image.
    docker run --name postgres16 -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:16-alpine

start-postgres16: ## Start available postgresql database docker container.
    docker start postgres16

stop-postgres: ## Stop postgresql database docker image.
    docker stop postgres16


4. CRUD

  • Create
  • Read
  • Update
  • Delete

DATABASE/SQL

  • Очень быстро и просто
  • Ручное сопоставление полей SQL с переменными
  • Легко допускать ошибки, которые не обнаруживаются до выполнения

GORM

  • Функции CRUD уже реализованы, очень короткий рабочий код
  • Необходимо научиться писать запросы с использованием функции gorm
  • Выполняется медленно при высокой нагрузке
  • В 3 - 5 раз медленнее работает

SQLX

  • Довольно быстрый и простой в использовании
  • Сопоставление полей с помощью тегов текста запроса и структуры
  • Сбой не произойдет до времени выполнения

SQLC

  • Очень быстрый и простой в использовании
  • Автоматическая генерация кода
  • Отслеживание ошибок запроса SQL перед генерацией кодов
  • Полная поддержка Postgres. MySQL является экспериментальным

sqlc.dev
github.com/sqlc-dev/sqlc

$ brew install sqlc
$ sqlc version
$ sqlc help
$ sqlc init

sqlc.yaml
docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html#setting-up

$ sqlc generate

db/query/account.sql
docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html#schema-and-queries

5. Тесты

_ "github.com/lib/pq"  // без драйвера работать не будет

$ go test -v   // все тесты

$ go test -timeout 30s ./db/sqlc -run ^TestMain$             
    ok  	github.com/Country7/backend-webwizards/db/sqlc	0.433s [no tests to run]

$ make test   // команда test из файла Makefile


6. Транзакции

Перевод 10 USD из банка аккаунта 1 в банк аккаунта 2:

  1. Создайте запись транзакции о переводе с суммой = 10
  2. Создайте учетную запись для учетной записи 1 с суммой = -10
  3. Создайте учетную запись для учетной записи 2 с суммой = +10
  4. Вычтите 10 из баланса учетной записи 1
  5. Добавьте 10 к балансу учетной записи 2

  • BEGIN
  • ...
  • COMMIT or
  • BEGIN
  • ...
  • ROLLBACK

    type Store interface {
        Querier
        TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error)
    }

    // SQLStore provides all functions to execute SQL queries and transactions.
    type SQLStore struct {
        *Queries
        db *sql.DB
    }

    func NewStore(db *sql.DB) Store {
        return &SQLStore{db: db, Queries: New(db)}
    }

    // execTx executes a function within a database transaction.
    func (s *SQLStore) execTx(ctx context.Context, fn func(queries *Queries) error) error {
        tx, err := s.db.BeginTx(ctx, nil)
        if err != nil {
            return err
        }

        q := New(tx)
        err = fn(q)
        if err != nil {
            if rbErr := tx.Rollback(); rbErr != nil {
                return fmt.Errorf("tx err: %v, rb err: %v", err, rbErr)
            }
            return err
        }

        return tx.Commit()
    }


7. Блокировка транзакции

BEGIN;

SELECT * FROM accounts WHERE id = 1;

SELECT * FROM WHERE id = 1 FOR UPDATE;     // блокировка запросов
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;

$ sqlc generate

Deadlock detected

INSERT INTO entries (account_id, amount) VALUES ($1, $2) RETURNING *;

и

SELECT * FROM accounts WHERE id = $1 LIMIT 1 FOR UPDATE;

заблокируют друг друга (Deadlock detected) несмотря на то, что обращение идет к разным таблицам

Эти две таблицы имеют связи FOREIGN KEY:
ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
при обращении к accounts происходит обновление ключа id в таблице accounts по связям с entries
чтобы этого не происходило необходима команда !!! NO KEY UPDATE

SELECT * FROM accounts WHERE id = $1 LIMIT 1 FOR NO KEY UPDATE;



8. Взаимоблокировки

    BEGIN:
    UPDATE accounts SET balance = balance - 10 WHERE id = 1 RETURNING *;
    UPDATE accounts SET balance = balance + 10 WHERE id = 2 RETURNING *;
    ROLLBACK;

    BEGIN:
    UPDATE accounts SET balance = balance - 10 WHERE id = 2 RETURNING *;
    UPDATE accounts SET balance = balance + 10 WHERE id = 1 RETURNING *;
    ROLLBACK;

Одновременно две эти транзакции приведут в взаимоблокировке (Deadlock detected)

    if arg.FromAccountID < arg.ToAccountID {
        result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount)
    } else {
        result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount)
    }


9. Уровень изоляции транзакций

  1. Чтение незафиксированных транзакций (read uncommitted)
  2. Чтение зафиксированных данных (read committed)
  3. Повторяемый уровень изоляции чтения (repeatable read)
  4. Параллельные разрешения (serializable)
    mysql> select @@transaction_isolation;
    mysql> select @@global.transaction_isolation;

    mysql> set session transaction isolation level read uncommitted;
    mysql> set session transaction isolation level read committed;
    mysql> set session transaction isolation level repeatable read;
    mysql> set session transaction isolation level serializable;
    postgres=# show transaction isolation level;

    postgres=# begin;
    postgres=# set transaction isolation level read uncommitted;
    # в postgres уровень read uncommitted ведет себя как read committed, как будто его нет
    postgres=# set transaction isolation level read committed;
    postgres=# set transaction isolation level repeatable read;
    postgres=# set transaction isolation level serializable;
    postgres=# show transaction isolation level;
    postgres=# commit;
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
DIRTY READV---
NON-REPEATABLE READVV--
PHANTOM READVV--
SERIALIZATION ANOMALYVVV-


10. Действие на Github Go + Postgres

Рабочий процесс:

  • Является автоматизированной процедурой
  • Состоит из 1+ заданий
  • Запускается по событиям, по расписанию или вручную
  • Добавьте файл .yml в репозиторий

Запуск (Runner)

  • Является ли сервер для запуска заданий
  • Запускайте по 1 заданию за раз
  • Размещено на github или самостоятельно
  • Сообщайте о ходе выполнения, журналах и результатах на github

Задания (Job)

  • Представляет собой набор шагов, выполняемых в одном и том же runner
  • Обычные задания выполняются параллельно
  • Зависимые задания выполняются последовательно

Шаг

  • Является отдельной задачей
  • Выполняется последовательно в рамках задания
  • Содержит более 1 действия

Действие

  • Является автономной командой
  • Выполняется последовательно в пределах шага
  • Может использоваться повторно


---------------------------------------------

---------------------------------------------

II. Создание RESTful HTTP JSON API

11. Реализация RESTful HTTP API в Go с помощью Gin (2.1)

Стандартный пакет net/http


Popular web frameworks

  • Gin
  • Beego
  • Echo
  • Revel
  • Martini
  • Fiber
  • Buffalo

Popular HTTP routers

  • FastHttp
  • Gorilla Mux
  • HttpRouter
  • Chi

Самый популярный - Gin - github.com/gin-gonic/gin

api/server.go
api/account.go
main.go

в main.go обязательно добавить импорт драйвера

_ "github.com/lib/pq"

Makefile:

server: ## Run the application server.
    go run main.go

В целях тестирования запросов установить Postman

GET http://localhost:8080/accounts


12. Конфигурация из файла и переменных окружения - Viper (2.2)

    $ go get github.com/spf13/viper

app.env
util/config.go
main.go

    config, err := util.LoadConfig(".")
    $ SERVER_ADDRESS=0.0.0.0:8081 make server

db/sqlc/main_test.go

13. Mock DB (макет) - тестирование HTTP API и 100% охвата (2.3)

Подготовка:

    $ go get go.uber.org/mock
    $ go install go.uber.org/mock/mockgen@latest
    $ ls -l ~/go/bin
        // проверить ~/go/bin/mockgen
    $ which mockgen
        ~/go/bin/mockgen
    Если нет, то:
    $ vi ~/.zshrc           // для mac
    $ vi ~/.bash_profile    // или для другого терминала
        i
        export PATH=$PATH:~/go/bin
        esc
        :wq
    $ source ~/.zshrc
    $ which mockgen
        ~/go/bin/mockgen
    $ mockgen -help
    // БЫЛО:

    // api/server.go
    func NewServer(config util.Config, store *db.Store) (*Server, error)
        // для подключения к реальной базе данных используется store *db.Store
        // для тестов mock (с макетом) его надо заменить интерфейсом
    
    // db/sqlc/store.go
    type Store struct {
        db *sql.DB
        *Queries
    }
    func NewStore(db *sqL.DB) *Store {
        return &Store {
            db: db,
            Queries: New(db),
        }
    }
    func (s *Store) execTx(ctx context.Context, fn func(queries *Queries) error) error
    func (s *Store) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error)

    // sqlc.yaml
    emit_interface: false

    // ПЕРЕПИСЫВАЕМ:

    // sqlc.yaml
    emit_interface: true    // было false
    $ make sqlc             // обновить в терминале
        // создался новый файл с интерфейсом db/sqlc/querier.go  

    // db/sqlc/store.go
    type Store interface {
        Querier             // интерфейс из нового файла  db/sqlc/querier.go
        TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error)
    }
    type SQLStore struct {
        *Queries
        db *sql.DB
    }
    func NewStore(db *sql.DB) Store {
        return &SQLStore{db: db, Queries: New(db)}
    }
    func (s *SQLStore) execTx(ctx context.Context, fn func(queries *Queries) error) error
    func (s *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error)

    // api/server.go
    type Server struct {
        config     util.Config
        store      db.Store     // убрали * у db.Store, это теперь не указатель, а интерфейс
        tokenMaker token.Maker
        router     *gin.Engine
    }
    func NewServer(config util.Config, store db.Store) (*Server, error)  // убрали * у db.Store, это теперь не указатель, а интерфейс

Создаем пакет db/moc:

создаем папку db/moc

    $ mockgen -package mockdb -destination db/mock/store.go github.com/Country7/backend-webwizards/db/sqlc Store
        // создался файл db/mock/store.go

    // добавляем команду в файл Makefile
    mock: ## Generate a store mock.
	    mockgen -package mockdb -destination db/mock/store.go github.com/Country7/backend-webwizards/db/sqlc Store

Приступаем к написанию тестов:

api/account_test.go


14. Пользовательский валидатор параметров - transfer (2.4)

api/transfer.go

api/server.go

    authRoute.POST("/transfers", server.createTransfer)

Postman:

POST http://localhost:8080/transfers
Body raw JSON
{
    "from_account_id": 1,
    "to_account_id": 2,
    "amount": 10,
    "currency": "USD"
}

api/validator.go

    import ( "github.com/go-playground/validator/v10" )
    var validCurrency validator.Func = func(fieldLevel validator.FieldLevel) bool {
        if currency, ok := fieldLevel.Field().Interface().(string); ok {
            return util.IsSupportedCurrency(currency)
        }
        return false
    }

util/currency.go

    const (
        USD = "USD"
        EUR = "EUR"
        CAD = "CAD" )
    // IsSupportedCurrency returns true if the currency is supported.
    func IsSupportedCurrency(currency string) bool {
        switch currency {
        case CAD, EUR, USD:
            return true
        default:
            return false
        }
    }

Регистрация вадидатора на сервере
api/server.go

    import ("github.com/gin-gonic/gin/binding")
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("currency", validCurrency)
	}

api/account.go

    type createAccountRequest struct {
        ...
        Currency string `json:"currency" binding:"required,currency"`
    }

api/transfer.go

    type transferRequest struct {
        ...
        Currency string `json:"currency" binding:"required,currency"`
    }

Postman:

    POST http://localhost:8080/transfers
    Body raw JSON
    {
        "from_account_id": 1,
        "to_account_id": 2,
        "amount": 10,
        "currency": "EUR"
    }

util/random.go

    // RandomCurrency generates a random currency code.
    func RandomCurrency() string {
        currencies := []string{EUR, USD, CAD}
        n := len(currencies)
        return currencies[rand.Intn(n)]
    }


15. Добавление таблицы users с ограничениями уникальности и внешнего ключа (2.5)

diagram.io

-> Go to App

Table users as U {
    username varchar [pk]
    role varchar [not null, default: 'depositor']
    hashed_password varchar [not null]
    full_name varchar [not null]
    email varchar [unique, not null]
    is_email_verified bool [not null, default: false]
    password_changed_at timestamptz [not null, default: '0001-01-01']
    created_at timestamptz [not null, default: `now()`]
}

Table accounts as A {
    ...
    owner varchar [ref: > U.username, not null]
    ...
    Indexes {
        owner
        (owner, currency) [unique]  // в одной валюте только один счет у пользователя
                                    // в разной валюте может быть несколько счетов
    }
}

Export PostgreSQL

    $ migrate -help
    $ migrate create -ext sql -dir db/migration -seq add_users

В созданный файл db/migration/000002_add_users.up.sql копируем изменения из doc/schema.sql (таблицу users и ключи)

    $ make migrateup
        // ошибка, так как данные accounts есть, а их в новой таблице users - нет
    $ make migratedown
        // ошибка, надо вручную менять значение в БД / таблице schema_migrations с TRUE на FALSE
    $ make migratedown
        // удалились все таблицы
    $ make migrateup

В файле db/migration/000002_add_users.down.sql грохаем ключи, грохаем таблицу users



16. Обработка ошибок базы данных (2.6)

db/query/user.sql

    $ make sqlc

Появился db/sqlc/user.sql.go

Изменились
db/sqlc/models.go
db/sqlc/querier.go

Пишем db/sqlc/user_test.go

Правим db/sqlc/account_test.go

    func createRandomAccount(t *testing.T) Account {
        user := createRandomUser(t)
        arg := CreateAccountParams{
            Owner:    user.Username,
            ...
        }
    $ make mock
    $ make test

Допиливаем api/account.go

	account, err := server.store.CreateAccount(ctx, arg)
	if err != nil {
		var pqErr *pq.Error                         // добавляем от сюда
		if errors.As(err, &pqErr) {
			switch pqErr.Code.Name() {
			case "foreign_key_violation", "unique_violation":
                    // ошибки на сервере при создании аккаунта без юзера,
                    // и создании аккаунта с одинаковой валютой счета
				ctx.JSON(http.StatusForbidden, errorResponse(err))
				return
			}
		}                                           // до сюда
		ctx.JSON(http.StatusInternalServerError, errorResponse(err))
		return
	}

Postman:

    POST http://localhost:8080/users
    Body raw JSON
    {
        "username": "QuangBang",
        "password": "secret",
        "full_name": "Quang Bang",
        "email": "[email protected]"
    }
    POST http://localhost:8080/users/login
    Body raw JSON
    {
        "username": "QuangBang",
        "password": "secret"
    }
    POST http://localhost:8080/accounts
    Authorization  Bearer Token  ...
    Body raw JSON
    {
        "owner": "QuangBang",
        "currency": "USD"
    }
    GET http://localhost:8080/accounts?page_id=1&page_size=5
    key   page_id 1   page_size 5
    Body raw JSON
    {
        "owner":  "QuangBang",
        "limit":  5,
        "offset": 0
    }


---------------------------------------------

17. Безопасное хранение паролей Hash password в Go с помощью Bcrypt (2.7)

util/password.go
api/user.go
api/validator.go

github.com/go-playground/validator

alphanum	Alphanumeric
email	    E-mail String


18. Модульные тесты с помощью gomock (2.8)


19. Почему PASETO лучше, чем JWT, для аутентификации на основе токенов (2.9)

Token-based Authentication

Client1. POST /users/login
----------------------->
{username, password}
Server
Client200 OK
<-----------------------
{access_token: JWT, PASETO, ...}
<-- Sign tokenServer
Client2. GET /accounts
----------------------->
Authorization: Bearer <access_token>
Server
Client200 OK
<-----------------------
[account1, account2, ...]
<-- Verify tokenServer

АЛГОРИТМЫ ПОДПИСИ JWT

    header:
    {
        "typ": "JWT",
        "alg": "HS256"
    }
    payload:
    {
        "id": "1337",
        "username": "bizone",
        "iat": 1594209600,
        "role": "user"
    }
    signature:
    ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I

Алгоритм симметричной цифровой подписи

  • Для подписи и проверки используется один и тот же секретный ключ, токен
  • Для локального использования: внутренние службы, где можно совместно использовать секретный ключ
  • HS256, HS384, HS512
    • HS256 = HMAC + SHA256
    • HMAC: Hash-based Message Authentication Code - Код аутентификации сообщения на основе хэша
    • SHA: Secure Hash Algorithm - Алгоритм безопасного хэширования
    • 256/384/512: количество выходных битов

Алгоритм асимметричной цифровой подписи

  • Закрытый ключ используется для подписи токена
  • Открытый ключ используется для проверки токена
  • Для публичного использования: внутренняя служба подписывает токен, но внешняя служба должна его подтвердить
  • RS256, RS384, RS512 || PS256, PS384, PS512 || ES256, ES384, ES512
    • RS256 = RSA PKCSv1.5 + SHA256 [PKCS: Public-Key Cryptography Standards - Стандарты криптографии с открытым ключом]
    • PS256 = RSA PSS + SHA256 [PSS: Probabilistic Signature Scheme - Вероятностная схема подписи]
    • ES256 = ECDSA + SHA256 [ECDSA: Elliptic Curve Digital Signature Algorithm - Алгоритм цифровой подписи с эллиптической кривой]

В чем проблема JWT?

Слабые алгоритмы

  • Дают разработчикам слишком много алгоритмов на выбор
  • Известно, что некоторые алгоритмы уязвимы:
  • RSA PKCSv1.5: атака на oracle с дополнением
  • ECDSA: атака с недопустимой кривой

Тривиальная подделка

  • Установите для заголовка "alg" значение "none"
  • Установите для заголовка "alg" значение "HS256", в то время как сервер обычно проверяет токен с помощью открытого ключа RSA

Platform-Agnostic SEcurity TOkens [PASETO] Независимые от платформы токены безопасности

Более надежные алгоритмы

  • Разработчикам не нужно выбирать алгоритм
  • Нужно только выбрать версию PASETO
  • Каждая версия имеет 1 набор надежных шифров
  • Принимаются только 2 самые последние версии PASETO

Нетривиальная подделка

  • Больше никакого заголовка "alg" или алгоритма "none"
  • Все аутентифицировано
  • Зашифрованная полезная нагрузка для локального использования <симметричный ключ>
  • v1 [совместима с устаревшей системой]
    • локальный: <симметричный ключ>
      • Аутентифицированное шифрование
      • AES256 CTR + HMAC SHA384
    • открытый: <асимметричный ключ>
      • Цифровая подпись
      • RSA PSS + SHA384
  • v2 [рекомендуется]
    • локальный: <симметричный ключ>
      • Аутентифицированное шифрование
      • XChaCha20-Poly1305
    • открытый: <асимметричный ключ>
      • Цифровая подпись
      • Ed25519 [EdDSA + Curve25519]
• Version: v2
• Purpose: public [asymmetric-key digital signature]
• Payload:
    • Body:
        • Encoded: [base64]
        eyJeHAiO¡IyMDM5LTAxLTAxVDAwOjAwOjAwKzAwOjAwIiwiZGFOYSI
        6InRoaXMgaXMgYSBzaWduZWQgbWVzc2FnZSJ91g
        • Decoded:
        {
        "data": "this is a signed message" ,
        "exp": "2039-01-01T00:00:00+00:00"
        }
    • Signature: [hex-encoded]
    d600bbfa3096b0dde6bf8b89699c59a746ed2c981cc95c0bfacbc90fb7
    f8207c86b5e29edc74cb8c761318723532d0aa27e1120cb36813ba2d90
    8cda985b2408


20. Создать и верифицировать токен JWT & PASETO (2.10)

token/maker.go
token/payload.go
token/jwt_maker.go
token/jwt_maker_test.go

    $ go get github.com/google/uuid
    $ go get github.com/golang-jwt/jwt/v5

token/paseto_maker.go
token/paseto_maker_test.go

    $ go get github.com/o1egl/paseto/v2


21. API для входа в систему через токен PASETO или JWT (2.11)

api/server.go
app.env
util/config.go
api/main_test.go
api/transfer_test.go
api/user_test.go
api/account_test.go
main.go

api/user.go
api/server.go router

утилита Postman:

    POST http://localhost:8080/users/login
    Body raw JSON
        {
            "username": "qwe",
            "password": "1234567"
        }

    Response 200 OK
        {
            "session_id": "e7a1856e-aaa0-4226-9681-6ff7993dd789",
            "access_token": "v2.local. ...",
            "access_token_expires_at": "2024-03-30T16:05:55.116046+03:00",
            "refresh_token": "v2.local. ...",
            "refresh_token_expires_at": "2024-03-31T15:50:55.116324+03:00",
            "user": {
                "username": "qwe",
                "full_name": "asdfgh",
                "email": "[email protected]",
                "password_changed_at": "0001-01-01T00:00:00Z",
                "created_at": "2024-03-27T17:34:06.837219Z"
            }
        }


22. Middleware авторизации (2.12)

Что такое промежуточное программное обеспечение?

|        | Send request                            |         |
|        | -------------->        Route            |         |
|        |                  /accounts/create       |         |
|        |                          |              |         |
|        |                          |              |         |
|        |                          V              |         |
|        |  ctx. Abort()       Middlewares         |         |
| CLIENT | <--------------    Logger (ctx),        | SERVER  |
|        | Send response        Auth(ctx)          |         |
|        |                          |              |         |
|        |                          | ctx.Next()   |         |
|        |                          |              |         |
|        |                          V              | Авторизация:
|        | Send response         Handler  <--------- У пользователя
|        | <--------------  createAccount(ctx)     | есть разрешение?
|        |                                         |         |

api/middleware.go
api/middleware_test.go

api/server.go

    router := gin.Default()
    router.POST("/users/login", server.loginUSer)

    authRoute := router.Group("/").Use(authMiddleware(server.tokenMaker))
    authRoute.GET("/accounts", server.listAccount)

ПРАВИЛА АВТОРИЗАЦИИ

API
Create account
------->Правило
Авторизованный пользователь может создать
учетную запись только для себя
API
Get account
------->Правило
Авторизованный пользователь может получить
только те учетные записи, которыми он владеет
API
List accounts
------->Правило
Авторизованный пользователь может перечислять
только те учетные записи, которые принадлежат ему
API
Transfer money
------->Правило
Авторизованный пользователь может отправлять
деньги только со своего собственного аккаунта

api/account.go func createAccount

    authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)    // добавили
    arg := db.CreateAccountParams{
        Owner:    authPayload.Username,   // было req.Owner
        Balance:  0,
        Currency: req.Currency,
    }

api/account.go func getAccount
    
	authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)    // добавили
	if account.Owner != authPayload.Username {
		err := errors.New("account doesn't belong to the authenticated user")
		ctx.JSON(http.StatusUnauthorized, errorResponse(err))
		return
	}
db/query/account.sql

    -- name: ListAccounts :many
    SELECT * FROM accounts
    WHERE owner = $1            // добавлено условие
    ORDER BY id LIMIT $2
    OFFSET $3;

    make sqlc       // db/sqlc/account.sql.go listAccounts обновился
    make mock
db/sqlc/account_test.go  TestListAccounts   

api/account.go  func listAccount

	authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)    // добавили
	arg := db.ListAccountsParams{
		Owner:  authPayload.Username,       // добавили
		Limit:  req.PageSize,
		Offset: (req.PageID - 1) * req.PageSize,
	}

api/transfer.go  

    func (server *Server) validAccount(ctx *gin.Context, accountID int64, currency string) (db.Account, bool)
    // добавили db.Account
    return account, true

    func createTransfer
        fromAccount, valid := server.validAccount(ctx, req.FromAccountID, req.Currency)
        if !valid {
            return
        }
        authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
        if fromAccount.Owner != authPayload.Username {
            err := errors.New("from account doesn't belong to the authenticated user")
            ctx.JSON(http.StatusUnauthorized, errorResponse(err))
            return
        }
        _, valid = server.validAccount(ctx, req.ToAccountID, req.Currency)
        if !valid {
            return
        }

api/account_test.go



---------------------------------------------

---------------------------------------------

III. Развертывание приложения в рабочей среде (Deploying the application to production)

23. Образ Golang Docker с помощью многоступенчатого файла Dockerfile (3.1)

$ git checkout -b deploying

update go

go.mod

go 1.22

.github/workflows/test.yml

go-version: '1.22'

$ git status
$ git add .
$ git status
$ git commit -m"update go to 1.22"
$ git push -u origin deploying

Из терминала переходим по ссылке
https://github.com/Country7/backend-webwizards/pull/new/deploying

Name -> Add docker
-> Create pull request

$ brew upgrade golang-migrate
$ migrate -version
    v4.17.0
$ make migrate-up

.github/workflows/test.yml

curl -L https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz | tar xvz

$ git add .
$ git status
$ git commit -m"upgrade golang-migrate to v4.17.0"
$ git push

Если тесты на github пройдены, то приложение готово к 1 запуску

Создаем Dockerfile:

https://hub.docker.com/_/golang

FROM golang:1.22.2-alpine3.19
WORKDIR /app
COPY . .
RUN go build -o main main.go
EXPOSE 8080
CMD [ "/app/main" ]

$ docker build --help
$ docker build -t webwizards:latest .
$ docker images
    webwizards latest 601MB

Размер образа получился 601Мб, чтобы уменьшить размер образа нужно применить многоступенчатую сборку. Нам в образе нужен только исполняемый файл.
Dockerfile:

# Build stage
    FROM golang:1.22.2-alpine3.19 AS builder
    WORKDIR /app
    COPY . .
    RUN go build -o main main.go
# Run stage
    FROM alpine:3.19
    WORKDIR /app
    COPY --from=builder /app/main .
    EXPOSE 8080
    CMD [ "/app/main" ]

$ docker build -t webwizards:latest .
$ docker images
    webwizards latest 21MB

$ docker rmi 61504815c89a   // удалить старый образ IMAGE ID = 61504815c89a


24. Подключить контейнеры в одной сети docker (3.2)

$ docker run --name webwizards -p 8080:8080 webwizards:latest
    cannot load config:Config File "app" Not Found in "[/app]"
$ docker ps -a
$ docker rm webwizards
$ docker images
$ docker rmi a4809227e909

Dockerfile:

COPY app.env .

$ docker build -t webwizards:latest .
$ docker images
$ docker run --name webwizards -p 8080:8080 webwizards:latest
    Running in "debug" mode. Switch to "release" mode in production.
    - using env:	export GIN_MODE=release
$ docker rm webwizards
$ docker run --name webwizards -p 8080:8080 -e GIN_MODE=release webwizards:latest
$ docker ps

Postman: "error": "dial tcp 127.0.0.1:5432: connect: connection refused"
Terminal: [GIN] | 500 | 1.437083ms | 192.168.65.1 | POST "/users/login"
app.env: DB_SOURCE=postgresql://root:secret@localhost:5432/main_db?sslmode=disable

$ docker container inspect postgres16
    "NetworkSettings": "Networks": "bridge": "IPAddress": "172.17.0.2"
$ docker container inspect webwizards
    "NetworkSettings": "Networks": "bridge": "IPAddress": "172.17.0.3"

$ docker stop webwizards
$ docker rm webwizards
$ docker run --name webwizards -p 8080:8080 -e GIN_MODE=release -e "DB_SOURCE=postgresql://root:[email protected]:5432/main_db?sslmode=disable" webwizards:latest

Postman: Status 200 OK

Способ получше (подключиться к контейнеру postgres16 по имени, а не по ip адресу)

$ docker rm webwizards
$ docker network ls
    9a00594f4037 bridge bridge local
$ docker network inspect bridge
    "Containers": "Name": "postgres16"
// контейнеры в мостовой сети bridge не могут видеть друг друга по имени, как в других сетях
// поэтому нужно создать свою сеть и подключить к ней контейнер
$ docker network --help
$ docker network create ww-network
$ docker network connect --help 
$ docker network connect ww-network postgres16
$ docker container inspect postgres16
    "Networks":
        "bridge": "IPAddress": "172.17.0.2"
        "ww-network": "IPAddress": "172.18.0.2"
$ docker run --name webwizards --network ww-network -p 8080:8080 -e GIN_MODE=release -e "DB_SOURCE=postgresql://root:secret@postgres16:5432/main_db?sslmode=disable" webwizards:latest

Postman: Status 200 OK Terminal: [GIN] | 200 | 101.980875ms | 192.168.65.1 | POST "/users/login"

$ docker network inspect ww-network
    "Containers":
        "Name": "postgres16"
        "Name": "webwizards",

Makefile

run-postgres: ## Run postgresql database docker image.
    docker run --name postgres16 --network ww-network -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:16-alpine



PS

Собрать все зависимости из go.mod

$ go mod tidy

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author