Compare commits
25 Commits
feature/su
...
pkg/dev/ub
Author | SHA1 | Date | |
---|---|---|---|
ab90365c9c | |||
303ea6b1d6 | |||
ccf911322b | |||
0cb6c7c67e | |||
ec6de8a217 | |||
17cd58d68f | |||
08bd11f4d9 | |||
99fb4ac6d9 | |||
2ceba1f219 | |||
7122677351 | |||
0d308acd5c | |||
36c253d4d7 | |||
ed219ddd11 | |||
758c166f27 | |||
05dd505d6b | |||
8b8f322630 | |||
3bcebdfcd1 | |||
d0228b6c11 | |||
00e331b985 | |||
3fd8bf7e69 | |||
a096b506e2 | |||
591112a800 | |||
1120474ad9 | |||
ff70a6d570 | |||
1d526a37d0 |
7
.env.dist
Normal file
7
.env.dist
Normal file
@ -0,0 +1,7 @@
|
||||
DEBUG=true
|
||||
OIDC_CLIENT_ID=daddy
|
||||
OIDC_CLIENT_SECRET=daddycool
|
||||
OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect
|
||||
HTTP_COOKIE_AUTHENTICATION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
|
||||
HTTP_COOKIE_ENCRYPTION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
|
||||
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy port=5432 sslmode=disable"
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/vendor
|
||||
/data
|
||||
/bin
|
||||
/.env
|
||||
/release
|
55
Makefile
55
Makefile
@ -1,17 +1,28 @@
|
||||
build:
|
||||
SHELL := /bin/bash
|
||||
|
||||
build: build-server
|
||||
|
||||
docker:
|
||||
docker-compose build
|
||||
|
||||
deps:
|
||||
cd frontend && npm install
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
up: build
|
||||
( cd frontend && NODE_ENV=development npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait
|
||||
build-server:
|
||||
CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server
|
||||
|
||||
sg:
|
||||
docker-compose exec -u $(shell id -u) super-graph sh
|
||||
deps: generate
|
||||
cd client && npm install
|
||||
go get ./...
|
||||
|
||||
sgr:
|
||||
docker-compose run -u $(shell id -u) super-graph sh
|
||||
client-dist:
|
||||
cd client && NODE_ENV=production npm run build
|
||||
|
||||
up: docker
|
||||
docker-compose up
|
||||
|
||||
watch:
|
||||
go run github.com/cortesi/modd/cmd/modd
|
||||
|
||||
down:
|
||||
docker-compose down -v --remove-orphans
|
||||
@ -19,5 +30,29 @@ down:
|
||||
db-shell:
|
||||
docker-compose exec postgres psql -Udaddy
|
||||
|
||||
migrate: build-server
|
||||
( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml -migrate $(MIGRATE) )
|
||||
|
||||
migrate-latest:
|
||||
$(MAKE) MIGRATE=latest migrate
|
||||
|
||||
migrate-up:
|
||||
$(MAKE) MIGRATE=up migrate
|
||||
|
||||
migrate-down:
|
||||
$(MAKE) MIGRATE=down migrate
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
hydra-shell:
|
||||
docker-compose exec hydra /bin/sh
|
||||
docker-compose exec hydra /bin/sh
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
./misc/script/release
|
||||
|
||||
clean:
|
||||
rm -rf client/node_modules bin data .env internal/graph/generated internal/graph/server.go
|
||||
rm -rf vendor
|
||||
go clean -modcache
|
28
README.md
28
README.md
@ -17,39 +17,45 @@ Application de gestion des Dossiers d'Aide à la Décision (D.A.D.) à Cadoles.
|
||||
```bash
|
||||
git clone https://forge.cadoles.com/Cadoles/daddy.git # Cloner le projet
|
||||
cd daddy # Se placer dans le répertoire
|
||||
make deps # Installer les dépendances NPM
|
||||
make up # Démarrer l'environnement de développement
|
||||
make clean # On s'assure d'avoir un environnement propre
|
||||
make deps # Installer les dépendances
|
||||
make up # Démarrer l'environnement docker-compose (hydra, hydra-passwordless et fake-smtp)
|
||||
# Dans un second terminal
|
||||
make watch # Suivre les modifications et compiler à la volée le backend et frontend
|
||||
```
|
||||
|
||||
Les services suivants devraient être disponibles après démarrage de l'environnement:
|
||||
|
||||
|Service|Type|Accès|Description|
|
||||
|-------|----|-----|-----------|
|
||||
|Application React|HTTP (UI)|http://localhost:8081/|Page d'accueil de l'application React (serveur Webpack)|
|
||||
|Interface Web GraphQL|HTTP (UI)|http://localhost:8080/|Interface Web de développement de l'API GraphQL **\***|
|
||||
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8080/api/v1/graphql|Point d'entrée de l'API GraphQL|
|
||||
|Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)|
|
||||
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/playground|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|
||||
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql (POST)|Point d'entrée de l'API GraphQL|
|
||||
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|
||||
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|
|
||||
|Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp)
|
||||
|Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement|
|
||||
|
||||
**\*** Pensez à passer l'attribut `auth_fail_block: false` dans le fichier `backend/config/dev.yml` si vous voulez pouvoir utiliser cette interface sans avoir à définir l'entête `Authorization`.
|
||||
|
||||
#### Fichiers/répertoires notables
|
||||
|
||||
|Chemin|Description|
|
||||
|------------------|-----------|
|
||||
|`docker-compose.yml`|Configuration de l'environnement Docker Compose|
|
||||
|`frontend/src`|Sources du frontend ([React](https://reactjs.org))|
|
||||
|`backend/config/migrations`|Migrations SQL pour le backend, voir [la documentation de SuperGraph à ce sujet](https://supergraph.dev/docs/start#migrations)|
|
||||
|`client/src`|Sources du frontend ([React](https://reactjs.org))|
|
||||
|
||||
#### Commandes utiles
|
||||
|
||||
|Commande|Description|
|
||||
|--------|-----------|
|
||||
|`make up`|Démarrer l'environnement de développement, `Ctrl+C` pour le stopper.|
|
||||
|`make down`|Stopper et supprimer l'environnement de développement.|
|
||||
|`make up`|Démarrer l'environnement Docker Compose, `Ctrl+C` pour le stopper.|
|
||||
|`make down`|Stopper et supprimer l'environnement Docker Compose.|
|
||||
|`make watch`|Suerveiller les sources et recompiler à la volée le client/server.|
|
||||
|`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.|
|
||||
|`make hydra-shell`|Ouvrir un shell interactif dans le conteneur Hydra. (`hydra --help` pour voir les commandes disponibles pour l'administration)|
|
||||
|`make migrate-latest`|Migrer la base de données à la dernière version disponible du schéma.|
|
||||
|`make migrate-down`|Migrer la base de données à la version précédente du schéma.|
|
||||
|`make migrate-up`|Migrer la base de données à la version suivante du schéma.|
|
||||
|`make clean`|Nettoyer l'environnement.|
|
||||
|
||||
#### Ressources
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
/* fetchUser */
|
||||
|
||||
variables {
|
||||
"email": ""
|
||||
}
|
||||
|
||||
|
||||
query fetchUser {
|
||||
user(where: {email: {eq: $email}}) {
|
||||
id
|
||||
created_at
|
||||
updated_at
|
||||
email,
|
||||
full_name
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 <stripe_api_key>
|
||||
|
||||
# - # 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: []
|
@ -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
|
@ -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
|
@ -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 });
|
||||
}
|
0
frontend/.gitignore → client/.gitignore
vendored
0
frontend/.gitignore → client/.gitignore
vendored
198
frontend/package-lock.json → client/package-lock.json
generated
198
frontend/package-lock.json → client/package-lock.json
generated
@ -1230,8 +1230,7 @@
|
||||
"@types/node": {
|
||||
"version": "13.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz",
|
||||
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw=="
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
@ -1372,6 +1371,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/zen-observable": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
|
||||
"integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg=="
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||
@ -1547,6 +1551,23 @@
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"@wry/context": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz",
|
||||
"integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==",
|
||||
"requires": {
|
||||
"@types/node": ">=6",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@wry/equality": {
|
||||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
|
||||
"integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
@ -1723,6 +1744,93 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apollo-cache": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz",
|
||||
"integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==",
|
||||
"requires": {
|
||||
"apollo-utilities": "^1.3.4",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"apollo-cache-inmemory": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz",
|
||||
"integrity": "sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==",
|
||||
"requires": {
|
||||
"apollo-cache": "^1.3.5",
|
||||
"apollo-utilities": "^1.3.4",
|
||||
"optimism": "^0.10.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"apollo-client": {
|
||||
"version": "2.6.10",
|
||||
"resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz",
|
||||
"integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==",
|
||||
"requires": {
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"apollo-cache": "1.3.5",
|
||||
"apollo-link": "^1.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"symbol-observable": "^1.0.2",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0",
|
||||
"zen-observable": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"apollo-link": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz",
|
||||
"integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==",
|
||||
"requires": {
|
||||
"apollo-utilities": "^1.3.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.9.3",
|
||||
"zen-observable-ts": "^0.8.21"
|
||||
}
|
||||
},
|
||||
"apollo-link-http": {
|
||||
"version": "1.5.17",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz",
|
||||
"integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==",
|
||||
"requires": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-http-common": "^0.2.16",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"apollo-link-http-common": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz",
|
||||
"integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==",
|
||||
"requires": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"apollo-link-ws": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz",
|
||||
"integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==",
|
||||
"requires": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"apollo-utilities": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
|
||||
"integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==",
|
||||
"requires": {
|
||||
"@wry/equality": "^0.1.2",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
@ -1875,8 +1983,7 @@
|
||||
"async-limiter": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
@ -2724,6 +2831,11 @@
|
||||
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
|
||||
"dev": true
|
||||
},
|
||||
"backo2": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
@ -5029,8 +5141,7 @@
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"fastparse": {
|
||||
"version": "1.1.2",
|
||||
@ -5499,11 +5610,21 @@
|
||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
||||
"dev": true
|
||||
},
|
||||
"graphql": {
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
|
||||
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
|
||||
},
|
||||
"graphql-request": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz",
|
||||
"integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ=="
|
||||
},
|
||||
"graphql-tag": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz",
|
||||
"integrity": "sha512-O7vG5BT3w6Sotc26ybcvLKNTdfr4GfsIVMD+LdYqXCeJIYPRyp8BIsDOUtxw7S1PYvRw5vH3278J2EDezR6mfA=="
|
||||
},
|
||||
"handle-thing": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
||||
@ -6308,6 +6429,11 @@
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
||||
"dev": true
|
||||
},
|
||||
"iterall": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
|
||||
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
|
||||
},
|
||||
"js-base64": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
|
||||
@ -7340,6 +7466,14 @@
|
||||
"is-wsl": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"optimism": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz",
|
||||
"integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==",
|
||||
"requires": {
|
||||
"@wry/context": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"original": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
|
||||
@ -9420,6 +9554,33 @@
|
||||
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
|
||||
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
|
||||
},
|
||||
"subscriptions-transport-ws": {
|
||||
"version": "0.9.17",
|
||||
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz",
|
||||
"integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==",
|
||||
"requires": {
|
||||
"backo2": "^1.0.2",
|
||||
"eventemitter3": "^3.1.0",
|
||||
"iterall": "^1.2.1",
|
||||
"symbol-observable": "^1.0.4",
|
||||
"ws": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
||||
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
|
||||
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@ -9658,6 +9819,14 @@
|
||||
"glob": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"ts-invariant": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||
"integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz",
|
||||
@ -9725,8 +9894,7 @@
|
||||
"tslib": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
|
||||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
@ -10833,6 +11001,20 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"zen-observable": {
|
||||
"version": "0.8.15",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
||||
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
|
||||
},
|
||||
"zen-observable-ts": {
|
||||
"version": "0.8.21",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz",
|
||||
"integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3",
|
||||
"zen-observable": "^0.8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "dadd-",
|
||||
"name": "daddy",
|
||||
"version": "0.0.0",
|
||||
"description": "Daddy",
|
||||
"main": "index.js",
|
||||
@ -52,9 +52,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.3",
|
||||
"apollo-cache-inmemory": "^1.6.6",
|
||||
"apollo-client": "^2.6.10",
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
"apollo-utilities": "^1.3.4",
|
||||
"bulma": "^0.7.2",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-request": "^2.0.0",
|
||||
"graphql-tag": "^2.10.4",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.12.0",
|
||||
@ -65,6 +73,7 @@
|
||||
"redux": "^4.0.4",
|
||||
"redux-saga": "^1.1.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"subscriptions-transport-ws": "^0.9.17",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ 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';
|
||||
import { ProfilePage } from './ProfilePage/ProfilePage';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
@ -12,7 +12,7 @@ export class App extends React.Component {
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/oauth2/:action" exact component={OAuth2Page} />
|
||||
<Route path="/profile" exact component={ProfilePage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
@ -7,15 +7,15 @@ export function HomePage() {
|
||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
||||
|
||||
return (
|
||||
<Page title="Daddy - Accueil">
|
||||
<Page title="Accueil">
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-4 is-offset-4">
|
||||
<div className="box">
|
||||
{
|
||||
currentUser && currentUser.full_name ?
|
||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.full_name}</span> !</p> :
|
||||
currentUser && currentUser.email ?
|
||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.name ? currentUser.name : currentUser.email}</span> !</p> :
|
||||
<p>Veuillez vous authentifier.</p>
|
||||
}
|
||||
</div>
|
66
client/src/components/Navbar.tsx
Normal file
66
client/src/components/Navbar.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store/reducers/root';
|
||||
import { Config } from '../config';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function Navbar() {
|
||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
||||
|
||||
const [ isActive, setActive ] = useState(false);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setActive(active => !active);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<Link className="navbar-item" to="/">
|
||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||
<h1 className="is-size-4">Daddy</h1>
|
||||
</Link>
|
||||
<a role="button"
|
||||
className={`navbar-burger ${isActive ? 'is-active' : ''}`}
|
||||
onClick={toggleMenu}
|
||||
aria-label="menu"
|
||||
aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<div className="buttons">
|
||||
{
|
||||
isAuthenticated ?
|
||||
<Fragment>
|
||||
<Link to="/profile" className="button">
|
||||
<span className="icon">
|
||||
<i className="fas fa-user"></i>
|
||||
</span>
|
||||
</Link>
|
||||
<a className="button" href={Config.logoutURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</Fragment> :
|
||||
<a className="button" href={Config.loginURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
@ -25,6 +25,6 @@ export class Page extends React.PureComponent<PageProps> {
|
||||
|
||||
updateTitle() {
|
||||
const { title } = this.props;
|
||||
if (title !== undefined) window.document.title = title;
|
||||
if (title !== undefined) window.document.title = title + ' - Daddy';
|
||||
}
|
||||
}
|
38
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
38
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { RootState } from '../../store/reducers/root';
|
||||
import { UserForm } from '../UserForm';
|
||||
import { Loader } from '../Loader';
|
||||
import { User } from '../../types/user';
|
||||
import { updateProfile } from '../../store/actions/profile';
|
||||
|
||||
export function ProfilePage() {
|
||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onUserChange = (user: User) => {
|
||||
if (currentUser.name !== user.name) {
|
||||
dispatch(updateProfile({ name: user.name }))
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Mon profil">
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-6 is-offset-3">
|
||||
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
||||
{
|
||||
currentUser ?
|
||||
<UserForm onChange={onUserChange} user={currentUser} /> :
|
||||
<Loader />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
80
client/src/components/UserForm.tsx
Normal file
80
client/src/components/UserForm.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { User } from '../types/user';
|
||||
|
||||
export interface UserFormProps {
|
||||
user: User
|
||||
onChange?: (user: User) => void
|
||||
}
|
||||
|
||||
export function UserForm({ user, onChange }: UserFormProps) {
|
||||
const [ state, setState ] = useState({
|
||||
changed: false,
|
||||
user: {
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: null,
|
||||
connectedAt: null,
|
||||
...user,
|
||||
}
|
||||
});
|
||||
|
||||
const onSaveClick = () => {
|
||||
if (!state.changed) return;
|
||||
if (typeof onChange !== 'function') return;
|
||||
onChange(state.user);
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: false,
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const onUserAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||
const value = evt.currentTarget.value;
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: true,
|
||||
user: {
|
||||
...state.user,
|
||||
[attrName]: value,
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form">
|
||||
<div className="field">
|
||||
<label className="label">Nom d'utilisateur</label>
|
||||
<div className="control">
|
||||
<input type="text" className="input" value={state.user.name}
|
||||
onChange={onUserAttrChange.bind(null, "name")} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Adresse courriel</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Date de dernière connexion</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.connectedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Date de création</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="buttons is-right">
|
||||
<button disabled={!state.changed}
|
||||
className="button is-primary" onClick={onSaveClick}>Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
15
client/src/config.ts
Normal file
15
client/src/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const Config = {
|
||||
loginURL: get<string>("loginURL", "http://localhost:8081/login"),
|
||||
logoutURL: get<string>("logoutURL", "http://localhost:8081/logout"),
|
||||
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"),
|
||||
subscriptionEndpoint: get<string>("subscriptionEndpoint", "ws://localhost:8081/api/v1/graphql"),
|
||||
};
|
||||
|
||||
function get<T>(key: string, defaultValue: T):T {
|
||||
const config = window['__CONFIG__'] || {};
|
||||
if (config && config.hasOwnProperty(key)) {
|
||||
return config[key] as T;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
11
client/src/store/actions/auth.ts
Normal file
11
client/src/store/actions/auth.ts
Normal file
@ -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 };
|
||||
}
|
40
client/src/store/actions/profile.ts
Normal file
40
client/src/store/actions/profile.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Action } from "redux";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
|
||||
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
|
||||
export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE';
|
||||
|
||||
export interface fetchProfileRequestAction extends Action {
|
||||
|
||||
}
|
||||
|
||||
export interface fetchProfileSuccessAction extends Action {
|
||||
profile: User
|
||||
}
|
||||
|
||||
|
||||
export function fetchProfile(): fetchProfileRequestAction {
|
||||
return { type: FETCH_PROFILE_REQUEST }
|
||||
}
|
||||
|
||||
export const UPDATE_PROFILE_REQUEST = 'UPDATE_PROFILE_REQUEST';
|
||||
export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS';
|
||||
export const UPDATE_PROFILE_FAILURE = 'UPDATE_PROFILE_FAILURE';
|
||||
|
||||
export interface ProfileChanges {
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface updateProfileRequestAction extends Action {
|
||||
changes: ProfileChanges
|
||||
}
|
||||
|
||||
export interface updateProfileSuccessAction extends Action {
|
||||
profile: User
|
||||
}
|
||||
|
||||
|
||||
export function updateProfile(changes: ProfileChanges): updateProfileRequestAction {
|
||||
return { type: UPDATE_PROFILE_REQUEST, changes }
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Action } from "redux";
|
||||
import { User } from "../../types/user";
|
||||
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
|
||||
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile";
|
||||
import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth";
|
||||
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile";
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
@ -17,10 +17,10 @@ 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);
|
||||
case UPDATE_PROFILE_SUCCESS:
|
||||
return handleFetchProfileSuccess(state, action as updateProfileSuccessAction);
|
||||
|
||||
}
|
||||
return state;
|
||||
@ -36,14 +36,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,
|
||||
@ -52,4 +44,14 @@ function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSu
|
||||
...profile,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function handleUpdateProfileSuccess(state: AuthState, { profile }: updateProfileSuccessAction): AuthState {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
currentUser: {
|
||||
...profile,
|
||||
}
|
||||
};
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
12
client/src/store/sagas/init.ts
Normal file
12
client/src/store/sagas/init.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { all, put } from "redux-saga/effects";
|
||||
import { fetchProfile } from "../actions/profile";
|
||||
|
||||
export function* initRootSaga() {
|
||||
yield all([
|
||||
fetchUserProfileSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* fetchUserProfileSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
46
client/src/store/sagas/profile.ts
Normal file
46
client/src/store/sagas/profile.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { DaddyClient, getClient } from "../../util/daddy";
|
||||
import { Config } from "../../config";
|
||||
import { all, takeLatest, put, select } from "redux-saga/effects";
|
||||
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS, updateProfileRequestAction, UPDATE_PROFILE_REQUEST, UPDATE_PROFILE_FAILURE, UPDATE_PROFILE_SUCCESS } from "../actions/profile";
|
||||
import { SET_CURRENT_USER } from "../actions/auth";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export function* profileRootSaga() {
|
||||
yield all([
|
||||
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
|
||||
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
|
||||
takeLatest(UPDATE_PROFILE_REQUEST, updateProfileSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* onCurrentUserChangeSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
||||
|
||||
export function* fetchProfileSaga() {
|
||||
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
profile = yield client.fetchProfile().then(result => result.userProfile);
|
||||
} catch(err) {
|
||||
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
||||
}
|
||||
|
||||
export function* updateProfileSaga({ changes }: updateProfileRequestAction) {
|
||||
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
profile = yield client.updateProfile(changes).then(result => result.updateProfile);
|
||||
} catch(err) {
|
||||
yield put({ type: UPDATE_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: UPDATE_PROFILE_SUCCESS, profile });
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { failureRootSaga } from './failure';
|
||||
import { authRootSaga } from './auth';
|
||||
import { initRootSaga } from './init';
|
||||
import { usersRootSaga } from './users';
|
||||
import { profileRootSaga } from './profile';
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
initRootSaga(),
|
||||
failureRootSaga(),
|
||||
authRootSaga(),
|
||||
usersRootSaga(),
|
||||
profileRootSaga(),
|
||||
]);
|
||||
}
|
6
client/src/types/user.ts
Normal file
6
client/src/types/user.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface User {
|
||||
email: string
|
||||
name?: string
|
||||
connectedAt?: Date
|
||||
createdAt?: Date
|
||||
}
|
115
client/src/util/daddy.ts
Normal file
115
client/src/util/daddy.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import ApolloClient, { DefaultOptions } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { split } from 'apollo-link';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { WebSocketLink } from 'apollo-link-ws';
|
||||
import { getMainDefinition, variablesInOperation } from 'apollo-utilities';
|
||||
import gql from 'graphql-tag';
|
||||
import { ProfileChanges } from '../store/actions/profile';
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(...args: any[]) {
|
||||
super(...args)
|
||||
Object.setPrototypeOf(this, UnauthorizedError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
let client: DaddyClient
|
||||
|
||||
export function getClient(graphQLEndpoint: string, subscriptionEndpoint: string): DaddyClient {
|
||||
if (!client) {
|
||||
client = new DaddyClient(graphQLEndpoint, subscriptionEndpoint);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export class DaddyClient {
|
||||
|
||||
gql: ApolloClient<InMemoryCache>
|
||||
|
||||
constructor(graphQLEndpoint: string, subscriptionEndpoint: string) {
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: subscriptionEndpoint,
|
||||
options: {
|
||||
reconnect: true
|
||||
}
|
||||
});
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: graphQLEndpoint,
|
||||
fetchOptions: {
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
}
|
||||
});
|
||||
|
||||
const link = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
httpLink,
|
||||
);
|
||||
|
||||
const defaultOptions: DefaultOptions = {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
this.gql = new ApolloClient<any>({
|
||||
link: link,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions,
|
||||
});
|
||||
}
|
||||
|
||||
fetchProfile() {
|
||||
return this.gql.query({
|
||||
query: gql`
|
||||
query {
|
||||
userProfile {
|
||||
name,
|
||||
email,
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}`
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
updateProfile(changes: ProfileChanges) {
|
||||
return this.gql.mutate({
|
||||
variables: {
|
||||
changes,
|
||||
},
|
||||
mutation: gql`
|
||||
mutation updateProfile($changes: ProfileChanges!) {
|
||||
updateProfile(changes: $changes) {
|
||||
name,
|
||||
email,
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}`,
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
assertAuthorization({ status, data }: any) {
|
||||
if (status === 401) return Promise.reject(new UnauthorizedError());
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
},
|
100
cmd/server/container.go
Normal file
100
cmd/server/container.go
Normal file
@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/wader/gormstore"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
"gitlab.com/wpetit/goweb/service/build"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
"gitlab.com/wpetit/goweb/session/gorilla"
|
||||
)
|
||||
|
||||
func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Container, error) {
|
||||
// Initialize and configure service container
|
||||
ctn := service.NewContainer()
|
||||
|
||||
ctn.Provide(build.ServiceName, build.ServiceProvider(ProjectVersion, GitRef, BuildDate))
|
||||
|
||||
// Generate random cookie authentication key if none is set
|
||||
if conf.HTTP.CookieAuthenticationKey == "" {
|
||||
logger.Info(ctx, "could not find cookie authentication key. generating one...")
|
||||
|
||||
cookieAuthenticationKey, err := gorilla.GenerateRandomBytes(64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not generate cookie authentication key")
|
||||
}
|
||||
|
||||
conf.HTTP.CookieAuthenticationKey = string(cookieAuthenticationKey)
|
||||
}
|
||||
|
||||
// Generate random cookie encryption key if none is set
|
||||
if conf.HTTP.CookieEncryptionKey == "" {
|
||||
logger.Info(ctx, "could not find cookie encryption key. generating one...")
|
||||
|
||||
cookieEncryptionKey, err := gorilla.GenerateRandomBytes(32)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not generate cookie encryption key")
|
||||
}
|
||||
|
||||
conf.HTTP.CookieEncryptionKey = string(cookieEncryptionKey)
|
||||
}
|
||||
|
||||
ctn.Provide(orm.ServiceName, orm.ServiceProvider("postgres", conf.Database.DSN, conf.Debug))
|
||||
|
||||
orm, err := orm.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Create and initialize HTTP session service provider
|
||||
sessionStore := gormstore.NewOptions(
|
||||
orm.DB(),
|
||||
gormstore.Options{
|
||||
TableName: "sessions",
|
||||
SkipCreateTable: false,
|
||||
},
|
||||
[]byte(conf.HTTP.CookieAuthenticationKey),
|
||||
[]byte(conf.HTTP.CookieEncryptionKey),
|
||||
)
|
||||
|
||||
quit := make(chan struct{})
|
||||
go sessionStore.PeriodicCleanup(1*time.Hour, quit)
|
||||
|
||||
// Define default cookie options
|
||||
sessionStore.SessionOpts.Path = "/"
|
||||
sessionStore.SessionOpts.HttpOnly = true
|
||||
sessionStore.SessionOpts.MaxAge = conf.HTTP.CookieMaxAge
|
||||
sessionStore.SessionOpts.SameSite = http.SameSiteStrictMode
|
||||
|
||||
ctn.Provide(
|
||||
session.ServiceName,
|
||||
gorilla.ServiceProvider("daddy", sessionStore),
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
183
cmd/server/main.go
Normal file
183
cmd/server/main.go
Normal file
@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/route"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var (
|
||||
configFile = ""
|
||||
workdir = ""
|
||||
dumpConfig = false
|
||||
version = false
|
||||
migrate = ""
|
||||
)
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var (
|
||||
GitRef = "unknown"
|
||||
ProjectVersion = "unknown"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
flag.StringVar(&configFile, "config", configFile, "configuration file")
|
||||
flag.StringVar(&workdir, "workdir", workdir, "working directory")
|
||||
flag.BoolVar(&dumpConfig, "dump-config", dumpConfig, "dump configuration and exit")
|
||||
flag.BoolVar(&version, "version", version, "show version and exit")
|
||||
flag.StringVar(&migrate, "migrate", migrate, "migrate data schema version and exit, possible values: latest, down, up")
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if version {
|
||||
fmt.Printf("%s (%s) - %s\n", ProjectVersion, GitRef, BuildDate)
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Switch to new working directory if defined
|
||||
if workdir != "" {
|
||||
if err := os.Chdir(workdir); err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not change working directory",
|
||||
logger.E(err),
|
||||
logger.F("workdir", workdir),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration file if defined, use default configuration otherwise
|
||||
var conf *config.Config
|
||||
|
||||
var err error
|
||||
|
||||
if configFile != "" {
|
||||
conf, err = config.NewFromFile(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.Wrapf(err, " '%s'", configFile))
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not load config file",
|
||||
logger.E(err),
|
||||
logger.F("configFile", configFile),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if dumpConfig {
|
||||
conf = config.NewDumpDefault()
|
||||
} else {
|
||||
conf = config.NewDefault()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Dump configuration if asked
|
||||
if dumpConfig {
|
||||
if err := config.Dump(conf, os.Stdout); err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not dump config",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err := config.WithEnvironment(conf); err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not override config with environment",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
ctx,
|
||||
"starting",
|
||||
logger.F("gitRef", GitRef),
|
||||
logger.F("projectVersion", ProjectVersion),
|
||||
logger.F("buildDate", BuildDate),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "setting log format", logger.F("format", conf.Log.Format))
|
||||
logger.SetFormat(conf.Log.Format)
|
||||
|
||||
logger.Debug(ctx, "setting log level", logger.F("level", conf.Log.Level.String()))
|
||||
logger.SetLevel(conf.Log.Level)
|
||||
|
||||
// Create service container
|
||||
ctn, err := getServiceContainer(ctx, conf)
|
||||
if err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not create service container",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
ctx = container.WithContainer(ctx, ctn)
|
||||
|
||||
if migrate != "" {
|
||||
if err := applyMigration(ctx, ctn); err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not apply migration",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
106
cmd/server/migration.go
Normal file
106
cmd/server/migration.go
Normal file
@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const (
|
||||
migrateUp = "up"
|
||||
migrateLatest = "latest"
|
||||
migrateDown = "down"
|
||||
)
|
||||
|
||||
func applyMigration(ctx context.Context, ctn *service.Container) error {
|
||||
orm, err := orm.From(ctn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migr := orm.Migration()
|
||||
|
||||
// Register available migrations
|
||||
migr.Register(
|
||||
m000initialSchema(),
|
||||
)
|
||||
|
||||
currentVersion, err := migr.CurrentVersion(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current data schema version")
|
||||
}
|
||||
|
||||
switch migrate {
|
||||
case migrateUp:
|
||||
if err := migr.Up(ctx); err != nil {
|
||||
return errors.Wrap(err, "could not apply up migration")
|
||||
}
|
||||
|
||||
case migrateLatest:
|
||||
latestVersion, err := migr.LatestVersion()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve latest data schema version")
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
ctx,
|
||||
"migrating data schema to latest version",
|
||||
logger.F("currentVersion", currentVersion),
|
||||
logger.F("latestVersion", latestVersion),
|
||||
)
|
||||
|
||||
// Execute migration to latest available version
|
||||
if err := migr.Latest(ctx); err != nil {
|
||||
return errors.Wrap(err, "could not migrate to latest data schema")
|
||||
}
|
||||
|
||||
case migrateDown:
|
||||
if err := migr.Down(ctx); err != nil {
|
||||
return errors.Wrap(err, "could not apply down migration")
|
||||
}
|
||||
|
||||
default:
|
||||
return errors.Errorf("unknown migration command: '%s'", migrate)
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
ctx,
|
||||
"migration completed",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var initialModels = []interface{}{
|
||||
&model.User{},
|
||||
}
|
||||
|
||||
func m000initialSchema() orm.Migration {
|
||||
return orm.NewDBMigration(
|
||||
"00_initial_schema",
|
||||
func(ctx context.Context, tx *gorm.DB) error {
|
||||
for _, m := range initialModels {
|
||||
if err := tx.AutoMigrate(m).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(ctx context.Context, tx *gorm.DB) error {
|
||||
for _, m := range initialModels {
|
||||
if err := tx.DropTableIfExists(m).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@ -0,0 +1 @@
|
||||
9
|
14
debian/control
vendored
Normal file
14
debian/control
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
Source: daddy
|
||||
Section: unknown
|
||||
Priority: optional
|
||||
Maintainer: Cadoles <contact@cadoles.com>
|
||||
Build-Depends: debhelper (>= 8.0.0), wget, ca-certificates, tar
|
||||
Standards-Version: 3.9.4
|
||||
Homepage: http://forge.cadoles.com/Cadoles/daddy
|
||||
Vcs-Git: http://forge.cadoles.com/Cadoles/daddy.git
|
||||
Vcs-Browser: http://forge.cadoles.com/Cadoles/daddy
|
||||
|
||||
Package: daddy
|
||||
Architecture: amd64
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Description: Daddy app
|
1
debian/daddy.links
vendored
Normal file
1
debian/daddy.links
vendored
Normal file
@ -0,0 +1 @@
|
||||
/etc/daddy/client-config.js /usr/share/daddy/public/config.js
|
11
debian/daddy.service
vendored
Normal file
11
debian/daddy.service
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Daddy app
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/daddy -workdir /usr/share/daddy -config /etc/daddy/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
56
debian/rules
vendored
Normal file
56
debian/rules
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
export DH_VERBOSE=1
|
||||
|
||||
GO_VERSION := 1.13.5
|
||||
OS := linux
|
||||
ARCH := amd64
|
||||
GOPATH=$(HOME)/go
|
||||
|
||||
ifeq (, $(shell which go 2>/dev/null))
|
||||
override_dh_auto_build: install-go
|
||||
|
||||
override_dh_auto_clean: install-go
|
||||
endif
|
||||
|
||||
ifeq (, $(shell which node 2>/dev/null))
|
||||
override_dh_auto_build: install-nodejs
|
||||
endif
|
||||
|
||||
%:
|
||||
dh $@ --with systemd
|
||||
|
||||
override_dh_auto_build: $(GOPATH)
|
||||
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" make deps
|
||||
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" ARCH_TARGETS=$(ARCH) make release
|
||||
|
||||
$(GOPATH):
|
||||
mkdir -p $(GOPATH)
|
||||
|
||||
install-go:
|
||||
wget -nc https://dl.google.com/go/go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
|
||||
tar -C /usr/local -xzf go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
|
||||
|
||||
install-nodejs:
|
||||
wget -O- https://deb.nodesource.com/setup_12.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
override_dh_auto_install:
|
||||
|
||||
mkdir -p debian/daddy/usr/share/daddy
|
||||
mkdir -p debian/daddy/etc/daddy
|
||||
mkdir -p debian/daddy/usr/bin
|
||||
|
||||
cp -r release/server-$(OS)-$(ARCH)/* debian/daddy/usr/share/daddy/
|
||||
|
||||
mv debian/daddy/usr/share/daddy/bin/server debian/daddy/usr/bin/daddy
|
||||
mv debian/daddy/usr/share/daddy/server.conf debian/daddy/etc/daddy/config.yml
|
||||
mv debian/daddy/usr/share/daddy/public/config.js debian/daddy/etc/daddy/client-config.js
|
||||
|
||||
install -d debian/daddy
|
||||
|
||||
override_dh_strip:
|
||||
|
||||
override_dh_auto_test:
|
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@ -0,0 +1 @@
|
||||
3.0 (native)
|
@ -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:
|
||||
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store/reducers/root';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function Navbar() {
|
||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
||||
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#/">
|
||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||
<h1 className="is-size-4">Daddy</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar-menu">
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
{
|
||||
isAuthenticated ?
|
||||
<Link className="button is-small" to="/oauth2/logout">
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
<span>Se déconnecter</span>
|
||||
</Link> :
|
||||
<Link className="button is-small" to="/oauth2/login">
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
<span>Se connecter</span>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<Page title="Daddy - OAuth2">
|
||||
|
||||
</Page>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
export const Config = {
|
||||
// The OpenID Connect client_id
|
||||
oauth2ClientId: get<string>("oauth2ClientId", "daddy"),
|
||||
oauth2Scope: get<string>("oauth2Scope", "email email_verified openid offline_access"),
|
||||
oauth2RedirectURI: get<string>("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"),
|
||||
oauth2Audience: get<string>("oauth2Audience", ""),
|
||||
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
|
||||
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
|
||||
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
|
||||
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081"),
|
||||
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8080/api/v1/graphql")
|
||||
};
|
||||
|
||||
function get<T>(key: string, defaultValue: T):T {
|
||||
const config = window['__CONFIG__'] || {};
|
||||
if (config && config.hasOwnProperty(key)) {
|
||||
return config[key] as T;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
@ -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 };
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Action } from "redux";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
|
||||
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
|
||||
export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE';
|
||||
|
||||
export interface fetchProfileRequestAction extends Action {
|
||||
|
||||
}
|
||||
|
||||
export interface fetchProfileSuccessAction extends Action {
|
||||
profile: User
|
||||
}
|
||||
|
||||
|
||||
export function fetchProfile(): fetchProfileRequestAction {
|
||||
return { type: FETCH_PROFILE_REQUEST }
|
||||
}
|
@ -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));
|
||||
};
|
@ -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));
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { DaddyClient } from "../../util/daddy";
|
||||
import { Config } from "../../config";
|
||||
import { getSavedAccessGrant } from "../../util/auth";
|
||||
import { all, takeLatest, put, select } from "redux-saga/effects";
|
||||
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile";
|
||||
import { SET_CURRENT_USER } from "../actions/auth";
|
||||
import { RootState } from "../reducers/root";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export function* usersRootSaga() {
|
||||
yield all([
|
||||
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
|
||||
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* onCurrentUserChangeSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function* fetchProfileSaga() {
|
||||
const grant = getSavedAccessGrant();
|
||||
const client = new DaddyClient(Config.graphQLEndpoint, grant.id_token);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
const currentUser: User = yield select((state: RootState) => state.auth.currentUser);
|
||||
profile = yield client.fetchUser(currentUser.email).then(result => result.user);
|
||||
} catch(err) {
|
||||
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export interface IdToken {
|
||||
email: string
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export interface User {
|
||||
email: string
|
||||
full_name?: string
|
||||
updated_at?: Date
|
||||
created_at?: Date
|
||||
}
|
@ -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<any> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(plain);
|
||||
return window.crypto.subtle.digest('SHA-256', data);
|
||||
}
|
||||
|
||||
export function pkceChallengeFromVerifier(v): PromiseLike<string> {
|
||||
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<LoginSession> {
|
||||
// Based on https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce
|
||||
const state = generateRandomString();
|
||||
const verifier = generateRandomString();
|
||||
|
||||
return new Promise<LoginSession>((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');
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { Config } from "../config";
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(...args: any[]) {
|
||||
super(...args)
|
||||
Object.setPrototypeOf(this, UnauthorizedError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class DaddyClient {
|
||||
|
||||
gql: GraphQLClient
|
||||
|
||||
constructor(endpoint: string, idToken: string) {
|
||||
this.gql = new GraphQLClient(endpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${idToken}`,
|
||||
mode: 'cors',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchUser(email: string) {
|
||||
return this.gql.rawRequest(`
|
||||
query fetchUser {
|
||||
user(where: {email: {eq: $email}}) {
|
||||
id
|
||||
created_at
|
||||
updated_at
|
||||
email,
|
||||
full_name
|
||||
}
|
||||
}
|
||||
`, { email })
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
assertAuthorization({ status, data }: any) {
|
||||
if (status === 401) return Promise.reject(new UnauthorizedError());
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
23
go.mod
Normal file
23
go.mod
Normal file
@ -0,0 +1,23 @@
|
||||
module forge.cadoles.com/Cadoles/daddy
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032
|
||||
github.com/99designs/gqlgen v0.11.3
|
||||
github.com/caarlos0/env/v6 v6.2.2
|
||||
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-chi/chi v4.1.0+incompatible
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/websocket v1.2.0
|
||||
github.com/jackc/pgx v3.6.2+incompatible
|
||||
github.com/jackc/pgx/v4 v4.7.1
|
||||
github.com/jinzhu/gorm v1.9.14
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
520
go.sum
Normal file
520
go.sum
Normal file
@ -0,0 +1,520 @@
|
||||
cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ=
|
||||
cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ=
|
||||
cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032 h1:qTYaLPsLDlvqDkatONsvrisvfvpHaGe3lQqIaX7FFQQ=
|
||||
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032/go.mod h1:gkfqGyk7fCj2Z0ngEOCJ3K0FVmqft/8dFV/OnYT1vec=
|
||||
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
|
||||
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
|
||||
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw=
|
||||
github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
||||
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/caarlos0/env/v6 v6.2.2 h1:R0NIFXaB/LhwuGrjnsldzpnVNjFU/U+hTVHt+cq0yDY=
|
||||
github.com/caarlos0/env/v6 v6.2.2/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 h1:3CQigZV4Vgu4XX34CGsQFHbO5re8boAbn0dqUza1LrQ=
|
||||
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450/go.mod h1:nZYoHDEpIB+Hv0ns85UxQDkHQ1uuaUQIFJ99VPctjq8=
|
||||
github.com/cortesi/moddwatch v0.0.0-20200427000745-d26468c93cf0 h1:7tjBO+RH4BoxJUUysxGORQI27+72DfxxA2+i3Tixey0=
|
||||
github.com/cortesi/moddwatch v0.0.0-20200427000745-d26468c93cf0/go.mod h1:QYGP4Q0SeEUNSC+dsNSKTmONSd1PpZVYUXIRAzxxpXo=
|
||||
github.com/cortesi/termlog v0.0.0-20190809035425-7871d363854c h1:D5UylL3xKRrrqZKk/NhrOhoQVdCQwuEeyFgTfN9n9O4=
|
||||
github.com/cortesi/termlog v0.0.0-20190809035425-7871d363854c/go.mod h1:gh6GQA3zOsGU4pz+X6ZHqW63KxI/V7KLmBCG9ODJ+l4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w=
|
||||
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE=
|
||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q=
|
||||
github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.6.1 h1:lwofaXKPbIx6qEaK8mNm7uZuOwxHw+PnAFGDsDFpkRI=
|
||||
github.com/jackc/pgconn v1.6.1/go.mod h1:g8mKMqmSUO6AzAvha7vy07g1rbGOlc7iF0nU0ei83hc=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
|
||||
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8 h1:Q3tB+ExeflWUW7AFcAhXqk40s9mnNYLk1nOkKNZ5GnU=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.4.0 h1:pHQfb4jh9iKqHyxPthq1fr+0HwSNIl3btYPbw2m2lbM=
|
||||
github.com/jackc/pgtype v1.4.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
|
||||
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.7.1 h1:aqUSOcStk6fik+lSE+tqfFhvt/EwT8q/oMtJbP9CjXI=
|
||||
github.com/jackc/pgx/v4 v4.7.1/go.mod h1:nu42q3aPjuC1M0Nak4bnoprKlXPINqopEKqbq5AZSC4=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
|
||||
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU=
|
||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23 h1:gtfR002LWpH9vQ1/GLbWBOTcS92cBi5PAR021lArKF8=
|
||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23/go.mod h1:2z7nYWeR0xUeFNCmlyH6Qt6qigF+Kl/k4LbQbj6Ksus=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce h1:B3inZUHFr/FpA3jb+ZeSSHk3FSpB0xkQ0TjePhRokxw=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2 h1:9WJw0v6BzHV8fP8EywjcqAz8PyCZxLn8ioTiMP4SBog=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191202203127-2b6af5f9ace7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM=
|
||||
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
3
internal/.gitignore
vendored
Normal file
3
internal/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/server.go
|
||||
/graph/generated
|
||||
/model/models_gen.go
|
127
internal/config/config.go
Normal file
127
internal/config/config.go
Normal file
@ -0,0 +1,127 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"github.com/caarlos0/env/v6"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Debug bool `yaml:"debug" env:"DEBUG"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
OIDC OIDCConfig `yaml:"oidc"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
}
|
||||
|
||||
// NewFromFile retrieves the configuration from the given file
|
||||
func NewFromFile(filepath string) (*Config, error) {
|
||||
config := NewDefault()
|
||||
|
||||
data, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read file '%s'", filepath)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return nil, errors.Wrapf(err, "could not unmarshal configuration")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Address string `yaml:"address" env:"HTTP_ADDRESS"`
|
||||
CookieAuthenticationKey string `yaml:"cookieAuthenticationKey" env:"HTTP_COOKIE_AUTHENTICATION_KEY"`
|
||||
CookieEncryptionKey string `yaml:"cookieEncryptionKey" env:"HTTP_COOKIE_ENCRYPTION_KEY"`
|
||||
CookieMaxAge int `yaml:"cookieMaxAge" env:"HTTP_COOKIE_MAX_AGE"`
|
||||
TemplateDir string `yaml:"templateDir" env:"HTTP_TEMPLATE_DIR"`
|
||||
PublicDir string `yaml:"publicDir" env:"HTTP_PUBLIC_DIR"`
|
||||
FrontendURL string `yaml:"frontendURL" env:"HTTP_FRONTEND_URL"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
AllowedOrigins []string `yaml:"allowedOrigins" env:"HTTP_CORS_ALLOWED_ORIGINS"`
|
||||
AllowCredentials bool `yaml:"allowCredentials" env:"HTTP_CORS_ALLOW_CREDENTIALS"`
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
ClientID string `yaml:"clientId" env:"OIDC_CLIENT_ID"`
|
||||
ClientSecret string `yaml:"clientSecret" env:"OIDC_CLIENT_SECRET"`
|
||||
IssuerURL string `yaml:"issuerUrl" env:"OIDC_ISSUER_URL"`
|
||||
RedirectURL string `yaml:"redirectUrl" env:"OIDC_REDIRECT_URL"`
|
||||
PostLogoutRedirectURL string `yaml:"postLogoutRedirectURL" env:"OIDC_POST_LOGOUT_REDIRECT_URL"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Level logger.Level `yaml:"level" env:"LOG_LEVEL"`
|
||||
Format logger.Format `yaml:"format" env:"LOG_FORMAT"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
DSN string `yaml:"dsn" env:"DATABASE_DSN"`
|
||||
}
|
||||
|
||||
func NewDumpDefault() *Config {
|
||||
config := NewDefault()
|
||||
return config
|
||||
}
|
||||
|
||||
func NewDefault() *Config {
|
||||
return &Config{
|
||||
Debug: false,
|
||||
Log: LogConfig{
|
||||
Level: logger.LevelInfo,
|
||||
Format: logger.FormatHuman,
|
||||
},
|
||||
HTTP: HTTPConfig{
|
||||
Address: ":8081",
|
||||
CookieAuthenticationKey: "",
|
||||
CookieEncryptionKey: "",
|
||||
CookieMaxAge: int((time.Hour * 1).Seconds()), // 1 hour
|
||||
TemplateDir: "template",
|
||||
PublicDir: "public",
|
||||
FrontendURL: "http://localhost:8080",
|
||||
CORS: CORSConfig{
|
||||
AllowedOrigins: []string{"http://localhost:8080"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
OIDC: OIDCConfig{
|
||||
IssuerURL: "http://localhost:4444/",
|
||||
RedirectURL: "http://localhost:8081/oauth2/callback",
|
||||
PostLogoutRedirectURL: "http://localhost:8081",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
DSN: "host=localhost database=daddy",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Dump(config *Config, w io.Writer) error {
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not dump config")
|
||||
}
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithEnvironment(conf *Config) error {
|
||||
if err := env.Parse(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
9
internal/config/provider.go
Normal file
9
internal/config/provider.go
Normal file
@ -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
|
||||
}
|
||||
}
|
33
internal/config/service.go
Normal file
33
internal/config/service.go
Normal file
@ -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
|
||||
}
|
56
internal/gqlgen.yml
Normal file
56
internal/gqlgen.yml
Normal file
@ -0,0 +1,56 @@
|
||||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- graph/*.graphql
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
filename: graph/generated/generated.go
|
||||
package: generated
|
||||
|
||||
# Uncomment to enable federation
|
||||
# federation:
|
||||
# filename: graph/generated/federation.go
|
||||
# package: generated
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
resolver:
|
||||
layout: follow-schema
|
||||
dir: graph
|
||||
package: graph
|
||||
|
||||
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
|
||||
# struct_tag: json
|
||||
|
||||
# Optional: turn on to use []Thing instead of []*Thing
|
||||
# omit_slice_element_pointers: false
|
||||
|
||||
# Optional: set to speed up generation time by not performing a final validation pass.
|
||||
# skip_validation: true
|
||||
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
autobind:
|
||||
- "forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
# The first line in each type will be used as defaults for resolver arguments and
|
||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
||||
# your liking
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
Int:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
25
internal/graph/helper.go
Normal file
25
internal/graph/helper.go
Normal file
@ -0,0 +1,25 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func getDB(ctx context.Context) (*gorm.DB, error) {
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
orm, err := orm.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return orm.DB(), nil
|
||||
}
|
7
internal/graph/mutation.graphql
Normal file
7
internal/graph/mutation.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
input ProfileChanges {
|
||||
name: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
updateProfile(changes: ProfileChanges!): User!
|
||||
}
|
20
internal/graph/mutation.resolvers.go
Normal file
20
internal/graph/mutation.resolvers.go
Normal file
@ -0,0 +1,20 @@
|
||||
package graph
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
||||
return handleUpdateUserProfile(ctx, changes)
|
||||
}
|
||||
|
||||
// Mutation returns generated.MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
12
internal/graph/query.graphql
Normal file
12
internal/graph/query.graphql
Normal file
@ -0,0 +1,12 @@
|
||||
scalar Time
|
||||
|
||||
type User {
|
||||
name: String
|
||||
email: String!
|
||||
connectedAt: Time!
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
type Query {
|
||||
userProfile: User
|
||||
}
|
20
internal/graph/query.resolvers.go
Normal file
20
internal/graph/query.resolvers.go
Normal file
@ -0,0 +1,20 @@
|
||||
package graph
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
model1 "forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
)
|
||||
|
||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||
return handleUserProfile(ctx)
|
||||
}
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type queryResolver struct{ *Resolver }
|
9
internal/graph/resolver.go
Normal file
9
internal/graph/resolver.go
Normal file
@ -0,0 +1,9 @@
|
||||
package graph
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen
|
||||
|
||||
type Resolver struct{}
|
58
internal/graph/user_profile.go
Normal file
58
internal/graph/user_profile.go
Normal file
@ -0,0 +1,58 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func handleUserProfile(ctx context.Context) (*model.User, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
userEmail, err := session.UserEmail(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
user, err := repo.FindUserByEmail(ctx, userEmail)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
userEmail, err := session.UserEmail(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
userChanges := &model.User{}
|
||||
|
||||
if changes.Name != nil {
|
||||
userChanges.Name = changes.Name
|
||||
}
|
||||
|
||||
user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
15
internal/model/user.go
Normal file
15
internal/model/user.go
Normal file
@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID *uint `gorm:"primary_key"`
|
||||
Name *string `json:"name"`
|
||||
Email string `json:"email" gorm:"unique;not null"`
|
||||
ConnectedAt time.Time `json:"connectedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type ProfileChanges struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
74
internal/model/user_repository.go
Normal file
74
internal/model/user_repository.go
Normal file
@ -0,0 +1,74 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) {
|
||||
user := &User{
|
||||
Email: email,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
err := tx.Where("email = ?", email).FirstOrCreate(user).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := tx.Model(user).UpdateColumn("connected_at", time.Now()).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create user")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
user := &User{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
err := r.db.First(user, "email = ?", email).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateUserByEmail(ctx context.Context, email string, changes *User) (*User, error) {
|
||||
user := &User{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
err := r.db.First(user, "email = ?", email).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
if err := r.db.Model(user).Updates(changes).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "could not update user")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db}
|
||||
}
|
84
internal/orm/migration.go
Normal file
84
internal/orm/migration.go
Normal file
@ -0,0 +1,84 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type MigrationFunc func(ctx context.Context, tx *gorm.DB) error
|
||||
|
||||
type Migration interface {
|
||||
Version() string
|
||||
Up(context.Context) error
|
||||
Down(context.Context) error
|
||||
}
|
||||
|
||||
type DBMigration struct {
|
||||
version string
|
||||
up MigrationFunc
|
||||
down MigrationFunc
|
||||
}
|
||||
|
||||
func (m *DBMigration) Version() string {
|
||||
return m.version
|
||||
}
|
||||
|
||||
func (m *DBMigration) Up(ctx context.Context) error {
|
||||
db, err := m.getDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return m.up(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply up migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DBMigration) Down(ctx context.Context) error {
|
||||
db, err := m.getDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return m.down(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply down migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DBMigration) getDatabase(ctx context.Context) (*gorm.DB, error) {
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
orm, err := From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve orm service")
|
||||
}
|
||||
|
||||
return orm.DB(), nil
|
||||
}
|
||||
|
||||
func NewDBMigration(version string, up, down MigrationFunc) *DBMigration {
|
||||
return &DBMigration{
|
||||
version: version,
|
||||
up: up,
|
||||
down: down,
|
||||
}
|
||||
}
|
146
internal/orm/migration_manager.go
Normal file
146
internal/orm/migration_manager.go
Normal file
@ -0,0 +1,146 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAvailableMigration = errors.New("no available migration")
|
||||
ErrMigrationNotFound = errors.New("migration not found")
|
||||
)
|
||||
|
||||
type MigrationManager struct {
|
||||
migrations []Migration
|
||||
resolver VersionResolver
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Up(ctx context.Context) error {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current version")
|
||||
}
|
||||
|
||||
migrate := func(up Migration) error {
|
||||
if err := up.Up(ctx); err != nil {
|
||||
return errors.Wrapf(err, "could not apply '%s' up migration", up.Version())
|
||||
}
|
||||
|
||||
if err := m.resolver.Set(ctx, up.Version()); err != nil {
|
||||
return errors.Wrapf(err, "could not update schema version to '%s'", up.Version())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentVersion == "" {
|
||||
up := m.migrations[0]
|
||||
|
||||
return migrate(up)
|
||||
}
|
||||
|
||||
for i, mi := range m.migrations {
|
||||
if mi.Version() != currentVersion && currentVersion != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Already at latest, do nothing
|
||||
if i >= len(m.migrations)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
up := m.migrations[i+1]
|
||||
|
||||
return migrate(up)
|
||||
}
|
||||
|
||||
return errors.WithStack(ErrMigrationNotFound)
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Down(ctx context.Context) error {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current version")
|
||||
}
|
||||
|
||||
for i, mi := range m.migrations {
|
||||
if mi.Version() != currentVersion {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := mi.Down(ctx); err != nil {
|
||||
return errors.Wrapf(err, "could not apply '%s' down migration", mi.Version())
|
||||
}
|
||||
|
||||
var version string
|
||||
|
||||
// Already at oldest, do nothing
|
||||
if i != 0 {
|
||||
down := m.migrations[i-1]
|
||||
version = down.Version()
|
||||
}
|
||||
|
||||
if err := m.resolver.Set(ctx, version); err != nil {
|
||||
return errors.Wrapf(err, "could not update schema version to '%s'", version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(ErrMigrationNotFound)
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Latest(ctx context.Context) error {
|
||||
for {
|
||||
isLatest, err := m.IsLatest(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve schema state")
|
||||
}
|
||||
|
||||
if isLatest {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.Up(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Register(migrations ...Migration) {
|
||||
m.migrations = migrations
|
||||
}
|
||||
|
||||
func (m *MigrationManager) CurrentVersion(ctx context.Context) (string, error) {
|
||||
return m.resolver.Current(ctx)
|
||||
}
|
||||
|
||||
func (m *MigrationManager) LatestVersion() (string, error) {
|
||||
if len(m.migrations) == 0 {
|
||||
return "", errors.WithStack(ErrNoAvailableMigration)
|
||||
}
|
||||
|
||||
return m.migrations[len(m.migrations)-1].Version(), nil
|
||||
}
|
||||
|
||||
func (m *MigrationManager) IsLatest(ctx context.Context) (bool, error) {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "could not retrieve current version")
|
||||
}
|
||||
|
||||
latestVersion, err := m.LatestVersion()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "could not retrieve latest version")
|
||||
}
|
||||
|
||||
return currentVersion == latestVersion, nil
|
||||
}
|
||||
|
||||
func NewMigrationManager(resolver VersionResolver) *MigrationManager {
|
||||
return &MigrationManager{
|
||||
resolver: resolver,
|
||||
migrations: make([]Migration, 0),
|
||||
}
|
||||
}
|
49
internal/orm/provider.go
Normal file
49
internal/orm/provider.go
Normal file
@ -0,0 +1,49 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
|
||||
// Import postgres dialect
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
)
|
||||
|
||||
func ServiceProvider(dialect, dsn string, debug bool) service.Provider {
|
||||
db, err := gorm.Open(dialect, dsn)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not connect to database")
|
||||
}
|
||||
|
||||
var srv *Service
|
||||
|
||||
if err == nil {
|
||||
db = db.LogMode(debug)
|
||||
|
||||
versionResolver := NewDBVersionResolver(db)
|
||||
ctx := context.Background()
|
||||
|
||||
err := versionResolver.Init(ctx)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not initialize version resolver")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
srv = &Service{
|
||||
db: db,
|
||||
migration: NewMigrationManager(versionResolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
}
|
47
internal/orm/service.go
Normal file
47
internal/orm/service.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "orm"
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
migration *MigrationManager
|
||||
}
|
||||
|
||||
func (s *Service) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Service) Migration() *MigrationManager {
|
||||
return s.migration
|
||||
}
|
||||
|
||||
// From retrieves the orm service in the given container.
|
||||
func From(container *service.Container) (*Service, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Service)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the orm pool service in the given container or panic otherwise.
|
||||
func Must(container *service.Container) *Service {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
47
internal/orm/tx.go
Normal file
47
internal/orm/tx.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WithTx(ctx context.Context, db *gorm.DB, fn func(context.Context, *gorm.DB) error) error {
|
||||
tx := db.BeginTx(ctx, &sql.TxOptions{})
|
||||
|
||||
defer func() {
|
||||
if err := tx.Rollback().Error; err != nil && !isGormError(err, gorm.ErrInvalidTransaction) {
|
||||
panic(errors.Wrap(err, "could not rollback transaction"))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, tx); err != nil {
|
||||
err := errors.Wrap(err, "could not apply down migration")
|
||||
|
||||
if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
|
||||
return errors.Wrap(err, rollbackErr.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.Wrap(err, "could not commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isGormError(err error, compErr error) bool {
|
||||
if errs, ok := err.(gorm.Errors); ok {
|
||||
for _, err := range errs {
|
||||
if errors.Is(err, compErr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Is(err, compErr)
|
||||
}
|
112
internal/orm/version_resolver.go
Normal file
112
internal/orm/version_resolver.go
Normal file
@ -0,0 +1,112 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type VersionResolver interface {
|
||||
Current(context.Context) (string, error)
|
||||
Set(context.Context, string) error
|
||||
}
|
||||
|
||||
type DBVersionResolver struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type DatabaseVersion struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
Version string `gorm:"unique; not null"`
|
||||
MigratedAt time.Time
|
||||
IsCurrent bool
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Current(ctx context.Context) (string, error) {
|
||||
var version string
|
||||
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
dbVersion := &DatabaseVersion{}
|
||||
err := tx.Where("is_current = ?", true).First(dbVersion).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
version = dbVersion.Version
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Set(ctx context.Context, version string) error {
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
dbVersion := &DatabaseVersion{
|
||||
Version: version,
|
||||
MigratedAt: time.Now(),
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
if err := tx.FirstOrCreate(dbVersion).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := tx.Model(dbVersion).
|
||||
UpdateColumn("is_current", true).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.Model(&DatabaseVersion{}).
|
||||
Where("version <> ?", version).
|
||||
UpdateColumn("is_current", false).Error
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not update schema version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Init(ctx context.Context) error {
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&DatabaseVersion{}).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := tx.Model(&DatabaseVersion{}).AddUniqueIndex("idx_unique_version", "version").Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDBVersionResolver(db *gorm.DB) *DBVersionResolver {
|
||||
return &DBVersionResolver{
|
||||
db: db,
|
||||
}
|
||||
}
|
77
internal/route/login.go
Normal file
77
internal/route/login.go
Normal file
@ -0,0 +1,77 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
ctn := container.Must(r.Context())
|
||||
client := oidc.Must(ctn)
|
||||
client.Login(w, r)
|
||||
}
|
||||
|
||||
type emailClaims struct {
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
}
|
||||
|
||||
func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctn := container.Must(ctx)
|
||||
conf := config.Must(ctn)
|
||||
|
||||
idToken, err := oidc.IDToken(w, r)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve idToken", logger.E(err))
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info(ctx, "user logged in", logger.F("sub", idToken.Subject))
|
||||
|
||||
claims := &emailClaims{}
|
||||
if err := idToken.Claims(claims); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
// TODO implements better UX in case of errors
|
||||
|
||||
if claims.Email == "" {
|
||||
http.Error(w, "an email is expected to access this app", http.StatusForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !claims.EmailVerified {
|
||||
http.Error(w, "your email must be verified to access this app", http.StatusForbidden)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
db := orm.Must(ctn).DB()
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
if _, err := repo.CreateOrConnectUser(ctx, claims.Email); err != nil {
|
||||
panic(errors.Wrap(err, "could not upsert user"))
|
||||
}
|
||||
|
||||
if err := session.SaveUserEmail(w, r, claims.Email); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
http.Redirect(w, r, conf.HTTP.FrontendURL, http.StatusSeeOther)
|
||||
}
|
40
internal/route/logout.go
Normal file
40
internal/route/logout.go
Normal file
@ -0,0 +1,40 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func 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),
|
||||
)
|
||||
|
||||
if err := session.ClearUserEmail(w, r, false); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
70
internal/route/mount.go
Normal file
70
internal/route/mount.go
Normal file
@ -0,0 +1,70 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/rs/cors"
|
||||
"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(cors.New(cors.Options{
|
||||
AllowedOrigins: config.HTTP.CORS.AllowedOrigins,
|
||||
AllowCredentials: config.HTTP.CORS.AllowCredentials,
|
||||
Debug: config.Debug,
|
||||
}).Handler)
|
||||
r.Use(oidc.Middleware)
|
||||
r.Use(session.UserEmailMiddleware)
|
||||
|
||||
gql := handler.New(
|
||||
generated.NewExecutableSchema(generated.Config{
|
||||
Resolvers: &graph.Resolver{},
|
||||
}),
|
||||
)
|
||||
|
||||
gql.AddTransport(transport.POST{})
|
||||
gql.AddTransport(&transport.Websocket{
|
||||
KeepAlivePingInterval: 10 * time.Second,
|
||||
Upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// TODO Check WS connection origin
|
||||
return true
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
},
|
||||
})
|
||||
|
||||
if config.Debug {
|
||||
gql.Use(extension.Introspection{})
|
||||
r.Get("/v1/playground", playground.Handler("GraphQL playground", "/api/v1/graphql"))
|
||||
}
|
||||
|
||||
r.Handle("/v1/graphql", gql)
|
||||
})
|
||||
|
||||
notFoundHandler := r.NotFoundHandler()
|
||||
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", notFoundHandler))
|
||||
|
||||
return nil
|
||||
}
|
114
internal/session/user_email.go
Normal file
114
internal/session/user_email.go
Normal file
@ -0,0 +1,114 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const userEmailKey contextKey = "user_email"
|
||||
|
||||
var (
|
||||
ErrUserEmailNotFound = errors.New("user email not found")
|
||||
)
|
||||
|
||||
func UserEmailMiddleware(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
userEmail, err := GetUserEmail(w, r)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not find user email"))
|
||||
}
|
||||
|
||||
ctx := WithUserEmail(r.Context(), userEmail)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func WithUserEmail(ctx context.Context, email string) context.Context {
|
||||
return context.WithValue(ctx, userEmailKey, email)
|
||||
}
|
||||
|
||||
func UserEmail(ctx context.Context) (string, error) {
|
||||
email, ok := ctx.Value(userEmailKey).(string)
|
||||
if !ok {
|
||||
return "", errors.WithStack(ErrUserEmailNotFound)
|
||||
}
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func SaveUserEmail(w http.ResponseWriter, r *http.Request, email string) error {
|
||||
sess, err := getSession(w, r)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess.Set(string(userEmailKey), email)
|
||||
|
||||
if err := sess.Save(w, r); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearUserEmail(w http.ResponseWriter, r *http.Request, saveSession bool) error {
|
||||
sess, err := getSession(w, r)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess.Unset(string(userEmailKey))
|
||||
|
||||
if saveSession {
|
||||
if err := sess.Save(w, r); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
sess, err := getSession(w, r)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
email, ok := sess.Get(string(userEmailKey)).(string)
|
||||
if !ok {
|
||||
return "", errors.WithStack(ErrUserEmailNotFound)
|
||||
}
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func getSession(w http.ResponseWriter, r *http.Request) (session.Session, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
session, err := session.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess, err := session.Get(w, r)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
@ -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
|
@ -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"]
|
@ -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
|
15
misc/debian/nginx/daddy.conf
Normal file
15
misc/debian/nginx/daddy.conf
Normal file
@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name daddy.local;
|
||||
|
||||
root /usr/share/daddy/client/public;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
location /api/v1/graphql {
|
||||
proxy_pass http://127.0.0.1:8080/api/v1/graphql;
|
||||
}
|
||||
}
|
1
misc/debian/systemd/daddy.env
Normal file
1
misc/debian/systemd/daddy.env
Normal file
@ -0,0 +1 @@
|
||||
GO_ENV=prod
|
119
misc/script/release
Executable file
119
misc/script/release
Executable file
@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
OS_TARGETS=(linux)
|
||||
ARCH_TARGETS=${ARCH_TARGETS:-amd64 arm 386}
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
PROJECT_DIR="$DIR/../.."
|
||||
|
||||
function build {
|
||||
|
||||
local name=$1
|
||||
local srcdir=$2
|
||||
local os=$3
|
||||
local arch=$4
|
||||
|
||||
local dirname="$name-$os-$arch"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
rm -rf "$destdir"
|
||||
mkdir -p "$destdir"
|
||||
|
||||
echo "building $dirname..."
|
||||
|
||||
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
|
||||
-ldflags="-s -w -X main.GitCommit=$(current_commit_ref) -X main.ProjectVersion=$(current_version)" \
|
||||
-gcflags=-trimpath="${PWD}" \
|
||||
-asmflags=-trimpath="${PWD}" \
|
||||
-o "$destdir/bin/$name" \
|
||||
"$srcdir"
|
||||
|
||||
if [ ! -z "$(which upx)" ]; then
|
||||
upx --best "$destdir/bin/$name"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function current_commit_ref {
|
||||
git rev-list -1 HEAD
|
||||
}
|
||||
|
||||
function current_version {
|
||||
local latest_tag=$(git describe --abbrev=0 2>/dev/null)
|
||||
echo ${latest_tag:-0.0.0}
|
||||
}
|
||||
|
||||
function copy {
|
||||
|
||||
local name=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
local src=$4
|
||||
local dest=$5
|
||||
|
||||
local dirname="$name-$os-$arch"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
echo "copying '$src' to '$destdir/$dest'..."
|
||||
|
||||
mkdir -p "$(dirname $destdir/$dest)"
|
||||
|
||||
cp -rfL $src "$destdir/$dest"
|
||||
|
||||
}
|
||||
|
||||
function dump_default_conf {
|
||||
# Generate and copy configuration file
|
||||
local command=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
local tmp_conf=$(mktemp)
|
||||
|
||||
go run "$PROJECT_DIR/cmd/$command" -dump-config > "$tmp_conf"
|
||||
copy "$command" $os $arch "$tmp_conf" "$command.conf"
|
||||
rm -f "$tmp_conf"
|
||||
}
|
||||
|
||||
function compress {
|
||||
|
||||
local name=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
|
||||
local dirname="$name-$os-$arch"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
echo "compressing $dirname..."
|
||||
tar -czf "$destdir.tar.gz" -C "$destdir/../" "$dirname"
|
||||
}
|
||||
|
||||
function release_server {
|
||||
|
||||
local os=$1
|
||||
local arch=$2
|
||||
|
||||
build 'server' "$PROJECT_DIR/cmd/server" $os $arch
|
||||
|
||||
dump_default_conf 'server' $os $arch
|
||||
|
||||
copy 'server' $os $arch "$PROJECT_DIR/README.md" "README.md"
|
||||
copy 'server' $os $arch "$PROJECT_DIR/client/dist" "public"
|
||||
|
||||
compress 'server' $os $arch
|
||||
|
||||
}
|
||||
|
||||
function main {
|
||||
|
||||
make client-dist
|
||||
|
||||
for os in ${OS_TARGETS[@]}; do
|
||||
for arch in ${ARCH_TARGETS[@]}; do
|
||||
release_server $os $arch
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
main
|
24
modd.conf
Normal file
24
modd.conf
Normal file
@ -0,0 +1,24 @@
|
||||
internal/graph/*.graphql
|
||||
internal/gqlgen.yml {
|
||||
prep: make generate
|
||||
}
|
||||
|
||||
**/*.go
|
||||
!**/*_test.go
|
||||
data/config.yml
|
||||
.env
|
||||
modd.conf {
|
||||
prep: make build
|
||||
prep: [ -e data/config.yml ] || ( mkdir -p data && bin/server -dump-config > data/config.yml )
|
||||
prep: [ -e .env ] || ( cp .env.dist .env )
|
||||
prep: make migrate-latest
|
||||
daemon: ( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml )
|
||||
}
|
||||
|
||||
**/*.go {
|
||||
prep: make test
|
||||
}
|
||||
|
||||
{
|
||||
daemon: cd client && NODE_ENV=development npm run server -- --display=minimal
|
||||
}
|
Reference in New Issue
Block a user