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