Недавно у нас встала задача разработать сервис бэк-офиса.

Основные требования: 

Авторизация по СМС либо ссылкой на е-майл. У пользователя отсутствует пароль, авторизацию можно активировать ссылкой на любом устройстве. При этом фронт должен понять что произошла авторизация и подтянуть профиль пользователя, поставить Cookie с токеном и тд. 

Дополнительное требование:

Заменяем Rest API на GraphQL

Для решения задачи был выбран стек:

Подготовка

Чтобы не раздувать статью, мы упустим некоторые основы. Необходимы предварительные знания:

Структура проекта:

  • backoffice

  • front – react + apollo client

Развернем сервис бэк офиса

Структура сервиса:

  • cmd

  • pkg

  • schema – директория для схем .graphqls

  • model – структуры генерируемые из .graphqls

Установка Gqlgen, в директории backoffice

# Команда создаст структуру проекта.
go get github.com/99designs/gqlgen
go run github.com/99designs/gqlgen init

Далее необходимо отредактировать файл: gqlgen.yml

# Изменим экспорт файлов схемы
schema:
  - schema/*.graphqls

# Изменим расположение моделей
model:
  filename: models/models_gen.go
  package: model

# Добавим исключение
# Чтобы генератор моделей не перезатирал уже имеющееся
autobind:
  - "react-apollo-gqlgen-tutorial/backoffice/models"

Переименуем файл server.go в main.go и поместим его в cmd

Создадим файл с командой для генерации GraphQL, и также поместим его в cmd.
В дальнейшем для генерации схем будем выполнять команду:

go run cmd/gqlgen.go

Сам файл:

// +build tools
package main

import (
	"fmt"
	"github.com/99designs/gqlgen/cmd"
)

func main() {
	fmt.Println("Building Graphql schema")
	cmd.Execute()
}

Создание моделей

Для подтягивания сессии и установки токена пользователя, нам необходимо создать идентификатор клиента в приложении (Client ID - cid). Под клиентом мы понимаем браузер.

Когда когда пользователь "логинится", мы создаем сессию ожидания подтверждения авторизации, к ней привязываем uid и cid. В дальнейшем мы найдем cid в слушателях Websocket и отправим сигнал об имеющейся авторизации.

То есть мы проходим 2 процедуры авторизации, клиента и пользователя.

Создадим схемы в директории schema

Схема авторизации клиента:

# schema/auth.graphqls
type Auth {
	authorized: Boolean!
  method: 		String!
  reconnect: 	Boolean!
}
  • authorized – нужен для того чтобы приложение поняло что следует запросить юзера

  • method – используемый метод авторизации: email, phone и тд. Нужен чтобы приложение понимало как действовать дальше: показать окно с вводом кода из СМС, либо сообщение о необходимости открыть емайл и перейти по ссылке

  • reconnect – сообщает клиенту о необходимости обновить соединение.

Схема данных пользователя:

# schema/auth.graphqls
type User {
    uid: 			Int!,
    username: String!,
    active: 	Boolean!,
    email: 		String!,
    phone: 		String!,
    method: 	String!,
}

Определим методы:

# schema/schema.graphqls
# GET
type Query {
	# Запрашивает авторизацию при подключении клиента
  auth: Auth!
  user: User!
}

# POST
type Mutation {
	# Отправляет данные из формы авторизации
  authLogin(login: String!): Auth!
  
	# Отправляет данные из формы подтверждения кода из СМС
  authVerifyCode(code: String!): Auth!
}

# Websockets
type Subscription {
	# Подписка на данные авторизации
  authSubscription: Auth!
}

Все готово, просто выполним команду:

go run cmd/gqlgen.go

После выполнения команды в каталоге backoffice появится
Нас здесь интересует 2 файла:

  • resolver.go

  • schema.resolvers.go

Организовываем проект

Создадим стор.

// pkg/store/store.go
package store

type Store struct {

}

func NewStore(opt Options) *Store {
	return &Store{}
}

Store работает над бизнес логикой, стразу реализуем методы которые понадобятся для старта проекта.

pkg/store/auth.go

// AuthQuery - queryResolver
// Возвращает доступную модель Auth. GET запрос
func (s *Store) AuthQuery(ctx context.Context) (*model.Auth, error) {
	auth := &model.Auth{}
	// ...
	return auth, nil
}

// AuthorizeForUsername - mutationResolver
// Метод осуществляющий авторизацию по username
func (s *Store) AuthorizeForUsername(ctx context.Context, username string) (*model.Auth, error) {
	auth := &model.Auth{}
	// ...
	return auth, nil
}

// AuthVerifyCode - mutationResolver
// Метод осуществляющий подтверждение кода из СМС
func (s *Store) AuthVerifyCode(ctx context.Context, code string) (*model.Auth, error) {
	auth := &model.Auth{}
	// ...
	return auth, nil
}

// AuthWebsocket - subscriptionResolver
// Метод вызывается в pkg/graph/auth.go. Осуществляет подключение по Websocket
func (s *Store) AuthWebsocket(ctx context.Context) (<-chan *model.Auth, error) {
	ch := make(chan *model.Auth)
	// ...
	return ch, nil
}

// AuthorizationHTTP
// Здесь будем работать с HTTP заголовками и Cookie
func (s *Store) AuthorizationHTTP(w http.ResponseWriter, r *http.Request) *http.Request {
	ctx := r.Context()
	// ...
	return r.WithContext(ctx)
}

// Cors
// Здесь будем работать с Cors
func (s *Store) Cors(w http.ResponseWriter, r *http.Request) *http.Request {
	ctx := r.Context()
	// ...
	return r.WithContext(ctx)
}

pkg/store/user.go

// UserQuery - queryResolver
// Возвращает доступную модель User. GET запрос
func (s *Store) UserQuery(ctx context.Context) (*model.User, error) {
	user := &model.User{}
	// ...
	return user, nil
}

Вернемся к GraphQL директория /graph. Переместим resolver.go и schema.resolvers.go в директорию pkg/graph

schema.resolvers.go

В этом файле находятся сгенерированные методы из нашей схемы

Методы Auth:

AuthLogin(ctx context.Context, login string) (*model.Auth, error)
AuthVerifyCode(ctx context.Context, code string) (*model.Auth, error)
Auth(ctx context.Context) (*model.Auth, error) (*model.Auth, error)
AuthSubscription(ctx context.Context) (<-chan *model.Auth, error)

Методы User:

User(ctx context.Context) (*model.User, error)

resolver.go

Добавим функцию New, и подключим стор для работы с бизнес логикой.

// pkg/qraph/resolver.go

type Resolver struct{
	
	// Подключим стор
	store *store.Store
}

// Создадим функцию New
func NewServer(opt Options) *handler.Server {
	
	return handler.New(
		generated.NewExecutableSchema(
			generated.Config{
				Resolvers: &Resolver{
					store: opt.Store,
				},
			},
		),
	)
}

type Options struct {
	Store *store.Store
}

Первый запуск

Отредактируем main.go, для HTTP транспорта используем Gorilla mux + websocket.
Настроим middleware для обработки Cors, HTTP заголовков и cookie:

var (
	mb int64 = 1 << 20
	defaultPort = "8080"
)

func main() {
  port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}
  
	// Инициализируем стор
	st := store.NewStore(store.Options{})

  // GraphQL
	srv := graph.NewServer(graph.Options{
		Store: st,
	})
	srv.AddTransport(transport.MultipartForm{
		MaxMemory:     32 * mb,
		MaxUploadSize: 50 * mb,
	})
	srv.AddTransport(transport.POST{})
	srv.AddTransport(transport.GET{})
	srv.AddTransport(transport.Websocket{
		KeepAlivePingInterval: 10 * time.Second,
		Upgrader: websocket.Upgrader{
			CheckOrigin: func(r *http.Request) bool {
				return true
			},
			ReadBufferSize:  1024,
			WriteBufferSize: 1024,
		},
		InitFunc: transport.WebsocketInitFunc(func(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) {
			return ctx, nil
		}),
	})
	srv.Use(extension.Introspection{})

	// Создадим роутер
	router := mux.NewRouter()

	// Инициализируем middleware
	// Передадим Store в качестве параметра
	router.Use(middleware.AuthMiddleware(st))
	router.Use(middleware.CorsMiddleware(st))

	router.Handle("/", playground.Handler("GraphQL playground", "/graph"))
	router.Handle("/graph", srv)
  
	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, router))
}

Подключим стор в Resolvers

pkg/graph/auth.go

func (r *queryResolver) Auth(ctx context.Context) (*model.Auth, error) {
	return r.store.AuthQuery(ctx)
}

func (r *mutationResolver) AuthLogin(ctx context.Context, login string) (*model.Auth, error) {
	return r.store.AuthorizeForUsername(ctx, login)
}

func (r *mutationResolver) AuthVerifyCode(ctx context.Context, code string) (*model.Auth, error) {
	return r.store.AuthVerifyCode(ctx, code)
}

func (r *subscriptionResolver) AuthSubscription(ctx context.Context) (<-chan *model.Auth, error) {
	return r.store.AuthWebsocket(ctx)
}

pkg/graph/user.go

func (r *queryResolver) User(ctx context.Context) (*model.User, error) {
	return r.store.UserQuery(ctx)
}

Запускаем сервис:

go run cmd/main.go

Переходим на: http://localhost:8080/

Видим GraphQL плейграунд с нашими схемами запросов:

GraphQL playground
GraphQL playground

Исходники данного этапа доступны здесь

Это окончание первой части.

Дальше мы разберем подключение к graphGL API с помощью React + Apollo Client и начнем создавать бизнес логику сервиса бэк-офиса.

Комментарии (0)