From 1120474ad905d1cf6407ecd643973d5ba68b2c2d Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 10 Jul 2020 18:07:41 +0200 Subject: [PATCH] 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