diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1e874e7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{ts,tsx,js,jsx}] +charset = utf-8 +indent_size = 2 +indent_style = space \ No newline at end of file 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..ea84feb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor +/data +/bin +/.env +/release \ No newline at end of file diff --git a/Makefile b/Makefile index 06c50ad..b389300 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,58 @@ -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 && 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 +client-dist: + cd client && NODE_ENV=production npm run build + +up: build-docker + docker-compose up + +watch: + go run github.com/cortesi/modd/cmd/modd down: docker-compose down -v --remove-orphans db-shell: - docker-compose exec postgres psql -Usupergraph \ No newline at end of file + 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 + +.PHONY: release +release: + ./misc/script/release + +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 e4c4816..1aae405 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,23 @@ 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| #### Fichiers/répertoires notables @@ -35,16 +41,25 @@ Les services suivants devraient être disponibles après démarrage de l'environ |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 + +- [Execute an Authorization Code Grant Flow with PKCE](https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce) ## Licence diff --git a/backend/config/allow.list b/backend/config/allow.list deleted file mode 100644 index e69de29..0000000 diff --git a/backend/config/dev.yml b/backend/config/dev.yml deleted file mode 100644 index 185f4ce..0000000 --- a/backend/config/dev.yml +++ /dev/null @@ -1,203 +0,0 @@ -app_name: "Test Development" -host_port: 0.0.0.0:8080 -web_ui: true - -# debug, error, warn, info -log_level: "info" - -# 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: false - -# 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: ["*"] - -# 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: true - - # jwt: - # provider: auth0 - # secret: abc335bfcfdb04e50db5bb0a4d67ab9 - # public_key_file: /secrets/public_key.pem - # public_key_type: ecdsa #rsa - - # 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: db - 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: false - - # 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 id = $user_id" - -roles: - - name: anon - tables: - - name: users - query: - limit: 10 - - - name: user - tables: - - name: users - query: - filters: ["{ id: { _eq: $user_id } }"] - - - 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 17d993c..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() -); - ----- 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 8d1e42f..0000000 --- a/backend/config/prod.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Inherit config from this other config file -# so I only need to overwrite some values -inherits: dev - -app_name: "Daddy Backend" -host_port: 127.0.0.1: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: 127.0.0.1 - port: 5432 - dbname: daddy - user: daddy - #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: true - - # database ping timeout is used for db health checking - ping_timeout: 5m - - migrations_path: ./migrations - - cors_allowed_origins: ["*"] \ No newline at end of file diff --git a/backend/config/seed.js b/backend/config/seed.js deleted file mode 100644 index e027af3..0000000 --- a/backend/config/seed.js +++ /dev/null @@ -1,19 +0,0 @@ -// Example script to seed database - -var users = []; - -for (i = 0; i < 10; i++) { - var data = { - full_name: fake.name(), - email: fake.email() - } - - var res = graphql(" \ - mutation { \ - user(insert: $data) { \ - id \ - } \ - }", { data: data }) - - users.push(res.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 94% rename from frontend/package-lock.json rename to client/package-lock.json index e14c9f2..1e597da 100644 --- a/frontend/package-lock.json +++ b/client/package-lock.json @@ -1033,15 +1033,6 @@ "regenerator-runtime": "^0.13.4" } }, - "@babel/runtime-corejs2": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.2.tgz", - "integrity": "sha512-ZLwsFnNm3WpIARU1aLFtufjMHsmEnc8TjtrfAjmbgMbeoyR+LuQoyESoNdTfeDhL6IdY12SpeycXMgSgl8XGXA==", - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - } - }, "@babel/template": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz", @@ -1102,12 +1093,47 @@ "integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==", "dev": true }, - "@lourenci/react-kanban": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@lourenci/react-kanban/-/react-kanban-0.15.0.tgz", - "integrity": "sha512-/2XjB26iXcvpwDwlT3sz8/ptQ7QyTpMGlrPf1f02+V1Z4jdbVMo6Luz1sGlHe/TP68N8yz69/YT9qwqHZ6YYmQ==", + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, "requires": { - "react-beautiful-dnd": "^11.0.0" + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } } }, "@redux-saga/core": { @@ -1157,6 +1183,12 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, "@types/glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz", @@ -1207,6 +1239,11 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "dev": true }, + "@types/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==" + }, "@types/react": { "version": "16.9.36", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.36.tgz", @@ -1259,12 +1296,82 @@ "@types/react-router": "*" } }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/tapable": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", + "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.2.tgz", + "integrity": "sha512-d6dIfpPbF+8B7WiCi2ELY7m0w1joD8cRW4ms88Emdb2w062NeEpbNCeWwVCgzLRpVG+5e74VFSg4rgJ2xXjEiQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "@types/uuid": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.4.tgz", "integrity": "sha512-WGZCqBZZ0mXN2RxvLHL6/7RCu+OWs28jgQMP04LWfpyJlQUMTR6YU9CNJAKDgbw+EV/u687INXuLUc7FuML/4g==", "dev": true }, + "@types/webpack": { + "version": "4.41.17", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz", + "integrity": "sha512-6FfeCidTSHozwKI67gIVQQ5Mp0g4X96c2IXxX75hYEQJwST/i6NyZexP//zzMOBb+wG9jJ7oO8fk9yObP2HWAw==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack-sources": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-1.4.0.tgz", + "integrity": "sha512-c88dKrpSle9BtTqR6ifdaxu1Lvjsl3C5OsfvuUbUwdXymshv1TkufUAXBajCCUM/f/TmnkZC/Esb03MinzSiXQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -1521,6 +1628,24 @@ } } }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + } + } + }, "ajv": { "version": "6.12.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", @@ -3208,6 +3333,22 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "clean-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "dev": true, + "requires": { + "@types/webpack": "^4.4.31", + "del": "^4.1.1" + } + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -3468,10 +3609,263 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "copy-webpack-plugin": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.0.2.tgz", + "integrity": "sha512-9Gm8X0c6eXlKnmltMPFCBeGOKjtcRIyTt4VaO3k1TkNgVTe5Ov2lYsYVuyLp0kp8DItO3apewflM+1GYgh6V2Q==", + "dev": true, + "requires": { + "cacache": "^15.0.4", + "fast-glob": "^3.2.2", + "find-cache-dir": "^3.3.1", + "glob-parent": "^5.1.1", + "globby": "^11.0.1", + "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.7.0", + "serialize-javascript": "^3.1.0", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "cacache": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.4.tgz", + "integrity": "sha512-YlnKQqTbD/6iyoJvEY3KJftjrdBYroCbxxYXzhOzsFLWlp6KX4BOlEf4mTx0cMUfVaTS3ENL2QtDWeRYoGLkkw==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "tar": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", + "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true }, "core-js-compat": { "version": "3.6.5", @@ -3591,14 +3985,6 @@ } } }, - "css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "requires": { - "tiny-invariant": "^1.0.6" - } - }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -3898,6 +4284,23 @@ } } }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -4555,6 +4958,74 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4567,6 +5038,15 @@ "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", "dev": true }, + "fastq": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -4783,6 +5263,15 @@ "readable-stream": "^2.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -5010,6 +5499,11 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, + "graphql-request": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz", + "integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ==" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -5431,6 +5925,12 @@ "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "dev": true }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -5881,6 +6381,11 @@ "verror": "1.10.0" } }, + "jwt-decode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", + "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -5981,12 +6486,6 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", - "dev": true - }, "loglevel": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", @@ -6142,6 +6641,12 @@ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -6274,6 +6779,68 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz", + "integrity": "sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -7348,10 +7915,9 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" }, "querystring": { "version": "0.2.0", @@ -7371,11 +7937,6 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, - "raf-schd": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", - "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7431,21 +7992,6 @@ "prop-types": "^15.6.2" } }, - "react-beautiful-dnd": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.5.tgz", - "integrity": "sha512-7llby9U+jIfkINcyxPHVWU0HFYzqxMemUYgGHsFsbx4fZo1n/pW6sYKYzhxGxR3Ap5HxqswcQkKUZX4uEUWhlw==", - "requires": { - "@babel/runtime-corejs2": "^7.4.5", - "css-box-model": "^1.1.2", - "memoize-one": "^5.0.4", - "raf-schd": "^4.0.0", - "react-redux": "^7.0.3", - "redux": "^4.0.1", - "tiny-invariant": "^1.0.4", - "use-memo-one": "^1.1.0" - } - }, "react-dom": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", @@ -7593,12 +8139,6 @@ "source-map": "~0.5.0" } }, - "recursive-readdir-sync": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/recursive-readdir-sync/-/recursive-readdir-sync-1.0.6.tgz", - "integrity": "sha1-Hb9tMvPFu4083pemxYjVR6nhPVY=", - "dev": true - }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -7800,6 +8340,14 @@ "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } } }, "require-directory": { @@ -7968,6 +8516,12 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rework": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", @@ -8011,6 +8565,12 @@ "inherits": "^2.0.1" } }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -9432,11 +9992,6 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, - "use-memo-one": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", - "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" - }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", @@ -9485,7 +10040,8 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true }, "v8-compile-cache": { "version": "2.0.3", @@ -9727,28 +10283,6 @@ } } }, - "webpack-cleanup-plugin": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/webpack-cleanup-plugin/-/webpack-cleanup-plugin-0.5.1.tgz", - "integrity": "sha1-3y1wa9dTZMBuZbBRGGMW1nTrlq8=", - "dev": true, - "requires": { - "lodash.union": "4.6.0", - "minimatch": "3.0.3", - "recursive-readdir-sync": "1.0.6" - }, - "dependencies": { - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", - "dev": true, - "requires": { - "brace-expansion": "^1.0.0" - } - } - } - }, "webpack-cli": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", diff --git a/frontend/package.json b/client/package.json similarity index 90% rename from frontend/package.json rename to client/package.json index 0a69375..ded5e04 100644 --- a/frontend/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "dadd-", + "name": "daddy", "version": "0.0.0", "description": "Daddy", "main": "index.js", @@ -32,6 +32,8 @@ "@types/react-router-dom": "^5.1.5", "@types/uuid": "^7.0.3", "babel-loader": "^8.0.6", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^6.0.2", "css-loader": "^1.0.1", "extract-loader": "^3.1.0", "file-loader": "^2.0.0", @@ -45,13 +47,16 @@ "style-loader": "^0.23.1", "ts-loader": "^7.0.2", "webpack": "^4.25.0", - "webpack-cleanup-plugin": "^0.5.1", "webpack-cli": "^3.1.2", "webpack-dev-server": "^3.11.0" }, "dependencies": { + "@types/qs": "^6.9.3", "bulma": "^0.7.2", "bulma-switch": "^2.0.0", + "graphql-request": "^2.0.0", + "jwt-decode": "^2.2.0", + "qs": "^6.9.4", "react": "^16.12.0", "react-dom": "^16.12.0", "react-redux": "^7.1.3", diff --git a/frontend/src/components/App.tsx b/client/src/components/App.tsx similarity index 54% rename from frontend/src/components/App.tsx rename to client/src/components/App.tsx index 00f2376..3bea7f4 100644 --- a/frontend/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -1,29 +1,20 @@ import React from 'react'; -import { HashRouter as Router, Route, Redirect, Switch } from "react-router-dom"; +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 { logout } from '../store/actions/logout'; export class App extends React.Component { render() { return ( - + - { - this.logout(); - return ; - }} /> } /> - + ); } - - logout() { - store.dispatch(logout()); - } } \ No newline at end of file diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx new file mode 100644 index 0000000..b9ac584 --- /dev/null +++ b/client/src/components/HomePage/HomePage.tsx @@ -0,0 +1,28 @@ +import React, { useEffect } from 'react'; +import { Page } from '../Page'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../store/reducers/root'; + +export function HomePage() { + const currentUser = useSelector((state: RootState) => state.auth.currentUser); + + return ( + +
+
+
+
+
+ { + currentUser && currentUser.full_name ? +

Bonjour {currentUser.full_name} !

: +

Veuillez vous authentifier.

+ } +
+
+
+
+
+
+ ); +} \ No newline at end of file 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/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx new file mode 100644 index 0000000..def6d3b --- /dev/null +++ b/client/src/components/Navbar.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import logo from '../resources/logo.svg'; +import { useSelector } from 'react-redux'; +import { RootState } from '../store/reducers/root'; +import { Config } from '../config'; + +export function Navbar() { + const isAuthenticated = useSelector(state => state.auth.isAuthenticated); + + return ( + + ); +}; \ No newline at end of file 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 60% rename from frontend/src/index.html rename to client/src/index.html index 234eb2a..9089b6a 100644 --- a/frontend/src/index.html +++ b/client/src/index.html @@ -5,16 +5,18 @@ Daddy <% for (var css in htmlWebpackPlugin.files.css) { %> - + <% } %> <% if (htmlWebpackPlugin.files.favicon) { %> - + <% } %> +
+ <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> - + <% } %> \ No newline at end of file diff --git a/frontend/src/index.tsx b/client/src/index.tsx similarity index 92% rename from frontend/src/index.tsx rename to client/src/index.tsx index 11d88f4..9dde128 100644 --- a/frontend/src/index.tsx +++ b/client/src/index.tsx @@ -2,6 +2,7 @@ import './sass/_all.scss'; import React from 'react'; import ReactDOM from 'react-dom'; import { App } from './components/App'; +import { Config } from './config'; import '@fortawesome/fontawesome-free/js/fontawesome' import '@fortawesome/fontawesome-free/js/solid' diff --git a/client/src/resources/config.sample.js b/client/src/resources/config.sample.js new file mode 100644 index 0000000..8b9dd81 --- /dev/null +++ b/client/src/resources/config.sample.js @@ -0,0 +1,3 @@ +// Use this file to customize client configuration. +// See frontend/src/config.ts for more informations. +window['__CONFIG__'] = {}; \ No newline at end of file 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/client/src/store/actions/profile.ts b/client/src/store/actions/profile.ts new file mode 100644 index 0000000..c46a75f --- /dev/null +++ b/client/src/store/actions/profile.ts @@ -0,0 +1,19 @@ +import { Action } from "redux"; +import { User } from "../../types/user"; + +export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST'; +export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS'; +export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE'; + +export interface fetchProfileRequestAction extends Action { + +} + +export interface fetchProfileSuccessAction extends Action { + profile: User +} + + +export function fetchProfile(): fetchProfileRequestAction { + return { type: FETCH_PROFILE_REQUEST } +} \ No newline at end of file diff --git a/client/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts new file mode 100644 index 0000000..b25f0c4 --- /dev/null +++ b/client/src/store/reducers/auth.ts @@ -0,0 +1,45 @@ +import { Action } from "redux"; +import { User } from "../../types/user"; +import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; +import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile"; + +export interface AuthState { + isAuthenticated: boolean + currentUser: User +} + +const defaultState = { + isAuthenticated: false, + currentUser: null, +}; + +export function authReducer(state = defaultState, action: Action): AuthState { + switch (action.type) { + case SET_CURRENT_USER: + return handleSetCurrentUser(state, action as setCurrentUserAction); + case FETCH_PROFILE_SUCCESS: + return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction); + + } + return state; +} + +function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState { + return { + ...state, + isAuthenticated: true, + currentUser: { + email + } + }; +}; + +function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState { + return { + ...state, + isAuthenticated: true, + currentUser: { + ...profile, + } + }; +}; \ No newline at end of file diff --git a/frontend/src/store/reducers/flags.ts b/client/src/store/reducers/flags.ts similarity index 57% rename from frontend/src/store/reducers/flags.ts rename to client/src/store/reducers/flags.ts index d0d22b8..f42701b 100644 --- a/frontend/src/store/reducers/flags.ts +++ b/client/src/store/reducers/flags.ts @@ -1,8 +1,18 @@ +import { Action } from "redux"; + +export interface FlagsState { + actions: { [actionName: string]: ActionState } +} + +export interface ActionState { + isLoading: boolean +} + const defaultState = { actions: {} }; -export function flagsReducer(state = defaultState, action: any) { +export function flagsReducer(state = defaultState, action: Action): FlagsState { const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type); if(!matches) return state; diff --git a/client/src/store/reducers/root.ts b/client/src/store/reducers/root.ts new file mode 100644 index 0000000..5835800 --- /dev/null +++ b/client/src/store/reducers/root.ts @@ -0,0 +1,13 @@ +import { combineReducers } from 'redux'; +import { flagsReducer, FlagsState } from './flags'; +import { authReducer, AuthState } from './auth'; + +export interface RootState { + auth: AuthState, + flags: FlagsState, +} + +export const rootReducer = combineReducers({ + flags: flagsReducer, + auth: authReducer, +}); \ No newline at end of file diff --git a/client/src/store/sagas/failure.ts b/client/src/store/sagas/failure.ts new file mode 100644 index 0000000..de90822 --- /dev/null +++ b/client/src/store/sagas/failure.ts @@ -0,0 +1,21 @@ +import { UnauthorizedError } from "../../util/daddy"; +import { all, takeEvery } from 'redux-saga/effects'; + +export function* failureRootSaga() { + yield all([ + takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), + ]); +} + +export function* failuresSaga(action) { + if (action.error instanceof UnauthorizedError) { + // TODO Implements better authorization error handling + window.location.reload(); + } +} + +export function patternFromRegExp(re: any) { + return (action: any) => { + return re.test(action.type); + }; +} \ No newline at end of file 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/client/src/store/sagas/root.ts b/client/src/store/sagas/root.ts new file mode 100644 index 0000000..3b29c86 --- /dev/null +++ b/client/src/store/sagas/root.ts @@ -0,0 +1,11 @@ +import { all } from 'redux-saga/effects'; +import { failureRootSaga } from './failure'; +import { initRootSaga } from './init'; +import { usersRootSaga } from './users'; + +export function* rootSaga() { + yield all([ + initRootSaga(), + failureRootSaga(), + ]); +} diff --git a/client/src/store/sagas/users.ts b/client/src/store/sagas/users.ts new file mode 100644 index 0000000..42f0ac2 --- /dev/null +++ b/client/src/store/sagas/users.ts @@ -0,0 +1,33 @@ +import { DaddyClient } from "../../util/daddy"; +import { Config } from "../../config"; +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"; +import { RootState } from "../reducers/root"; +import { User } from "../../types/user"; + +export function* usersRootSaga() { + yield all([ + takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga), + takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga), + ]); +} + +export function* onCurrentUserChangeSaga() { + yield put(fetchProfile()); +} + +export function* fetchProfileSaga() { + const client = new DaddyClient(Config.graphQLEndpoint); + + let profile: User; + try { + const currentUser: User = yield select((state: RootState) => state.auth.currentUser); + profile = yield client.fetchUser(currentUser.email).then(result => result.user); + } catch(err) { + yield put({ type: FETCH_PROFILE_FAILURE, err }); + return; + } + + yield put({type: FETCH_PROFILE_SUCCESS, profile }); +} \ No newline at end of file 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/client/src/types/user.ts b/client/src/types/user.ts new file mode 100644 index 0000000..13c920d --- /dev/null +++ b/client/src/types/user.ts @@ -0,0 +1,6 @@ +export interface User { + email: string + full_name?: string + updated_at?: Date + created_at?: Date +} \ No newline at end of file diff --git a/client/src/util/daddy.ts b/client/src/util/daddy.ts new file mode 100644 index 0000000..9d3d104 --- /dev/null +++ b/client/src/util/daddy.ts @@ -0,0 +1,43 @@ +import { GraphQLClient } from 'graphql-request' +import { Config } from "../config"; + +export class UnauthorizedError extends Error { + constructor(...args: any[]) { + super(...args) + Object.setPrototypeOf(this, UnauthorizedError.prototype); + } +} + +export class DaddyClient { + + gql: GraphQLClient + + constructor(endpoint: string) { + this.gql = new GraphQLClient(endpoint, { + headers: { + mode: 'cors', + } + }); + } + + fetchUser(email: string) { + return this.gql.rawRequest(` + query fetchUser { + user(where: {email: {eq: $email}}) { + id + created_at + updated_at + email, + full_name + } + } + `, { email }) + .then(this.assertAuthorization) + } + + assertAuthorization({ status, data }: any) { + if (status === 401) return Promise.reject(new UnauthorizedError()); + return data; + } + +} \ No newline at end of file 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 72% rename from frontend/webpack.config.js rename to client/webpack.config.js index 7db40c0..be40da5 100644 --- a/frontend/webpack.config.js +++ b/client/webpack.config.js @@ -3,7 +3,8 @@ const path = require('path'); // Plugins const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); const env = process.env; @@ -12,6 +13,7 @@ module.exports = { entry: './src/index.tsx', devtool: 'inline-source-map', output: { + filename: '[name].[contenthash].js', path: path.join(__dirname, 'dist') }, resolve: { @@ -20,7 +22,10 @@ module.exports = { devServer: { contentBase: path.join(__dirname, 'dist'), compress: true, - port: 8081 + host: '0.0.0.0', + port: 8080, + historyApiFallback: true, + writeToDisk: true, }, module: { rules: [{ @@ -48,7 +53,7 @@ module.exports = { use: [{ loader: 'file-loader', options: { - name: '[name].[ext]', + name: '[name].[contenthash].[ext]', outputPath: '/resources/' } }] @@ -59,17 +64,20 @@ module.exports = { }] }, plugins: [ + new CleanWebpackPlugin(), new MiniCssExtractPlugin({ - filename: "css/[name].css", + filename: "css/[name].[contenthash].css", chunkFilename: "css/[id].css" }), new HtmlWebpackPlugin({ template: './src/index.html', inject: false, - favicon: "./src/resources/favicon.png" + favicon: "./src/resources/favicon.png", + }), + new CopyPlugin({ + patterns: [ + { from: './src/resources/config.sample.js', to: 'config.js' }, + ], }), - new WebpackCleanupPlugin({ - exclude: ['resources/logo.svg'] - }) ] } \ No newline at end of file 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 bbad58e..9846a19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,67 @@ version: '2.4' services: - super-graph: + postgres: build: - context: ./misc/containers/super-graph + context: ./misc/containers/postgres 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 + - POSTGRES_PASSWORD=postgres + ports: + - 5432:5432 volumes: - - ./backend:/app - links: - - postgres - ports: - - 8080:8080 - postgres: - image: postgres:12-alpine + - postgres_data:/var/lib/postgresql/data + + hydra: + build: + context: ./misc/containers/hydra environment: - - POSTGRES_PASSWORD=daddy - - POSTGRES_USER=daddy - - POSTGRES_DB=daddy + DSN: postgres://hydra:hydra@postgres:5432/hydra + URLS_LOGIN: http://localhost:3000/login + URLS_CONSENT: http://localhost:3000/consent + URLS_LOGOUT: http://localhost:3000/logout + SUPPORTED_SCOPES: email + SUPPORTED_CLAIMS: email,email_verified + SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX + HYDRA_ADMIN_URL: http://localhost:4445 ports: - - 5432:5432 \ No newline at end of file + - 4444:4444 + command: hydra serve all --dangerous-force-http + + hydra-passwordless: + image: bornholm/hydra-passwordless:latest + ports: + - 3000:3000 + environment: + - HTTP_COOKIE_AUTHENTICATION_KEY=XNFEWQwYB9WiVSnkHoFnMtNDL6X88apR4DmDBwh7gVgdJ3LTdLRLwGZAALnVN2yg + - HTTP_COOKIE_ENCRYPTION_KEY=xtHEd36Uo4DFeS2JgPPm94fPBfinY3xi + - HTTP_TOKEN_AUTHENTICATION_KEY=sGToi4yiP5yWrZzKdKaDA3XNpkcg9CRAaycuhr5gy2XnPKzUS7N6wGEFhMq9WPuf + - HTTP_TOKEN_ENCRYPTION_KEY=LAbuEWUeNDCLniRcyjiBCZ8ecgwN9Van + - SMTP_HOST=smtp + - SMTP_PORT=2525 + - SMTP_USE_START_TLS=false + - SMTP_USER= + - SMTP_PASSWORD= + - SMTP_INSECURE_SKIP_VERIFY=true + - HYDRA_BASE_URL=http://hydra:4445 + - HYDRA_FAKE_SSL_TERMINATION=false + + smtp: + image: bornholm/fake-smtp + ports: + - 8082:8080 + - 2525:2525 + environment: + - FAKESMTP_SMTP_ADDRESS=:2525 + - FAKESMTP_SMTP_DEBUG=true + - FAKESMTP_SMTP_USERNAME= + - FAKESMTP_SMTP_PASSWORD= + - FAKESMTP_SMTP_ALLOWINSECUREAUTH=true + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro +volumes: + postgres_data: \ No newline at end of file diff --git a/frontend/src/components/HomePage/HomePage.tsx b/frontend/src/components/HomePage/HomePage.tsx deleted file mode 100644 index 842dc63..0000000 --- a/frontend/src/components/HomePage/HomePage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { Page } from '../Page'; - -export function HomePage() { - return ( - -
- -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx deleted file mode 100644 index 0dcb81d..0000000 --- a/frontend/src/components/Navbar.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import logo from '../resources/logo.svg'; - -export class Navbar extends React.PureComponent { - render() { - return ( - - ); - } -} \ No newline at end of file diff --git a/frontend/src/store/actions/logout.ts b/frontend/src/store/actions/logout.ts deleted file mode 100644 index 3537185..0000000 --- a/frontend/src/store/actions/logout.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const LOGOUT_REQUEST = "LOGOUT_REQUEST"; -export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS"; -export const LOGOUT_FAILURE = "LOGOUT_FAILURE"; - -export function logout() { - return { type: LOGOUT_REQUEST }; -}; \ No newline at end of file diff --git a/frontend/src/store/reducers/root.ts b/frontend/src/store/reducers/root.ts deleted file mode 100644 index 9203f3e..0000000 --- a/frontend/src/store/reducers/root.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { combineReducers } from 'redux'; -import { flagsReducer } from './flags'; - -export const rootReducer = combineReducers({ - flags: flagsReducer, -}); \ No newline at end of file diff --git a/frontend/src/store/sagas/failure.ts b/frontend/src/store/sagas/failure.ts deleted file mode 100644 index 3bfc784..0000000 --- a/frontend/src/store/sagas/failure.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UnauthorizedError } from "../../util/daddy"; -import { put } from 'redux-saga/effects'; -import { logout } from '../actions/logout'; - -export function* failuresSaga(action) { - if (action.error instanceof UnauthorizedError) { - yield put(logout()); - } -} diff --git a/frontend/src/store/sagas/logout.ts b/frontend/src/store/sagas/logout.ts deleted file mode 100644 index 26e3fe0..0000000 --- a/frontend/src/store/sagas/logout.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { call, put } from 'redux-saga/effects'; -import { LOGOUT_FAILURE, LOGOUT_SUCCESS } from '../actions/logout'; - -export function* logoutSaga() { - try { - yield call(fetch, '/logout', { mode: 'no-cors', credentials: 'include' }); - } catch(err) { - yield put({ type: LOGOUT_FAILURE, error: err }); - return; - } - - yield put({ type: LOGOUT_SUCCESS }); -} - -export function* logoutSuccessSaga() { - window.location.reload(); -} \ No newline at end of file diff --git a/frontend/src/store/sagas/root.ts b/frontend/src/store/sagas/root.ts deleted file mode 100644 index 47bb19d..0000000 --- a/frontend/src/store/sagas/root.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { all, takeEvery, takeLatest } from 'redux-saga/effects'; -import { failuresSaga } from './failure'; -import { LOGOUT_REQUEST, LOGOUT_SUCCESS } from '../actions/logout'; -import { logoutSaga, logoutSuccessSaga } from './logout'; - -export function* rootSaga() { - yield all([ - takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), - takeLatest(LOGOUT_REQUEST, logoutSaga), - takeLatest(LOGOUT_SUCCESS, logoutSuccessSaga) - ]); -} - -export function patternFromRegExp(re: any) { - return (action: any) => { - return re.test(action.type); - }; -} \ No newline at end of file diff --git a/frontend/src/util/daddy.ts b/frontend/src/util/daddy.ts deleted file mode 100644 index 22bb4c5..0000000 --- a/frontend/src/util/daddy.ts +++ /dev/null @@ -1,22 +0,0 @@ -export class UnauthorizedError extends Error { - constructor(...args: any[]) { - super(...args) - Object.setPrototypeOf(this, UnauthorizedError.prototype); - } -} - -export class DaddyClient { - - assertOk(res: any) { - if (!res.ok) return Promise.reject(new Error('Request failed')); - return res; - } - - assertAuthorization(res: any) { - if (res.status === 401 || res.status === 404) return Promise.reject(new UnauthorizedError()); - return res; - } - -} - -export const daddy = new DaddyClient(); \ 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..7d178b5 --- /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 `yaml:"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/Dockerfile b/misc/containers/hydra/Dockerfile new file mode 100644 index 0000000..f63eb44 --- /dev/null +++ b/misc/containers/hydra/Dockerfile @@ -0,0 +1,18 @@ +FROM oryd/hydra:v1.4.2-alpine + +USER root + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint +RUN chmod a+x /usr/local/bin/docker-entrypoint + +COPY first-run.sh /usr/local/bin/docker-first-run +RUN chmod a+x /usr/local/bin/docker-first-run + +COPY hydra-init.d /hydra-init.d + +RUN mkdir -p /home/ory && chown -R ory: /home/ory +USER ory + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint"] + +CMD ["hydra", "serve", "all"] \ No newline at end of file diff --git a/misc/containers/hydra/docker-entrypoint.sh b/misc/containers/hydra/docker-entrypoint.sh new file mode 100644 index 0000000..86526e6 --- /dev/null +++ b/misc/containers/hydra/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -xeo pipefail + +LIFECYCLEFLAGS_DIR="$HOME/.container-lifecycle" + +mkdir -p "$LIFECYCLEFLAGS_DIR" + +if [ ! -f "$LIFECYCLEFLAGS_DIR/first-run" ]; then + /usr/local/bin/docker-first-run + touch "$LIFECYCLEFLAGS_DIR/first-run" +fi + +exec "$@" \ No newline at end of file diff --git a/misc/containers/hydra/first-run.sh b/misc/containers/hydra/first-run.sh new file mode 100644 index 0000000..99f8de5 --- /dev/null +++ b/misc/containers/hydra/first-run.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +hydra migrate sql -e -y + +hydra serve all --dangerous-force-http & +HYDRA_PID=$! +run-parts --exit-on-error /hydra-init.d +kill $HYDRA_PID \ No newline at end of file diff --git a/misc/containers/hydra/hydra-init.d/create-client b/misc/containers/hydra/hydra-init.d/create-client new file mode 100755 index 0000000..2e28302 --- /dev/null +++ b/misc/containers/hydra/hydra-init.d/create-client @@ -0,0 +1,12 @@ +#!/bin/sh + +set -x + +hydra clients create \ + --id daddy \ + --secret daddycool \ + -n Daddy \ + -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/postgres/Dockerfile b/misc/containers/postgres/Dockerfile new file mode 100644 index 0000000..f4ea954 --- /dev/null +++ b/misc/containers/postgres/Dockerfile @@ -0,0 +1,3 @@ +FROM postgres:12-alpine + +COPY ./initdb.d /docker-entrypoint-initdb.d \ No newline at end of file diff --git a/misc/containers/postgres/initdb.d/init-databases.sh b/misc/containers/postgres/initdb.d/init-databases.sh new file mode 100644 index 0000000..9831c3a --- /dev/null +++ b/misc/containers/postgres/initdb.d/init-databases.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER hydra WITH ENCRYPTED PASSWORD 'hydra'; + CREATE DATABASE hydra; + GRANT ALL PRIVILEGES ON DATABASE hydra TO hydra; +EOSQL + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER daddy WITH ENCRYPTED PASSWORD 'daddy'; + CREATE DATABASE daddy; + GRANT ALL PRIVILEGES ON DATABASE daddy TO daddy; + ALTER DATABASE daddy OWNER TO daddy; +EOSQL \ 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 2e2ce94..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=v0.14.17 -ARG WAITFORIT_VERSION=v2.4.1 - -RUN apk add --no-cache go make git curl bash ca-certificates - -RUN git clone https://github.com/dosco/super-graph \ - && 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/misc/script/release b/misc/script/release new file mode 100755 index 0000000..2e2c123 --- /dev/null +++ b/misc/script/release @@ -0,0 +1,119 @@ +#!/bin/bash + +set -eo pipefail + +OS_TARGETS=(linux) +ARCH_TARGETS=${ARCH_TARGETS:-amd64 arm 386} + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +PROJECT_DIR="$DIR/../.." + +function build { + + local name=$1 + local srcdir=$2 + local os=$3 + local arch=$4 + + local dirname="$name-$os-$arch" + local destdir="$PROJECT_DIR/release/$dirname" + + rm -rf "$destdir" + mkdir -p "$destdir" + + echo "building $dirname..." + + CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \ + -ldflags="-s -w -X main.GitCommit=$(current_commit_ref) -X main.ProjectVersion=$(current_version)" \ + -gcflags=-trimpath="${PWD}" \ + -asmflags=-trimpath="${PWD}" \ + -o "$destdir/bin/$name" \ + "$srcdir" + + if [ ! -z "$(which upx)" ]; then + upx --best "$destdir/bin/$name" + fi + +} + +function current_commit_ref { + git rev-list -1 HEAD +} + +function current_version { + local latest_tag=$(git describe --abbrev=0 2>/dev/null) + echo ${latest_tag:-0.0.0} +} + +function copy { + + local name=$1 + local os=$2 + local arch=$3 + local src=$4 + local dest=$5 + + local dirname="$name-$os-$arch" + local destdir="$PROJECT_DIR/release/$dirname" + + echo "copying '$src' to '$destdir/$dest'..." + + mkdir -p "$(dirname $destdir/$dest)" + + cp -rfL $src "$destdir/$dest" + +} + +function dump_default_conf { + # Generate and copy configuration file + local command=$1 + local os=$2 + local arch=$3 + local tmp_conf=$(mktemp) + + go run "$PROJECT_DIR/cmd/$command" -dump-config > "$tmp_conf" + copy "$command" $os $arch "$tmp_conf" "$command.conf" + rm -f "$tmp_conf" +} + +function compress { + + local name=$1 + local os=$2 + local arch=$3 + + local dirname="$name-$os-$arch" + local destdir="$PROJECT_DIR/release/$dirname" + + echo "compressing $dirname..." + tar -czf "$destdir.tar.gz" -C "$destdir/../" "$dirname" +} + +function release_server { + + local os=$1 + local arch=$2 + + build 'server' "$PROJECT_DIR/cmd/server" $os $arch + + dump_default_conf 'server' $os $arch + + copy 'server' $os $arch "$PROJECT_DIR/README.md" "README.md" + copy 'server' $os $arch "$PROJECT_DIR/client/dist" "public" + + compress 'server' $os $arch + +} + +function main { + + make client-dist + + for os in ${OS_TARGETS[@]}; do + for arch in ${ARCH_TARGETS[@]}; do + release_server $os $arch + done + done +} + +main \ No newline at end of file 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