Недавно у нас встала задача разработать сервис бэк-офиса.
Основные требования:
Авторизация по СМС либо ссылкой на е-майл. У пользователя отсутствует пароль, авторизацию можно активировать ссылкой на любом устройстве. При этом фронт должен понять что произошла авторизация и подтянуть профиль пользователя, поставить Cookie с токеном и тд.
Дополнительное требование:
Заменяем Rest API на GraphQL
Для решения задачи был выбран стек:
Golang
React js
Gorilla mux – https://github.com/gorilla/mux
Gorilla websocket – https://github.com/gorilla/websocket
Gqlgen – https://github.com/99designs/gqlgen
Apollo Client – https://www.apollographql.com/docs/react
Подготовка
Чтобы не раздувать статью, мы упустим некоторые основы. Необходимы предварительные знания:
Typescript
Golang, channels, goroutines
React: hooks, context
Graphql – https://graphql.org/
Структура проекта:
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 плейграунд с нашими схемами запросов:
Исходники данного этапа доступны здесь
Это окончание первой части.
Дальше мы разберем подключение к graphGL API с помощью React + Apollo Client и начнем создавать бизнес логику сервиса бэк-офиса.