Utilisation d'un serveur Go classique pour le backend au lieu de super-graph #12

Manually merged
wpetit merged 6 commits from feature/go-server into develop 2020-07-13 15:50:22 +02:00
87 changed files with 2307 additions and 856 deletions

7
.env.dist Normal file
View File

@ -0,0 +1,7 @@
DEBUG=true
OIDC_CLIENT_ID=daddy
OIDC_CLIENT_SECRET=daddycool
OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect
HTTP_COOKIE_AUTHENTICATION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
HTTP_COOKIE_ENCRYPTION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy"

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor
/data
/bin
/.env

View File

@ -1,17 +1,25 @@
build:
SHELL := /bin/bash
build: build-docker build-server
build-docker:
docker-compose build
deps:
cd frontend && npm install
generate:
cd internal && go run github.com/99designs/gqlgen generate
up: build
( cd frontend && NODE_ENV=development npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait
build-server:
CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server
sg:
docker-compose exec -u $(shell id -u) super-graph sh
deps: generate
cd client && npm install
go get ./...
sgr:
docker-compose run -u $(shell id -u) super-graph sh
up: build-docker
docker-compose up
watch:
go run github.com/cortesi/modd/cmd/modd
down:
docker-compose down -v --remove-orphans
@ -19,5 +27,25 @@ down:
db-shell:
docker-compose exec postgres psql -Udaddy
migrate: build-server
( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml -migrate $(MIGRATE) )
migrate-latest:
$(MAKE) MIGRATE=latest migrate
migrate-up:
$(MAKE) MIGRATE=up migrate
migrate-down:
$(MAKE) MIGRATE=down migrate
test:
go test -v ./...
hydra-shell:
docker-compose exec hydra /bin/sh
docker-compose exec hydra /bin/sh
clean: down
rm -rf client/node_modules bin data .env internal/graph/generated internal/graph/server.go
rm -rf vendor
go clean -modcache

View File

@ -17,39 +17,45 @@ Application de gestion des Dossiers d'Aide à la Décision (D.A.D.) à Cadoles.
```bash
git clone https://forge.cadoles.com/Cadoles/daddy.git # Cloner le projet
cd daddy # Se placer dans le répertoire
make deps # Installer les dépendances NPM
make up # Démarrer l'environnement de développement
make clean # On s'assure d'avoir un environnement propre
make deps # Installer les dépendances
make up # Démarrer l'environnement docker-compose (hydra, hydra-passwordless et fake-smtp)
# Dans un second terminal
make watch # Suivre les modifications et compiler à la volée le backend et frontend
```
Les services suivants devraient être disponibles après démarrage de l'environnement:
|Service|Type|Accès|Description|
|-------|----|-----|-----------|
|Application React|HTTP (UI)|http://localhost:8081/|Page d'accueil de l'application React (serveur Webpack)|
|Interface Web GraphQL|HTTP (UI)|http://localhost:8080/|Interface Web de développement de l'API GraphQL **\***|
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8080/api/v1/graphql|Point d'entrée de l'API GraphQL|
|Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)|
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/graphql (GET)|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql (POST)|Point d'entrée de l'API GraphQL|
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|
|Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp)
|Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement|
**\*** Pensez à passer l'attribut `auth_fail_block: false` dans le fichier `backend/config/dev.yml` si vous voulez pouvoir utiliser cette interface sans avoir à définir l'entête `Authorization`.
#### Fichiers/répertoires notables
|Chemin|Description|
|------------------|-----------|
|`docker-compose.yml`|Configuration de l'environnement Docker Compose|
|`frontend/src`|Sources du frontend ([React](https://reactjs.org))|
|`backend/config/migrations`|Migrations SQL pour le backend, voir [la documentation de SuperGraph à ce sujet](https://supergraph.dev/docs/start#migrations)|
|`client/src`|Sources du frontend ([React](https://reactjs.org))|
#### Commandes utiles
|Commande|Description|
|--------|-----------|
|`make up`|Démarrer l'environnement de développement, `Ctrl+C` pour le stopper.|
|`make down`|Stopper et supprimer l'environnement de développement.|
|`make up`|Démarrer l'environnement Docker Compose, `Ctrl+C` pour le stopper.|
|`make down`|Stopper et supprimer l'environnement Docker Compose.|
|`make watch`|Suerveiller les sources et recompiler à la volée le client/server.|
|`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.|
|`make hydra-shell`|Ouvrir un shell interactif dans le conteneur Hydra. (`hydra --help` pour voir les commandes disponibles pour l'administration)|
|`make migrate-latest`|Migrer la base de données à la dernière version disponible du schéma.|
|`make migrate-down`|Migrer la base de données à la version précédente du schéma.|
|`make migrate-up`|Migrer la base de données à la version suivante du schéma.|
|`make clean`|Nettoyer l'environnement.|
#### Ressources

View File

@ -1,18 +0,0 @@
/* fetchUser */
variables {
"email": ""
}
query fetchUser {
user(where: {email: {eq: $email}}) {
id
created_at
updated_at
email,
full_name
}
}

View File

@ -1,225 +0,0 @@
app_name: "Daddy Dev"
host_port: 0.0.0.0:8080
web_ui: true
# debug, error, warn, info
log_level: debug
# enable or disable http compression (uses gzip)
http_compress: true
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: false
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: true
# Latency tracing for database queries and remote joins
# the resulting latency information is returned with the
# response
enable_tracing: true
# Watch the config folder and reload Super Graph
# with the new configs when a change is detected
reload_on_config_change: true
# File that points to the database seeding script
# seed_file: seed.js
# Path pointing to where the migrations can be found
# this must be a relative path under the config path
migrations_path: ./migrations
# Secret key for general encryption operations like
# encrypting the cursor data
secret_key: supercalifajalistics
# CORS: A list of origins a cross-domain request can be executed from.
# If the special * value is present in the list, all origins will be allowed.
# An origin may contain a wildcard (*) to replace 0 or more
# characters (i.e.: http://*.domain.com).
cors_allowed_origins: ["*"]
cors_allowed_headers: ["Authorization", "Content-Type", "Mode"]
cors_allowed_methods: ["POST"]
# Debug Cross Origin Resource Sharing requests
cors_debug: false
# Postgres related environment Variables
# SG_DATABASE_HOST
# SG_DATABASE_PORT
# SG_DATABASE_USER
# SG_DATABASE_PASSWORD
# Auth related environment Variables
# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE
# SG_AUTH_RAILS_REDIS_URL
# SG_AUTH_RAILS_REDIS_PASSWORD
# SG_AUTH_JWT_PUBLIC_KEY_FILE
# inflections:
# person: people
# sheep: sheep
auth:
# Can be 'rails', 'jwt' or 'header'
type: jwt
#cookie: _supergraph_session
# Comment this out if you want to disable setting
# the user_id via a header for testing.
# Disable in production
#creds_in_header: false
jwt:
provider: hydra
jwks_url: http://hydra:4444/.well-known/jwks.json
# header:
# name: dnt
# exists: true
# value: localhost:8080
# You can add additional named auths to use with actions
# In this example actions using this auth can only be
# called from the Google Appengine Cron service that
# sets a special header to all it's requests
# auths:
# - name: from_taskqueue
# type: header
# header:
# name: X-Appengine-Cron
# exists: true
database:
type: postgres
host: localhost
port: 5432
dbname: daddy
user: daddy
password: daddy
#schema: "public"
#pool_size: 10
#max_retries: 0
log_level: "debug"
# Set session variable "user.id" to the user id
# Enable this if you need the user id in triggers, etc
set_user_id: true
# database ping timeout is used for db health checking
ping_timeout: 1m
# Define additional variables here to be used with filters
variables:
# admin_account_id: "5"
# admin_account_id: "sql:select id from users where admin = true limit 1"
# Field and table names that you wish to block
blocklist:
- ar_internal_metadata
- schema_migrations
- secret
- password
- encrypted
- token
# Create custom actions with their own api endpoints
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
# A request to this url will execute the configured SQL query
# which in this case refreshes a materialized view in the database.
# The auth_name is from one of the configured auths
actions:
# - name: refresh_leaderboard_users
# sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
# auth_name: from_taskqueue
tables:
# - name: customers
# remotes:
# - name: payments
# id: stripe_id
# url: http://rails_app:3000/stripe/$id
# path: data
# # debug: true
# pass_headers:
# - cookie
# set_headers:
# - name: Host
# value: 0.0.0.0
# - name: Authorization
# value: Bearer <stripe_api_key>
# - # You can create new fields that have a
# # real db table backing them
# name: me
# table: users
roles_query: "select * from users where users.email = $user_id"
roles:
# Rôle par défaut si l'utilisateur n'existe pas dans la table `users`
- name: anon
tables:
- name: users
# insert:
# block: true
# query:
# block: true
# update:
# block: true
# delete:
# block: true
# Rôle par défaut si l'utilisateur existe dans la table `users`
# mais que la valeur de la colonne `role` n'est pas définie
- name: user
tables:
- name: users
insert:
block: true
query:
filters: ["{ email: { _eq: $user_id } }"]
update:
columns:
- full_name
filters: ["{ email: { _eq: $user_id } }"]
delete:
block: true
- name: admin
match: role = 'admin'
tables:
- name: users
# - name: products
# query:
# limit: 50
# filters: ["{ user_id: { eq: $user_id } }"]
# disable_functions: false
# insert:
# filters: ["{ user_id: { eq: $user_id } }"]
# presets:
# - user_id: "$user_id"
# - created_at: "now"
# update:
# filters: ["{ user_id: { eq: $user_id } }"]
# presets:
# - updated_at: "now"
# delete:
# block: true
# - name: admin
# match: id = 1000
# tables:
# - name: users
# filters: []

View File

@ -1,17 +0,0 @@
-- Write your migrate up statements here
CREATE TABLE public.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
full_name text,
email text UNIQUE NOT NULL CHECK (length(email) < 255),
created_at timestamptz NOT NULL NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL NOT NULL DEFAULT NOW(),
role varchar(64)
);
---- create above / drop below ----
-- Write your down migrate statements here. If this migration is irreversible
-- then delete the separator line above.
DROP TABLE public.users

View File

@ -1,67 +0,0 @@
# Inherit config from this other config file
# so I only need to overwrite some values
inherits: dev
app_name: "Backend Production"
host_port: 0.0.0.0:8080
web_ui: false
# debug, info, warn, error, fatal, panic, disable
log_level: "warn"
# enable or disable http compression (uses gzip)
http_compress: true
# When production mode is 'true' only queries
# from the allow list are permitted.
# When it's 'false' all queries are saved to the
# the allow list in ./config/allow.list
production: true
# Throw a 401 on auth failure for queries that need auth
auth_fail_block: true
# Latency tracing for database queries and remote joins
# the resulting latency information is returned with the
# response
enable_tracing: true
# File that points to the database seeding script
# seed_file: seed.js
# Path pointing to where the migrations can be found
# migrations_path: migrations
# Secret key for general encryption operations like
# encrypting the cursor data
# secret_key: supercalifajalistics
# Postgres related environment Variables
# SG_DATABASE_HOST
# SG_DATABASE_PORT
# SG_DATABASE_USER
# SG_DATABASE_PASSWORD
# Auth related environment Variables
# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE
# SG_AUTH_RAILS_REDIS_URL
# SG_AUTH_RAILS_REDIS_PASSWORD
# SG_AUTH_JWT_PUBLIC_KEY_FILE
database:
type: postgres
host: db
port: 5432
dbname: backend_development
user: postgres
password: postgres
#pool_size: 10
#max_retries: 0
#log_level: "debug"
# Set session variable "user.id" to the user id
# Enable this if you need the user id in triggers, etc
set_user_id: false
# database ping timeout is used for db health checking
ping_timeout: 5m

View File

@ -1,33 +0,0 @@
// Voir https://supergraph.dev/docs/seed
var users = [
{
full_name: 'Admin',
email: 'admin@cadoles.com',
role: 'admin',
},
{
full_name: 'User 1',
email: 'user1@cadoles.com',
role: 'user',
},
{
full_name: 'User 2',
email: 'user2@cadoles.com',
role: 'user',
},
{
full_name: 'User 3',
email: 'user3@cadoles.com',
role: 'user',
}
];
for (var user, i = 0; (user = users[i]); i++) {
var res = graphql(" \
mutation { \
user(insert: $data) { \
id \
} \
}", { data: user });
}

View File

@ -3,7 +3,6 @@ import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import { HomePage } from './HomePage/HomePage';
import { store } from '../store/store';
import { Provider } from 'react-redux';
import { OAuth2Page } from './OAuth2Page/OAuth2Page';
export class App extends React.Component {
render() {
@ -12,7 +11,6 @@ export class App extends React.Component {
<BrowserRouter>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/oauth2/:action" exact component={OAuth2Page} />
<Route component={() => <Redirect to="/" />} />
</Switch>
</BrowserRouter>

View File

@ -2,7 +2,7 @@ import React from 'react';
import logo from '../resources/logo.svg';
import { useSelector } from 'react-redux';
import { RootState } from '../store/reducers/root';
import { Link } from 'react-router-dom';
import { Config } from '../config';
export function Navbar() {
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
@ -26,18 +26,18 @@ export function Navbar() {
<div className="navbar-item">
{
isAuthenticated ?
<Link className="button is-small" to="/oauth2/logout">
<a className="button is-small" href={Config.logoutURL}>
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Se déconnecter</span>
</Link> :
<Link className="button is-small" to="/oauth2/login">
</a> :
<a className="button is-small" href={Config.loginURL}>
<span className="icon">
<i className="fas fa-sign-in-alt"></i>
</span>
<span>Se connecter</span>
</Link>
</a>
}
</div>
</div>

14
client/src/config.ts Normal file
View File

@ -0,0 +1,14 @@
export const Config = {
loginURL: get<string>("loginURL", "http://localhost:8081/login"),
logoutURL: get<string>("logoutURL", "http://localhost:8081/logout"),
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"),
};
function get<T>(key: string, defaultValue: T):T {
const config = window['__CONFIG__'] || {};
if (config && config.hasOwnProperty(key)) {
return config[key] as T;
} else {
return defaultValue;
}
}

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,11 @@
import { Action } from "redux";
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
export interface setCurrentUserAction extends Action {
email: string
}
export function setCurrentUser(email: string): setCurrentUserAction {
return { type: SET_CURRENT_USER, email };
}

View File

@ -1,6 +1,6 @@
import { Action } from "redux";
import { User } from "../../types/user";
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth";
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile";
export interface AuthState {
@ -17,8 +17,6 @@ export function authReducer(state = defaultState, action: Action): AuthState {
switch (action.type) {
case SET_CURRENT_USER:
return handleSetCurrentUser(state, action as setCurrentUserAction);
case LOGOUT:
return handleLogout(state);
case FETCH_PROFILE_SUCCESS:
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
@ -36,14 +34,6 @@ function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction)
};
};
function handleLogout(state: AuthState): AuthState {
return {
...state,
isAuthenticated: false,
currentUser: null,
};
};
function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState {
return {
...state,

View File

@ -1,6 +1,5 @@
import { UnauthorizedError } from "../../util/daddy";
import { put, all, takeEvery } from 'redux-saga/effects';
import { logout } from '../actions/auth';
import { all, takeEvery } from 'redux-saga/effects';
export function* failureRootSaga() {
yield all([
@ -10,7 +9,8 @@ export function* failureRootSaga() {
export function* failuresSaga(action) {
if (action.error instanceof UnauthorizedError) {
yield put(logout());
// TODO Implements better authorization error handling
window.location.reload();
}
}

View File

@ -0,0 +1,7 @@
import { all, put } from "redux-saga/effects";
export function* initRootSaga() {
yield all([
]);
}

View File

@ -1,6 +1,5 @@
import { all } from 'redux-saga/effects';
import { failureRootSaga } from './failure';
import { authRootSaga } from './auth';
import { initRootSaga } from './init';
import { usersRootSaga } from './users';
@ -8,7 +7,5 @@ export function* rootSaga() {
yield all([
initRootSaga(),
failureRootSaga(),
authRootSaga(),
usersRootSaga(),
]);
}

View File

@ -1,6 +1,5 @@
import { DaddyClient } from "../../util/daddy";
import { Config } from "../../config";
import { getSavedAccessGrant } from "../../util/auth";
import { all, takeLatest, put, select } from "redux-saga/effects";
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile";
import { SET_CURRENT_USER } from "../actions/auth";
@ -18,11 +17,8 @@ export function* onCurrentUserChangeSaga() {
yield put(fetchProfile());
}
export function* fetchProfileSaga() {
const grant = getSavedAccessGrant();
const client = new DaddyClient(Config.graphQLEndpoint, grant.id_token);
const client = new DaddyClient(Config.graphQLEndpoint);
let profile: User;
try {

View File

@ -12,10 +12,9 @@ export class DaddyClient {
gql: GraphQLClient
constructor(endpoint: string, idToken: string) {
constructor(endpoint: string) {
this.gql = new GraphQLClient(endpoint, {
headers: {
Authorization: `Bearer ${idToken}`,
mode: 'cors',
}
});

View File

@ -22,7 +22,8 @@ module.exports = {
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 8081,
host: '0.0.0.0',
port: 8080,
historyApiFallback: true,
writeToDisk: true,
},

104
cmd/server/container.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"context"
"net/http"
"forge.cadoles.com/Cadoles/daddy/internal/migration"
"gitlab.com/wpetit/goweb/cqrs"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/Cadoles/daddy/internal/config"
oidc "forge.cadoles.com/wpetit/goweb-oidc"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
"gitlab.com/wpetit/goweb/service/build"
"gitlab.com/wpetit/goweb/service/session"
"gitlab.com/wpetit/goweb/session/gorilla"
)
func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Container, error) {
// Initialize and configure service container
ctn := service.NewContainer()
ctn.Provide(build.ServiceName, build.ServiceProvider(ProjectVersion, GitRef, BuildDate))
// Generate random cookie authentication key if none is set
if conf.HTTP.CookieAuthenticationKey == "" {
logger.Info(ctx, "could not find cookie authentication key. generating one...")
cookieAuthenticationKey, err := gorilla.GenerateRandomBytes(64)
if err != nil {
return nil, errors.Wrap(err, "could not generate cookie authentication key")
}
conf.HTTP.CookieAuthenticationKey = string(cookieAuthenticationKey)
}
// Generate random cookie encryption key if none is set
if conf.HTTP.CookieEncryptionKey == "" {
logger.Info(ctx, "could not find cookie encryption key. generating one...")
cookieEncryptionKey, err := gorilla.GenerateRandomBytes(32)
if err != nil {
return nil, errors.Wrap(err, "could not generate cookie encryption key")
}
conf.HTTP.CookieEncryptionKey = string(cookieEncryptionKey)
}
// Create and initialize HTTP session service provider
cookieStore := sessions.NewCookieStore(
[]byte(conf.HTTP.CookieAuthenticationKey),
[]byte(conf.HTTP.CookieEncryptionKey),
)
// Define default cookie options
cookieStore.Options = &sessions.Options{
Path: "/",
HttpOnly: true,
MaxAge: conf.HTTP.CookieMaxAge,
SameSite: http.SameSiteStrictMode,
}
ctn.Provide(
session.ServiceName,
gorilla.ServiceProvider("daddy", cookieStore),
)
// Create and expose config service provider
ctn.Provide(config.ServiceName, config.ServiceProvider(conf))
provider, err := oidc.NewProvider(ctx, conf.OIDC.IssuerURL)
if err != nil {
return nil, errors.Wrap(err, "could not create oidc provider")
}
ctn.Provide(oidc.ServiceName, oidc.ServiceProvider(
oidc.WithCredentials(conf.OIDC.ClientID, conf.OIDC.ClientSecret),
oidc.WithProvider(provider),
oidc.WithScopes("email", "openid"),
))
ctn.Provide(database.ServiceName, database.ServiceProvider(conf.Database.DSN))
dbpool, err := database.From(ctn)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve database service")
}
versionResolver := database.NewVersionResolver(dbpool)
if err := versionResolver.Init(ctx); err != nil {
return nil, errors.Wrap(err, "could not initialize database version resolver")
}
ctn.Provide(migration.ServiceName, migration.ServiceProvider(versionResolver))
ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider())
return ctn, nil
}

37
cmd/server/cqrs.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
"forge.cadoles.com/Cadoles/daddy/internal/command"
"forge.cadoles.com/Cadoles/daddy/internal/query"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/service"
)
func initCommands(ctn *service.Container) error {
dispatcher, err := cqrs.From(ctn)
if err != nil {
return errors.WithStack(err)
}
dispatcher.RegisterCommand(
cqrs.MatchCommandRequest(&command.CreateUserCommandRequest{}),
cqrs.CommandHandlerFunc(command.HandleCreateUserCommand),
)
return nil
}
func initQueries(ctn *service.Container) error {
dispatcher, err := cqrs.From(ctn)
if err != nil {
return errors.WithStack(err)
}
dispatcher.RegisterQuery(
cqrs.MatchQueryRequest(&query.FindUserQueryRequest{}),
cqrs.QueryHandlerFunc(query.HandleFindUserQuery),
)
return nil
}

200
cmd/server/main.go Normal file
View File

@ -0,0 +1,200 @@
package main
import (
"context"
"net/http"
"forge.cadoles.com/Cadoles/daddy/internal/config"
"forge.cadoles.com/Cadoles/daddy/internal/route"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"gitlab.com/wpetit/goweb/middleware/container"
"flag"
"fmt"
"log"
"os"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
//nolint: gochecknoglobals
var (
configFile = ""
workdir = ""
dumpConfig = false
version = false
migrate = ""
)
// nolint: gochecknoglobals
var (
GitRef = "unknown"
ProjectVersion = "unknown"
BuildDate = "unknown"
)
//nolint: gochecknoinits
func init() {
flag.StringVar(&configFile, "config", configFile, "configuration file")
flag.StringVar(&workdir, "workdir", workdir, "working directory")
flag.BoolVar(&dumpConfig, "dump-config", dumpConfig, "dump configuration and exit")
flag.BoolVar(&version, "version", version, "show version and exit")
flag.StringVar(&migrate, "migrate", migrate, "migrate data schema version and exit, possible values: latest, down, up")
}
func main() {
ctx := context.Background()
flag.Parse()
if version {
fmt.Printf("%s (%s) - %s\n", ProjectVersion, GitRef, BuildDate)
os.Exit(0)
}
// Switch to new working directory if defined
if workdir != "" {
if err := os.Chdir(workdir); err != nil {
logger.Fatal(
ctx,
"could not change working directory",
logger.E(err),
logger.F("workdir", workdir),
)
}
}
// Load configuration file if defined, use default configuration otherwise
var conf *config.Config
var err error
if configFile != "" {
conf, err = config.NewFromFile(configFile)
if err != nil {
log.Fatalf("%+v", errors.Wrapf(err, " '%s'", configFile))
logger.Fatal(
ctx,
"could not load config file",
logger.E(err),
logger.F("configFile", configFile),
)
}
} else {
if dumpConfig {
conf = config.NewDumpDefault()
} else {
conf = config.NewDefault()
}
}
// Dump configuration if asked
if dumpConfig {
if err := config.Dump(conf, os.Stdout); err != nil {
logger.Fatal(
ctx,
"could not dump config",
logger.E(err),
)
}
os.Exit(0)
}
if err := config.WithEnvironment(conf); err != nil {
logger.Fatal(
ctx,
"could not override config with environment",
logger.E(err),
)
}
logger.Info(
ctx,
"starting",
logger.F("gitRef", GitRef),
logger.F("projectVersion", ProjectVersion),
logger.F("buildDate", BuildDate),
)
logger.Debug(ctx, "setting log format", logger.F("format", conf.Log.Format))
logger.SetFormat(conf.Log.Format)
logger.Debug(ctx, "setting log level", logger.F("level", conf.Log.Level.String()))
logger.SetLevel(conf.Log.Level)
// Create service container
ctn, err := getServiceContainer(ctx, conf)
if err != nil {
logger.Fatal(
ctx,
"could not create service container",
logger.E(err),
)
}
ctx = container.WithContainer(ctx, ctn)
if migrate != "" {
if err := applyMigration(ctx, ctn); err != nil {
logger.Fatal(
ctx,
"could not apply migration",
logger.E(err),
)
}
os.Exit(0)
}
// Init commands and queries
if err := initCommands(ctn); err != nil {
logger.Fatal(
ctx,
"could not init commands",
logger.E(err),
)
}
if err := initQueries(ctn); err != nil {
logger.Fatal(
ctx,
"could not init queries",
logger.E(err),
)
}
r := chi.NewRouter()
// Define base middlewares
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Expose service container on router
r.Use(container.ServiceContainer(ctn))
// Define routes
if err := route.Mount(r, conf); err != nil {
logger.Fatal(
ctx,
"could not mount http routes",
logger.E(err),
)
}
logger.Info(ctx, "listening", logger.F("address", conf.HTTP.Address))
if err := http.ListenAndServe(conf.HTTP.Address, r); err != nil {
logger.Fatal(
ctx,
"could not listen",
logger.E(err),
logger.F("address", conf.HTTP.Address),
)
}
}

102
cmd/server/migration.go Normal file
View File

@ -0,0 +1,102 @@
package main
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"forge.cadoles.com/Cadoles/daddy/internal/migration"
"github.com/jackc/pgx/v4"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/service"
)
const (
migrateUp = "up"
migrateLatest = "latest"
migrateDown = "down"
)
func applyMigration(ctx context.Context, ctn *service.Container) error {
migr, err := migration.From(ctn)
if err != nil {
return err
}
// Register available migrations
migr.Register(
m000initialSchema(),
)
currentVersion, err := migr.CurrentVersion(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve current data schema version")
}
switch migrate {
case migrateUp:
if err := migr.Up(ctx); err != nil {
return errors.Wrap(err, "could not apply up migration")
}
case migrateLatest:
latestVersion, err := migr.LatestVersion()
if err != nil {
return errors.Wrap(err, "could not retrieve latest data schema version")
}
logger.Info(
ctx,
"migrating data schema to latest version",
logger.F("currentVersion", currentVersion),
logger.F("latestVersion", latestVersion),
)
// Execute migration to latest available version
if err := migr.Latest(ctx); err != nil {
return errors.Wrap(err, "could not migrate to latest data schema")
}
case migrateDown:
if err := migr.Down(ctx); err != nil {
return errors.Wrap(err, "could not apply down migration")
}
default:
return errors.Errorf("unknown migration command: '%s'", migrate)
}
logger.Info(
ctx,
"migration completed",
)
return nil
}
func m000initialSchema() migration.Migration {
return database.NewMigration(
"00_initial_schema",
func(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
connected_at TIMESTAMPTZ,
CONSTRAINT unique_email unique(email)
);
`)
return err
},
func(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
DROP TABLE users;
`)
return err
},
)
}

View File

@ -1,26 +1,5 @@
version: '2.4'
services:
super-graph:
build:
context: ./misc/containers/super-graph
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- http_proxy=${http_proxy}
- https_proxy=${https_proxy}
environment:
- SG_DATABASE_HOST=postgres
- SG_DATABASE_USER=daddy
- SG_DATABASE_PASSWORD=daddy
- USER_ID=${USER_ID}
- GO_ENV=dev
volumes:
- ./backend:/app
links:
- postgres
ports:
- 8080:8080
postgres:
build:
context: ./misc/containers/postgres
@ -48,15 +27,12 @@ services:
SUPPORTED_CLAIMS: email,email_verified
SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX
HYDRA_ADMIN_URL: http://localhost:4445
SERVE_PUBLIC_CORS_ENABLED: "true"
SERVE_PUBLIC_CORS_ALLOWED_ORIGINS: http://localhost:8081
WEBFINGER_JWKS_BROADCAST_KEYS: hydra.openid.id-token,hydra.jwt.access-token
ports:
- 4444:4444
command: hydra serve all --dangerous-force-http
hydra-passwordless:
image: bornholm/hydra-passwordless
image: bornholm/hydra-passwordless:latest
ports:
- 3000:3000
environment:

View File

@ -1,31 +0,0 @@
import React, { useEffect } from 'react';
import { Page } from '../Page';
import { useDispatch } from 'react-redux';
import { logout, login, handleOAuth2Callback } from '../../store/actions/auth';
export function OAuth2Page({ match, location, history }) {
const dispatch = useDispatch();
const { action } = match.params;
useEffect(() => {
switch(action) {
case 'logout':
dispatch(logout());
history.push("/");
break;
case 'login':
dispatch(login());
break;
case 'callback':
dispatch(handleOAuth2Callback(location.search));
history.push("/");
break;
}
}, [action]);
return (
<Page title="Daddy - OAuth2">
</Page>
);
}

View File

@ -1,21 +0,0 @@
export const Config = {
// The OpenID Connect client_id
oauth2ClientId: get<string>("oauth2ClientId", "daddy"),
oauth2Scope: get<string>("oauth2Scope", "email email_verified openid offline_access"),
oauth2RedirectURI: get<string>("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"),
oauth2Audience: get<string>("oauth2Audience", ""),
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081"),
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8080/api/v1/graphql")
};
function get<T>(key: string, defaultValue: T):T {
const config = window['__CONFIG__'] || {};
if (config && config.hasOwnProperty(key)) {
return config[key] as T;
} else {
return defaultValue;
}
}

View File

@ -1,69 +0,0 @@
import { Action } from "redux";
import { AccessGrant } from "../../util/auth";
import { IdToken } from "../../types/idToken";
export const LOGOUT = "LOGOUT_REQUEST";
export function logout() {
return { type: LOGOUT };
};
export const LOGIN_REQUEST = "LOGIN_REQUEST";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAILURE = "LOGIN_FAILURE";
export function login() {
return { type: LOGIN_REQUEST };
};
export const HANDLE_OAUTH2_CALLBACK_REQUEST = "HANDLE_OAUTH2_CALLBACK_REQUEST";
export const HANDLE_OAUTH2_CALLBACK_SUCCESS = "HANDLE_OAUTH2_CALLBACK_SUCCESS";
export const HANDLE_OAUTH2_CALLBACK_FAILURE = "HANDLE_OAUTH2_CALLBACK_FAILURE";
export interface handleOAuth2CallbackAction extends Action {
search: string
}
export function handleOAuth2Callback(search: string): handleOAuth2CallbackAction {
return { type: HANDLE_OAUTH2_CALLBACK_REQUEST, search };
};
export interface handleOAuth2CallbackSuccessAction extends Action {
grant: AccessGrant
}
export function handleOAuth2CallbackSuccess(grant: AccessGrant): handleOAuth2CallbackSuccessAction {
return { type: HANDLE_OAUTH2_CALLBACK_SUCCESS, grant };
};
export const PARSE_ID_TOKEN_REQUEST = "PARSE_ID_TOKEN_REQUEST";
export const PARSE_ID_TOKEN_SUCCESS = "PARSE_ID_TOKEN_SUCCESS";
export const PARSE_ID_TOKEN_FAILURE = "PARSE_ID_TOKEN_FAILURE";
export interface parseIdTokenAction extends Action {
rawIdToken: string
};
export function parseIdToken(rawIdToken: string): parseIdTokenAction {
return { type: PARSE_ID_TOKEN_REQUEST, rawIdToken };
};
export interface parseIdTokenSuccessAction extends Action {
idToken: IdToken
}
export function parseIdTokenSuccess(idToken: IdToken): parseIdTokenSuccessAction {
return { type: PARSE_ID_TOKEN_SUCCESS, idToken };
};
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
export interface setCurrentUserAction extends Action {
email: string
}
export function setCurrentUser(email: string): setCurrentUserAction {
return { type: SET_CURRENT_USER, email };
}

View File

@ -1,98 +0,0 @@
import { put, takeLatest, all } from 'redux-saga/effects';
import {
LOGOUT, LOGIN_REQUEST,
HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackAction,
HANDLE_OAUTH2_CALLBACK_FAILURE, handleOAuth2CallbackSuccess,
parseIdTokenAction, parseIdToken,
PARSE_ID_TOKEN_REQUEST, PARSE_ID_TOKEN_FAILURE, parseIdTokenSuccess,
setCurrentUser, LOGIN_FAILURE,
} from '../actions/auth';
import {
createLoginSession, LoginSession,
createAccessTokenRequest, saveAccessGrant,
saveLoginSessionState, getSavedLoginSessionState,
getLogoutURL, getSavedAccessGrant, clearAccessGrant
} from '../../util/auth';
import qs from 'qs';
import { UnauthorizedError } from '../../util/daddy';
import jwtDecode from 'jwt-decode';
import { IdToken } from '../../types/idToken';
export function* authRootSaga() {
yield all([
takeLatest(LOGIN_REQUEST, loginSaga),
takeLatest(LOGOUT, logoutSaga),
takeLatest(HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackSaga),
takeLatest(PARSE_ID_TOKEN_REQUEST, parseIDTokenSaga),
]);
}
export function* loginSaga() {
try {
const loginSession: LoginSession = yield createLoginSession();
console.log('Code verifier is ', loginSession.verifier);
console.log('State is ', loginSession.state);
saveLoginSessionState(loginSession.verifier, loginSession.state);
window.location.replace(loginSession.redirectUrl);
} catch(err) {
yield put({ type: LOGIN_FAILURE, err });
}
}
export function* logoutSaga() {
const accessGrant = getSavedAccessGrant();
const logoutURL = getLogoutURL(accessGrant.id_token);
clearAccessGrant();
window.location.replace(logoutURL);
}
export function* handleOAuth2CallbackSaga({ search }: handleOAuth2CallbackAction) {
const query = search.substring(1);
const params = qs.parse(query);
const loginSession = getSavedLoginSessionState();
console.log('Stored state verifier is', loginSession.state);
if (loginSession.state !== params.state) {
yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err: new Error("Invalid state") });
return;
}
console.log('Stored code verifier is', loginSession.verifier);
console.log('Authorization code is', params.code);
const req = createAccessTokenRequest(params.code as string, loginSession.verifier);
let grant;
try {
grant = yield fetch(req.url, { method: "POST", body: req.data })
.then(res => {
if (res.status === 401) return Promise.reject(new UnauthorizedError());
return res;
})
.then(res => res.json());
} catch(err) {
yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err });
return;
}
console.log("Access grant is", grant);
saveAccessGrant(grant);
yield put(handleOAuth2CallbackSuccess(grant));
yield put(parseIdToken(grant.id_token));
};
export function* parseIDTokenSaga({ rawIdToken }: parseIdTokenAction) {
let idToken: IdToken;
try {
idToken = jwtDecode(rawIdToken);
} catch(err) {
yield put({ type: PARSE_ID_TOKEN_FAILURE, err });
return;
}
yield put(parseIdTokenSuccess(idToken));
yield put(setCurrentUser(idToken.email));
};

View File

@ -1,18 +0,0 @@
import { all, put } from "redux-saga/effects";
import { getSavedAccessGrant } from "../../util/auth";
import { parseIdToken } from "../actions/auth";
export function* initRootSaga() {
yield all([
retrieveSessionSaga(),
]);
}
export function* retrieveSessionSaga() {
console.log("Checking session status...");
const accessGrant = getSavedAccessGrant();
if (!accessGrant) return;
yield put(parseIdToken(accessGrant.id_token));
}

View File

@ -1,3 +0,0 @@
export interface IdToken {
email: string
}

View File

@ -1,126 +0,0 @@
import { Config } from '../config';
export interface LoginSession {
state: string
redirectUrl: string
verifier: string
}
export function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
export function sha256(plain): PromiseLike<any> {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
export function pkceChallengeFromVerifier(v): PromiseLike<string> {
return sha256(v)
.then(hashed => base64urlencode(hashed));
}
export function base64urlencode(str) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
export function createLoginSession(): Promise<LoginSession> {
// Based on https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce
const state = generateRandomString();
const verifier = generateRandomString();
return new Promise<LoginSession>((resolve, reject) => {
try {
pkceChallengeFromVerifier(verifier).then(challenge => {
console.log('Code challenge is', challenge);
let redirectUrl=`${Config.oauth2AuthorizeURL}`;
redirectUrl += `?audience=${encodeURIComponent(Config.oauth2Audience)}`;
redirectUrl += `&scope=${encodeURIComponent(Config.oauth2Scope)}`;
redirectUrl += `&response_type=code`;
redirectUrl += `&client_id=${encodeURIComponent(Config.oauth2ClientId)}`
redirectUrl += `&code_challenge=${encodeURIComponent(challenge)}`;
redirectUrl += `&code_challenge_method=S256`
redirectUrl += `&redirect_uri=${encodeURIComponent(Config.oauth2RedirectURI)}`;
redirectUrl += `&state=${encodeURIComponent(state)}`;
return resolve({
state,
redirectUrl,
verifier,
});
});
} catch(err) {
return reject(err);
}
});
};
export interface AccessTokenRequest {
data: FormData,
url: string
}
export function createAccessTokenRequest(code: string, verifier: string): AccessTokenRequest {
const data = new FormData();
data.append('grant_type', 'authorization_code');
data.append('client_id', Config.oauth2ClientId);
data.append('code_verifier', verifier);
data.append('code', code);
data.append('redirect_uri', Config.oauth2RedirectURI);
return {
url: Config.oauth2TokenURL,
data,
};
};
export function getLogoutURL(rawIdToken: string): string {
let logoutURL = Config.oauth2LogoutURL;
logoutURL += `?post_logout_redirect_uri=${encodeURIComponent(Config.oauth2PostLogoutRedirectURI)}`;
logoutURL += `&id_token_hint=${encodeURIComponent(rawIdToken)}`;
return logoutURL;
}
export interface AccessGrant {
access_token: string
expires_in: number
id_token: string
refresh_token: string
scope: string
token_type: string
}
export function saveLoginSessionState(verifier: string, state: string) {
window.localStorage.setItem('login_verifier', verifier);
window.localStorage.setItem('login_state', state);
}
export function getSavedLoginSessionState(cleanup = true) {
const loginSession = {
verifier: window.localStorage.getItem('login_verifier'),
state: window.localStorage.getItem('login_state')
};
if (cleanup) {
window.localStorage.removeItem('login_verifier');
window.localStorage.removeItem('login_state');
}
return loginSession;
}
export function saveAccessGrant(grant: AccessGrant) {
window.localStorage.setItem('access_grant', JSON.stringify(grant));
}
export function getSavedAccessGrant(): AccessGrant {
const raw = window.localStorage.getItem('access_grant');
if (raw === "") return null;
return JSON.parse(raw) as AccessGrant;
}
export function clearAccessGrant() {
window.localStorage.removeItem('access_grant');
}

18
go.mod Normal file
View File

@ -0,0 +1,18 @@
module forge.cadoles.com/Cadoles/daddy
go 1.14
require (
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032
github.com/99designs/gqlgen v0.11.3
github.com/caarlos0/env/v6 v6.2.2
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 // indirect
github.com/go-chi/chi v4.1.0+incompatible
github.com/gorilla/sessions v1.2.0
github.com/jackc/pgx v3.6.2+incompatible
github.com/jackc/pgx/v4 v4.7.1
github.com/pkg/errors v0.9.1
github.com/vektah/gqlparser/v2 v2.0.1
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
gopkg.in/yaml.v2 v2.2.8
)

482
go.sum Normal file
View File

@ -0,0 +1,482 @@
cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ=
cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ=
cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032 h1:qTYaLPsLDlvqDkatONsvrisvfvpHaGe3lQqIaX7FFQQ=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032/go.mod h1:gkfqGyk7fCj2Z0ngEOCJ3K0FVmqft/8dFV/OnYT1vec=
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw=
github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/caarlos0/env/v6 v6.2.2 h1:R0NIFXaB/LhwuGrjnsldzpnVNjFU/U+hTVHt+cq0yDY=
github.com/caarlos0/env/v6 v6.2.2/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 h1:3CQigZV4Vgu4XX34CGsQFHbO5re8boAbn0dqUza1LrQ=
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450/go.mod h1:nZYoHDEpIB+Hv0ns85UxQDkHQ1uuaUQIFJ99VPctjq8=
github.com/cortesi/moddwatch v0.0.0-20200427000745-d26468c93cf0 h1:7tjBO+RH4BoxJUUysxGORQI27+72DfxxA2+i3Tixey0=
github.com/cortesi/moddwatch v0.0.0-20200427000745-d26468c93cf0/go.mod h1:QYGP4Q0SeEUNSC+dsNSKTmONSd1PpZVYUXIRAzxxpXo=
github.com/cortesi/termlog v0.0.0-20190809035425-7871d363854c h1:D5UylL3xKRrrqZKk/NhrOhoQVdCQwuEeyFgTfN9n9O4=
github.com/cortesi/termlog v0.0.0-20190809035425-7871d363854c/go.mod h1:gh6GQA3zOsGU4pz+X6ZHqW63KxI/V7KLmBCG9ODJ+l4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w=
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q=
github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.6.1 h1:lwofaXKPbIx6qEaK8mNm7uZuOwxHw+PnAFGDsDFpkRI=
github.com/jackc/pgconn v1.6.1/go.mod h1:g8mKMqmSUO6AzAvha7vy07g1rbGOlc7iF0nU0ei83hc=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8 h1:Q3tB+ExeflWUW7AFcAhXqk40s9mnNYLk1nOkKNZ5GnU=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.4.0 h1:pHQfb4jh9iKqHyxPthq1fr+0HwSNIl3btYPbw2m2lbM=
github.com/jackc/pgtype v1.4.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.7.1 h1:aqUSOcStk6fik+lSE+tqfFhvt/EwT8q/oMtJbP9CjXI=
github.com/jackc/pgx/v4 v4.7.1/go.mod h1:nu42q3aPjuC1M0Nak4bnoprKlXPINqopEKqbq5AZSC4=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU=
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce h1:B3inZUHFr/FpA3jb+ZeSSHk3FSpB0xkQ0TjePhRokxw=
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2 h1:9WJw0v6BzHV8fP8EywjcqAz8PyCZxLn8ioTiMP4SBog=
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191202203127-2b6af5f9ace7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

2
internal/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/server.go
/graph/generated

View File

@ -0,0 +1,99 @@
package command
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
const (
createConnectedUserStatement = `
INSERT INTO users (email, connected_at) VALUES ($1, now())
ON CONFLICT ON CONSTRAINT unique_email
DO UPDATE SET connected_at = now();
`
createUserStatement = `
INSERT INTO users (email) VALUES ($1)
ON CONFLICT ON CONSTRAINT unique_email
DO NOTHING;
`
)
type CreateUserCommandRequest struct {
Email string
Connected bool
}
func HandleCreateUserCommand(ctx context.Context, cmd cqrs.Command) error {
req, ok := cmd.Request().(*CreateUserCommandRequest)
if !ok {
return errors.WithStack(cqrs.ErrUnexpectedRequest)
}
ctn, err := container.From(ctx)
if err != nil {
return errors.WithStack(err)
}
pool, err := database.From(ctn)
if err != nil {
return errors.WithStack(err)
}
conn, err := pool.Acquire(ctx)
if err != nil {
return errors.WithStack(err)
}
defer conn.Release()
if req.Connected {
if err := createConnectedUser(ctx, conn, req.Email); err != nil {
return errors.WithStack(err)
}
} else {
if err := createUser(ctx, conn, req.Email); err != nil {
return errors.WithStack(err)
}
}
return nil
}
func createConnectedUser(ctx context.Context, conn *pgxpool.Conn, email string) error {
_, err := conn.Conn().Prepare(
ctx, "create_connected_user",
createConnectedUserStatement,
)
if err != nil {
return errors.WithStack(err)
}
if _, err := conn.Exec(ctx, "create_connected_user", email); err != nil {
return errors.WithStack(err)
}
return nil
}
func createUser(ctx context.Context, conn *pgxpool.Conn, email string) error {
_, err := conn.Conn().Prepare(
ctx, "create_user",
createUserStatement,
)
if err != nil {
return errors.WithStack(err)
}
if _, err := conn.Exec(ctx, "create_user", email); err != nil {
return errors.WithStack(err)
}
return nil
}

117
internal/config/config.go Normal file
View File

@ -0,0 +1,117 @@
package config
import (
"io"
"io/ioutil"
"time"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"github.com/caarlos0/env/v6"
"gopkg.in/yaml.v2"
)
type Config struct {
Debug bool `yaml:"debug" env:"DEBUG"`
Log LogConfig `yaml:"log"`
HTTP HTTPConfig `yaml:"http"`
OIDC OIDCConfig `yaml:"oidc"`
Database DatabaseConfig `yaml:"database"`
}
// NewFromFile retrieves the configuration from the given file
func NewFromFile(filepath string) (*Config, error) {
config := NewDefault()
data, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, errors.Wrapf(err, "could not read file '%s'", filepath)
}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, errors.Wrapf(err, "could not unmarshal configuration")
}
return config, nil
}
type HTTPConfig struct {
Address string `yaml:"address" env:"HTTP_ADDRESS"`
CookieAuthenticationKey string `yaml:"cookieAuthenticationKey" env:"HTTP_COOKIE_AUTHENTICATION_KEY"`
CookieEncryptionKey string `yaml:"cookieEncryptionKey" env:"HTTP_COOKIE_ENCRYPTION_KEY"`
CookieMaxAge int `yaml:"cookieMaxAge" env:"HTTP_COOKIE_MAX_AGE"`
TemplateDir string `yaml:"templateDir" env:"HTTP_TEMPLATE_DIR"`
PublicDir string `yaml:"publicDir" env:"HTTP_PUBLIC_DIR"`
FrontendURL string `yaml:"frontendURL" env:"HTTP_FRONTEND_URL"`
}
type OIDCConfig struct {
ClientID string `yaml:"clientId" env:"OIDC_CLIENT_ID"`
ClientSecret string `yaml:"clientSecret" env:"OIDC_CLIENT_SECRET"`
IssuerURL string `ymal:"issuerUrl" env:"OIDC_ISSUER_URL"`
RedirectURL string `yaml:"redirectUrl" env:"OIDC_REDIRECT_URL"`
PostLogoutRedirectURL string `yaml:"postLogoutRedirectURL" env:"OIDC_POST_LOGOUT_REDIRECT_URL"`
}
type LogConfig struct {
Level logger.Level `yaml:"level" env:"LOG_LEVEL"`
Format logger.Format `yaml:"format" env:"LOG_FORMAT"`
}
type DatabaseConfig struct {
DSN string `yaml:"dsn" env:"DATABASE_DSN"`
}
func NewDumpDefault() *Config {
config := NewDefault()
return config
}
func NewDefault() *Config {
return &Config{
Debug: false,
Log: LogConfig{
Level: logger.LevelInfo,
Format: logger.FormatHuman,
},
HTTP: HTTPConfig{
Address: ":8081",
CookieAuthenticationKey: "",
CookieEncryptionKey: "",
CookieMaxAge: int((time.Hour * 1).Seconds()), // 1 hour
TemplateDir: "template",
PublicDir: "public",
FrontendURL: "http://localhost:8080",
},
OIDC: OIDCConfig{
IssuerURL: "http://localhost:4444/",
RedirectURL: "http://localhost:8081/oauth2/callback",
PostLogoutRedirectURL: "http://localhost:8081",
},
Database: DatabaseConfig{
DSN: "host=localhost database=daddy",
},
}
}
func Dump(config *Config, w io.Writer) error {
data, err := yaml.Marshal(config)
if err != nil {
return errors.Wrap(err, "could not dump config")
}
if _, err := w.Write(data); err != nil {
return err
}
return nil
}
func WithEnvironment(conf *Config) error {
if err := env.Parse(conf); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,9 @@
package config
import "gitlab.com/wpetit/goweb/service"
func ServiceProvider(config *Config) service.Provider {
return func(ctn *service.Container) (interface{}, error) {
return config, nil
}
}

View File

@ -0,0 +1,33 @@
package config
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "config"
// From retrieves the config service in the given container
func From(container *service.Container) (*Config, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Config)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the config service in the given container or panic otherwise
func Must(container *service.Container) *Config {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

View File

@ -0,0 +1,79 @@
package database
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
)
type MigrationFunc func(ctx context.Context, tx pgx.Tx) error
type Migration struct {
version string
up MigrationFunc
down MigrationFunc
}
func (m *Migration) Version() string {
return m.version
}
func (m *Migration) Up(ctx context.Context) error {
pool, err := m.getDatabaseService(ctx)
if err != nil {
return err
}
err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error {
return m.up(ctx, tx)
})
if err != nil {
return errors.Wrap(err, "could not apply up migration")
}
return nil
}
func (m *Migration) Down(ctx context.Context) error {
pool, err := m.getDatabaseService(ctx)
if err != nil {
return err
}
err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error {
return m.down(ctx, tx)
})
if err != nil {
return errors.Wrap(err, "could not apply down migration")
}
return nil
}
func (m *Migration) getDatabaseService(ctx context.Context) (*pgxpool.Pool, error) {
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve service container")
}
pool, err := From(ctn)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve database service")
}
return pool, nil
}
func NewMigration(version string, up, down MigrationFunc) *Migration {
return &Migration{
version: version,
up: up,
down: down,
}
}

View File

@ -0,0 +1,24 @@
package database
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
func ServiceProvider(dsn string) service.Provider {
pool, err := pgxpool.Connect(context.Background(), dsn)
if err != nil {
err = errors.Wrap(err, "could not connect to database")
}
return func(ctn *service.Container) (interface{}, error) {
if err != nil {
return nil, err
}
return pool, nil
}
}

View File

@ -0,0 +1,34 @@
package database
import (
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "database"
// From retrieves the database pool service in the given container.
func From(container *service.Container) (*pgxpool.Pool, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*pgxpool.Pool)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the database pool service in the given container or panic otherwise.
func Must(container *service.Container) *pgxpool.Pool {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

38
internal/database/tx.go Normal file
View File

@ -0,0 +1,38 @@
package database
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
)
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(context.Context, pgx.Tx) error) error {
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return errors.Wrap(err, "could not begin transaction")
}
defer func() {
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
panic(errors.Wrap(err, "could not rollback transaction"))
}
}()
if err := fn(ctx, tx); err != nil {
err := errors.Wrap(err, "could not apply down migration")
if rollbackErr := tx.Rollback(ctx); rollbackErr != nil {
return errors.Wrap(err, rollbackErr.Error())
}
return err
}
if err := tx.Commit(ctx); err != nil {
return errors.Wrap(err, "could not commit transaction")
}
return nil
}

View File

@ -0,0 +1,94 @@
package database
import (
"context"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/pkg/errors"
)
type VersionResolver struct {
pool *pgxpool.Pool
}
func (r *VersionResolver) Current(ctx context.Context) (string, error) {
var version string
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
err := tx.QueryRow(ctx, `SELECT version FROM database_schema WHERE is_current = true;`).
Scan(&version)
if errors.Is(err, pgx.ErrNoRows) {
return nil
}
return err
})
if err != nil {
return "", errors.Wrap(err, "could execute version resolver init transaction")
}
return version, nil
}
func (r *VersionResolver) Set(ctx context.Context, version string) error {
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
if version != "" {
_, err := tx.Exec(ctx, `
INSERT INTO database_schema (version, is_current, migrated_at)
VALUES
(
$1,
true,
now()
)
ON CONFLICT ON CONSTRAINT unique_version
DO UPDATE SET migrated_at = now(), is_current = true;
`, version)
if err != nil {
return err
}
}
_, err := tx.Exec(ctx, `
UPDATE database_schema SET is_current = false, migrated_at = null WHERE version <> $1;
`, version)
return err
})
if err != nil {
return errors.Wrap(err, "could not update schema version")
}
return nil
}
func (r *VersionResolver) Init(ctx context.Context) error {
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
CREATE TABLE IF NOT EXISTS database_schema(
version TEXT NOT NULL,
migrated_at TIME,
is_current BOOLEAN,
CONSTRAINT unique_version UNIQUE(version)
);`)
return err
})
if err != nil {
return errors.Wrap(err, "could execute version resolver init transaction")
}
return nil
}
func NewVersionResolver(pool *pgxpool.Pool) *VersionResolver {
return &VersionResolver{
pool: pool,
}
}

