Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Слой приложения #2

Open
zhikh23 opened this issue Aug 2, 2024 · 0 comments
Open

Слой приложения #2

zhikh23 opened this issue Aug 2, 2024 · 0 comments
Assignees
Labels
feat New feature or request

Comments

@zhikh23
Copy link
Collaborator

zhikh23 commented Aug 2, 2024

Следующим этапом будет реализация слоя приложения. Мы разместим там остальную часть бизнес-логики.

Немного теории

Слой приложения

Слой приложения - это сервисы.

В отличие от доменного слоя, слой приложения (application) уже знает о инфраструктурном слое (БД, удаленные сервисы...), но не знает о конкретной реализации - то есть использует через интерфейс. Этот факт нам пригодится позже.

Ещё один немаловажный факт - "входы-выходы" в слой приложения не являются доменными сущностями. То есть когда мы используем слой приложения извне, мы не используем сущности - только примитивы; и, если нужна структура, так называемые DTO - Data Transfer Object, который представляет из себя структуру без методов и, обычно, без конструктора, поля которого являются примитивами или другими DTO.

Также, на основе предыдущего PR. Здесь обязательно нужно сделать так, что те ошибки, которые случились по вине вызывающей стороны (например, невалидная почта или ненайденный пользователь) возвращать как Err<что-то там>, а не fmt.Errorf. А те ошибки, которые "незапланированные" - например, ошибка в БД - уже возвращать как

fmt.Errorf("%s: %v", op, err)

где op - название пакета и метода, например:

func (p *Processor) Process(
	ctx context.Context,
	botId uint64,
	userId uint64,
	ans string,
) ([]dto.Message, error) {
	const op = "Processor.Process"
	...
	return nil, fmt.Errorf("%s: %v", op, err)

JWT

Мы будем использовать JWT аутентификацию. JWT токен - это строка, которая умеет:

  • нести в себе метаинформацию в заголовке (что строка является JWT токеном);
  • нести в себе полезную нагрузку (payload); в нашем случае payload-ом будет только userId;
  • шифроваться, чтобы токен нельзя было подделать.

Шифрование JWT может быть:

  1. Симметричным, например, алгоритм HBAC (если я не ошибаюсь). Тогда, чтобы "сделать" ключ (подписать) и проверить, что он настоящий, используется один и тот же ключ. Ключ, считай, что просто строка) Он же иногда называется secret. Важно его никому не показывать!
  2. Асимметричным, например, алгоритм RSA (да-да, который используется, например, для SSH). Тогда, чтобы подписать ключ используется приватный ключ, а для проверки подлинности - публичный.

В целом, больше теории можно почитать в гугле, а то я могу где-то ошибаться. Например, тут.

Для создания JWT токенов можно использовать библиотеку github.com/golang-jwt/jwt/v5.

В тестах нужно будет расшифровывать JWT токен и проверять его payload.

Что нужно сделать?

Разработать AuthService. Можно расположить его в application/auth/auth.go.

Какие функции выполняет сервис аутентификации?

  1. Регистрация, он же Register или Signup
  2. Аутентификация, он же Login или Signin

Регистрация

О пользователе на момент регистрации известно:

  • email
  • password

В процессе регистрации:

  • проверяется валидность данных (в домене);
  • сохраняется в БД;
  • возвращается идентификатор пользователя.

Возможные ошибки:

  • пользователь с такой почтой уже есть;
  • невалидная почта;
  • слабый пароль.

Аутентификация

На вход:

  • email
  • password

В процессе аутентификации:

  • достается из БД запись о пользователе, если есть;
  • проверяется совпадение пароля и хэша;
  • возвращается JWT токен как строка, если успешно.

В JWT токене в качестве payload-а должен быть только userId.

Дополнительно: к вопросу о хранении интерфейсов

В Go принято хранить интерфейс в месте использования. Но мы немного отойдем от этой практики, и я аргументирую почему.

Что есть интерфейс? Условно, набор правил взаимодействия. Это в первую очередь методы. И, если бы у нас были только методы... то можно было бы спокойно класть интерфейс в слой приложения и не париться. Но в правила взаимодействия также включены и ошибки. Ошибки, которые возвращает инфраструктурный слой, например, о том, что пользователь не найден. Если ошибки инфраструктурного слоя будут лежать в слое приложения, инфраструктурный слой будет знать о слое приложения... ужс. Либо делить - интерфейсы в слое приложения, а ошибки... где-то отдельно. Короче говоря есть красивое решение.

Между инфраструктурным слоем и слоем приложения мы разместим пакет интерфейсов. Там будет сам интерфейс и набор ошибок, которые эти методы возвращают. Этот пакет можно разместить в infrastructure/interfaces/repository.go (потом будет понятно, почему).

Наполнение будет такого рода:

package interfaces

import (
        ...
)

var (
	ErrBotNotFound = errors.New("bot not found")
)

type BotRepository interface {
	Save(context.Context, *entity.Bot) (value.BotId, error)
	Bot(context.Context, value.BotId) (*entity.Bot, error)
}

И использование:

botRepos interfaces.BotRepository

...

b, err = botRepos.Bot(ctx, botId)
if err != nil {
      if errors.Is(err, interfaces.ErrBotNotFound) {
              return специфическая-ошибка-для-слоя-приложения
      }
}

(да, слой приложения возвращает свои ошибки, а не ошибки инфраструктуры).

Ещё важный момент -- инфраструктура работает с доменными сущностями. То есть условный UserRepository принимает UserId и возвращает User. Хотя иногда встречаю решения, где это сделано как в слое приложения - DTO и примитивы. Но нам дополнительные мапперы не нужны)

@zhikh23 zhikh23 added the feat New feature or request label Aug 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat New feature or request
Projects
None yet
Development

When branches are created from issues, their pull requests are automatically linked.

2 participants