From 59806edc102e0b3704c2647185bfb1fb71b3ea6f Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 18 Jun 2020 09:34:22 +0200 Subject: [PATCH 01/23] =?UTF-8?q?Int=C3=A9gration=20d'hydra/hydra-password?= =?UTF-8?q?less/fake-smtp=20dans=20l'infra=20Docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 7 +- README.md | 7 ++ docker-compose.yml | 68 +++++++++++++++++-- misc/containers/hydra/Dockerfile | 18 +++++ misc/containers/hydra/docker-entrypoint.sh | 14 ++++ misc/containers/hydra/first-run.sh | 8 +++ .../hydra/hydra-init.d/create-client | 8 +++ misc/containers/postgres/Dockerfile | 3 + .../postgres/initdb.d/init-databases.sh | 15 ++++ 9 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 misc/containers/hydra/Dockerfile create mode 100644 misc/containers/hydra/docker-entrypoint.sh create mode 100644 misc/containers/hydra/first-run.sh create mode 100755 misc/containers/hydra/hydra-init.d/create-client create mode 100644 misc/containers/postgres/Dockerfile create mode 100644 misc/containers/postgres/initdb.d/init-databases.sh diff --git a/Makefile b/Makefile index 06c50ad..20566c0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ deps: cd frontend && npm install up: build - ( cd frontend && npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait + ( cd frontend && NODE_ENV=development npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait sg: docker-compose exec -u $(shell id -u) super-graph sh @@ -17,4 +17,7 @@ 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 -Usupergraph + +hydra-shell: + docker-compose exec hydra /bin/sh \ No newline at end of file diff --git a/README.md b/README.md index e4c4816..8620f09 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ Les services suivants devraient être disponibles après démarrage de l'environ |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| +|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 @@ -46,6 +49,10 @@ Les services suivants devraient être disponibles après démarrage de l'environ |`make down`|Stopper et supprimer l'environnement de développement.| |`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.| +#### Ressources + +- [Execute an Authorization Code Grant Flow with PKCE](https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce) + ## Licence AGPL-3.0 diff --git a/docker-compose.yml b/docker-compose.yml index bbad58e..e6e3855 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,11 +20,69 @@ services: - postgres ports: - 8080:8080 + postgres: - image: postgres:12-alpine + build: + context: ./misc/containers/postgres + args: + - HTTP_PROXY=${HTTP_PROXY} + - HTTPS_PROXY=${HTTPS_PROXY} + - http_proxy=${http_proxy} + - https_proxy=${https_proxy} environment: - - POSTGRES_PASSWORD=daddy - - POSTGRES_USER=daddy - - POSTGRES_DB=daddy + - POSTGRES_PASSWORD=postgres ports: - - 5432:5432 \ No newline at end of file + - 5432:5432 + volumes: + - postgres_data:/var/lib/postgresql/data + + hydra: + build: + context: ./misc/containers/hydra + environment: + 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: + - 4444:4444 + command: hydra serve all --dangerous-force-http + + hydra-passwordless: + image: bornholm/hydra-passwordless + 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/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..70e294c --- /dev/null +++ b/misc/containers/hydra/hydra-init.d/create-client @@ -0,0 +1,8 @@ +#!/bin/sh + +hydra clients create \ + --id daddy \ + -n Daddy \ + --secret 'KE9wOXR-~7qCXNKWzw23EpNroq' \ + -a email,email_verified \ + -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..ddbbb85 --- /dev/null +++ b/misc/containers/postgres/initdb.d/init-databases.sh @@ -0,0 +1,15 @@ +#!/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; +EOSQL \ No newline at end of file From 713b8cc3ea9b8bf9ee4925956066dbb2787bb0c9 Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 18 Jun 2020 09:48:45 +0200 Subject: [PATCH 02/23] Authentification OpenID Connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation du modèle d'authentification "Authorization code with PKCE [1]" [1] https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce --- .editorconfig | 4 + docker-compose.yml | 2 + frontend/package-lock.json | 201 ++++++++++-------- frontend/package.json | 5 +- frontend/src/components/App.tsx | 17 +- frontend/src/components/HomePage/HomePage.tsx | 18 +- frontend/src/components/Navbar.tsx | 64 +++--- .../src/components/OAuth2Page/OAuth2Page.tsx | 31 +++ frontend/src/config.ts | 20 ++ frontend/src/index.html | 8 +- frontend/src/index.tsx | 1 + frontend/src/store/actions/auth.ts | 69 ++++++ frontend/src/store/actions/logout.ts | 7 - frontend/src/store/reducers/auth.ts | 41 ++++ frontend/src/store/reducers/flags.ts | 12 +- frontend/src/store/reducers/root.ts | 9 +- frontend/src/store/sagas/auth.ts | 98 +++++++++ frontend/src/store/sagas/failure.ts | 16 +- frontend/src/store/sagas/logout.ts | 17 -- frontend/src/store/sagas/root.ts | 18 +- frontend/src/types/idToken.ts | 3 + frontend/src/types/user.ts | 3 + frontend/src/util/auth.ts | 126 +++++++++++ frontend/webpack.config.js | 17 +- .../hydra/hydra-init.d/create-client | 5 +- 25 files changed, 628 insertions(+), 184 deletions(-) create mode 100644 .editorconfig create mode 100644 frontend/src/components/OAuth2Page/OAuth2Page.tsx create mode 100644 frontend/src/config.ts create mode 100644 frontend/src/store/actions/auth.ts delete mode 100644 frontend/src/store/actions/logout.ts create mode 100644 frontend/src/store/reducers/auth.ts create mode 100644 frontend/src/store/sagas/auth.ts delete mode 100644 frontend/src/store/sagas/logout.ts create mode 100644 frontend/src/types/idToken.ts create mode 100644 frontend/src/types/user.ts create mode 100644 frontend/src/util/auth.ts 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/docker-compose.yml b/docker-compose.yml index e6e3855..1c75906 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,8 @@ services: SUPPORTED_CLAIMS: email,email_verified SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX HYDRA_ADMIN_URL: http://localhost:4445 + SERVE_PUBLIC_CORS_ENABLED: "true" + SERVE_PUBLIC_CORS_ALLOWED_ORIGINS: http://localhost:8081 ports: - 4444:4444 command: hydra serve all --dangerous-force-http diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e14c9f2..5979b02 100644 --- a/frontend/package-lock.json +++ b/frontend/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,14 +1093,6 @@ "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==", - "requires": { - "react-beautiful-dnd": "^11.0.0" - } - }, "@redux-saga/core": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", @@ -1157,6 +1140,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 +1196,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 +1253,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", @@ -3208,6 +3272,16 @@ } } }, + "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", @@ -3471,7 +3545,8 @@ "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 +3666,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", @@ -5881,6 +5948,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 +6053,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", @@ -7348,10 +7414,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 +7436,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 +7491,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 +7638,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 +7839,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": { @@ -9432,11 +9479,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 +9527,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 +9770,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/frontend/package.json index 0a69375..f744221 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "@types/react-router-dom": "^5.1.5", "@types/uuid": "^7.0.3", "babel-loader": "^8.0.6", + "clean-webpack-plugin": "^3.0.0", "css-loader": "^1.0.1", "extract-loader": "^3.1.0", "file-loader": "^2.0.0", @@ -45,13 +46,15 @@ "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", + "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/frontend/src/components/App.tsx index 00f2376..53df347 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -1,29 +1,22 @@ 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'; +import { OAuth2Page } from './OAuth2Page/OAuth2Page'; export class App extends React.Component { render() { return ( - + - { - this.logout(); - return ; - }} /> + } /> - + ); } - - logout() { - store.dispatch(logout()); - } } \ No newline at end of file diff --git a/frontend/src/components/HomePage/HomePage.tsx b/frontend/src/components/HomePage/HomePage.tsx index 842dc63..150726a 100644 --- a/frontend/src/components/HomePage/HomePage.tsx +++ b/frontend/src/components/HomePage/HomePage.tsx @@ -1,11 +1,27 @@ import React from 'react'; import { Page } from '../Page'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../store/reducers/root'; export function HomePage() { + const currentUser = useSelector((state: RootState) => state.auth.currentUser); + return (
- +
+
+
+
+ { + currentUser ? +

Bonjour {currentUser.email} !

: +

Veuillez vous authentifier.

+ } +
+
+
+
); diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 0dcb81d..d603547 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,36 +1,48 @@ import React from 'react'; import logo from '../resources/logo.svg'; +import { useSelector } from 'react-redux'; +import { RootState } from '../store/reducers/root'; +import { Link } from 'react-router-dom'; -export class Navbar extends React.PureComponent { - render() { - return ( - + ); +}; \ No newline at end of file diff --git a/frontend/src/components/OAuth2Page/OAuth2Page.tsx b/frontend/src/components/OAuth2Page/OAuth2Page.tsx new file mode 100644 index 0000000..7ae1daa --- /dev/null +++ b/frontend/src/components/OAuth2Page/OAuth2Page.tsx @@ -0,0 +1,31 @@ +import React, { useEffect } from 'react'; +import { Page } from '../Page'; +import { useDispatch } from 'react-redux'; +import { logout, login, handleOAuth2Callback } from '../../store/actions/auth'; + +export function OAuth2Page({ match, location, history }) { + const dispatch = useDispatch(); + const { action } = match.params; + + useEffect(() => { + switch(action) { + case 'logout': + dispatch(logout()); + history.push("/"); + break; + case 'login': + dispatch(login()); + break; + case 'callback': + dispatch(handleOAuth2Callback(location.search)); + history.push("/"); + break; + } + }, [action]); + + return ( + + + + ); +} diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..9b59e24 --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1,20 @@ +export const Config = { + // The OpenID Connect client_id + oauth2ClientId: get("oauth2ClientId", "daddy"), + oauth2Scope: get("oauth2Scope", "email email_verified openid offline_access"), + oauth2RedirectURI: get("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"), + oauth2Audience: get("oauth2Audience", ""), + oauth2AuthorizeURL: get("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"), + oauth2TokenURL: get("oauth2TokenURL", "http://localhost:4444/oauth2/token"), + oauth2LogoutURL: get("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"), + oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081") +}; + +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/index.html b/frontend/src/index.html index 234eb2a..9089b6a 100644 --- a/frontend/src/index.html +++ b/frontend/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/frontend/src/index.tsx index 11d88f4..9dde128 100644 --- a/frontend/src/index.tsx +++ b/frontend/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/frontend/src/store/actions/auth.ts b/frontend/src/store/actions/auth.ts new file mode 100644 index 0000000..ee55183 --- /dev/null +++ b/frontend/src/store/actions/auth.ts @@ -0,0 +1,69 @@ +import { Action } from "redux"; +import { AccessGrant } from "../../util/auth"; +import { IdToken } from "../../types/idToken"; + +export const LOGOUT = "LOGOUT_REQUEST"; + +export function logout() { + return { type: LOGOUT }; +}; + +export const LOGIN_REQUEST = "LOGIN_REQUEST"; +export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; +export const LOGIN_FAILURE = "LOGIN_FAILURE"; + +export function login() { + return { type: LOGIN_REQUEST }; +}; + +export const HANDLE_OAUTH2_CALLBACK_REQUEST = "HANDLE_OAUTH2_CALLBACK_REQUEST"; +export const HANDLE_OAUTH2_CALLBACK_SUCCESS = "HANDLE_OAUTH2_CALLBACK_SUCCESS"; +export const HANDLE_OAUTH2_CALLBACK_FAILURE = "HANDLE_OAUTH2_CALLBACK_FAILURE"; + +export interface handleOAuth2CallbackAction extends Action { + search: string +} + +export function handleOAuth2Callback(search: string): handleOAuth2CallbackAction { + return { type: HANDLE_OAUTH2_CALLBACK_REQUEST, search }; +}; + +export interface handleOAuth2CallbackSuccessAction extends Action { + grant: AccessGrant +} + +export function handleOAuth2CallbackSuccess(grant: AccessGrant): handleOAuth2CallbackSuccessAction { + return { type: HANDLE_OAUTH2_CALLBACK_SUCCESS, grant }; +}; + +export const PARSE_ID_TOKEN_REQUEST = "PARSE_ID_TOKEN_REQUEST"; +export const PARSE_ID_TOKEN_SUCCESS = "PARSE_ID_TOKEN_SUCCESS"; +export const PARSE_ID_TOKEN_FAILURE = "PARSE_ID_TOKEN_FAILURE"; + +export interface parseIdTokenAction extends Action { + rawIdToken: string +}; + +export function parseIdToken(rawIdToken: string): parseIdTokenAction { + return { type: PARSE_ID_TOKEN_REQUEST, rawIdToken }; +}; + + +export interface parseIdTokenSuccessAction extends Action { + idToken: IdToken +} + +export function parseIdTokenSuccess(idToken: IdToken): parseIdTokenSuccessAction { + return { type: PARSE_ID_TOKEN_SUCCESS, idToken }; +}; + + +export const SET_CURRENT_USER = 'SET_CURRENT_USER'; + +export interface setCurrentUserAction extends Action { + email: string +} + +export function setCurrentUser(email: string): setCurrentUserAction { + return { type: SET_CURRENT_USER, email }; +} \ No newline at end of file diff --git a/frontend/src/store/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/auth.ts b/frontend/src/store/reducers/auth.ts new file mode 100644 index 0000000..a0bd507 --- /dev/null +++ b/frontend/src/store/reducers/auth.ts @@ -0,0 +1,41 @@ +import { Action } from "redux"; +import { User } from "../../types/user"; +import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth"; + +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 LOGOUT: + return handleLogout(state); + } + return state; +} + +function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState { + return { + ...state, + isAuthenticated: true, + currentUser: { + email + } + }; +}; + +function handleLogout(state: AuthState): AuthState { + return { + ...state, + isAuthenticated: false, + currentUser: null, + }; +} \ No newline at end of file diff --git a/frontend/src/store/reducers/flags.ts b/frontend/src/store/reducers/flags.ts index d0d22b8..f42701b 100644 --- a/frontend/src/store/reducers/flags.ts +++ b/frontend/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/frontend/src/store/reducers/root.ts b/frontend/src/store/reducers/root.ts index 9203f3e..5835800 100644 --- a/frontend/src/store/reducers/root.ts +++ b/frontend/src/store/reducers/root.ts @@ -1,6 +1,13 @@ import { combineReducers } from 'redux'; -import { flagsReducer } from './flags'; +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/frontend/src/store/sagas/auth.ts b/frontend/src/store/sagas/auth.ts new file mode 100644 index 0000000..7c91d14 --- /dev/null +++ b/frontend/src/store/sagas/auth.ts @@ -0,0 +1,98 @@ +import { put, takeLatest, all } from 'redux-saga/effects'; +import { + LOGOUT, LOGIN_REQUEST, + HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackAction, + HANDLE_OAUTH2_CALLBACK_FAILURE, handleOAuth2CallbackSuccess, + parseIdTokenAction, parseIdToken, + PARSE_ID_TOKEN_REQUEST, PARSE_ID_TOKEN_FAILURE, parseIdTokenSuccess, + setCurrentUser, LOGIN_FAILURE, +} from '../actions/auth'; +import { + createLoginSession, LoginSession, + createAccessTokenRequest, saveAccessGrant, + saveLoginSessionState, getSavedLoginSessionState, + getLogoutURL, getSavedAccessGrant, clearAccessGrant +} from '../../util/auth'; +import qs from 'qs'; +import { UnauthorizedError } from '../../util/daddy'; +import jwtDecode from 'jwt-decode'; +import { IdToken } from '../../types/idToken'; + +export function* authRootSaga() { + yield all([ + takeLatest(LOGIN_REQUEST, loginSaga), + takeLatest(LOGOUT, logoutSaga), + takeLatest(HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackSaga), + takeLatest(PARSE_ID_TOKEN_REQUEST, parseIDTokenSaga), + ]); +} + +export function* loginSaga() { + try { + const loginSession: LoginSession = yield createLoginSession(); + console.log('Code verifier is ', loginSession.verifier); + console.log('State is ', loginSession.state); + saveLoginSessionState(loginSession.verifier, loginSession.state); + window.location.replace(loginSession.redirectUrl); + } catch(err) { + yield put({ type: LOGIN_FAILURE, err }); + } +} + +export function* logoutSaga() { + const accessGrant = getSavedAccessGrant(); + const logoutURL = getLogoutURL(accessGrant.id_token); + clearAccessGrant(); + window.location.replace(logoutURL); +} + +export function* handleOAuth2CallbackSaga({ search }: handleOAuth2CallbackAction) { + const query = search.substring(1); + const params = qs.parse(query); + + const loginSession = getSavedLoginSessionState(); + + console.log('Stored state verifier is', loginSession.state); + if (loginSession.state !== params.state) { + yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err: new Error("Invalid state") }); + return; + } + + console.log('Stored code verifier is', loginSession.verifier); + console.log('Authorization code is', params.code); + + const req = createAccessTokenRequest(params.code as string, loginSession.verifier); + + let grant; + try { + grant = yield fetch(req.url, { method: "POST", body: req.data }) + .then(res => { + if (res.status === 401) return Promise.reject(new UnauthorizedError()); + return res; + }) + .then(res => res.json()); + } catch(err) { + yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err }); + return; + } + + console.log("Access grant is", grant); + saveAccessGrant(grant); + + yield put(handleOAuth2CallbackSuccess(grant)); + yield put(parseIdToken(grant.id_token)); +}; + + +export function* parseIDTokenSaga({ rawIdToken }: parseIdTokenAction) { + let idToken: IdToken; + try { + idToken = jwtDecode(rawIdToken); + } catch(err) { + yield put({ type: PARSE_ID_TOKEN_FAILURE, err }); + return; + } + + yield put(parseIdTokenSuccess(idToken)); + yield put(setCurrentUser(idToken.email)); +}; \ No newline at end of file diff --git a/frontend/src/store/sagas/failure.ts b/frontend/src/store/sagas/failure.ts index 3bfc784..97bc18c 100644 --- a/frontend/src/store/sagas/failure.ts +++ b/frontend/src/store/sagas/failure.ts @@ -1,9 +1,21 @@ import { UnauthorizedError } from "../../util/daddy"; -import { put } from 'redux-saga/effects'; -import { logout } from '../actions/logout'; +import { put, all, takeEvery } from 'redux-saga/effects'; +import { logout } from '../actions/auth'; + +export function* failureRootSaga() { + yield all([ + takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), + ]); +} export function* failuresSaga(action) { if (action.error instanceof UnauthorizedError) { yield put(logout()); } } + +export function patternFromRegExp(re: any) { + return (action: any) => { + return re.test(action.type); + }; +} \ No newline at end of file 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 index 47bb19d..dfa853a 100644 --- a/frontend/src/store/sagas/root.ts +++ b/frontend/src/store/sagas/root.ts @@ -1,18 +1,10 @@ -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'; +import { all } from 'redux-saga/effects'; +import { failureRootSaga } from './failure'; +import { authRootSaga } from './auth'; export function* rootSaga() { yield all([ - takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), - takeLatest(LOGOUT_REQUEST, logoutSaga), - takeLatest(LOGOUT_SUCCESS, logoutSuccessSaga) + failureRootSaga(), + authRootSaga(), ]); } - -export function patternFromRegExp(re: any) { - return (action: any) => { - return re.test(action.type); - }; -} \ No newline at end of file diff --git a/frontend/src/types/idToken.ts b/frontend/src/types/idToken.ts new file mode 100644 index 0000000..2ef7808 --- /dev/null +++ b/frontend/src/types/idToken.ts @@ -0,0 +1,3 @@ +export interface IdToken { + email: string +} \ No newline at end of file diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts new file mode 100644 index 0000000..70d7e8b --- /dev/null +++ b/frontend/src/types/user.ts @@ -0,0 +1,3 @@ +export interface User { + email: string +} \ No newline at end of file diff --git a/frontend/src/util/auth.ts b/frontend/src/util/auth.ts new file mode 100644 index 0000000..adcff7d --- /dev/null +++ b/frontend/src/util/auth.ts @@ -0,0 +1,126 @@ +import { Config } from '../config'; + +export interface LoginSession { + state: string + redirectUrl: string + verifier: string +} + +export function generateRandomString() { + var array = new Uint32Array(28); + window.crypto.getRandomValues(array); + return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join(''); +} + +export function sha256(plain): PromiseLike { + const encoder = new TextEncoder(); + const data = encoder.encode(plain); + return window.crypto.subtle.digest('SHA-256', data); +} + +export function pkceChallengeFromVerifier(v): PromiseLike { + return sha256(v) + .then(hashed => base64urlencode(hashed)); +} + +export function base64urlencode(str) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(str))) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +export function createLoginSession(): Promise { + // Based on https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce + const state = generateRandomString(); + const verifier = generateRandomString(); + + return new Promise((resolve, reject) => { + try { + pkceChallengeFromVerifier(verifier).then(challenge => { + console.log('Code challenge is', challenge); + + let redirectUrl=`${Config.oauth2AuthorizeURL}`; + redirectUrl += `?audience=${encodeURIComponent(Config.oauth2Audience)}`; + redirectUrl += `&scope=${encodeURIComponent(Config.oauth2Scope)}`; + redirectUrl += `&response_type=code`; + redirectUrl += `&client_id=${encodeURIComponent(Config.oauth2ClientId)}` + redirectUrl += `&code_challenge=${encodeURIComponent(challenge)}`; + redirectUrl += `&code_challenge_method=S256` + redirectUrl += `&redirect_uri=${encodeURIComponent(Config.oauth2RedirectURI)}`; + redirectUrl += `&state=${encodeURIComponent(state)}`; + + return resolve({ + state, + redirectUrl, + verifier, + }); + }); + } catch(err) { + return reject(err); + } + }); +}; + +export interface AccessTokenRequest { + data: FormData, + url: string +} + +export function createAccessTokenRequest(code: string, verifier: string): AccessTokenRequest { + const data = new FormData(); + data.append('grant_type', 'authorization_code'); + data.append('client_id', Config.oauth2ClientId); + data.append('code_verifier', verifier); + data.append('code', code); + data.append('redirect_uri', Config.oauth2RedirectURI); + return { + url: Config.oauth2TokenURL, + data, + }; +}; + +export function getLogoutURL(rawIdToken: string): string { + let logoutURL = Config.oauth2LogoutURL; + logoutURL += `?post_logout_redirect_uri=${encodeURIComponent(Config.oauth2PostLogoutRedirectURI)}`; + logoutURL += `&id_token_hint=${encodeURIComponent(rawIdToken)}`; + return logoutURL; +} + +export interface AccessGrant { + access_token: string + expires_in: number + id_token: string + refresh_token: string + scope: string + token_type: string +} + +export function saveLoginSessionState(verifier: string, state: string) { + window.localStorage.setItem('login_verifier', verifier); + window.localStorage.setItem('login_state', state); +} + +export function getSavedLoginSessionState(cleanup = true) { + const loginSession = { + verifier: window.localStorage.getItem('login_verifier'), + state: window.localStorage.getItem('login_state') + }; + if (cleanup) { + window.localStorage.removeItem('login_verifier'); + window.localStorage.removeItem('login_state'); + } + return loginSession; +} + +export function saveAccessGrant(grant: AccessGrant) { + window.localStorage.setItem('access_grant', JSON.stringify(grant)); +} + +export function getSavedAccessGrant(): AccessGrant { + const raw = window.localStorage.getItem('access_grant'); + if (raw === "") return null; + return JSON.parse(raw) as AccessGrant; +} + +export function clearAccessGrant() { + window.localStorage.removeItem('access_grant'); +} \ No newline at end of file diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 7db40c0..ce20669 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -3,7 +3,7 @@ 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 env = process.env; @@ -12,6 +12,7 @@ module.exports = { entry: './src/index.tsx', devtool: 'inline-source-map', output: { + filename: '[name].[contenthash].js', path: path.join(__dirname, 'dist') }, resolve: { @@ -20,7 +21,9 @@ module.exports = { devServer: { contentBase: path.join(__dirname, 'dist'), compress: true, - port: 8081 + port: 8081, + historyApiFallback: true, + writeToDisk: true, }, module: { rules: [{ @@ -48,7 +51,7 @@ module.exports = { use: [{ loader: 'file-loader', options: { - name: '[name].[ext]', + name: '[name].[contenthash].[ext]', outputPath: '/resources/' } }] @@ -59,17 +62,15 @@ 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 WebpackCleanupPlugin({ - exclude: ['resources/logo.svg'] - }) ] } \ 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 index 70e294c..85d085b 100755 --- a/misc/containers/hydra/hydra-init.d/create-client +++ b/misc/containers/hydra/hydra-init.d/create-client @@ -3,6 +3,7 @@ hydra clients create \ --id daddy \ -n Daddy \ - --secret 'KE9wOXR-~7qCXNKWzw23EpNroq' \ - -a email,email_verified \ + -a email,email_verified,offline_access,openid \ + --token-endpoint-auth-method none \ + --post-logout-callbacks http://localhost:8081 \ -c http://localhost:8081/oauth2/callback \ No newline at end of file From 6b1f56a86f7257ed16541f725c1962542f6cca64 Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 11:30:34 +0200 Subject: [PATCH 03/23] Correction utilisateur commande make db-shell --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 20566c0..cf6a758 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ down: docker-compose down -v --remove-orphans db-shell: - docker-compose exec postgres psql -Usupergraph + docker-compose exec postgres psql -Udaddy hydra-shell: docker-compose exec hydra /bin/sh \ No newline at end of file From 569a44591cc9cdf47b0158e43b2383ce5619a74f Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 11:30:59 +0200 Subject: [PATCH 04/23] Ajout utilisateurs Cadoles dans les seeds --- backend/config/seed.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/config/seed.js b/backend/config/seed.js index e027af3..84d50c3 100644 --- a/backend/config/seed.js +++ b/backend/config/seed.js @@ -1,19 +1,29 @@ // Example script to seed database -var users = []; +var users = [ + { + full_name: 'William Petit', + email: 'wpetit@cadoles.com' + }, + { + full_name: 'Teddy Cornaut', + email: 'tcornaut@cadoles.com' + }, + { + full_name: 'Benjamin Gaudé', + email: 'bgaude@cadoles.com' + }, + { + full_name: 'Charles Maxime Sassot', + email: 'cmsassot@cadoles.com' + } +]; -for (i = 0; i < 10; i++) { - var data = { - full_name: fake.name(), - email: fake.email() - } - - var res = graphql(" \ +for (var user, i = 0; (user = users[i]); i++) { + var res = graphql(" \ mutation { \ user(insert: $data) { \ id \ } \ - }", { data: data }) - - users.push(res.user) + }", { data: user }); } \ No newline at end of file From d85edd949ccb858b5423d39ca6976e52c68d49cc Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 14:22:06 +0200 Subject: [PATCH 05/23] =?UTF-8?q?Configuration=20basique=20des=20r=C3=B4le?= =?UTF-8?q?s=20en=20mode=20'dev'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/dev.yml | 146 ++++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 62 deletions(-) diff --git a/backend/config/dev.yml b/backend/config/dev.yml index 185f4ce..bca6371 100644 --- a/backend/config/dev.yml +++ b/backend/config/dev.yml @@ -1,9 +1,9 @@ -app_name: "Test Development" +app_name: "Daddy Dev" host_port: 0.0.0.0:8080 web_ui: true # debug, error, warn, info -log_level: "info" +log_level: debug # enable or disable http compression (uses gzip) http_compress: true @@ -15,7 +15,7 @@ http_compress: true production: false # Throw a 401 on auth failure for queries that need auth -auth_fail_block: false +auth_fail_block: true # Latency tracing for database queries and remote joins # the resulting latency information is returned with the @@ -65,18 +65,16 @@ cors_debug: false auth: # Can be 'rails', 'jwt' or 'header' type: jwt - cookie: _supergraph_session + #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 + #creds_in_header: false - # jwt: - # provider: auth0 - # secret: abc335bfcfdb04e50db5bb0a4d67ab9 - # public_key_file: /secrets/public_key.pem - # public_key_type: ecdsa #rsa + jwt: + provider: hydra + jwks_url: http://hydra:4444/.well-known/jwks.json # header: # name: dnt @@ -87,16 +85,16 @@ auth: # 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 +# auths: + # - name: from_taskqueue + # type: header + # header: + # name: X-Appengine-Cron + # exists: true database: type: postgres - host: db + host: localhost port: 5432 dbname: daddy user: daddy @@ -105,19 +103,19 @@ database: #schema: "public" #pool_size: 10 #max_retries: 0 - #log_level: "debug" + 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 + set_user_id: true # database ping timeout is used for db health checking ping_timeout: 1m # Define additional variables here to be used with filters variables: - #admin_account_id: "5" - admin_account_id: "sql:select id from users where admin = true limit 1" + # 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 @@ -135,67 +133,91 @@ database: # 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 + # - 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: 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 + # - # 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_query: "select * from users where users.email = $user_id" roles: + # Rôle par défaut si l'utilisateur n'existe pas dans la table `users` - name: anon tables: - - name: users - query: - limit: 10 + # - name: users + # insert: + # block: true + # query: + # block: true + # update: + # block: true + # delete: + # block: true + # Rôle par défaut si l'utilisateur existe dans la table `users` + # mais que la valeur de la colonne `role` n'est pas définie - name: user tables: - name: users - 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" - + block: true + query: + filters: ["{ email: { _eq: $user_id } }"] update: - filters: ["{ user_id: { eq: $user_id } }"] - presets: - - updated_at: "now" - + columns: + - full_name + filters: ["{ email: { _eq: $user_id } }"] delete: block: true + - name: admin + match: role = 'admin' + tables: + - name: users + query: + filters: [] + + # - 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: From 01c6abdbb49a7aa965d7888b38fe24f37433a2cb Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 14:23:13 +0200 Subject: [PATCH 06/23] =?UTF-8?q?Ajout=20du=20r=C3=B4le=20dans=20la=20fich?= =?UTF-8?q?ie=20utilisateur=20+=20cr=C3=A9ation=20de=20fixtures=20avec=20l?= =?UTF-8?q?es=20r=C3=B4les=20user/admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/migrations/0_init.sql | 4 ++-- backend/config/seed.js | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/config/migrations/0_init.sql b/backend/config/migrations/0_init.sql index 17d993c..6db6d3c 100644 --- a/backend/config/migrations/0_init.sql +++ b/backend/config/migrations/0_init.sql @@ -5,7 +5,8 @@ CREATE TABLE public.users ( 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() + updated_at timestamptz NOT NULL NOT NULL DEFAULT NOW(), + role varchar(64) ); ---- create above / drop below ---- @@ -14,4 +15,3 @@ CREATE TABLE public.users ( -- then delete the separator line above. DROP TABLE public.users - diff --git a/backend/config/seed.js b/backend/config/seed.js index 84d50c3..f016d11 100644 --- a/backend/config/seed.js +++ b/backend/config/seed.js @@ -1,21 +1,25 @@ -// Example script to seed database +// Voir https://supergraph.dev/docs/seed var users = [ { - full_name: 'William Petit', - email: 'wpetit@cadoles.com' + full_name: 'Admin', + email: 'admin@cadoles.com', + role: 'admin', }, { - full_name: 'Teddy Cornaut', - email: 'tcornaut@cadoles.com' + full_name: 'User 1', + email: 'user1@cadoles.com', + role: 'user', }, { - full_name: 'Benjamin Gaudé', - email: 'bgaude@cadoles.com' + full_name: 'User 2', + email: 'user2@cadoles.com', + role: 'user', }, { - full_name: 'Charles Maxime Sassot', - email: 'cmsassot@cadoles.com' + full_name: 'User 3', + email: 'user3@cadoles.com', + role: 'user', } ]; From 01f8ca730ce0d5f140c17d3253b8e8b12150f737 Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 14:23:38 +0200 Subject: [PATCH 07/23] Configuration du endpoint JWKS pour Hydra --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 1c75906..873279d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,6 +50,7 @@ services: HYDRA_ADMIN_URL: http://localhost:4445 SERVE_PUBLIC_CORS_ENABLED: "true" SERVE_PUBLIC_CORS_ALLOWED_ORIGINS: http://localhost:8081 + WEBFINGER_JWKS_BROADCAST_KEYS: hydra.openid.id-token,hydra.jwt.access-token ports: - 4444:4444 command: hydra serve all --dangerous-force-http From 28ba0f5ba04ffcd58dbef8fb7ebf93d3b3d83f89 Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 14:24:19 +0200 Subject: [PATCH 08/23] =?UTF-8?q?Passage=20du=20owner=20de=20la=20BDD=20da?= =?UTF-8?q?ddy=20=C3=A0=20l'utilisateur=20daddy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- misc/containers/postgres/initdb.d/init-databases.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/misc/containers/postgres/initdb.d/init-databases.sh b/misc/containers/postgres/initdb.d/init-databases.sh index ddbbb85..9831c3a 100644 --- a/misc/containers/postgres/initdb.d/init-databases.sh +++ b/misc/containers/postgres/initdb.d/init-databases.sh @@ -12,4 +12,5 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E 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 From 2f1e6232787b875c794f5b361d64bf89d6c7e946 Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 14:24:35 +0200 Subject: [PATCH 09/23] =?UTF-8?q?Utilisation=20du=20fork=20de=20super-grap?= =?UTF-8?q?h=20pour=20la=20compatibilit=C3=A9=20avec=20Hydra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Principalement dans l'optique de gérer: - Le champ `audience` au format tableau, voir https://forge.cadoles.com/wpetit/super-graph/commit/b37171c19f7b827ad89505161974a3f989e2490f - La validation des jetons JWT via un endpoint JWKS, voir https://forge.cadoles.com/wpetit/super-graph/commit/788f0459fbf8455ef8c3e09935a1538352bb4f02 --- misc/containers/super-graph/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/containers/super-graph/Dockerfile b/misc/containers/super-graph/Dockerfile index 2e2ce94..46727da 100644 --- a/misc/containers/super-graph/Dockerfile +++ b/misc/containers/super-graph/Dockerfile @@ -5,12 +5,12 @@ ARG HTTPS_PROXY= ARG http_proxy= ARG https_proxy= -ARG SUPERGRAPH_VERSION=v0.14.17 +ARG SUPERGRAPH_VERSION=hydra-compat 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 \ +RUN git clone https://forge.cadoles.com/wpetit/super-graph.git \ && export PATH="$PATH:/root/go/bin" \ && export CGO_ENABLED=0 \ && cd super-graph \ From 62fe6fbeebbb08715bec36b85a85acacfb6cd132 Mon Sep 17 00:00:00 2001 From: William Petit Date: Sun, 21 Jun 2020 14:51:51 +0200 Subject: [PATCH 10/23] =?UTF-8?q?Ajout=20d'un=20fichier=20'=C3=A9chantillo?= =?UTF-8?q?n'=20pour=20la=20configuration=20du=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 508 ++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/resources/config.sample.js | 3 + frontend/webpack.config.js | 6 + 4 files changed, 518 insertions(+) create mode 100644 frontend/src/resources/config.sample.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5979b02..24c785f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1093,6 +1093,49 @@ "integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==", "dev": true }, + "@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": { + "@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": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", @@ -1585,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", @@ -3272,6 +3333,12 @@ } } }, + "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", @@ -3542,6 +3609,258 @@ "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", @@ -3965,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", @@ -4622,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", @@ -4634,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", @@ -4850,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", @@ -5498,6 +5920,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", @@ -6208,6 +6636,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", @@ -6340,6 +6774,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", @@ -8015,6 +8511,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", @@ -8058,6 +8560,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", diff --git a/frontend/package.json b/frontend/package.json index f744221..8dfffd3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "@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", diff --git a/frontend/src/resources/config.sample.js b/frontend/src/resources/config.sample.js new file mode 100644 index 0000000..8b9dd81 --- /dev/null +++ b/frontend/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/webpack.config.js b/frontend/webpack.config.js index ce20669..f552187 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -4,6 +4,7 @@ const path = require('path'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); const env = process.env; @@ -72,5 +73,10 @@ module.exports = { inject: false, favicon: "./src/resources/favicon.png", }), + new CopyPlugin({ + patterns: [ + { from: './src/resources/config.sample.js', to: 'config.js' }, + ], + }), ] } \ No newline at end of file From 369be98bd8f5710170cd23d21bf7c0f49555b6ca Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 22 Jun 2020 12:06:42 +0200 Subject: [PATCH 11/23] =?UTF-8?q?R=C3=A9cup=C3=A9ration=20du=20profil=20ut?= =?UTF-8?q?ilisateur=20apr=C3=A8s=20connexion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/allow.list | 18 +++++++++ frontend/package-lock.json | 5 +++ frontend/package.json | 1 + frontend/src/components/HomePage/HomePage.tsx | 8 ++-- frontend/src/config.ts | 3 +- frontend/src/store/actions/profile.ts | 19 ++++++++++ frontend/src/store/reducers/auth.ts | 16 +++++++- frontend/src/store/sagas/init.ts | 18 +++++++++ frontend/src/store/sagas/root.ts | 4 ++ frontend/src/store/sagas/users.ts | 37 ++++++++++++++++++ frontend/src/types/user.ts | 3 ++ frontend/src/util/daddy.ts | 38 +++++++++++++++---- 12 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 frontend/src/store/actions/profile.ts create mode 100644 frontend/src/store/sagas/init.ts create mode 100644 frontend/src/store/sagas/users.ts diff --git a/backend/config/allow.list b/backend/config/allow.list index e69de29..92cf2d2 100644 --- a/backend/config/allow.list +++ b/backend/config/allow.list @@ -0,0 +1,18 @@ +/* fetchUser */ + +variables { + "email": "" +} + + + query fetchUser { + user(where: {email: {eq: $email}}) { + id + created_at + updated_at + email, + full_name + } + } + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 24c785f..1e597da 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5499,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", diff --git a/frontend/package.json b/frontend/package.json index 8dfffd3..6f0d883 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "@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", diff --git a/frontend/src/components/HomePage/HomePage.tsx b/frontend/src/components/HomePage/HomePage.tsx index 150726a..b9ac584 100644 --- a/frontend/src/components/HomePage/HomePage.tsx +++ b/frontend/src/components/HomePage/HomePage.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Page } from '../Page'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../store/reducers/root'; export function HomePage() { @@ -14,8 +14,8 @@ export function HomePage() {
{ - currentUser ? -

Bonjour {currentUser.email} !

: + currentUser && currentUser.full_name ? +

Bonjour {currentUser.full_name} !

:

Veuillez vous authentifier.

}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 9b59e24..98ee0a4 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -7,7 +7,8 @@ export const Config = { oauth2AuthorizeURL: get("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"), oauth2TokenURL: get("oauth2TokenURL", "http://localhost:4444/oauth2/token"), oauth2LogoutURL: get("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"), - oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081") + oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081"), + graphQLEndpoint: get("graphQLEndpoint", "http://localhost:8080/api/v1/graphql") }; function get(key: string, defaultValue: T):T { diff --git a/frontend/src/store/actions/profile.ts b/frontend/src/store/actions/profile.ts new file mode 100644 index 0000000..c46a75f --- /dev/null +++ b/frontend/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/frontend/src/store/reducers/auth.ts b/frontend/src/store/reducers/auth.ts index a0bd507..e888748 100644 --- a/frontend/src/store/reducers/auth.ts +++ b/frontend/src/store/reducers/auth.ts @@ -1,6 +1,7 @@ import { Action } from "redux"; import { User } from "../../types/user"; import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth"; +import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile"; export interface AuthState { isAuthenticated: boolean @@ -18,6 +19,9 @@ export function authReducer(state = defaultState, action: Action): AuthState { return handleSetCurrentUser(state, action as setCurrentUserAction); case LOGOUT: return handleLogout(state); + case FETCH_PROFILE_SUCCESS: + return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction); + } return state; } @@ -38,4 +42,14 @@ function handleLogout(state: AuthState): AuthState { isAuthenticated: false, currentUser: null, }; -} \ No newline at end of file +}; + +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/sagas/init.ts b/frontend/src/store/sagas/init.ts new file mode 100644 index 0000000..da4d595 --- /dev/null +++ b/frontend/src/store/sagas/init.ts @@ -0,0 +1,18 @@ +import { all, put } from "redux-saga/effects"; +import { getSavedAccessGrant } from "../../util/auth"; +import { parseIdToken } from "../actions/auth"; + +export function* initRootSaga() { + yield all([ + retrieveSessionSaga(), + ]); +} + +export function* retrieveSessionSaga() { + console.log("Checking session status..."); + + const accessGrant = getSavedAccessGrant(); + if (!accessGrant) return; + + yield put(parseIdToken(accessGrant.id_token)); +} \ No newline at end of file diff --git a/frontend/src/store/sagas/root.ts b/frontend/src/store/sagas/root.ts index dfa853a..71fdf80 100644 --- a/frontend/src/store/sagas/root.ts +++ b/frontend/src/store/sagas/root.ts @@ -1,10 +1,14 @@ import { all } from 'redux-saga/effects'; import { failureRootSaga } from './failure'; import { authRootSaga } from './auth'; +import { initRootSaga } from './init'; +import { usersRootSaga } from './users'; export function* rootSaga() { yield all([ + initRootSaga(), failureRootSaga(), authRootSaga(), + usersRootSaga(), ]); } diff --git a/frontend/src/store/sagas/users.ts b/frontend/src/store/sagas/users.ts new file mode 100644 index 0000000..40521b3 --- /dev/null +++ b/frontend/src/store/sagas/users.ts @@ -0,0 +1,37 @@ +import { DaddyClient } from "../../util/daddy"; +import { Config } from "../../config"; +import { getSavedAccessGrant } from "../../util/auth"; +import { all, takeLatest, put, select } from "redux-saga/effects"; +import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile"; +import { SET_CURRENT_USER } from "../actions/auth"; +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 grant = getSavedAccessGrant(); + const client = new DaddyClient(Config.graphQLEndpoint, grant.id_token); + + 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/types/user.ts b/frontend/src/types/user.ts index 70d7e8b..13c920d 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -1,3 +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/frontend/src/util/daddy.ts b/frontend/src/util/daddy.ts index 22bb4c5..953ea9d 100644 --- a/frontend/src/util/daddy.ts +++ b/frontend/src/util/daddy.ts @@ -1,3 +1,6 @@ +import { GraphQLClient } from 'graphql-request' +import { Config } from "../config"; + export class UnauthorizedError extends Error { constructor(...args: any[]) { super(...args) @@ -7,16 +10,35 @@ export class UnauthorizedError extends Error { export class DaddyClient { - assertOk(res: any) { - if (!res.ok) return Promise.reject(new Error('Request failed')); - return res; + gql: GraphQLClient + + constructor(endpoint: string, idToken: string) { + this.gql = new GraphQLClient(endpoint, { + headers: { + Authorization: `Bearer ${idToken}`, + mode: 'cors', + } + }); } - assertAuthorization(res: any) { - if (res.status === 401 || res.status === 404) return Promise.reject(new UnauthorizedError()); - return res; + 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; + } -export const daddy = new DaddyClient(); \ No newline at end of file +} \ No newline at end of file From 3bef551ad629f7325eafc1a5b02212ffc9201b20 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 22 Jun 2020 12:07:27 +0200 Subject: [PATCH 12/23] Ajout tip sur l'utilisation de la webui GraphQL --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8620f09..43e9f35 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,15 @@ Les services suivants devraient être disponibles après démarrage de l'environ |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| +|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| |Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)| |Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)| |Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp) |Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement| +**\*** Pensez à passer l'attribut `auth_fail_block: false` dans le fichier `backend/config/dev.yml` si vous voulez pouvoir utiliser cette interface sans avoir à définir l'entête `Authorization`. + #### Fichiers/répertoires notables |Chemin|Description| From 13571d76178d893d52e5eeecee346e852e574de3 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 22 Jun 2020 12:07:48 +0200 Subject: [PATCH 13/23] Configuration basique du backend pour l'authentification/autorisation via JWT --- backend/config/dev.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/config/dev.yml b/backend/config/dev.yml index bca6371..a32f542 100644 --- a/backend/config/dev.yml +++ b/backend/config/dev.yml @@ -42,6 +42,8 @@ secret_key: supercalifajalistics # An origin may contain a wildcard (*) to replace 0 or more # characters (i.e.: http://*.domain.com). cors_allowed_origins: ["*"] +cors_allowed_headers: ["Authorization", "Content-Type", "Mode"] +cors_allowed_methods: ["POST"] # Debug Cross Origin Resource Sharing requests cors_debug: false @@ -165,15 +167,15 @@ roles: # Rôle par défaut si l'utilisateur n'existe pas dans la table `users` - name: anon tables: - # - name: users - # insert: - # block: true - # query: - # block: true - # update: - # block: true - # delete: - # block: true + - name: users + # insert: + # block: true + # query: + # block: true + # update: + # block: true + # delete: + # block: true # Rôle par défaut si l'utilisateur existe dans la table `users` # mais que la valeur de la colonne `role` n'est pas définie @@ -195,8 +197,6 @@ roles: match: role = 'admin' tables: - name: users - query: - filters: [] # - name: products # query: From 8c6e52731dd25826829a9d6dfec8ab8f92e01684 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 22 Jun 2020 12:08:31 +0200 Subject: [PATCH 14/23] =?UTF-8?q?Mise=20=C3=A0=20jour=20version=20super-gr?= =?UTF-8?q?aph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- misc/containers/super-graph/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/containers/super-graph/Dockerfile b/misc/containers/super-graph/Dockerfile index 46727da..dfca8ca 100644 --- a/misc/containers/super-graph/Dockerfile +++ b/misc/containers/super-graph/Dockerfile @@ -5,7 +5,7 @@ ARG HTTPS_PROXY= ARG http_proxy= ARG https_proxy= -ARG SUPERGRAPH_VERSION=hydra-compat +ARG SUPERGRAPH_VERSION=88ba105b70c60b2c7467dc1f76f041cec2614a04 ARG WAITFORIT_VERSION=v2.4.1 RUN apk add --no-cache go make git curl bash ca-certificates From 1120474ad905d1cf6407ecd643973d5ba68b2c2d Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 10 Jul 2020 18:07:41 +0200 Subject: [PATCH 15/23] Utilisation d'un serveur Go custom pour le backend au lieu de super-graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Malheureusement, super-graph n'a pas tenu les promesses qu'il semblait annoncer. Je propose donc de basculer sur un serveur Go classique (via goweb). L'authentification OpenID Connect étant gérée côté backend et non plus côté frontend. --- .env.dist | 3 + .gitignore | 4 + Makefile | 24 +- README.md | 10 +- backend/config/allow.list | 18 -- backend/config/dev.yml | 225 --------------- backend/config/migrations/0_init.sql | 17 -- backend/config/prod.yml | 67 ----- backend/config/seed.js | 33 --- {frontend => client}/.gitignore | 0 {frontend => client}/package-lock.json | 0 {frontend => client}/package.json | 0 {frontend => client}/src/components/App.tsx | 2 - .../src/components/HomePage/HomePage.tsx | 0 .../src/components/Loader.tsx | 0 {frontend => client}/src/components/Modal.tsx | 0 .../src/components/Navbar.tsx | 10 +- {frontend => client}/src/components/Page.tsx | 0 client/src/config.ts | 14 + {frontend => client}/src/custom.d.ts | 0 {frontend => client}/src/index.html | 0 {frontend => client}/src/index.tsx | 0 .../src/resources/config.sample.js | 0 .../src/resources/favicon.png | Bin {frontend => client}/src/resources/logo.svg | 0 {frontend => client}/src/sass/_all.scss | 0 {frontend => client}/src/sass/_base.scss | 0 {frontend => client}/src/sass/_loader.scss | 0 client/src/store/actions/auth.ts | 11 + .../src/store/actions/profile.ts | 0 .../src/store/reducers/auth.ts | 12 +- .../src/store/reducers/flags.ts | 0 .../src/store/reducers/root.ts | 0 .../src/store/sagas/failure.ts | 6 +- client/src/store/sagas/init.ts | 7 + {frontend => client}/src/store/sagas/root.ts | 3 - {frontend => client}/src/store/sagas/users.ts | 6 +- .../src/store/selectors/flags.ts | 0 {frontend => client}/src/store/store.ts | 0 {frontend => client}/src/types/user.ts | 0 {frontend => client}/src/util/daddy.ts | 3 +- {frontend => client}/tsconfig.json | 0 {frontend => client}/webpack.config.js | 3 +- cmd/server/container.go | 90 ++++++ cmd/server/main.go | 167 +++++++++++ docker-compose.yml | 26 +- .../src/components/OAuth2Page/OAuth2Page.tsx | 31 -- frontend/src/config.ts | 21 -- frontend/src/store/actions/auth.ts | 69 ----- frontend/src/store/sagas/auth.ts | 98 ------- frontend/src/store/sagas/init.ts | 18 -- frontend/src/types/idToken.ts | 3 - frontend/src/util/auth.ts | 126 -------- go.mod | 13 + go.sum | 268 ++++++++++++++++++ internal/config/config.go | 107 +++++++ internal/config/provider.go | 9 + internal/config/service.go | 33 +++ internal/route/login.go | 35 +++ internal/route/logout.go | 33 +++ internal/route/mount.go | 27 ++ .../hydra/hydra-init.d/create-client | 9 +- misc/containers/super-graph/Dockerfile | 36 --- .../super-graph/docker-entrypoint.sh | 13 - modd.conf | 18 ++ 65 files changed, 880 insertions(+), 848 deletions(-) create mode 100644 .env.dist create mode 100644 .gitignore delete mode 100644 backend/config/allow.list delete mode 100644 backend/config/dev.yml delete mode 100644 backend/config/migrations/0_init.sql delete mode 100644 backend/config/prod.yml delete mode 100644 backend/config/seed.js rename {frontend => client}/.gitignore (100%) rename {frontend => client}/package-lock.json (100%) rename {frontend => client}/package.json (100%) rename {frontend => client}/src/components/App.tsx (81%) rename {frontend => client}/src/components/HomePage/HomePage.tsx (100%) rename {frontend => client}/src/components/Loader.tsx (100%) rename {frontend => client}/src/components/Modal.tsx (100%) rename {frontend => client}/src/components/Navbar.tsx (86%) rename {frontend => client}/src/components/Page.tsx (100%) create mode 100644 client/src/config.ts rename {frontend => client}/src/custom.d.ts (100%) rename {frontend => client}/src/index.html (100%) rename {frontend => client}/src/index.tsx (100%) rename {frontend => client}/src/resources/config.sample.js (100%) rename {frontend => client}/src/resources/favicon.png (100%) rename {frontend => client}/src/resources/logo.svg (100%) rename {frontend => client}/src/sass/_all.scss (100%) rename {frontend => client}/src/sass/_base.scss (100%) rename {frontend => client}/src/sass/_loader.scss (100%) create mode 100644 client/src/store/actions/auth.ts rename {frontend => client}/src/store/actions/profile.ts (100%) rename {frontend => client}/src/store/reducers/auth.ts (79%) rename {frontend => client}/src/store/reducers/flags.ts (100%) rename {frontend => client}/src/store/reducers/root.ts (100%) rename {frontend => client}/src/store/sagas/failure.ts (73%) create mode 100644 client/src/store/sagas/init.ts rename {frontend => client}/src/store/sagas/root.ts (76%) rename {frontend => client}/src/store/sagas/users.ts (86%) rename {frontend => client}/src/store/selectors/flags.ts (100%) rename {frontend => client}/src/store/store.ts (100%) rename {frontend => client}/src/types/user.ts (100%) rename {frontend => client}/src/util/daddy.ts (89%) rename {frontend => client}/tsconfig.json (100%) rename {frontend => client}/webpack.config.js (98%) create mode 100644 cmd/server/container.go create mode 100644 cmd/server/main.go delete mode 100644 frontend/src/components/OAuth2Page/OAuth2Page.tsx delete mode 100644 frontend/src/config.ts delete mode 100644 frontend/src/store/actions/auth.ts delete mode 100644 frontend/src/store/sagas/auth.ts delete mode 100644 frontend/src/store/sagas/init.ts delete mode 100644 frontend/src/types/idToken.ts delete mode 100644 frontend/src/util/auth.ts create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/provider.go create mode 100644 internal/config/service.go create mode 100644 internal/route/login.go create mode 100644 internal/route/logout.go create mode 100644 internal/route/mount.go delete mode 100644 misc/containers/super-graph/Dockerfile delete mode 100644 misc/containers/super-graph/docker-entrypoint.sh create mode 100644 modd.conf diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..5aedd1c --- /dev/null +++ b/.env.dist @@ -0,0 +1,3 @@ +OIDC_CLIENT_ID=daddy +OIDC_CLIENT_SECRET=daddycool +OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29c0e1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +/data +/bin +/.env \ No newline at end of file diff --git a/Makefile b/Makefile index cf6a758..3e9f24a 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,20 @@ -build: +build: build-docker build-server + +build-docker: docker-compose build +build-server: + CGO_ENABLED=0 go build -mod=vendor -v -o ./bin/server ./cmd/server + deps: - cd frontend && npm install + cd client && npm install + env GO111MODULE=off go get github.com/cortesi/modd/cmd/modd -up: build - ( cd frontend && NODE_ENV=development npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait +up: build-docker + docker-compose up -sg: - docker-compose exec -u $(shell id -u) super-graph sh - -sgr: - docker-compose run -u $(shell id -u) super-graph sh +watch: + $(GOPATH)/bin/modd down: docker-compose down -v --remove-orphans @@ -19,5 +22,8 @@ down: db-shell: docker-compose exec postgres psql -Udaddy +test: + go test -v ./... + hydra-shell: docker-compose exec hydra /bin/sh \ No newline at end of file diff --git a/README.md b/README.md index 43e9f35..a63c794 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,18 @@ Application de gestion des Dossiers d'Aide à la Décision (D.A.D.) à Cadoles. 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 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/|Interface Web de développement de l'API GraphQL| +|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql|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) diff --git a/backend/config/allow.list b/backend/config/allow.list deleted file mode 100644 index 92cf2d2..0000000 --- a/backend/config/allow.list +++ /dev/null @@ -1,18 +0,0 @@ -/* fetchUser */ - -variables { - "email": "" -} - - - query fetchUser { - user(where: {email: {eq: $email}}) { - id - created_at - updated_at - email, - full_name - } - } - - diff --git a/backend/config/dev.yml b/backend/config/dev.yml deleted file mode 100644 index a32f542..0000000 --- a/backend/config/dev.yml +++ /dev/null @@ -1,225 +0,0 @@ -app_name: "Daddy Dev" -host_port: 0.0.0.0:8080 -web_ui: true - -# debug, error, warn, info -log_level: debug - -# enable or disable http compression (uses gzip) -http_compress: true - -# When production mode is 'true' only queries -# from the allow list are permitted. -# When it's 'false' all queries are saved to the -# the allow list in ./config/allow.list -production: false - -# Throw a 401 on auth failure for queries that need auth -auth_fail_block: true - -# Latency tracing for database queries and remote joins -# the resulting latency information is returned with the -# response -enable_tracing: true - -# Watch the config folder and reload Super Graph -# with the new configs when a change is detected -reload_on_config_change: true - -# File that points to the database seeding script -# seed_file: seed.js - -# Path pointing to where the migrations can be found -# this must be a relative path under the config path -migrations_path: ./migrations - -# Secret key for general encryption operations like -# encrypting the cursor data -secret_key: supercalifajalistics - -# CORS: A list of origins a cross-domain request can be executed from. -# If the special * value is present in the list, all origins will be allowed. -# An origin may contain a wildcard (*) to replace 0 or more -# characters (i.e.: http://*.domain.com). -cors_allowed_origins: ["*"] -cors_allowed_headers: ["Authorization", "Content-Type", "Mode"] -cors_allowed_methods: ["POST"] - -# Debug Cross Origin Resource Sharing requests -cors_debug: false - -# Postgres related environment Variables -# SG_DATABASE_HOST -# SG_DATABASE_PORT -# SG_DATABASE_USER -# SG_DATABASE_PASSWORD - -# Auth related environment Variables -# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE -# SG_AUTH_RAILS_REDIS_URL -# SG_AUTH_RAILS_REDIS_PASSWORD -# SG_AUTH_JWT_PUBLIC_KEY_FILE - -# inflections: -# person: people -# sheep: sheep - -auth: - # Can be 'rails', 'jwt' or 'header' - type: jwt - #cookie: _supergraph_session - - # Comment this out if you want to disable setting - # the user_id via a header for testing. - # Disable in production - #creds_in_header: false - - jwt: - provider: hydra - jwks_url: http://hydra:4444/.well-known/jwks.json - - # header: - # name: dnt - # exists: true - # value: localhost:8080 - -# You can add additional named auths to use with actions -# In this example actions using this auth can only be -# called from the Google Appengine Cron service that -# sets a special header to all it's requests -# auths: - # - name: from_taskqueue - # type: header - # header: - # name: X-Appengine-Cron - # exists: true - -database: - type: postgres - host: localhost - port: 5432 - dbname: daddy - user: daddy - password: daddy - - #schema: "public" - #pool_size: 10 - #max_retries: 0 - log_level: "debug" - - # Set session variable "user.id" to the user id - # Enable this if you need the user id in triggers, etc - set_user_id: true - - # database ping timeout is used for db health checking - ping_timeout: 1m - - # Define additional variables here to be used with filters - variables: - # admin_account_id: "5" - # admin_account_id: "sql:select id from users where admin = true limit 1" - - - # Field and table names that you wish to block - blocklist: - - ar_internal_metadata - - schema_migrations - - secret - - password - - encrypted - - token - -# Create custom actions with their own api endpoints -# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users -# A request to this url will execute the configured SQL query -# which in this case refreshes a materialized view in the database. -# The auth_name is from one of the configured auths -actions: - # - name: refresh_leaderboard_users - # sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users" - # auth_name: from_taskqueue - -tables: - # - name: customers - # remotes: - # - name: payments - # id: stripe_id - # url: http://rails_app:3000/stripe/$id - # path: data - # # debug: true - # pass_headers: - # - cookie - # set_headers: - # - name: Host - # value: 0.0.0.0 - # - name: Authorization - # value: Bearer - - # - # You can create new fields that have a - # # real db table backing them - # name: me - # table: users - - -roles_query: "select * from users where users.email = $user_id" - -roles: - # Rôle par défaut si l'utilisateur n'existe pas dans la table `users` - - name: anon - tables: - - name: users - # insert: - # block: true - # query: - # block: true - # update: - # block: true - # delete: - # block: true - - # Rôle par défaut si l'utilisateur existe dans la table `users` - # mais que la valeur de la colonne `role` n'est pas définie - - name: user - tables: - - name: users - insert: - block: true - query: - filters: ["{ email: { _eq: $user_id } }"] - update: - columns: - - full_name - filters: ["{ email: { _eq: $user_id } }"] - delete: - block: true - - - name: admin - match: role = 'admin' - tables: - - name: users - - # - name: products - # query: - # limit: 50 - # filters: ["{ user_id: { eq: $user_id } }"] - # disable_functions: false - - # insert: - # filters: ["{ user_id: { eq: $user_id } }"] - # presets: - # - user_id: "$user_id" - # - created_at: "now" - - # update: - # filters: ["{ user_id: { eq: $user_id } }"] - # presets: - # - updated_at: "now" - - # delete: - # block: true - - # - name: admin - # match: id = 1000 - # tables: - # - name: users - # filters: [] diff --git a/backend/config/migrations/0_init.sql b/backend/config/migrations/0_init.sql deleted file mode 100644 index 6db6d3c..0000000 --- a/backend/config/migrations/0_init.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Write your migrate up statements here - -CREATE TABLE public.users ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - full_name text, - email text UNIQUE NOT NULL CHECK (length(email) < 255), - created_at timestamptz NOT NULL NOT NULL DEFAULT NOW(), - updated_at timestamptz NOT NULL NOT NULL DEFAULT NOW(), - role varchar(64) -); - ----- create above / drop below ---- - --- Write your down migrate statements here. If this migration is irreversible --- then delete the separator line above. - -DROP TABLE public.users diff --git a/backend/config/prod.yml b/backend/config/prod.yml deleted file mode 100644 index 5987ea3..0000000 --- a/backend/config/prod.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Inherit config from this other config file -# so I only need to overwrite some values -inherits: dev - -app_name: "Backend Production" -host_port: 0.0.0.0:8080 -web_ui: false - -# debug, info, warn, error, fatal, panic, disable -log_level: "warn" - -# enable or disable http compression (uses gzip) -http_compress: true - -# When production mode is 'true' only queries -# from the allow list are permitted. -# When it's 'false' all queries are saved to the -# the allow list in ./config/allow.list -production: true - -# Throw a 401 on auth failure for queries that need auth -auth_fail_block: true - -# Latency tracing for database queries and remote joins -# the resulting latency information is returned with the -# response -enable_tracing: true - -# File that points to the database seeding script -# seed_file: seed.js - -# Path pointing to where the migrations can be found -# migrations_path: migrations - -# Secret key for general encryption operations like -# encrypting the cursor data -# secret_key: supercalifajalistics - -# Postgres related environment Variables -# SG_DATABASE_HOST -# SG_DATABASE_PORT -# SG_DATABASE_USER -# SG_DATABASE_PASSWORD - -# Auth related environment Variables -# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE -# SG_AUTH_RAILS_REDIS_URL -# SG_AUTH_RAILS_REDIS_PASSWORD -# SG_AUTH_JWT_PUBLIC_KEY_FILE - -database: - type: postgres - host: db - port: 5432 - dbname: backend_development - user: postgres - password: postgres - #pool_size: 10 - #max_retries: 0 - #log_level: "debug" - - # Set session variable "user.id" to the user id - # Enable this if you need the user id in triggers, etc - set_user_id: false - - # database ping timeout is used for db health checking - ping_timeout: 5m \ No newline at end of file diff --git a/backend/config/seed.js b/backend/config/seed.js deleted file mode 100644 index f016d11..0000000 --- a/backend/config/seed.js +++ /dev/null @@ -1,33 +0,0 @@ -// Voir https://supergraph.dev/docs/seed - -var users = [ - { - full_name: 'Admin', - email: 'admin@cadoles.com', - role: 'admin', - }, - { - full_name: 'User 1', - email: 'user1@cadoles.com', - role: 'user', - }, - { - full_name: 'User 2', - email: 'user2@cadoles.com', - role: 'user', - }, - { - full_name: 'User 3', - email: 'user3@cadoles.com', - role: 'user', - } -]; - -for (var user, i = 0; (user = users[i]); i++) { - var res = graphql(" \ - mutation { \ - user(insert: $data) { \ - id \ - } \ - }", { data: user }); -} \ No newline at end of file diff --git a/frontend/.gitignore b/client/.gitignore similarity index 100% rename from frontend/.gitignore rename to client/.gitignore diff --git a/frontend/package-lock.json b/client/package-lock.json similarity index 100% rename from frontend/package-lock.json rename to client/package-lock.json diff --git a/frontend/package.json b/client/package.json similarity index 100% rename from frontend/package.json rename to client/package.json diff --git a/frontend/src/components/App.tsx b/client/src/components/App.tsx similarity index 81% rename from frontend/src/components/App.tsx rename to client/src/components/App.tsx index 53df347..3bea7f4 100644 --- a/frontend/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -3,7 +3,6 @@ import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; import { HomePage } from './HomePage/HomePage'; import { store } from '../store/store'; import { Provider } from 'react-redux'; -import { OAuth2Page } from './OAuth2Page/OAuth2Page'; export class App extends React.Component { render() { @@ -12,7 +11,6 @@ export class App extends React.Component { - } /> diff --git a/frontend/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx similarity index 100% rename from frontend/src/components/HomePage/HomePage.tsx rename to client/src/components/HomePage/HomePage.tsx diff --git a/frontend/src/components/Loader.tsx b/client/src/components/Loader.tsx similarity index 100% rename from frontend/src/components/Loader.tsx rename to client/src/components/Loader.tsx diff --git a/frontend/src/components/Modal.tsx b/client/src/components/Modal.tsx similarity index 100% rename from frontend/src/components/Modal.tsx rename to client/src/components/Modal.tsx diff --git a/frontend/src/components/Navbar.tsx b/client/src/components/Navbar.tsx similarity index 86% rename from frontend/src/components/Navbar.tsx rename to client/src/components/Navbar.tsx index d603547..def6d3b 100644 --- a/frontend/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -2,7 +2,7 @@ import React from 'react'; import logo from '../resources/logo.svg'; import { useSelector } from 'react-redux'; import { RootState } from '../store/reducers/root'; -import { Link } from 'react-router-dom'; +import { Config } from '../config'; export function Navbar() { const isAuthenticated = useSelector(state => state.auth.isAuthenticated); @@ -26,18 +26,18 @@ export function Navbar() {
{ isAuthenticated ? - + Se déconnecter - : - + : + Se connecter - + }
diff --git a/frontend/src/components/Page.tsx b/client/src/components/Page.tsx similarity index 100% rename from frontend/src/components/Page.tsx rename to client/src/components/Page.tsx diff --git a/client/src/config.ts b/client/src/config.ts new file mode 100644 index 0000000..d8f51fe --- /dev/null +++ b/client/src/config.ts @@ -0,0 +1,14 @@ +export const Config = { + loginURL: get("loginURL", "http://localhost:8081/login"), + logoutURL: get("logoutURL", "http://localhost:8081/logout"), + graphQLEndpoint: get("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"), +}; + +function get(key: string, defaultValue: T):T { + const config = window['__CONFIG__'] || {}; + if (config && config.hasOwnProperty(key)) { + return config[key] as T; + } else { + return defaultValue; + } +} \ No newline at end of file diff --git a/frontend/src/custom.d.ts b/client/src/custom.d.ts similarity index 100% rename from frontend/src/custom.d.ts rename to client/src/custom.d.ts diff --git a/frontend/src/index.html b/client/src/index.html similarity index 100% rename from frontend/src/index.html rename to client/src/index.html diff --git a/frontend/src/index.tsx b/client/src/index.tsx similarity index 100% rename from frontend/src/index.tsx rename to client/src/index.tsx diff --git a/frontend/src/resources/config.sample.js b/client/src/resources/config.sample.js similarity index 100% rename from frontend/src/resources/config.sample.js rename to client/src/resources/config.sample.js diff --git a/frontend/src/resources/favicon.png b/client/src/resources/favicon.png similarity index 100% rename from frontend/src/resources/favicon.png rename to client/src/resources/favicon.png diff --git a/frontend/src/resources/logo.svg b/client/src/resources/logo.svg similarity index 100% rename from frontend/src/resources/logo.svg rename to client/src/resources/logo.svg diff --git a/frontend/src/sass/_all.scss b/client/src/sass/_all.scss similarity index 100% rename from frontend/src/sass/_all.scss rename to client/src/sass/_all.scss diff --git a/frontend/src/sass/_base.scss b/client/src/sass/_base.scss similarity index 100% rename from frontend/src/sass/_base.scss rename to client/src/sass/_base.scss diff --git a/frontend/src/sass/_loader.scss b/client/src/sass/_loader.scss similarity index 100% rename from frontend/src/sass/_loader.scss rename to client/src/sass/_loader.scss diff --git a/client/src/store/actions/auth.ts b/client/src/store/actions/auth.ts new file mode 100644 index 0000000..8a8c608 --- /dev/null +++ b/client/src/store/actions/auth.ts @@ -0,0 +1,11 @@ +import { Action } from "redux"; + +export const SET_CURRENT_USER = 'SET_CURRENT_USER'; + +export interface setCurrentUserAction extends Action { + email: string +} + +export function setCurrentUser(email: string): setCurrentUserAction { + return { type: SET_CURRENT_USER, email }; +} \ No newline at end of file diff --git a/frontend/src/store/actions/profile.ts b/client/src/store/actions/profile.ts similarity index 100% rename from frontend/src/store/actions/profile.ts rename to client/src/store/actions/profile.ts diff --git a/frontend/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts similarity index 79% rename from frontend/src/store/reducers/auth.ts rename to client/src/store/reducers/auth.ts index e888748..b25f0c4 100644 --- a/frontend/src/store/reducers/auth.ts +++ b/client/src/store/reducers/auth.ts @@ -1,6 +1,6 @@ import { Action } from "redux"; import { User } from "../../types/user"; -import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth"; +import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile"; export interface AuthState { @@ -17,8 +17,6 @@ export function authReducer(state = defaultState, action: Action): AuthState { switch (action.type) { case SET_CURRENT_USER: return handleSetCurrentUser(state, action as setCurrentUserAction); - case LOGOUT: - return handleLogout(state); case FETCH_PROFILE_SUCCESS: return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction); @@ -36,14 +34,6 @@ function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction) }; }; -function handleLogout(state: AuthState): AuthState { - return { - ...state, - isAuthenticated: false, - currentUser: null, - }; -}; - function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState { return { ...state, diff --git a/frontend/src/store/reducers/flags.ts b/client/src/store/reducers/flags.ts similarity index 100% rename from frontend/src/store/reducers/flags.ts rename to client/src/store/reducers/flags.ts diff --git a/frontend/src/store/reducers/root.ts b/client/src/store/reducers/root.ts similarity index 100% rename from frontend/src/store/reducers/root.ts rename to client/src/store/reducers/root.ts diff --git a/frontend/src/store/sagas/failure.ts b/client/src/store/sagas/failure.ts similarity index 73% rename from frontend/src/store/sagas/failure.ts rename to client/src/store/sagas/failure.ts index 97bc18c..de90822 100644 --- a/frontend/src/store/sagas/failure.ts +++ b/client/src/store/sagas/failure.ts @@ -1,6 +1,5 @@ import { UnauthorizedError } from "../../util/daddy"; -import { put, all, takeEvery } from 'redux-saga/effects'; -import { logout } from '../actions/auth'; +import { all, takeEvery } from 'redux-saga/effects'; export function* failureRootSaga() { yield all([ @@ -10,7 +9,8 @@ export function* failureRootSaga() { export function* failuresSaga(action) { if (action.error instanceof UnauthorizedError) { - yield put(logout()); + // TODO Implements better authorization error handling + window.location.reload(); } } diff --git a/client/src/store/sagas/init.ts b/client/src/store/sagas/init.ts new file mode 100644 index 0000000..fdd4fa2 --- /dev/null +++ b/client/src/store/sagas/init.ts @@ -0,0 +1,7 @@ +import { all, put } from "redux-saga/effects"; + +export function* initRootSaga() { + yield all([ + + ]); +} \ No newline at end of file diff --git a/frontend/src/store/sagas/root.ts b/client/src/store/sagas/root.ts similarity index 76% rename from frontend/src/store/sagas/root.ts rename to client/src/store/sagas/root.ts index 71fdf80..3b29c86 100644 --- a/frontend/src/store/sagas/root.ts +++ b/client/src/store/sagas/root.ts @@ -1,6 +1,5 @@ import { all } from 'redux-saga/effects'; import { failureRootSaga } from './failure'; -import { authRootSaga } from './auth'; import { initRootSaga } from './init'; import { usersRootSaga } from './users'; @@ -8,7 +7,5 @@ export function* rootSaga() { yield all([ initRootSaga(), failureRootSaga(), - authRootSaga(), - usersRootSaga(), ]); } diff --git a/frontend/src/store/sagas/users.ts b/client/src/store/sagas/users.ts similarity index 86% rename from frontend/src/store/sagas/users.ts rename to client/src/store/sagas/users.ts index 40521b3..42f0ac2 100644 --- a/frontend/src/store/sagas/users.ts +++ b/client/src/store/sagas/users.ts @@ -1,6 +1,5 @@ import { DaddyClient } from "../../util/daddy"; import { Config } from "../../config"; -import { getSavedAccessGrant } from "../../util/auth"; import { all, takeLatest, put, select } from "redux-saga/effects"; import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile"; import { SET_CURRENT_USER } from "../actions/auth"; @@ -18,11 +17,8 @@ export function* onCurrentUserChangeSaga() { yield put(fetchProfile()); } - - export function* fetchProfileSaga() { - const grant = getSavedAccessGrant(); - const client = new DaddyClient(Config.graphQLEndpoint, grant.id_token); + const client = new DaddyClient(Config.graphQLEndpoint); let profile: User; try { diff --git a/frontend/src/store/selectors/flags.ts b/client/src/store/selectors/flags.ts similarity index 100% rename from frontend/src/store/selectors/flags.ts rename to client/src/store/selectors/flags.ts diff --git a/frontend/src/store/store.ts b/client/src/store/store.ts similarity index 100% rename from frontend/src/store/store.ts rename to client/src/store/store.ts diff --git a/frontend/src/types/user.ts b/client/src/types/user.ts similarity index 100% rename from frontend/src/types/user.ts rename to client/src/types/user.ts diff --git a/frontend/src/util/daddy.ts b/client/src/util/daddy.ts similarity index 89% rename from frontend/src/util/daddy.ts rename to client/src/util/daddy.ts index 953ea9d..9d3d104 100644 --- a/frontend/src/util/daddy.ts +++ b/client/src/util/daddy.ts @@ -12,10 +12,9 @@ export class DaddyClient { gql: GraphQLClient - constructor(endpoint: string, idToken: string) { + constructor(endpoint: string) { this.gql = new GraphQLClient(endpoint, { headers: { - Authorization: `Bearer ${idToken}`, mode: 'cors', } }); diff --git a/frontend/tsconfig.json b/client/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to client/tsconfig.json diff --git a/frontend/webpack.config.js b/client/webpack.config.js similarity index 98% rename from frontend/webpack.config.js rename to client/webpack.config.js index f552187..be40da5 100644 --- a/frontend/webpack.config.js +++ b/client/webpack.config.js @@ -22,7 +22,8 @@ module.exports = { devServer: { contentBase: path.join(__dirname, 'dist'), compress: true, - port: 8081, + host: '0.0.0.0', + port: 8080, historyApiFallback: true, writeToDisk: true, }, diff --git a/cmd/server/container.go b/cmd/server/container.go new file mode 100644 index 0000000..18fe9f3 --- /dev/null +++ b/cmd/server/container.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "net/http" + + "gitlab.com/wpetit/goweb/logger" + "gitlab.com/wpetit/goweb/template/html" + + "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/service/template" + "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 template service provider + ctn.Provide(template.ServiceName, html.ServiceProvider( + conf.HTTP.TemplateDir, + )) + + // 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"), + )) + + return ctn, nil +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..e505a7a --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,167 @@ +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 +) + +// 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") +} + +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), + ) + } + + 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/docker-compose.yml b/docker-compose.yml index 873279d..9846a19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,5 @@ version: '2.4' services: - super-graph: - build: - context: ./misc/containers/super-graph - args: - - HTTP_PROXY=${HTTP_PROXY} - - HTTPS_PROXY=${HTTPS_PROXY} - - http_proxy=${http_proxy} - - https_proxy=${https_proxy} - environment: - - SG_DATABASE_HOST=postgres - - SG_DATABASE_USER=daddy - - SG_DATABASE_PASSWORD=daddy - - USER_ID=${USER_ID} - - GO_ENV=dev - volumes: - - ./backend:/app - links: - - postgres - ports: - - 8080:8080 - postgres: build: context: ./misc/containers/postgres @@ -48,15 +27,12 @@ services: SUPPORTED_CLAIMS: email,email_verified SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX HYDRA_ADMIN_URL: http://localhost:4445 - SERVE_PUBLIC_CORS_ENABLED: "true" - SERVE_PUBLIC_CORS_ALLOWED_ORIGINS: http://localhost:8081 - WEBFINGER_JWKS_BROADCAST_KEYS: hydra.openid.id-token,hydra.jwt.access-token ports: - 4444:4444 command: hydra serve all --dangerous-force-http hydra-passwordless: - image: bornholm/hydra-passwordless + image: bornholm/hydra-passwordless:latest ports: - 3000:3000 environment: diff --git a/frontend/src/components/OAuth2Page/OAuth2Page.tsx b/frontend/src/components/OAuth2Page/OAuth2Page.tsx deleted file mode 100644 index 7ae1daa..0000000 --- a/frontend/src/components/OAuth2Page/OAuth2Page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useEffect } from 'react'; -import { Page } from '../Page'; -import { useDispatch } from 'react-redux'; -import { logout, login, handleOAuth2Callback } from '../../store/actions/auth'; - -export function OAuth2Page({ match, location, history }) { - const dispatch = useDispatch(); - const { action } = match.params; - - useEffect(() => { - switch(action) { - case 'logout': - dispatch(logout()); - history.push("/"); - break; - case 'login': - dispatch(login()); - break; - case 'callback': - dispatch(handleOAuth2Callback(location.search)); - history.push("/"); - break; - } - }, [action]); - - return ( - - - - ); -} diff --git a/frontend/src/config.ts b/frontend/src/config.ts deleted file mode 100644 index 98ee0a4..0000000 --- a/frontend/src/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const Config = { - // The OpenID Connect client_id - oauth2ClientId: get("oauth2ClientId", "daddy"), - oauth2Scope: get("oauth2Scope", "email email_verified openid offline_access"), - oauth2RedirectURI: get("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"), - oauth2Audience: get("oauth2Audience", ""), - oauth2AuthorizeURL: get("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"), - oauth2TokenURL: get("oauth2TokenURL", "http://localhost:4444/oauth2/token"), - oauth2LogoutURL: get("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"), - oauth2PostLogoutRedirectURI: get("oauth2PostLogoutRedirectURI", "http://localhost:8081"), - graphQLEndpoint: get("graphQLEndpoint", "http://localhost:8080/api/v1/graphql") -}; - -function get(key: string, defaultValue: T):T { - const config = window['__CONFIG__'] || {}; - if (config && config.hasOwnProperty(key)) { - return config[key] as T; - } else { - return defaultValue; - } -} \ No newline at end of file diff --git a/frontend/src/store/actions/auth.ts b/frontend/src/store/actions/auth.ts deleted file mode 100644 index ee55183..0000000 --- a/frontend/src/store/actions/auth.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Action } from "redux"; -import { AccessGrant } from "../../util/auth"; -import { IdToken } from "../../types/idToken"; - -export const LOGOUT = "LOGOUT_REQUEST"; - -export function logout() { - return { type: LOGOUT }; -}; - -export const LOGIN_REQUEST = "LOGIN_REQUEST"; -export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; -export const LOGIN_FAILURE = "LOGIN_FAILURE"; - -export function login() { - return { type: LOGIN_REQUEST }; -}; - -export const HANDLE_OAUTH2_CALLBACK_REQUEST = "HANDLE_OAUTH2_CALLBACK_REQUEST"; -export const HANDLE_OAUTH2_CALLBACK_SUCCESS = "HANDLE_OAUTH2_CALLBACK_SUCCESS"; -export const HANDLE_OAUTH2_CALLBACK_FAILURE = "HANDLE_OAUTH2_CALLBACK_FAILURE"; - -export interface handleOAuth2CallbackAction extends Action { - search: string -} - -export function handleOAuth2Callback(search: string): handleOAuth2CallbackAction { - return { type: HANDLE_OAUTH2_CALLBACK_REQUEST, search }; -}; - -export interface handleOAuth2CallbackSuccessAction extends Action { - grant: AccessGrant -} - -export function handleOAuth2CallbackSuccess(grant: AccessGrant): handleOAuth2CallbackSuccessAction { - return { type: HANDLE_OAUTH2_CALLBACK_SUCCESS, grant }; -}; - -export const PARSE_ID_TOKEN_REQUEST = "PARSE_ID_TOKEN_REQUEST"; -export const PARSE_ID_TOKEN_SUCCESS = "PARSE_ID_TOKEN_SUCCESS"; -export const PARSE_ID_TOKEN_FAILURE = "PARSE_ID_TOKEN_FAILURE"; - -export interface parseIdTokenAction extends Action { - rawIdToken: string -}; - -export function parseIdToken(rawIdToken: string): parseIdTokenAction { - return { type: PARSE_ID_TOKEN_REQUEST, rawIdToken }; -}; - - -export interface parseIdTokenSuccessAction extends Action { - idToken: IdToken -} - -export function parseIdTokenSuccess(idToken: IdToken): parseIdTokenSuccessAction { - return { type: PARSE_ID_TOKEN_SUCCESS, idToken }; -}; - - -export const SET_CURRENT_USER = 'SET_CURRENT_USER'; - -export interface setCurrentUserAction extends Action { - email: string -} - -export function setCurrentUser(email: string): setCurrentUserAction { - return { type: SET_CURRENT_USER, email }; -} \ No newline at end of file diff --git a/frontend/src/store/sagas/auth.ts b/frontend/src/store/sagas/auth.ts deleted file mode 100644 index 7c91d14..0000000 --- a/frontend/src/store/sagas/auth.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { put, takeLatest, all } from 'redux-saga/effects'; -import { - LOGOUT, LOGIN_REQUEST, - HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackAction, - HANDLE_OAUTH2_CALLBACK_FAILURE, handleOAuth2CallbackSuccess, - parseIdTokenAction, parseIdToken, - PARSE_ID_TOKEN_REQUEST, PARSE_ID_TOKEN_FAILURE, parseIdTokenSuccess, - setCurrentUser, LOGIN_FAILURE, -} from '../actions/auth'; -import { - createLoginSession, LoginSession, - createAccessTokenRequest, saveAccessGrant, - saveLoginSessionState, getSavedLoginSessionState, - getLogoutURL, getSavedAccessGrant, clearAccessGrant -} from '../../util/auth'; -import qs from 'qs'; -import { UnauthorizedError } from '../../util/daddy'; -import jwtDecode from 'jwt-decode'; -import { IdToken } from '../../types/idToken'; - -export function* authRootSaga() { - yield all([ - takeLatest(LOGIN_REQUEST, loginSaga), - takeLatest(LOGOUT, logoutSaga), - takeLatest(HANDLE_OAUTH2_CALLBACK_REQUEST, handleOAuth2CallbackSaga), - takeLatest(PARSE_ID_TOKEN_REQUEST, parseIDTokenSaga), - ]); -} - -export function* loginSaga() { - try { - const loginSession: LoginSession = yield createLoginSession(); - console.log('Code verifier is ', loginSession.verifier); - console.log('State is ', loginSession.state); - saveLoginSessionState(loginSession.verifier, loginSession.state); - window.location.replace(loginSession.redirectUrl); - } catch(err) { - yield put({ type: LOGIN_FAILURE, err }); - } -} - -export function* logoutSaga() { - const accessGrant = getSavedAccessGrant(); - const logoutURL = getLogoutURL(accessGrant.id_token); - clearAccessGrant(); - window.location.replace(logoutURL); -} - -export function* handleOAuth2CallbackSaga({ search }: handleOAuth2CallbackAction) { - const query = search.substring(1); - const params = qs.parse(query); - - const loginSession = getSavedLoginSessionState(); - - console.log('Stored state verifier is', loginSession.state); - if (loginSession.state !== params.state) { - yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err: new Error("Invalid state") }); - return; - } - - console.log('Stored code verifier is', loginSession.verifier); - console.log('Authorization code is', params.code); - - const req = createAccessTokenRequest(params.code as string, loginSession.verifier); - - let grant; - try { - grant = yield fetch(req.url, { method: "POST", body: req.data }) - .then(res => { - if (res.status === 401) return Promise.reject(new UnauthorizedError()); - return res; - }) - .then(res => res.json()); - } catch(err) { - yield put({ type: HANDLE_OAUTH2_CALLBACK_FAILURE, err }); - return; - } - - console.log("Access grant is", grant); - saveAccessGrant(grant); - - yield put(handleOAuth2CallbackSuccess(grant)); - yield put(parseIdToken(grant.id_token)); -}; - - -export function* parseIDTokenSaga({ rawIdToken }: parseIdTokenAction) { - let idToken: IdToken; - try { - idToken = jwtDecode(rawIdToken); - } catch(err) { - yield put({ type: PARSE_ID_TOKEN_FAILURE, err }); - return; - } - - yield put(parseIdTokenSuccess(idToken)); - yield put(setCurrentUser(idToken.email)); -}; \ No newline at end of file diff --git a/frontend/src/store/sagas/init.ts b/frontend/src/store/sagas/init.ts deleted file mode 100644 index da4d595..0000000 --- a/frontend/src/store/sagas/init.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { all, put } from "redux-saga/effects"; -import { getSavedAccessGrant } from "../../util/auth"; -import { parseIdToken } from "../actions/auth"; - -export function* initRootSaga() { - yield all([ - retrieveSessionSaga(), - ]); -} - -export function* retrieveSessionSaga() { - console.log("Checking session status..."); - - const accessGrant = getSavedAccessGrant(); - if (!accessGrant) return; - - yield put(parseIdToken(accessGrant.id_token)); -} \ No newline at end of file diff --git a/frontend/src/types/idToken.ts b/frontend/src/types/idToken.ts deleted file mode 100644 index 2ef7808..0000000 --- a/frontend/src/types/idToken.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IdToken { - email: string -} \ No newline at end of file diff --git a/frontend/src/util/auth.ts b/frontend/src/util/auth.ts deleted file mode 100644 index adcff7d..0000000 --- a/frontend/src/util/auth.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Config } from '../config'; - -export interface LoginSession { - state: string - redirectUrl: string - verifier: string -} - -export function generateRandomString() { - var array = new Uint32Array(28); - window.crypto.getRandomValues(array); - return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join(''); -} - -export function sha256(plain): PromiseLike { - const encoder = new TextEncoder(); - const data = encoder.encode(plain); - return window.crypto.subtle.digest('SHA-256', data); -} - -export function pkceChallengeFromVerifier(v): PromiseLike { - return sha256(v) - .then(hashed => base64urlencode(hashed)); -} - -export function base64urlencode(str) { - return btoa(String.fromCharCode.apply(null, new Uint8Array(str))) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -} - -export function createLoginSession(): Promise { - // Based on https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce - const state = generateRandomString(); - const verifier = generateRandomString(); - - return new Promise((resolve, reject) => { - try { - pkceChallengeFromVerifier(verifier).then(challenge => { - console.log('Code challenge is', challenge); - - let redirectUrl=`${Config.oauth2AuthorizeURL}`; - redirectUrl += `?audience=${encodeURIComponent(Config.oauth2Audience)}`; - redirectUrl += `&scope=${encodeURIComponent(Config.oauth2Scope)}`; - redirectUrl += `&response_type=code`; - redirectUrl += `&client_id=${encodeURIComponent(Config.oauth2ClientId)}` - redirectUrl += `&code_challenge=${encodeURIComponent(challenge)}`; - redirectUrl += `&code_challenge_method=S256` - redirectUrl += `&redirect_uri=${encodeURIComponent(Config.oauth2RedirectURI)}`; - redirectUrl += `&state=${encodeURIComponent(state)}`; - - return resolve({ - state, - redirectUrl, - verifier, - }); - }); - } catch(err) { - return reject(err); - } - }); -}; - -export interface AccessTokenRequest { - data: FormData, - url: string -} - -export function createAccessTokenRequest(code: string, verifier: string): AccessTokenRequest { - const data = new FormData(); - data.append('grant_type', 'authorization_code'); - data.append('client_id', Config.oauth2ClientId); - data.append('code_verifier', verifier); - data.append('code', code); - data.append('redirect_uri', Config.oauth2RedirectURI); - return { - url: Config.oauth2TokenURL, - data, - }; -}; - -export function getLogoutURL(rawIdToken: string): string { - let logoutURL = Config.oauth2LogoutURL; - logoutURL += `?post_logout_redirect_uri=${encodeURIComponent(Config.oauth2PostLogoutRedirectURI)}`; - logoutURL += `&id_token_hint=${encodeURIComponent(rawIdToken)}`; - return logoutURL; -} - -export interface AccessGrant { - access_token: string - expires_in: number - id_token: string - refresh_token: string - scope: string - token_type: string -} - -export function saveLoginSessionState(verifier: string, state: string) { - window.localStorage.setItem('login_verifier', verifier); - window.localStorage.setItem('login_state', state); -} - -export function getSavedLoginSessionState(cleanup = true) { - const loginSession = { - verifier: window.localStorage.getItem('login_verifier'), - state: window.localStorage.getItem('login_state') - }; - if (cleanup) { - window.localStorage.removeItem('login_verifier'); - window.localStorage.removeItem('login_state'); - } - return loginSession; -} - -export function saveAccessGrant(grant: AccessGrant) { - window.localStorage.setItem('access_grant', JSON.stringify(grant)); -} - -export function getSavedAccessGrant(): AccessGrant { - const raw = window.localStorage.getItem('access_grant'); - if (raw === "") return null; - return JSON.parse(raw) as AccessGrant; -} - -export function clearAccessGrant() { - window.localStorage.removeItem('access_grant'); -} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..615813e --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module forge.cadoles.com/Cadoles/daddy + +go 1.14 + +require ( + forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032 + github.com/caarlos0/env/v6 v6.2.2 + github.com/go-chi/chi v4.1.0+incompatible + github.com/gorilla/sessions v1.2.0 + github.com/pkg/errors v0.9.1 + gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce + gopkg.in/yaml.v2 v2.2.8 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3dc4af0 --- /dev/null +++ b/go.sum @@ -0,0 +1,268 @@ +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/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/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/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/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/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/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/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/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/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/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/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.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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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/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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +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-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +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= +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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/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/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-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/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/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-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/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/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-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-20190506145303-2d16b83fe98c/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-20190816200558-6889da9d5479/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-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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/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/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/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.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= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..6d76a26 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,107 @@ +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 { + Log LogConfig `yaml:"log"` + HTTP HTTPConfig `yaml:"http"` + OIDC OIDCConfig `yaml:"oidc"` +} + +// NewFromFile retrieves the configuration from the given file +func NewFromFile(filepath string) (*Config, error) { + config := NewDefault() + + data, err := ioutil.ReadFile(filepath) + if err != nil { + return nil, errors.Wrapf(err, "could not read file '%s'", filepath) + } + + if err := yaml.Unmarshal(data, config); err != nil { + return nil, errors.Wrapf(err, "could not unmarshal configuration") + } + + return config, nil +} + +type HTTPConfig struct { + Address string `yaml:"address" env:"HTTP_ADDRESS"` + CookieAuthenticationKey string `yaml:"cookieAuthenticationKey" env:"HTTP_COOKIE_AUTHENTICATION_KEY"` + CookieEncryptionKey string `yaml:"cookieEncryptionKey" env:"HTTP_COOKIE_ENCRYPTION_KEY"` + CookieMaxAge int `yaml:"cookieMaxAge" env:"HTTP_COOKIE_MAX_AGE"` + TemplateDir string `yaml:"templateDir" env:"HTTP_TEMPLATE_DIR"` + PublicDir string `yaml:"publicDir" env:"HTTP_PUBLIC_DIR"` + FrontendURL string `yaml:"frontendURL" env:"HTTP_FRONTEND_URL"` +} + +type OIDCConfig struct { + ClientID string `yaml:"clientId" env:"OIDC_CLIENT_ID"` + ClientSecret string `yaml:"clientSecret" env:"OIDC_CLIENT_SECRET"` + IssuerURL string `ymal:"issuerUrl" env:"OIDC_ISSUER_URL"` + RedirectURL string `yaml:"redirectUrl" env:"OIDC_REDIRECT_URL"` + PostLogoutRedirectURL string `yaml:"postLogoutRedirectURL" env:"OIDC_POST_LOGOUT_REDIRECT_URL"` +} + +type LogConfig struct { + Level logger.Level `yaml:"level" env:"LOG_LEVEL"` + Format logger.Format `yaml:"format" env:"LOG_FORMAT"` +} + +func NewDumpDefault() *Config { + config := NewDefault() + return config +} + +func NewDefault() *Config { + return &Config{ + 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", + }, + } +} + +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/route/login.go b/internal/route/login.go new file mode 100644 index 0000000..bfaa7ca --- /dev/null +++ b/internal/route/login.go @@ -0,0 +1,35 @@ +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 handleLogin(w http.ResponseWriter, r *http.Request) { + ctn := container.Must(r.Context()) + client := oidc.Must(ctn) + client.Login(w, r) +} + +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)) + + 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..1fc9a23 --- /dev/null +++ b/internal/route/mount.go @@ -0,0 +1,27 @@ +package route + +import ( + "forge.cadoles.com/Cadoles/daddy/internal/config" + oidc "forge.cadoles.com/wpetit/goweb-oidc" + + "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) + + }) + + notFoundHandler := r.NotFoundHandler() + r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler)) + + return nil +} diff --git a/misc/containers/hydra/hydra-init.d/create-client b/misc/containers/hydra/hydra-init.d/create-client index 85d085b..2e28302 100755 --- a/misc/containers/hydra/hydra-init.d/create-client +++ b/misc/containers/hydra/hydra-init.d/create-client @@ -1,9 +1,12 @@ #!/bin/sh +set -x + hydra clients create \ --id daddy \ + --secret daddycool \ -n Daddy \ - -a email,email_verified,offline_access,openid \ - --token-endpoint-auth-method none \ - --post-logout-callbacks http://localhost:8081 \ + -a email,email_verified,openid \ + --token-endpoint-auth-method client_secret_post \ + --post-logout-callbacks http://localhost:8081/logout/redirect \ -c http://localhost:8081/oauth2/callback \ No newline at end of file diff --git a/misc/containers/super-graph/Dockerfile b/misc/containers/super-graph/Dockerfile deleted file mode 100644 index dfca8ca..0000000 --- a/misc/containers/super-graph/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM alpine:edge AS build - -ARG HTTP_PROXY= -ARG HTTPS_PROXY= -ARG http_proxy= -ARG https_proxy= - -ARG SUPERGRAPH_VERSION=88ba105b70c60b2c7467dc1f76f041cec2614a04 -ARG WAITFORIT_VERSION=v2.4.1 - -RUN apk add --no-cache go make git curl bash ca-certificates - -RUN git clone https://forge.cadoles.com/wpetit/super-graph.git \ - && export PATH="$PATH:/root/go/bin" \ - && export CGO_ENABLED=0 \ - && cd super-graph \ - && git checkout ${SUPERGRAPH_VERSION} \ - && make SHELL='bash -x' build - -RUN curl -sL \ - -o /usr/local/bin/waitforit \ - https://github.com/maxcnunes/waitforit/releases/download/${WAITFORIT_VERSION}/waitforit-linux_amd64 - -FROM alpine:3.11 - -COPY --from=build /super-graph/super-graph /usr/local/bin/super-graph -COPY --from=build /usr/local/bin/waitforit /usr/local/bin/waitforit - -RUN chmod +x /usr/local/bin/waitforit - -WORKDIR /app - -COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint -RUN chmod +x /usr/local/bin/docker-entrypoint - -CMD ["/usr/local/bin/docker-entrypoint"] \ No newline at end of file diff --git a/misc/containers/super-graph/docker-entrypoint.sh b/misc/containers/super-graph/docker-entrypoint.sh deleted file mode 100644 index c0bea55..0000000 --- a/misc/containers/super-graph/docker-entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -eo pipefail - -if [ ! -f /container-lifecycle/first_run ]; then - waitforit -debug -host $SG_DATABASE_HOST -port 5432 - super-graph db:migrate up - super-graph db:seed - mkdir /container-lifecycle - touch /container-lifecycle/first_run -fi - -super-graph serv diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..560773a --- /dev/null +++ b/modd.conf @@ -0,0 +1,18 @@ +**/*.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 ) + 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 From 591112a8005ab67a2b7c3368ab16ddc7eb341be6 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 Jul 2020 09:20:14 +0200 Subject: [PATCH 16/23] =?UTF-8?q?Int=C3=A9gration=20d'un=20point=20d'entr?= =?UTF-8?q?=C3=A9e=20GraphQL=20et=20d'un=20connecteur=20pour=20PostgreSQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Possibilité de migrer le schéma de la base de données via drapeau - Génération du code GraphQL avec https://gqlgen.com/ --- .env.dist | 6 +- Makefile | 19 ++- README.md | 4 +- cmd/server/container.go | 28 +++- cmd/server/cqrs.go | 11 ++ cmd/server/main.go | 33 +++++ cmd/server/migration.go | 100 +++++++++++++++ go.mod | 6 +- go.sum | 171 +++++++++++++++++++++++++ internal/.gitignore | 2 + internal/config/config.go | 16 ++- internal/database/migration.go | 79 ++++++++++++ internal/database/provider.go | 24 ++++ internal/database/service.go | 34 +++++ internal/database/tx.go | 38 ++++++ internal/database/version_resolver.go | 94 ++++++++++++++ internal/gqlgen.yml | 56 ++++++++ internal/graph/model/models_gen.go | 8 ++ internal/graph/resolver.go | 7 + internal/graph/schema.graphqls | 12 ++ internal/graph/schema.resolvers.go | 21 +++ internal/migration/manager.go | 146 +++++++++++++++++++++ internal/migration/migration.go | 9 ++ internal/migration/provider.go | 13 ++ internal/migration/service.go | 33 +++++ internal/migration/version_resolver.go | 8 ++ internal/route/mount.go | 15 +++ modd.conf | 6 + 28 files changed, 984 insertions(+), 15 deletions(-) create mode 100644 cmd/server/cqrs.go create mode 100644 cmd/server/migration.go create mode 100644 internal/.gitignore create mode 100644 internal/database/migration.go create mode 100644 internal/database/provider.go create mode 100644 internal/database/service.go create mode 100644 internal/database/tx.go create mode 100644 internal/database/version_resolver.go create mode 100644 internal/gqlgen.yml create mode 100644 internal/graph/model/models_gen.go create mode 100644 internal/graph/resolver.go create mode 100644 internal/graph/schema.graphqls create mode 100644 internal/graph/schema.resolvers.go create mode 100644 internal/migration/manager.go create mode 100644 internal/migration/migration.go create mode 100644 internal/migration/provider.go create mode 100644 internal/migration/service.go create mode 100644 internal/migration/version_resolver.go diff --git a/.env.dist b/.env.dist index 5aedd1c..2cb36ba 100644 --- a/.env.dist +++ b/.env.dist @@ -1,3 +1,7 @@ +DEBUG=true OIDC_CLIENT_ID=daddy OIDC_CLIENT_SECRET=daddycool -OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect \ No newline at end of file +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/Makefile b/Makefile index 3e9f24a..d7250e6 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,15 @@ +SHELL := /bin/bash + build: build-docker build-server build-docker: docker-compose build +generate: + cd internal && go run github.com/99designs/gqlgen generate + build-server: - CGO_ENABLED=0 go build -mod=vendor -v -o ./bin/server ./cmd/server + CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server deps: cd client && npm install @@ -22,6 +27,18 @@ down: db-shell: docker-compose exec postgres psql -Udaddy +migrate: build-server + ( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml -migrate $(MIGRATE) ) + +migrate-latest: + $(MAKE) MIGRATE=latest migrate + +migrate-up: + $(MAKE) MIGRATE=up migrate + +migrate-down: + $(MAKE) MIGRATE=down migrate + test: go test -v ./... diff --git a/README.md b/README.md index a63c794..ec1c27d 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ Les services suivants devraient être disponibles après démarrage de l'environ |Service|Type|Accès|Description| |-------|----|-----|-----------| |Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)| -|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/|Interface Web de développement de l'API GraphQL| -|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql|Point d'entrée de l'API GraphQL| +|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) diff --git a/cmd/server/container.go b/cmd/server/container.go index 18fe9f3..4984004 100644 --- a/cmd/server/container.go +++ b/cmd/server/container.go @@ -4,8 +4,12 @@ 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" - "gitlab.com/wpetit/goweb/template/html" "forge.cadoles.com/Cadoles/daddy/internal/config" oidc "forge.cadoles.com/wpetit/goweb-oidc" @@ -14,7 +18,6 @@ import ( "gitlab.com/wpetit/goweb/service" "gitlab.com/wpetit/goweb/service/build" "gitlab.com/wpetit/goweb/service/session" - "gitlab.com/wpetit/goweb/service/template" "gitlab.com/wpetit/goweb/session/gorilla" ) @@ -67,11 +70,6 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con gorilla.ServiceProvider("daddy", cookieStore), ) - // Create and expose template service provider - ctn.Provide(template.ServiceName, html.ServiceProvider( - conf.HTTP.TemplateDir, - )) - // Create and expose config service provider ctn.Provide(config.ServiceName, config.ServiceProvider(conf)) @@ -86,5 +84,21 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con 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..ef32ed0 --- /dev/null +++ b/cmd/server/cqrs.go @@ -0,0 +1,11 @@ +package main + +import "gitlab.com/wpetit/goweb/service" + +func initCommands(ctn *service.Container) error { + return nil +} + +func initQueries(ctn *service.Container) error { + return nil +} diff --git a/cmd/server/main.go b/cmd/server/main.go index e505a7a..a61c792 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -27,6 +27,7 @@ var ( workdir = "" dumpConfig = false version = false + migrate = "" ) // nolint: gochecknoglobals @@ -42,6 +43,7 @@ func init() { 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() { @@ -137,6 +139,37 @@ func main() { ) } + 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 diff --git a/cmd/server/migration.go b/cmd/server/migration.go new file mode 100644 index 0000000..7669d16 --- /dev/null +++ b/cmd/server/migration.go @@ -0,0 +1,100 @@ +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, + 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/go.mod b/go.mod index 615813e..6c6cdff 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ 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/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 - gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce + 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 index 3dc4af0..61fdd38 100644 --- a/go.sum +++ b/go.sum @@ -16,10 +16,15 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy 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= @@ -32,12 +37,20 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS 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/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/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/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/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= @@ -46,6 +59,7 @@ 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= @@ -53,12 +67,16 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF 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/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= @@ -81,37 +99,118 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi 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/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/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-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/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= @@ -124,27 +223,70 @@ github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAm 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/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-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= @@ -163,6 +305,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU 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= @@ -173,6 +316,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn 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-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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -185,41 +329,63 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ 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/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-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-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/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-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-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= @@ -252,12 +418,15 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ 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/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= @@ -266,3 +435,5 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh 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= 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/config/config.go b/internal/config/config.go index 6d76a26..cccb1d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,9 +13,11 @@ import ( ) type Config struct { - Log LogConfig `yaml:"log"` - HTTP HTTPConfig `yaml:"http"` - OIDC OIDCConfig `yaml:"oidc"` + 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 @@ -57,6 +59,10 @@ type LogConfig struct { 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 @@ -64,6 +70,7 @@ func NewDumpDefault() *Config { func NewDefault() *Config { return &Config{ + Debug: false, Log: LogConfig{ Level: logger.LevelInfo, Format: logger.FormatHuman, @@ -82,6 +89,9 @@ func NewDefault() *Config { RedirectURL: "http://localhost:8081/oauth2/callback", PostLogoutRedirectURL: "http://localhost:8081", }, + Database: DatabaseConfig{ + DSN: "host=localhost database=daddy", + }, } } 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..5aabf08 --- /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..20d3076 --- /dev/null +++ b/internal/graph/model/models_gen.go @@ -0,0 +1,8 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package model + +type User struct { + Name *string `json:"name"` + Email string `json:"email"` +} 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..38e0313 --- /dev/null +++ b/internal/graph/schema.graphqls @@ -0,0 +1,12 @@ +# GraphQL schema example +# +# https://gqlgen.com/getting-started/ + +type User { + name: String + email: String! +} + +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..67cbdbd --- /dev/null +++ b/internal/graph/schema.resolvers.go @@ -0,0 +1,21 @@ +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" + "fmt" + + "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) { + panic(fmt.Errorf("not implemented")) +} + +// Query returns generated.QueryResolver implementation. +func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } + +type queryResolver struct{ *Resolver } 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/route/mount.go b/internal/route/mount.go index 1fc9a23..6538247 100644 --- a/internal/route/mount.go +++ b/internal/route/mount.go @@ -2,7 +2,11 @@ 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" 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" @@ -18,6 +22,17 @@ func Mount(r *chi.Mux, config *config.Config) error { r.Route("/api", func(r chi.Router) { r.Use(oidc.Middleware) + 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() diff --git a/modd.conf b/modd.conf index 560773a..d95e3fd 100644 --- a/modd.conf +++ b/modd.conf @@ -6,6 +6,7 @@ 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 ) } @@ -15,4 +16,9 @@ modd.conf { { daemon: cd client && NODE_ENV=development npm run server -- --display=minimal +} + +internal/graph/schema.graphqls +internal/gqlgen.yml { + prep: make generate } \ No newline at end of file From a096b506e22b0f186dd1e10ddadcda9fe637b290 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 Jul 2020 12:01:20 +0200 Subject: [PATCH 17/23] =?UTF-8?q?Correction=20proc=C3=A9dure=20de=20d?= =?UTF-8?q?=C3=A9marrage=20et=20mise=20=C3=A0=20jour=20du=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 11 ++++++++--- README.md | 18 +++++++++++------- go.mod | 1 + go.sum | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index d7250e6..5ec8ebc 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,13 @@ build-server: deps: cd client && npm install - env GO111MODULE=off go get github.com/cortesi/modd/cmd/modd + go get ./... up: build-docker docker-compose up watch: - $(GOPATH)/bin/modd + go run github.com/cortesi/modd/cmd/modd down: docker-compose down -v --remove-orphans @@ -43,4 +43,9 @@ test: go test -v ./... hydra-shell: - docker-compose exec hydra /bin/sh \ No newline at end of file + docker-compose exec hydra /bin/sh + +clean: down + rm -rf client/node_modules bin data .env internal/graph/generated internal/graph/server.go + rm -rf vendor + go clean -modcache \ No newline at end of file diff --git a/README.md b/README.md index ec1c27d..1aae405 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ 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 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 @@ -35,23 +36,26 @@ Les services suivants devraient être disponibles après démarrage de l'environ |Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp) |Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement| -**\*** Pensez à passer l'attribut `auth_fail_block: false` dans le fichier `backend/config/dev.yml` si vous voulez pouvoir utiliser cette interface sans avoir à définir l'entête `Authorization`. - #### Fichiers/répertoires notables |Chemin|Description| |------------------|-----------| |`docker-compose.yml`|Configuration de l'environnement Docker Compose| -|`frontend/src`|Sources du frontend ([React](https://reactjs.org))| -|`backend/config/migrations`|Migrations SQL pour le backend, voir [la documentation de SuperGraph à ce sujet](https://supergraph.dev/docs/start#migrations)| +|`client/src`|Sources du frontend ([React](https://reactjs.org))| #### Commandes utiles |Commande|Description| |--------|-----------| -|`make up`|Démarrer l'environnement de développement, `Ctrl+C` pour le stopper.| -|`make down`|Stopper et supprimer l'environnement de développement.| +|`make up`|Démarrer l'environnement Docker Compose, `Ctrl+C` pour le stopper.| +|`make down`|Stopper et supprimer l'environnement Docker Compose.| +|`make watch`|Suerveiller les sources et recompiler à la volée le client/server.| |`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.| +|`make hydra-shell`|Ouvrir un shell interactif dans le conteneur Hydra. (`hydra --help` pour voir les commandes disponibles pour l'administration)| +|`make migrate-latest`|Migrer la base de données à la dernière version disponible du schéma.| +|`make migrate-down`|Migrer la base de données à la version précédente du schéma.| +|`make migrate-up`|Migrer la base de données à la version suivante du schéma.| +|`make clean`|Nettoyer l'environnement.| #### Ressources diff --git a/go.mod b/go.mod index 6c6cdff..f530b00 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ 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 diff --git a/go.sum b/go.sum index 61fdd38..385b725 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,15 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS 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= @@ -48,9 +55,16 @@ github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjs 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= @@ -67,6 +81,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF 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= @@ -95,6 +111,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a 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= @@ -177,10 +194,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv 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= @@ -196,6 +215,8 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa 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= @@ -208,6 +229,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky 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= @@ -222,6 +244,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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= @@ -279,6 +303,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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= @@ -316,9 +341,12 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn 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= @@ -328,8 +356,11 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ 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= @@ -341,6 +372,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w 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= @@ -350,6 +382,10 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtD 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= @@ -373,6 +409,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn 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= @@ -380,6 +417,7 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn 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= @@ -415,10 +453,13 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij 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= @@ -434,6 +475,8 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh 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= From 3fd8bf7e6901ac2c38e1611f590c6af661eadc8a Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 Jul 2020 14:44:05 +0200 Subject: [PATCH 18/23] =?UTF-8?q?Auto-cr=C3=A9ation=20du=20compte=20utilis?= =?UTF-8?q?ateur=20=C3=A0=20la=20premi=C3=A8re=20connexion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sauvegarde de l'adresse courriel de l'utilisateur en session - Implémentation d'une première Query GraphQL pour récupérer le profil de l'utilisateur connecté - Utilisation de la pattern CQRS pour les commandes/requêtes sur la base de données --- cmd/server/cqrs.go | 28 ++++++++- cmd/server/migration.go | 2 + internal/command/create_user.go | 99 ++++++++++++++++++++++++++++++ internal/database/service.go | 4 +- internal/graph/model/models_gen.go | 10 ++- internal/graph/schema.graphqls | 4 ++ internal/graph/schema.resolvers.go | 39 +++++++++++- internal/query/find_user.go | 71 +++++++++++++++++++++ internal/route/login.go | 46 ++++++++++++++ internal/route/mount.go | 2 + internal/session/middleware.go | 97 +++++++++++++++++++++++++++++ 11 files changed, 395 insertions(+), 7 deletions(-) create mode 100644 internal/command/create_user.go create mode 100644 internal/query/find_user.go create mode 100644 internal/session/middleware.go diff --git a/cmd/server/cqrs.go b/cmd/server/cqrs.go index ef32ed0..875c475 100644 --- a/cmd/server/cqrs.go +++ b/cmd/server/cqrs.go @@ -1,11 +1,37 @@ package main -import "gitlab.com/wpetit/goweb/service" +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/migration.go b/cmd/server/migration.go index 7669d16..fedce18 100644 --- a/cmd/server/migration.go +++ b/cmd/server/migration.go @@ -83,6 +83,8 @@ func m000initialSchema() migration.Migration { 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) ); `) 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/database/service.go b/internal/database/service.go index 5aabf08..cad5509 100644 --- a/internal/database/service.go +++ b/internal/database/service.go @@ -8,7 +8,7 @@ import ( const ServiceName service.Name = "database" -// From retrieves the database pool service in the given container +// 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 { @@ -23,7 +23,7 @@ func From(container *service.Container) (*pgxpool.Pool, error) { return srv, nil } -// Must retrieves the database pool service in the given container or panic otherwise +// 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 { diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 20d3076..8448226 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -2,7 +2,13 @@ package model +import ( + "time" +) + type User struct { - Name *string `json:"name"` - Email string `json:"email"` + Name *string `json:"name"` + Email string `json:"email"` + ConnectedAt time.Time `json:"connectedAt"` + CreatedAt time.Time `json:"createdAt"` } diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index 38e0313..7ae4e20 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -2,9 +2,13 @@ # # https://gqlgen.com/getting-started/ +scalar Time + type User { name: String email: String! + connectedAt: Time! + createdAt: Time! } type Query { diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 67cbdbd..6549cd6 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -5,14 +5,49 @@ package graph import ( "context" - "fmt" + + "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" "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) { - panic(fmt.Errorf("not implemented")) + 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 } // Query returns generated.QueryResolver implementation. 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 index bfaa7ca..6db2c4b 100644 --- a/internal/route/login.go +++ b/internal/route/login.go @@ -3,6 +3,13 @@ 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" @@ -15,6 +22,11 @@ func handleLogin(w http.ResponseWriter, r *http.Request) { 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) @@ -31,5 +43,39 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) { 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/mount.go b/internal/route/mount.go index 6538247..860def4 100644 --- a/internal/route/mount.go +++ b/internal/route/mount.go @@ -4,6 +4,7 @@ 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" @@ -21,6 +22,7 @@ func Mount(r *chi.Mux, config *config.Config) error { r.Route("/api", func(r chi.Router) { r.Use(oidc.Middleware) + r.Use(session.UserEmailMiddleware) gql := handler.NewDefaultServer( generated.NewExecutableSchema(generated.Config{ 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 +} From 00e331b9854ce4a147cd84b04928a88cb4209d8b Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 Jul 2020 14:55:32 +0200 Subject: [PATCH 19/23] =?UTF-8?q?Correction=20g=C3=A9n=C3=A9ration=20resol?= =?UTF-8?q?vers=20GraphQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/graph/schema.resolvers.go | 38 +------------------------ internal/graph/user_profile.go | 45 ++++++++++++++++++++++++++++++ modd.conf | 10 +++---- 3 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 internal/graph/user_profile.go diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 6549cd6..a8eee61 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -6,48 +6,12 @@ package graph import ( "context" - "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" - "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) { - 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 + return handleUserProfile(ctx) } // Query returns generated.QueryResolver implementation. 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/modd.conf b/modd.conf index d95e3fd..ac44863 100644 --- a/modd.conf +++ b/modd.conf @@ -1,3 +1,8 @@ +internal/graph/schema.graphqls +internal/gqlgen.yml { + prep: make generate +} + **/*.go !**/*_test.go data/config.yml @@ -16,9 +21,4 @@ modd.conf { { daemon: cd client && NODE_ENV=development npm run server -- --display=minimal -} - -internal/graph/schema.graphqls -internal/gqlgen.yml { - prep: make generate } \ No newline at end of file From d0228b6c11802c4da833d85125aff8b496794088 Mon Sep 17 00:00:00 2001 From: William Petit Date: Mon, 13 Jul 2020 15:07:55 +0200 Subject: [PATCH 20/23] =?UTF-8?q?Ex=C3=A9cuter=20make=20generate=20avant?= =?UTF-8?q?=20le=20make=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5ec8ebc..80a6fac 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ generate: build-server: CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server -deps: +deps: generate cd client && npm install go get ./... From ed219ddd11f373bccf93c7690ae27e811a79e4a1 Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 16 Jul 2020 22:29:33 +0200 Subject: [PATCH 21/23] Correction typo annotation --- internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index cccb1d2..7d178b5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,7 +49,7 @@ type HTTPConfig struct { type OIDCConfig struct { ClientID string `yaml:"clientId" env:"OIDC_CLIENT_ID"` ClientSecret string `yaml:"clientSecret" env:"OIDC_CLIENT_SECRET"` - IssuerURL string `ymal:"issuerUrl" env:"OIDC_ISSUER_URL"` + 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"` } From 36c253d4d7bbe4ea0e2e20b4dd6194c04ad1f6cd Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 16 Jul 2020 22:30:03 +0200 Subject: [PATCH 22/23] Correction nom projet client --- client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 6f0d883..ded5e04 100644 --- a/client/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "dadd-", + "name": "daddy", "version": "0.0.0", "description": "Daddy", "main": "index.js", From 0d308acd5c6f7baf986d7c3a530a846bafddd1ef Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 16 Jul 2020 22:31:02 +0200 Subject: [PATCH 23/23] Ajout script/commande de release --- .gitignore | 3 +- Makefile | 7 +++ misc/script/release | 119 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100755 misc/script/release diff --git a/.gitignore b/.gitignore index 29c0e1b..ea84feb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor /data /bin -/.env \ No newline at end of file +/.env +/release \ No newline at end of file diff --git a/Makefile b/Makefile index 80a6fac..b389300 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ deps: generate cd client && npm install go get ./... +client-dist: + cd client && NODE_ENV=production npm run build + up: build-docker docker-compose up @@ -45,6 +48,10 @@ test: 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 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