56
internal/gqlgen.yml Normal file
View File

@ -0,0 +1,56 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls
# Where should the generated server code go?
exec:
filename: graph/generated/generated.go
package: generated
# Uncomment to enable federation
# federation:
# filename: graph/generated/federation.go
# package: generated
# Where should any generated models go?
model:
filename: graph/model/models_gen.go
package: model
# Where should the resolver implementations go?
resolver:
layout: follow-schema
dir: graph
package: graph
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "forge.cadoles.com/Cadoles/daddy/internal/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32

View File

@ -0,0 +1,14 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
import (
"time"
)
type User struct {
Name *string `json:"name"`
Email string `json:"email"`
ConnectedAt time.Time `json:"connectedAt"`
CreatedAt time.Time `json:"createdAt"`
}

View File

@ -0,0 +1,7 @@
package graph
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{}

View File

@ -0,0 +1,16 @@
# GraphQL schema example
#
# https://gqlgen.com/getting-started/
scalar Time
type User {
name: String
email: String!
connectedAt: Time!
createdAt: Time!
}
type Query {
userProfile: User
}

View File

@ -0,0 +1,20 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
)
func (r *queryResolver) UserProfile(ctx context.Context) (*model.User, error) {
return handleUserProfile(ctx)
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }

View File

@ -0,0 +1,45 @@
package graph
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
"forge.cadoles.com/Cadoles/daddy/internal/query"
"forge.cadoles.com/Cadoles/daddy/internal/session"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
func handleUserProfile(ctx context.Context) (*model.User, error) {
userEmail, err := session.UserEmail(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
dispatcher, err := cqrs.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
qry := &query.FindUserQueryRequest{
Email: userEmail,
}
result, err := dispatcher.Query(ctx, qry)
if err != nil {
return nil, errors.WithStack(err)
}
findUserData, ok := result.Data().(*query.FindUserData)
if !ok {
return nil, errors.WithStack(cqrs.ErrUnexpectedData)
}
return findUserData.User, nil
}

View File

@ -0,0 +1,146 @@
package migration
import (
"context"
"github.com/pkg/errors"
)
var (
ErrNoAvailableMigration = errors.New("no available migration")
ErrMigrationNotFound = errors.New("migration not found")
)
type Manager struct {
migrations []Migration
resolver VersionResolver
}
func (m *Manager) Up(ctx context.Context) error {
currentVersion, err := m.resolver.Current(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve current version")
}
migrate := func(up Migration) error {
if err := up.Up(ctx); err != nil {
return errors.Wrapf(err, "could not apply '%s' up migration", up.Version())
}
if err := m.resolver.Set(ctx, up.Version()); err != nil {
return errors.Wrapf(err, "could not update schema version to '%s'", up.Version())
}
return nil
}
if currentVersion == "" {
up := m.migrations[0]
return migrate(up)
}
for i, mi := range m.migrations {
if mi.Version() != currentVersion && currentVersion != "" {
continue
}
// Already at latest, do nothing
if i >= len(m.migrations)-1 {
return nil
}
up := m.migrations[i+1]
return migrate(up)
}
return errors.WithStack(ErrMigrationNotFound)
}
func (m *Manager) Down(ctx context.Context) error {
currentVersion, err := m.resolver.Current(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve current version")
}
for i, mi := range m.migrations {
if mi.Version() != currentVersion {
continue
}
if err := mi.Down(ctx); err != nil {
return errors.Wrapf(err, "could not apply '%s' down migration", mi.Version())
}
var version string
// Already at oldest, do nothing
if i != 0 {
down := m.migrations[i-1]
version = down.Version()
}
if err := m.resolver.Set(ctx, version); err != nil {
return errors.Wrapf(err, "could not update schema version to '%s'", version)
}
return nil
}
return errors.WithStack(ErrMigrationNotFound)
}
func (m *Manager) Latest(ctx context.Context) error {
for {
isLatest, err := m.IsLatest(ctx)
if err != nil {
return errors.Wrap(err, "could not retrieve schema state")
}
if isLatest {
return nil
}
if err := m.Up(ctx); err != nil {
return errors.WithStack(err)
}
}
}
func (m *Manager) Register(migrations ...Migration) {
m.migrations = migrations
}
func (m *Manager) CurrentVersion(ctx context.Context) (string, error) {
return m.resolver.Current(ctx)
}
func (m *Manager) LatestVersion() (string, error) {
if len(m.migrations) == 0 {
return "", errors.WithStack(ErrNoAvailableMigration)
}
return m.migrations[len(m.migrations)-1].Version(), nil
}
func (m *Manager) IsLatest(ctx context.Context) (bool, error) {
currentVersion, err := m.resolver.Current(ctx)
if err != nil {
return false, errors.Wrap(err, "could not retrieve current version")
}
latestVersion, err := m.LatestVersion()
if err != nil {
return false, errors.Wrap(err, "could not retrieve latest version")
}
return currentVersion == latestVersion, nil
}
func NewManager(resolver VersionResolver) *Manager {
return &Manager{
resolver: resolver,
migrations: make([]Migration, 0),
}
}

View File

@ -0,0 +1,9 @@
package migration
import "context"
type Migration interface {
Version() string
Up(context.Context) error
Down(context.Context) error
}

View File

@ -0,0 +1,13 @@
package migration
import (
"gitlab.com/wpetit/goweb/service"
)
func ServiceProvider(resolver VersionResolver) service.Provider {
manager := NewManager(resolver)
return func(ctn *service.Container) (interface{}, error) {
return manager, nil
}
}

View File

@ -0,0 +1,33 @@
package migration
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "migration"
// From retrieves the migration service in the given container.
func From(container *service.Container) (*Manager, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Manager)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the migration service in the given container or panic otherwise.
func Must(container *service.Container) *Manager {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

View File

@ -0,0 +1,8 @@
package migration
import "context"
type VersionResolver interface {
Current(context.Context) (string, error)
Set(context.Context, string) error
}

View File

@ -0,0 +1,71 @@
package query
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
"forge.cadoles.com/Cadoles/daddy/internal/database"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/cqrs"
"gitlab.com/wpetit/goweb/middleware/container"
)
const (
findUserStatement = `SELECT email, connected_at, created_at FROM users WHERE email = $1`
)
type FindUserQueryRequest struct {
Email string
}
type FindUserData struct {
User *model.User
}
func HandleFindUserQuery(ctx context.Context, qry cqrs.Query) (interface{}, error) {
req, ok := qry.Request().(*FindUserQueryRequest)
if !ok {
return nil, errors.WithStack(cqrs.ErrUnexpectedRequest)
}
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
pool, err := database.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
conn, err := pool.Acquire(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
defer conn.Release()
_, err = conn.Conn().Prepare(
ctx, "find_user",
findUserStatement,
)
if err != nil {
return nil, errors.WithStack(err)
}
user := &model.User{}
err = conn.QueryRow(ctx, "find_user", req.Email).
Scan(&user.Email, &user.ConnectedAt, &user.CreatedAt)
if err != nil {
return nil, errors.WithStack(err)
}
data := &FindUserData{
User: user,
}
return data, nil
}

81
internal/route/login.go Normal file
View File

@ -0,0 +1,81 @@
package route
import (
"net/http"
"forge.cadoles.com/Cadoles/daddy/internal/command"
"gitlab.com/wpetit/goweb/cqrs"
"forge.cadoles.com/Cadoles/daddy/internal/session"
"github.com/pkg/errors"
"forge.cadoles.com/Cadoles/daddy/internal/config"
oidc "forge.cadoles.com/wpetit/goweb-oidc"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/middleware/container"
)
func handleLogin(w http.ResponseWriter, r *http.Request) {
ctn := container.Must(r.Context())
client := oidc.Must(ctn)
client.Login(w, r)
}
type emailClaims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctn := container.Must(ctx)
conf := config.Must(ctn)
idToken, err := oidc.IDToken(w, r)
if err != nil {
logger.Error(ctx, "could not retrieve idToken", logger.E(err))
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
logger.Info(ctx, "user logged in", logger.F("sub", idToken.Subject))
claims := &emailClaims{}
if err := idToken.Claims(claims); err != nil {
panic(errors.WithStack(err))
}
// TODO implements better UX in case of errors
if claims.Email == "" {
http.Error(w, "an email is expected to access this app", http.StatusForbidden)
return
}
if !claims.EmailVerified {
http.Error(w, "your email must be verified to access this app", http.StatusForbidden)
return
}
dispatcher := cqrs.Must(ctn)
cmd := &command.CreateUserCommandRequest{
Email: claims.Email,
Connected: true,
}
if _, err := dispatcher.Exec(ctx, cmd); err != nil {
panic(errors.WithStack(err))
}
if err := session.SaveUserEmail(w, r, claims.Email); err != nil {
panic(errors.WithStack(err))
}
http.Redirect(w, r, conf.HTTP.FrontendURL, http.StatusSeeOther)
}

33
internal/route/logout.go Normal file
View File

@ -0,0 +1,33 @@
package route
import (
"net/http"
"forge.cadoles.com/Cadoles/daddy/internal/config"
oidc "forge.cadoles.com/wpetit/goweb-oidc"
"gitlab.com/wpetit/goweb/logger"
"gitlab.com/wpetit/goweb/middleware/container"
)
func handleLogout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctn := container.Must(ctx)
conf := config.Must(ctn)
client := oidc.Must(ctn)
logger.Info(
ctx,
"logging out user",
logger.F("postLogoutURL", conf.OIDC.PostLogoutRedirectURL),
)
client.Logout(w, r, conf.OIDC.PostLogoutRedirectURL)
}
func handleLogoutRedirect(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctn := container.Must(ctx)
conf := config.Must(ctn)
http.Redirect(w, r, conf.HTTP.FrontendURL, http.StatusSeeOther)
}

44
internal/route/mount.go Normal file
View File

@ -0,0 +1,44 @@
package route
import (
"forge.cadoles.com/Cadoles/daddy/internal/config"
"forge.cadoles.com/Cadoles/daddy/internal/graph"
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
"forge.cadoles.com/Cadoles/daddy/internal/session"
oidc "forge.cadoles.com/wpetit/goweb-oidc"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi"
"gitlab.com/wpetit/goweb/static"
)
func Mount(r *chi.Mux, config *config.Config) error {
r.With(oidc.HandleCallback).Get("/oauth2/callback", handleLoginCallback)
r.Get("/logout", handleLogout)
r.Get("/login", handleLogin)
r.Get("/logout/redirect", handleLogoutRedirect)
r.Route("/api", func(r chi.Router) {
r.Use(oidc.Middleware)
r.Use(session.UserEmailMiddleware)
gql := handler.NewDefaultServer(
generated.NewExecutableSchema(generated.Config{
Resolvers: &graph.Resolver{},
}),
)
if config.Debug {
r.Get("/v1/graphql", playground.Handler("GraphQL playground", "/api/v1/graphql"))
}
r.Post("/v1/graphql", gql.ServeHTTP)
})
notFoundHandler := r.NotFoundHandler()
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))
return nil
}

