React Apollo + Gqlgen + Websocket – полное руководство

Kate

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

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

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

 
Сверху