diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..2cb36ba --- /dev/null +++ b/.env.dist @@ -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" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29c0e1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +/data +/bin +/.env \ No newline at end of file diff --git a/Makefile b/Makefile index cf6a758..80a6fac 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/README.md b/README.md index 43e9f35..1aae405 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/config/allow.list b/backend/config/allow.list deleted file mode 100644 index 92cf2d2..0000000 --- a/backend/config/allow.list +++ /dev/null @@ -1,18 +0,0 @@ -/* fetchUser */ - -variables { - "email": "" -} - - - query fetchUser { - user(where: {email: {eq: $email}}) { - id - created_at - updated_at - email, - full_name - } - } - - diff --git a/backend/config/dev.yml b/backend/config/dev.yml deleted file mode 100644 index a32f542..0000000 --- a/backend/config/dev.yml +++ /dev/null @@ -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 - - # - # 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: [] diff --git a/backend/config/migrations/0_init.sql b/backend/config/migrations/0_init.sql deleted file mode 100644 index 6db6d3c..0000000 --- a/backend/config/migrations/0_init.sql +++ /dev/null @@ -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 diff --git a/backend/config/prod.yml b/backend/config/prod.yml deleted file mode 100644 index 5987ea3..0000000 --- a/backend/config/prod.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/config/seed.js b/backend/config/seed.js deleted file mode 100644 index f016d11..0000000 --- a/backend/config/seed.js +++ /dev/null @@ -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 }); -} \ No newline at end of file diff --git a/frontend/.gitignore b/client/.gitignore similarity index 100% rename from frontend/.gitignore rename to client/.gitignore diff --git a/frontend/package-lock.json b/client/package-lock.json similarity index 100% rename from frontend/package-lock.json rename to client/package-lock.json diff --git a/frontend/package.json b/client/package.json similarity index 100% rename from frontend/package.json rename to client/package.json diff --git a/frontend/src/components/App.tsx b/client/src/components/App.tsx similarity index 81% rename from frontend/src/components/App.tsx rename to client/src/components/App.tsx index 53df347..3bea7f4 100644 --- a/frontend/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -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 { - } /> diff --git a/frontend/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx similarity index 100% rename from frontend/src/components/HomePage/HomePage.tsx rename to client/src/components/HomePage/HomePage.tsx diff --git a/frontend/src/components/Loader.tsx b/client/src/components/Loader.tsx similarity index 100% rename from frontend/src/components/Loader.tsx rename to client/src/components/Loader.tsx diff --git a/frontend/src/components/Modal.tsx b/client/src/components/Modal.tsx similarity index 100% rename from frontend/src/components/Modal.tsx rename to client/src/components/Modal.tsx diff --git a/frontend/src/components/Navbar.tsx b/client/src/components/Navbar.tsx similarity index 86% rename from frontend/src/components/Navbar.tsx rename to client/src/components/Navbar.tsx index d603547..def6d3b 100644 --- a/frontend/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -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(state => state.auth.isAuthenticated); @@ -26,18 +26,18 @@ export function Navbar() {
{ isAuthenticated ? - + Se déconnecter - : - + : + Se connecter - + }
diff --git a/frontend/src/components/Page.tsx b/client/src/components/Page.tsx similarity index 100% rename from frontend/src/components/Page.tsx rename to client/src/components/Page.tsx diff --git a/client/src/config.ts b/client/src/config.ts new file mode 100644 index 0000000..d8f51fe --- /dev/null +++ b/client/src/config.ts @@ -0,0 +1,14 @@ +export const Config = { + loginURL: get("loginURL", "http://localhost:8081/login"), + logoutURL: get("logoutURL", "http://localhost:8081/logout"), + graphQLEndpoint: get("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"), +}; + +function get(key: string, defaultValue: T):T { + const config = window['__CONFIG__'] || {}; + if (config && config.hasOwnProperty(key)) { + return config[key] as T; + } else { + return defaultValue; + } +} \ No newline at end of file diff --git a/frontend/src/custom.d.ts b/client/src/custom.d.ts similarity index 100% rename from frontend/src/custom.d.ts rename to client/src/custom.d.ts diff --git a/frontend/src/index.html b/client/src/index.html similarity index 100% rename from frontend/src/index.html rename to client/src/index.html diff --git a/frontend/src/index.tsx b/client/src/index.tsx similarity index 100% rename from frontend/src/index.tsx rename to client/src/index.tsx diff --git a/frontend/src/resources/config.sample.js b/client/src/resources/config.sample.js similarity index 100% rename from frontend/src/resources/config.sample.js rename to client/src/resources/config.sample.js diff --git a/frontend/src/resources/favicon.png b/client/src/resources/favicon.png similarity index 100% rename from frontend/src/resources/favicon.png rename to client/src/resources/favicon.png diff --git a/frontend/src/resources/logo.svg b/client/src/resources/logo.svg similarity index 100% rename from frontend/src/resources/logo.svg rename to client/src/resources/logo.svg diff --git a/frontend/src/sass/_all.scss b/client/src/sass/_all.scss similarity index 100% rename from frontend/src/sass/_all.scss rename to client/src/sass/_all.scss diff --git a/frontend/src/sass/_base.scss b/client/src/sass/_base.scss similarity index 100% rename from frontend/src/sass/_base.scss rename to client/src/sass/_base.scss diff --git a/frontend/src/sass/_loader.scss b/client/src/sass/_loader.scss similarity index 100% rename from frontend/src/sass/_loader.scss rename to client/src/sass/_loader.scss diff --git a/client/src/store/actions/auth.ts b/client/src/store/actions/auth.ts new file mode 100644 index 0000000..8a8c608 --- /dev/null +++ b/client/src/store/actions/auth.ts @@ -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 }; +} \ No newline at end of file diff --git a/frontend/src/store/actions/profile.ts b/client/src/store/actions/profile.ts similarity index 100% rename from frontend/src/store/actions/profile.ts rename to client/src/store/actions/profile.ts diff --git a/frontend/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts similarity index 79% rename from frontend/src/store/reducers/auth.ts rename to client/src/store/reducers/auth.ts index e888748..b25f0c4 100644 --- a/frontend/src/store/reducers/auth.ts +++ b/client/src/store/reducers/auth.ts @@ -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, diff --git a/frontend/src/store/reducers/flags.ts b/client/src/store/reducers/flags.ts similarity index 100% rename from frontend/src/store/reducers/flags.ts rename to client/src/store/reducers/flags.ts diff --git a/frontend/src/store/reducers/root.ts b/client/src/store/reducers/root.ts similarity index 100% rename from frontend/src/store/reducers/root.ts rename to client/src/store/reducers/root.ts diff --git a/frontend/src/store/sagas/failure.ts b/client/src/store/sagas/failure.ts similarity index 73% rename from frontend/src/store/sagas/failure.ts rename to client/src/store/sagas/failure.ts index 97bc18c..de90822 100644 --- a/frontend/src/store/sagas/failure.ts +++ b/client/src/store/sagas/failure.ts @@ -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(); } } diff --git a/client/src/store/sagas/init.ts b/client/src/store/sagas/init.ts new file mode 100644 index 0000000..fdd4fa2 --- /dev/null +++ b/client/src/store/sagas/init.ts @@ -0,0 +1,7 @@ +import { all, put } from "redux-saga/effects"; + +export function* initRootSaga() { + yield all([ + + ]); +} \ No newline at end of file diff --git a/frontend/src/store/sagas/root.ts b/client/src/store/sagas/root.ts similarity index 76% rename from frontend/src/store/sagas/root.ts rename to client/src/store/sagas/root.ts index 71fdf80..3b29c86 100644 --- a/frontend/src/store/sagas/root.ts +++ b/client/src/store/sagas/root.ts @@ -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(), ]); } diff --git a/frontend/src/store/sagas/users.ts b/client/src/store/sagas/users.ts similarity index 86% rename from frontend/src/store/sagas/users.ts rename to client/src/store/sagas/users.ts index 40521b3..42f0ac2 100644 --- a/frontend/src/store/sagas/users.ts +++ b/client/src/store/sagas/users.ts @@ -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 { diff --git a/frontend/src/store/selectors/flags.ts b/client/src/store/selectors/flags.ts similarity index 100% rename from frontend/src/store/selectors/flags.ts rename to client/src/store/selectors/flags.ts diff --git a/frontend/src/store/store.ts b/client/src/store/store.ts similarity index 100% rename from frontend/src/store/store.ts rename to client/src/store/store.ts diff --git a/frontend/src/types/user.ts b/client/src/types/user.ts similarity index 100% rename from frontend/src/types/user.ts rename to client/src/types/user.ts diff --git a/frontend/src/util/daddy.ts b/client/src/util/daddy.ts similarity index 89% rename from frontend/src/util/daddy.ts rename to client/src/util/daddy.ts index 953ea9d..9d3d104 100644 --- a/frontend/src/util/daddy.ts +++ b/client/src/util/daddy.ts @@ -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', } }); diff --git a/frontend/tsconfig.json b/client/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to client/tsconfig.json diff --git a/frontend/webpack.config.js b/client/webpack.config.js similarity index 98% rename from frontend/webpack.config.js rename to client/webpack.config.js index f552187..be40da5 100644 --- a/frontend/webpack.config.js +++ b/client/webpack.config.js @@ -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, }, diff --git a/cmd/server/container.go b/cmd/server/container.go new file mode 100644 index 0000000..4984004 --- /dev/null +++ b/cmd/server/container.go @@ -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 +} diff --git a/cmd/server/cqrs.go b/cmd/server/cqrs.go new file mode 100644 index 0000000..875c475 --- /dev/null +++ b/cmd/server/cqrs.go @@ -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 +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..a61c792 --- /dev/null +++ b/cmd/server/main.go @@ -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), + ) + } +} diff --git a/cmd/server/migration.go b/cmd/server/migration.go new file mode 100644 index 0000000..fedce18 --- /dev/null +++ b/cmd/server/migration.go @@ -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 + }, + ) +} diff --git a/docker-compose.yml b/docker-compose.yml index 873279d..9846a19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/frontend/src/components/OAuth2Page/OAuth2Page.tsx b/frontend/src/components/OAuth2Page/OAuth2Page.tsx deleted file mode 100644 index 7ae1daa..0000000 --- a/frontend/src/components/OAuth2Page/OAuth2Page.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/frontend/src/config.ts b/frontend/src/config.ts deleted file mode 100644 index 98ee0a4..0000000 --- a/frontend/src/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const Config = { - // The OpenID Connect client_id - oauth2ClientId: get("oauth2ClientId", "daddy"), - oauth2Scope: get("oauth2Scope", "email email_verified openid offline_access"), - oauth2RedirectURI: get("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"), - oauth2Audience: get("oauth2Audience", ""), - oauth2AuthorizeURL: get("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"), - oauth2TokenURL: get("oauth2TokenURL", "http://localhost:4444/oauth2/token"), - oauth2LogoutURL: get("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"), - oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081"), - graphQLEndpoint: get("graphQLEndpoint", "http://localhost:8080/api/v1/graphql") -}; - -function get(key: string, defaultValue: T):T { - const config = window['__CONFIG__'] || {}; - if (config && config.hasOwnProperty(key)) { - return config[key] as T; - } else { - return defaultValue; - } -} \ No newline at end of file diff --git a/frontend/src/store/actions/auth.ts b/frontend/src/store/actions/auth.ts deleted file mode 100644 index ee55183..0000000 --- a/frontend/src/store/actions/auth.ts +++ /dev/null @@ -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 }; -} \ No newline at end of file diff --git a/frontend/src/store/sagas/auth.ts b/frontend/src/store/sagas/auth.ts deleted file mode 100644 index 7c91d14..0000000 --- a/frontend/src/store/sagas/auth.ts +++ /dev/null @@ -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)); -}; \ No newline at end of file diff --git a/frontend/src/store/sagas/init.ts b/frontend/src/store/sagas/init.ts deleted file mode 100644 index da4d595..0000000 --- a/frontend/src/store/sagas/init.ts +++ /dev/null @@ -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)); -} \ No newline at end of file diff --git a/frontend/src/types/idToken.ts b/frontend/src/types/idToken.ts deleted file mode 100644 index 2ef7808..0000000 --- a/frontend/src/types/idToken.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IdToken { - email: string -} \ No newline at end of file diff --git a/frontend/src/util/auth.ts b/frontend/src/util/auth.ts deleted file mode 100644 index adcff7d..0000000 --- a/frontend/src/util/auth.ts +++ /dev/null @@ -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 { - const encoder = new TextEncoder(); - const data = encoder.encode(plain); - return window.crypto.subtle.digest('SHA-256', data); -} - -export function pkceChallengeFromVerifier(v): PromiseLike { - 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 { - // Based on https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce - const state = generateRandomString(); - const verifier = generateRandomString(); - - return new Promise((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'); -} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f530b00 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..385b725 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/.gitignore b/internal/.gitignore new file mode 100644 index 0000000..46767d5 --- /dev/null +++ b/internal/.gitignore @@ -0,0 +1,2 @@ +/server.go +/graph/generated \ No newline at end of file diff --git a/internal/command/create_user.go b/internal/command/create_user.go new file mode 100644 index 0000000..82ac4ea --- /dev/null +++ b/internal/command/create_user.go @@ -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 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..cccb1d2 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/config/provider.go b/internal/config/provider.go new file mode 100644 index 0000000..0e768ed --- /dev/null +++ b/internal/config/provider.go @@ -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 + } +} diff --git a/internal/config/service.go b/internal/config/service.go new file mode 100644 index 0000000..e57c05d --- /dev/null +++ b/internal/config/service.go @@ -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 +} diff --git a/internal/database/migration.go b/internal/database/migration.go new file mode 100644 index 0000000..e2dcbc9 --- /dev/null +++ b/internal/database/migration.go @@ -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, + } +} diff --git a/internal/database/provider.go b/internal/database/provider.go new file mode 100644 index 0000000..62c8c89 --- /dev/null +++ b/internal/database/provider.go @@ -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 + } +} diff --git a/internal/database/service.go b/internal/database/service.go new file mode 100644 index 0000000..cad5509 --- /dev/null +++ b/internal/database/service.go @@ -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 +} diff --git a/internal/database/tx.go b/internal/database/tx.go new file mode 100644 index 0000000..bafa9ff --- /dev/null +++ b/internal/database/tx.go @@ -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 +} diff --git a/internal/database/version_resolver.go b/internal/database/version_resolver.go new file mode 100644 index 0000000..eb681b0 --- /dev/null +++ b/internal/database/version_resolver.go @@ -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, + } +} diff --git a/internal/gqlgen.yml b/internal/gqlgen.yml new file mode 100644 index 0000000..053abac --- /dev/null +++ b/internal/gqlgen.yml @@ -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 diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go new file mode 100644 index 0000000..8448226 --- /dev/null +++ b/internal/graph/model/models_gen.go @@ -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"` +} diff --git a/internal/graph/resolver.go b/internal/graph/resolver.go new file mode 100644 index 0000000..a25c09c --- /dev/null +++ b/internal/graph/resolver.go @@ -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{} diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls new file mode 100644 index 0000000..7ae4e20 --- /dev/null +++ b/internal/graph/schema.graphqls @@ -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 +} \ No newline at end of file diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go new file mode 100644 index 0000000..a8eee61 --- /dev/null +++ b/internal/graph/schema.resolvers.go @@ -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 } diff --git a/internal/graph/user_profile.go b/internal/graph/user_profile.go new file mode 100644 index 0000000..229fd61 --- /dev/null +++ b/internal/graph/user_profile.go @@ -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 +} diff --git a/internal/migration/manager.go b/internal/migration/manager.go new file mode 100644 index 0000000..91de119 --- /dev/null +++ b/internal/migration/manager.go @@ -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), + } +} diff --git a/internal/migration/migration.go b/internal/migration/migration.go new file mode 100644 index 0000000..9695592 --- /dev/null +++ b/internal/migration/migration.go @@ -0,0 +1,9 @@ +package migration + +import "context" + +type Migration interface { + Version() string + Up(context.Context) error + Down(context.Context) error +} diff --git a/internal/migration/provider.go b/internal/migration/provider.go new file mode 100644 index 0000000..0a54705 --- /dev/null +++ b/internal/migration/provider.go @@ -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 + } +} diff --git a/internal/migration/service.go b/internal/migration/service.go new file mode 100644 index 0000000..62553ba --- /dev/null +++ b/internal/migration/service.go @@ -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 +} diff --git a/internal/migration/version_resolver.go b/internal/migration/version_resolver.go new file mode 100644 index 0000000..12e05c0 --- /dev/null +++ b/internal/migration/version_resolver.go @@ -0,0 +1,8 @@ +package migration + +import "context" + +type VersionResolver interface { + Current(context.Context) (string, error) + Set(context.Context, string) error +} diff --git a/internal/query/find_user.go b/internal/query/find_user.go new file mode 100644 index 0000000..7212754 --- /dev/null +++ b/internal/query/find_user.go @@ -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 +} diff --git a/internal/route/login.go b/internal/route/login.go new file mode 100644 index 0000000..6db2c4b --- /dev/null +++ b/internal/route/login.go @@ -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) +} diff --git a/internal/route/logout.go b/internal/route/logout.go new file mode 100644 index 0000000..edd59c0 --- /dev/null +++ b/internal/route/logout.go @@ -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) +} diff --git a/internal/route/mount.go b/internal/route/mount.go new file mode 100644 index 0000000..860def4 --- /dev/null +++ b/internal/route/mount.go @@ -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 +} diff --git a/internal/session/middleware.go b/internal/session/middleware.go new file mode 100644 index 0000000..6120bc9 --- /dev/null +++ b/internal/session/middleware.go @@ -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 +} diff --git a/misc/containers/hydra/hydra-init.d/create-client b/misc/containers/hydra/hydra-init.d/create-client index 85d085b..2e28302 100755 --- a/misc/containers/hydra/hydra-init.d/create-client +++ b/misc/containers/hydra/hydra-init.d/create-client @@ -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 \ No newline at end of file diff --git a/misc/containers/super-graph/Dockerfile b/misc/containers/super-graph/Dockerfile deleted file mode 100644 index dfca8ca..0000000 --- a/misc/containers/super-graph/Dockerfile +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/misc/containers/super-graph/docker-entrypoint.sh b/misc/containers/super-graph/docker-entrypoint.sh deleted file mode 100644 index c0bea55..0000000 --- a/misc/containers/super-graph/docker-entrypoint.sh +++ /dev/null @@ -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 diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..ac44863 --- /dev/null +++ b/modd.conf @@ -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 +} \ No newline at end of file