View File

@ -0,0 +1,97 @@
package session
import (
"context"
"net/http"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/service/session"
)
type contextKey string
const userEmailKey contextKey = "user_email"
var (
ErrUserEmailNotFound = errors.New("user email not found")
)
func UserEmailMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
userEmail, err := GetUserEmail(w, r)
if err != nil {
panic(errors.Wrap(err, "could not find user email"))
}
ctx := WithUserEmail(r.Context(), userEmail)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func WithUserEmail(ctx context.Context, email string) context.Context {
return context.WithValue(ctx, userEmailKey, email)
}
func UserEmail(ctx context.Context) (string, error) {
email, ok := ctx.Value(userEmailKey).(string)
if !ok {
return "", errors.WithStack(ErrUserEmailNotFound)
}
return email, nil
}
func SaveUserEmail(w http.ResponseWriter, r *http.Request, email string) error {
sess, err := getSession(w, r)
if err != nil {
return errors.WithStack(err)
}
sess.Set(string(userEmailKey), email)
if err := sess.Save(w, r); err != nil {
return errors.WithStack(err)
}
return nil
}
func GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
sess, err := getSession(w, r)
if err != nil {
return "", errors.WithStack(err)
}
email, ok := sess.Get(string(userEmailKey)).(string)
if !ok {
return "", errors.WithStack(ErrUserEmailNotFound)
}
return email, nil
}
func getSession(w http.ResponseWriter, r *http.Request) (session.Session, error) {
ctx := r.Context()
ctn, err := container.From(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
session, err := session.From(ctn)
if err != nil {
return nil, errors.WithStack(err)
}
sess, err := session.Get(w, r)
if err != nil {
return nil, errors.WithStack(err)
}
return sess, nil
}

View File

@ -1,9 +1,12 @@
#!/bin/sh
set -x
hydra clients create \
--id daddy \
--secret daddycool \
-n Daddy \
-a email,email_verified,offline_access,openid \
--token-endpoint-auth-method none \
--post-logout-callbacks http://localhost:8081 \
-a email,email_verified,openid \
--token-endpoint-auth-method client_secret_post \
--post-logout-callbacks http://localhost:8081/logout/redirect \
-c http://localhost:8081/oauth2/callback

View File

@ -1,36 +0,0 @@
FROM alpine:edge AS build
ARG HTTP_PROXY=
ARG HTTPS_PROXY=
ARG http_proxy=
ARG https_proxy=
ARG SUPERGRAPH_VERSION=88ba105b70c60b2c7467dc1f76f041cec2614a04
ARG WAITFORIT_VERSION=v2.4.1
RUN apk add --no-cache go make git curl bash ca-certificates
RUN git clone https://forge.cadoles.com/wpetit/super-graph.git \
&& export PATH="$PATH:/root/go/bin" \
&& export CGO_ENABLED=0 \
&& cd super-graph \
&& git checkout ${SUPERGRAPH_VERSION} \
&& make SHELL='bash -x' build
RUN curl -sL \
-o /usr/local/bin/waitforit \
https://github.com/maxcnunes/waitforit/releases/download/${WAITFORIT_VERSION}/waitforit-linux_amd64
FROM alpine:3.11
COPY --from=build /super-graph/super-graph /usr/local/bin/super-graph
COPY --from=build /usr/local/bin/waitforit /usr/local/bin/waitforit
RUN chmod +x /usr/local/bin/waitforit
WORKDIR /app
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint
RUN chmod +x /usr/local/bin/docker-entrypoint
CMD ["/usr/local/bin/docker-entrypoint"]

View File

@ -1,13 +0,0 @@
#!/bin/sh
set -eo pipefail
if [ ! -f /container-lifecycle/first_run ]; then
waitforit -debug -host $SG_DATABASE_HOST -port 5432
super-graph db:migrate up
super-graph db:seed
mkdir /container-lifecycle
touch /container-lifecycle/first_run
fi
super-graph serv

24
modd.conf Normal file
View File

@ -0,0 +1,24 @@
internal/graph/schema.graphqls
internal/gqlgen.yml {
prep: make generate
}
**/*.go
!**/*_test.go
data/config.yml
.env
modd.conf {
prep: make build-server
prep: [ -e data/config.yml ] || ( mkdir -p data && bin/server -dump-config > data/config.yml )
prep: [ -e .env ] || ( cp .env.dist .env )
prep: make migrate-latest
daemon: ( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml )
}
**/*.go {
prep: make test
}
{
daemon: cd client && NODE_ENV=development npm run server -- --display=minimal
}