Compare commits

...

42 Commits

Author SHA1 Message Date
4ee3de773c Possibilité de configurer les options du cookie 2020-10-13 14:59:25 +02:00
d4ca478b44 Mise à jour goweb-oidc 2020-10-13 14:56:28 +02:00
d10ce7c7ad Correction affichage profil 2020-10-13 14:27:40 +02:00
655ecd1a0f Mise en forme basique du loader de module 2020-10-13 13:47:29 +02:00
70fe86a9a5 Mise à jour goweb-oidc 2020-10-13 13:21:29 +02:00
17b44170d0 Redémarrage du serveur webpack lors d'une modification de la configuration 2020-10-13 12:09:04 +02:00
f2d6a72204 Correction chargement dynamique 2020-10-13 12:08:43 +02:00
f752865d33 Découpage du code et extraction des sourcemaps 2020-10-13 11:37:37 +02:00
85008d3265 Mise à jour goweb-oidc 2020-10-13 11:07:26 +02:00
f34b7e4439 Affichage multilignes des colonnes 2020-10-13 11:04:49 +02:00
b36ae791cb Persistence de l'UUID de conférence dans le localStorage 2020-10-13 11:04:32 +02:00
50ec72fcf4 Heartbeat de présence configurable dans la salle de conférence 2020-10-13 09:38:09 +02:00
0b93b0875e Conference: correction détection déconnexion 2020-10-12 21:56:19 +02:00
6f757002b1 Merge branch 'feature/conference-room' of Cadoles/daddy into develop 2020-10-12 21:14:32 +02:00
18dc4135c4 Salle de conférence expérimentale 2020-10-12 21:13:28 +02:00
54e8cf23f7 Merge branch 'feature/dsf-tab-deep-linking' of Cadoles/daddy into develop 2020-10-12 15:58:54 +02:00
5649cd2aad Gestion des liens profonds sur les tabs dans la page DAD 2020-10-12 15:58:19 +02:00
f032e83e71 Affichage des dossiers associés dans la page groupe de travail 2020-10-12 14:56:22 +02:00
19f0c8e0a4 Merge branch 'feature/sentry' of Cadoles/daddy into develop 2020-10-12 14:35:43 +02:00
d0cd9842ea Intégration de Sentry 2020-10-12 14:35:02 +02:00
27458b5b94 Merge branch 'feature/vote-report' of Cadoles/daddy into develop 2020-10-12 13:29:27 +02:00
e5152aa652 Ajout des dossiers votés dans la newsletter 2020-10-12 13:11:57 +02:00
1eaaa9065f Ajout d'un champ de rapport basique pour décrire la prise de décision
- Enregistrement et prise en compte dans l'affichage des évènements de
  vote/clotûre d'un DAD
2020-10-12 12:44:30 +02:00
7d0831ee57 Merge branch 'feature/unauthorized-page' of Cadoles/daddy into develop 2020-10-12 10:06:02 +02:00
0859202987 Ajout d'une page 'Non autorisée' et redirection automatique vers celle ci en cas d'accès via un compte non autorisé 2020-10-12 10:05:04 +02:00
7a6eedab9d Forcer l'utilisation du réseau pour les requêtes d'autorisation 2020-10-05 17:03:01 +02:00
89a147565c Correction DOM invalide 2020-10-05 16:37:34 +02:00
9b8adafe60 Ajout du suivi des opérations spécifiques dans les vues DAD et GdT 2020-10-05 16:04:10 +02:00
a3fa793706 Correction mise à forme de la timeline 2020-10-05 15:50:01 +02:00
27720219ee Correction détection de la session 2020-10-05 15:49:32 +02:00
f4528dd087 Correction affichage nom utilisateur dans la newsletter 2020-10-05 15:49:09 +02:00
fb954a3e5b Réorganisation visuelle du tableau de bord 2020-10-05 15:18:35 +02:00
92efdbd568 Merge branch 'feature/newsletter' of Cadoles/daddy into develop 2020-10-05 14:19:04 +02:00
137709adea Ajout d'une newsletter basique
La newsletter effectue une collecte des évènements sur une période de
temps donné et envoi un récapitulatif à l'ensemble des utilisateurs de
Daddy.

Actuellement, sont collectés et présentés:

- Les créations de groupes de travail
- Les créations de dossiers d'aide à la décision
- Les dossiers dont le statut à été modifié et prêt à voté
2020-10-05 14:16:25 +02:00
6cdbea92d1 Merge branch 'feature/events' of Cadoles/daddy into develop 2020-10-02 16:39:19 +02:00
f169169bc7 Enregistrement et affichage d'un flux d'évènements
- Ajout d'une nouvelle entité "Event"
- Affichage d'une "timeline" sur le tableau de bord
- Création semi-automatique des évènements lors des modifications par
  les utilisateurs
2020-10-02 16:37:24 +02:00
61eacefd6c Formatage des dates dans l'interface 2020-10-01 11:44:29 +02:00
11f54ab66e Utilisation de la stratégie de cache 'cache puis réseau' 2020-09-10 19:28:08 +02:00
772b09381c Ajout label 'Déconnexion' 2020-09-10 19:27:43 +02:00
978cc65c41 Dissimulation du bouton enregistrer en mode lecture seule 2020-09-10 19:27:17 +02:00
596108b4f4 Panel DADs: ajout onglets 'à voter' et 'votés' 2020-09-10 19:26:41 +02:00
6845e1ce50 Ajout types génériques pour autocomplétion 2020-09-10 19:25:52 +02:00
81 changed files with 2900 additions and 402 deletions

132
client/package-lock.json generated
View File

@ -1125,9 +1125,9 @@
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@fortawesome/fontawesome-free": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz",
"integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.0.tgz",
"integrity": "sha512-wXetjQBNMTP59MAYNR1tdahMDOLx3FYj3PKdso7PLFLDpTvmAIqhSSEqnSTmWKahRjD+Sh5I5635+5qaoib5lw==",
"dev": true
},
"@nodelib/fs.scandir": {
@ -3197,6 +3197,11 @@
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
"integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
},
"bulma-timeline": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/bulma-timeline/-/bulma-timeline-3.0.4.tgz",
"integrity": "sha512-gCUOcSUuzHoeVMkCpLF49j5Z5yl78XQ+KgJcT+1ju5WIGgBgVytRUob/dw5NHAxPLO2rmcvwYNbCJFp7w4WT4Q=="
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@ -4030,6 +4035,11 @@
"randomfill": "^1.0.3"
}
},
"crypto-js": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz",
"integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg=="
},
"css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
@ -4484,9 +4494,9 @@
"dev": true
},
"elliptic": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
@ -5414,6 +5424,11 @@
"integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
"dev": true
},
"get-browser-rtc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz",
"integrity": "sha1-u81AyEUaftTvXDc7gWmkCd0dEdk="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -6050,8 +6065,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "1.3.5",
@ -6371,6 +6385,11 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"isomorphic.js": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.5.tgz",
"integrity": "sha512-MkX5lLQApx/8IAIU31PKvpAZosnu2Jqcj1rM8TzxyA4CR96tv3SgMKQNTCxL58G7696Q57zd7ubHV/hTg+5fNA=="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -6491,6 +6510,14 @@
"leven": "^3.1.0"
}
},
"lib0": {
"version": "0.2.34",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.34.tgz",
"integrity": "sha512-cqsVIMPgFlDtgQcpkt7HOY6W3sbYPIe3qxMnbRSwHTgiQancgm+TRDPx28mC6GUZ6lG6Nr0bIWf4Nog6dWUNUg==",
"requires": {
"isomorphic.js": "^0.1.3"
}
},
"load-json-file": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@ -6551,9 +6578,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"loglevel": {
"version": "1.6.8",
@ -7052,9 +7079,9 @@
}
},
"node-forge": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
"dev": true
},
"node-gyp": {
@ -8001,11 +8028,15 @@
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
"dev": true
},
"queue-microtask": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.1.4.tgz",
"integrity": "sha512-eY/4Obve9cE5FK8YvC1cJsm5cr7XvAurul8UtBDJ2PR1p5NmAwHtvAt5ftcLtwYRCUKNhxCneZZlxmUDFoSeKA=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.0"
}
@ -8745,12 +8776,12 @@
"dev": true
},
"selfsigned": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
"dev": true,
"requires": {
"node-forge": "0.9.0"
"node-forge": "^0.10.0"
}
},
"semver": {
@ -8963,6 +8994,30 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
"simple-peer": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.7.2.tgz",
"integrity": "sha512-xeMyxa9B4V0eA6mf17fVr8nm2QhAYFu+ZZv8zkSFFTjJETGF227CshwobrIYZuspJglMD63egcevQXGOrTIsuA==",
"requires": {
"debug": "^4.0.1",
"get-browser-rtc": "^1.0.0",
"queue-microtask": "^1.1.0",
"randombytes": "^2.0.3",
"readable-stream": "^3.4.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
@ -9392,7 +9447,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@ -10109,8 +10163,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util.promisify": {
"version": "1.0.0",
@ -10806,6 +10859,33 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
},
"y-protocols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.1.tgz",
"integrity": "sha512-QP3fCM7c2gGfUi2nqf8gspyO4VW23zv3kNqPNdD3wNxMbuNQenMyoDVZYEo12jzR4RQ3aaDfPK62Sf31SVOmfg==",
"requires": {
"lib0": "^0.2.28"
}
},
"y-webrtc": {
"version": "10.1.6",
"resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.1.6.tgz",
"integrity": "sha512-b3pTIv9LcPuMb4nbDT3/kkgmcuQoTrBmaPbBqPH1LJMzI8HwYnMK8p5r0fBQJBI0YRor+i8BT15Evv1nQBP0zg==",
"requires": {
"lib0": "^0.2.32",
"simple-peer": "^9.7.2",
"ws": "^7.2.0",
"y-protocols": "^1.0.0"
},
"dependencies": {
"ws": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
"optional": true
}
}
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
@ -10931,6 +11011,14 @@
}
}
},
"yjs": {
"version": "13.4.1",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.4.1.tgz",
"integrity": "sha512-kIh0sprCTzIm2qyr1VsovkvjKzD2GR4WcU/McJpLAEvImCJHA78Q3S6uSLnhZX0i7FQdrLPCRT8DtTPEH73jnw==",
"requires": {
"lib0": "^0.2.33"
}
},
"zen-observable": {
"version": "0.8.15",
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",

View File

@ -25,7 +25,7 @@
"@babel/plugin-transform-runtime": "^7.7.4",
"@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.4",
"@fortawesome/fontawesome-free": "^5.11.2",
"@fortawesome/fontawesome-free": "^5.14.0",
"@types/node": "^13.13.4",
"@types/react-dom": "^16.9.7",
"@types/react-redux": "^7.1.7",
@ -55,6 +55,8 @@
"@types/qs": "^6.9.3",
"bs58": "^4.0.1",
"bulma": "^0.9.0",
"bulma-timeline": "^3.0.4",
"crypto-js": "^4.0.0",
"graphql": "^15.3.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
@ -65,6 +67,8 @@
"redux-saga": "^1.1.3",
"styled-components": "^4.4.1",
"subscriptions-transport-ws": "^0.9.17",
"typescript": "^3.8.3"
"typescript": "^3.8.3",
"y-webrtc": "^10.1.6",
"yjs": "^13.4.1"
}
}

View File

@ -1,10 +1,5 @@
import React, { FunctionComponent, useState, useEffect } from 'react';
import React, { FunctionComponent, useState, useEffect, Suspense } from 'react';
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
import { HomePage } from './HomePage/HomePage';
import { ProfilePage } from './ProfilePage/ProfilePage';
import { WorkgroupPage } from './WorkgroupPage/WorkgroupPage';
import { DecisionSupportFilePage } from './DecisionSupportFilePage/DecisionSupportFilePage';
import { DashboardPage } from './DashboardPage/DashboardPage';
import { useUserProfile } from '../gql/queries/profile';
import { LoggedInContext, getSavedLoggedIn, saveLoggedIn } from '../hooks/useLoggedIn';
import { PrivateRoute } from './PrivateRoute';
@ -12,14 +7,21 @@ import { useKonamiCode } from '../hooks/useKonamiCode';
import { Modal } from './Modal';
import { createClient } from '../util/apollo';
import { ApolloProvider } from '@apollo/client';
import { LogoutPage } from './LogoutPage';
import { AppLoader } from './AppLoader';
const LazyHomePage = React.lazy(() => import(/* webpackChunkName: "HomePage" */'./HomePage/HomePage'));
const LazyDashboardPage = React.lazy(() => import(/* webpackChunkName: "DashboardPage" */'./DashboardPage/DashboardPage'));
const LazyUnauthorizedPage = React.lazy(() => import(/* webpackChunkName: "UnauthorizedPage" */'./UnauthorizedPage/UnauthorizedPage'));
const LazyConferencePage = React.lazy(() => import(/* webpackChunkName: "ConferencePage" */'./ConferencePage/ConferencePage'));
const LazyDecisionSupportFilePage = React.lazy(() => import(/* webpackChunkName: "DecisionSupportFilePage" */'./DecisionSupportFilePage/DecisionSupportFilePage'));
const LazyProfilePage = React.lazy(() => import(/* webpackChunkName: "ProfilePage" */'./ProfilePage/ProfilePage'));
const LazyWorkgroupPage = React.lazy(() => import(/* webpackChunkName: "WorkgroupPage" */'./WorkgroupPage/WorkgroupPage'));
const LazyLogoutPage = React.lazy(() => import(/* webpackChunkName: "LogoutPage" */'./LogoutPage'));
export interface AppProps {
}
export const App: FunctionComponent<AppProps> = () => {
const [ loggedIn, setLoggedIn ] = useState(getSavedLoggedIn());
@ -35,33 +37,37 @@ export const App: FunctionComponent<AppProps> = () => {
useKonamiCode(() => setShowBoneyM(true));
return (
<ApolloProvider client={client}>
<LoggedInContext.Provider value={loggedIn}>
<UserSessionCheck setLoggedIn={setLoggedIn} />
<BrowserRouter>
<Switch>
<Route path="/" exact component={HomePage} />
<PrivateRoute path="/profile" exact component={ProfilePage} />
<PrivateRoute path="/workgroups/:id" exact component={WorkgroupPage} />
<PrivateRoute path="/decisions/:id" exact component={DecisionSupportFilePage} />
<PrivateRoute path="/dashboard" exact component={DashboardPage} />
<PrivateRoute path="/logout" exact component={LogoutPage} />
<Route component={() => <Redirect to="/" />} />
</Switch>
</BrowserRouter>
{
showBoneyM ?
<Modal active={true} showCloseButton={true} onClose={() => setShowBoneyM(false)}>
<iframe width={560} height={315}
frameBorder={0}
allowFullScreen={true}
src="https://www.youtube.com/embed/uVzT5QEEQ2c?autoplay=1" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture">
</iframe>
</Modal> :
null
}
</LoggedInContext.Provider>
</ApolloProvider>
<Suspense fallback={<AppLoader />}>
<ApolloProvider client={client}>
<LoggedInContext.Provider value={loggedIn}>
<UserSessionCheck setLoggedIn={setLoggedIn} />
<BrowserRouter>
<Switch>
<Route path="/" exact component={LazyHomePage} />
<Route path="/unauthorized" exact component={LazyUnauthorizedPage} />
<PrivateRoute path="/profile" exact component={LazyProfilePage} />
<PrivateRoute path="/conference" exact component={LazyConferencePage} />
<PrivateRoute path="/workgroups/:id" exact component={LazyWorkgroupPage} />
<PrivateRoute path="/decisions/:id" component={LazyDecisionSupportFilePage} />
<PrivateRoute path="/dashboard" exact component={LazyDashboardPage} />
<PrivateRoute path="/logout" exact component={LazyLogoutPage} />
<Route component={() => <Redirect to="/" />} />
</Switch>
</BrowserRouter>
{
showBoneyM ?
<Modal active={true} showCloseButton={true} onClose={() => setShowBoneyM(false)}>
<iframe width={560} height={315}
frameBorder={0}
allowFullScreen={true}
src="https://www.youtube.com/embed/uVzT5QEEQ2c?autoplay=1" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture">
</iframe>
</Modal> :
null
}
</LoggedInContext.Provider>
</ApolloProvider>
</Suspense>
);
}
@ -74,7 +80,7 @@ const UserSessionCheck: FunctionComponent<UserSessionCheckProps> = ({ setLoggedI
useEffect(() => {
if (loading) return;
setLoggedIn(user.id !== '');
setLoggedIn(user && user.id !== '');
}, [user]);
return null;

View File

@ -0,0 +1,9 @@
import React, { FunctionComponent } from "react";
export const AppLoader:FunctionComponent = () => {
return (
<div className="app-loader">
<i className="fas fa-spinner fa-spin fa-5x"></i>
</div>
)
}

View File

@ -0,0 +1,142 @@
import React, { FunctionComponent, useEffect } from 'react';
import { Config } from '../../config';
import { useUserProfile } from '../../gql/queries/profile';
import { useConference } from '../../hooks/useConference';
import { Page } from '../Page';
import { Gravatar } from './Gravatar';
export interface ConferencePageProps {
}
const StatusHandRaised = 'hand-raised';
const StatusThumbsUp = 'thumbs-up';
const StatusThumbsDown = 'thumbs-down';
const StatusNoVote = 'no-vote';
export const ConferencePage:FunctionComponent<ConferencePageProps> = () => {
const { user } = useUserProfile();
const { uuid, data, setNickname, setEmail, ping, setStatus, forget } = useConference();
const currentStatus = data.statuses[uuid];
useEffect(() => {
if (!user.name && !user.email) return;
setNickname(user.name || user.email.split('@')[0]);
setEmail(user.email);
}, [user.name, user.email]);
useEffect(() => {
ping();
const intervalId = setInterval(() => ping(), Config.conferenceHeartbeatInterval + (Math.random() * Config.conferenceHeartbeatInterval/2));
return () => clearInterval(intervalId);
}, []);
const onStatusChange = (status: string) => {
setStatus(currentStatus === status ? '' : status);
};
return (
<Page title="Conference">
<div className="container is-fluid">
<section className="mt-5">
<h3 className="is-size-3">Mes actions</h3>
<div className="buttons has-addons">
<button
className={`button is-medium ${currentStatus === StatusHandRaised ? 'is-info is-selected' : ''}`}
onClick={onStatusChange.bind(null, StatusHandRaised)}>
<span className="icon">
<i className="fa fa-hand-paper"></i>
</span>
<span>Lever la main</span>
</button>
<button
className={`button is-medium ${currentStatus === StatusThumbsUp ? 'is-success is-selected' : ''}`}
onClick={onStatusChange.bind(null, StatusThumbsUp)}>
<span className="icon">
<i className="fa fa-thumbs-up"></i>
</span>
<span>Voter pour</span>
</button>
<button
className={`button is-medium ${currentStatus === StatusNoVote ? 'is-warning is-selected' : ''}`}
onClick={onStatusChange.bind(null, StatusNoVote)}>
<span className="icon">
<i className="fa fa-mitten"></i>
</span>
<span>Ne se prononce pas</span>
</button>
<button
className={`button is-medium ${currentStatus === StatusThumbsDown ? 'is-danger is-selected' : ''}`}
onClick={onStatusChange.bind(null, StatusThumbsDown)}>
<span className="icon">
<i className="fa fa-thumbs-down"></i>
</span>
<span>Voter contre</span>
</button>
</div>
<h3 className="is-size-3">Assemblée</h3>
<div className="columns mt-1 is-multiline">
<UserCard className="column is-narrow"
nickname={data.nicknames[uuid]}
status={currentStatus}
email={user.email} />
{
Object.keys(data.peers).map(p => {
const now = new Date();
const lastHeartBeat = new Date(data.peers[p]);
if (p === uuid) return null;
if (now.getTime() > lastHeartBeat.getTime() + Config.conferenceHeartbeatInterval*2) {
forget(p);
return null;
}
const nickname = data.nicknames[p] || '???';
const email = data.emails[p] || '';
return (
<UserCard key={`peer-${p}`} className="column is-narrow"
nickname={nickname}
status={data.statuses[p]}
email={email} />
)
})
}
</div>
</section>
</div>
</Page>
);
}
export default ConferencePage;
export interface UserCardProps {
nickname: string
email: string
className?: string
status: string
};
export const UserCard:FunctionComponent<UserCardProps> = ({ nickname, email, className, status }) => {
return (
<div className={className}>
<div className="box">
<div className="has-text-centered">
<div className="mb-1">
{ !status ? <span className="icon"><i className="far fa-2x fa-meh-blank"></i></span> : null }
{ status === StatusHandRaised ? <span className="icon has-text-info"><i className="fa fa-2x fa-hand-paper"></i></span> : null }
{ status === StatusThumbsUp ? <span className="icon has-text-success"><i className="fa fa-2x fa-thumbs-up"></i></span> : null }
{ status === StatusNoVote ? <span className="icon has-text-warning"><i className="fa fa-2x fa-mitten"></i></span> : null }
{ status === StatusThumbsDown ? <span className="icon has-text-danger"><i className="fa fa-2x fa-thumbs-down"></i></span> : null }
</div>
<figure className="image is-128x128 is-inline-block">
<Gravatar className="is-rounded" email={email} />
</figure>
<h4 className="is-size-4">{nickname}</h4>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,20 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import md5 from 'crypto-js/md5';
export interface GravatarProps {
className?: string
email: string
}
const defaultAvatarUrl = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&s=128';
export const Gravatar:FunctionComponent<GravatarProps> = ({ className, email }) => {
const [ avatarUrl, setAvatarUrl ] = useState(defaultAvatarUrl);
useEffect(() => {
const hash = md5(email.trim().toLowerCase());
setAvatarUrl(`https://www.gravatar.com/avatar/${hash}?d=mp&s=128`);
}, [email]);
return (
<img className={className} src={avatarUrl} />
);
}

View File

@ -1,27 +1,46 @@
import React from 'react';
import { WorkgroupsPanel } from './WorkgroupsPanel';
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
import { Timeline } from '../Timeline';
import { useEvents } from '../../gql/queries/event';
const from = new Date();
from.setDate(from.getDate() - 7);
export function Dashboard() {
const { events } = useEvents({
variables: {
filter: {
from,
}
}
});
return (
<div className="columns">
<div className="column is-6">
<div className="column is-4">
<DecisionSupportFilePanel />
</div>
<div className="column is-3">
<div className="column is-4">
<WorkgroupsPanel />
</div>
<div className="column is-3">
<div className="box">
<div className="level">
<div className="column is-4">
<div className="panel is-info">
<div className="level panel-heading mb-0">
<div className="level-left">
<h3 className="is-size-3 subtitle level-item">Assemblées</h3>
<div className="level-item">
Ces 7 derniers jours
</div>
</div>
<div className="level-right">
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
<button disabled={true} className="button level-item is-outlined is-info is-inverted">
<i className="icon fa fa-sliders-h"></i>
</button>
</div>
</div>
<pre>TODO</pre>
<div className="panel-block">
<Timeline events={events} />
</div>
</div>
</div>
</div>

View File

@ -12,4 +12,6 @@ export function DashboardPage() {
</div>
</Page>
);
}
}
export default DashboardPage;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision';
import { ItemPanel, TabDefinition, Item } from './ItemPanel';
import { ItemPanel, TabDefinition, Item } from '../ItemPanel';
import { useUserProfile } from '../../gql/queries/profile';
import { inWorkgroup } from '../../types/workgroup';
import { useDecisionSupportFiles } from '../../gql/queries/dsf';
@ -21,6 +21,14 @@ export function DecisionSupportFilePanel() {
label: 'Brouillons',
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Draft
},
{
label: 'À voter',
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Ready
},
{
label: 'Votés',
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Voted
},
{
label: 'Clos',
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Closed

View File

@ -2,7 +2,7 @@ import React, { } from 'react';
import { Workgroup, inWorkgroup } from '../../types/workgroup';
import { useWorkgroups } from '../../gql/queries/workgroups';
import { useUserProfile } from '../../gql/queries/profile';
import { ItemPanel, Item } from './ItemPanel';
import { ItemPanel, Item } from '../ItemPanel';
export function WorkgroupsPanel() {
const { workgroups } = useWorkgroups();

View File

@ -0,0 +1,25 @@
import React, { FunctionComponent } from "react";
import { Link } from "react-router-dom";
import { useWorkgroups } from "../gql/queries/workgroups";
import { useDecisionSupportFiles } from "../gql/queries/dsf";
export interface DecisioSupportFileLinkProps {
decisionSupportFileId: number|string
}
export const DecisionSupportFileLink: FunctionComponent<DecisioSupportFileLinkProps> = ({ decisionSupportFileId }) => {
const { decisionSupportFiles } = useDecisionSupportFiles({
fetchPolicy: "cache-first",
variables: {
filter: {
ids: [decisionSupportFileId]
}
}
});
const title = decisionSupportFiles.length > 0 ? decisionSupportFiles[0].title : `#${decisionSupportFileId}`;
return (
<Link to={`/decisions/${decisionSupportFileId}`}>{title}</Link>
);
};

View File

@ -49,90 +49,88 @@ export const ClarificationSection: FunctionComponent<ClarificationSectionProps>
return (
<section>
<div className="box">
<div className="field">
<label className="label is-medium">Intitulé du dossier</label>
<div className="control">
<input className="input is-medium" type="text" readOnly={readOnly} value={dsf.title} onChange={onTitleChange} />
<div className="field">
<label className="label is-medium">Intitulé du dossier</label>
<div className="control">
<input className="input is-medium" type="text" readOnly={readOnly} value={dsf.title} onChange={onTitleChange} />
</div>
</div>
<div className="field">
<label className="label is-medium">Quelle décision devons nous prendre ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.objectives}
onChange={onSectionAttrChange.bind(null, 'objectives')}
placeholder="Décrire globalement les tenants et aboutissants de la décision à prendre."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Ne pas essayer de rentrer trop dans les détails ici. Préférer l'utilisation des annexes et y faire référence.</p>
</div>
<div className="field">
<label className="label is-medium">Pourquoi devons nous prendre cette décision ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.motivations}
onChange={onSectionAttrChange.bind(null, 'motivations')}
placeholder="Décrire pourquoi il est important de prendre cette décision."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer si des obligations légales pèsent sur cette prise de décision.</p>
</div>
<div className="field">
<label className="label is-medium">Portée de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'scope')}
value={state.section.scope}>
<option></option>
<option value="individual">Individuelle</option>
<option value="identified-group">Groupe identifié</option>
<option value="collective">Collective</option>
</select>
</div>
</div>
<div className="field">
<label className="label is-medium">Quelle décision devons nous prendre ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.objectives}
onChange={onSectionAttrChange.bind(null, 'objectives')}
placeholder="Décrire globalement les tenants et aboutissants de la décision à prendre."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Ne pas essayer de rentrer trop dans les détails ici. Préférer l'utilisation des annexes et y faire référence.</p>
</div>
<div className="field">
<label className="label is-medium">Pourquoi devons nous prendre cette décision ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.motivations}
onChange={onSectionAttrChange.bind(null, 'motivations')}
placeholder="Décrire pourquoi il est important de prendre cette décision."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer si des obligations légales pèsent sur cette prise de décision.</p>
</div>
<div className="field">
<label className="label is-medium">Portée de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'scope')}
value={state.section.scope}>
<option></option>
<option value="individual">Individuelle</option>
<option value="identified-group">Groupe identifié</option>
<option value="collective">Collective</option>
</select>
</div>
</div>
<div className="field">
<label className="label is-medium">Nature de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'nature')}
value={state.section.nature}>
<option></option>
<option value="operational">Opérationnelle</option>
<option value="tactic">Tactique</option>
<option value="strategic">Stratégique</option>
</select>
</div>
</div>
<div className="field">
<label className="label is-medium">Nature de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'nature')}
value={state.section.nature}>
<option></option>
<option value="operational">Opérationnelle</option>
<option value="tactic">Tactique</option>
<option value="strategic">Stratégique</option>
</select>
</div>
</div>
</div>
<div className="columns">
<div className="column">
<label className="checkbox">
<input type="checkbox"
className="is-medium"
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'hasDeadline')}
checked={state.section.hasDeadline} />
<span className="ml-1 has-text-weight-bold is-size-5">Existe t'il une échéance particulière pour cette décision ?</span>
</label>
<div className="field">
<div className="control">
<input disabled={!state.section.hasDeadline}
readOnly={readOnly}
value={state.section.deadline ? asDate(state.section.deadline).toISOString().substr(0, 10) : ''}
onChange={onDeadlineChange}
type="date" className="input is-medium" />
</div>
</div>
<div className="columns">
<div className="column">
<label className="checkbox">
<input type="checkbox"
className="is-medium"
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'hasDeadline')}
checked={state.section.hasDeadline} />
<span className="ml-1 has-text-weight-bold is-size-5">Existe t'il une échéance particulière pour cette décision ?</span>
</label>
<div className="field">
<div className="control">
<input disabled={!state.section.hasDeadline}
readOnly={readOnly}
value={state.section.deadline ? asDate(state.section.deadline).toISOString().substr(0, 10) : ''}
onChange={onDeadlineChange}
type="date" className="input is-medium" />
</div>
</div>
</div>

View File

@ -0,0 +1,49 @@
import React, { FunctionComponent, useState, ChangeEvent, useEffect } from 'react';
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
export interface DecisionReportSectionProps extends DecisionSupportFileUpdaterProps {};
const DecisionReportSectionName = 'decision-report';
export const DecisionReportSection: FunctionComponent<DecisionReportSectionProps> = ({ dsf, updateDSF, readOnly }) => {
const [ state, setState ] = useState({
changed: false,
section: {
report: "",
}
});
useEffect(() => {
if (!state.changed) return;
updateDSF({ ...dsf, sections: { ...dsf.sections, [DecisionReportSectionName]: { ...state.section }} })
setState(state => ({ ...state, changed: false }));
}, [state.changed]);
useEffect(() => {
if (!dsf.sections[DecisionReportSectionName]) return;
setState(state => ({ ...state, changed: false, section: {...state.section, ...dsf.sections[DecisionReportSectionName] }}));
}, [dsf.sections[DecisionReportSectionName]]);
const onSectionAttrChange = (attrName: string, evt: ChangeEvent<HTMLInputElement>) => {
const target = evt.currentTarget;
const value = target.hasOwnProperty('checked') ? target.checked : target.value;
setState(state => ({ ...state, changed: true, section: {...state.section, [attrName]: value }}));
};
return (
<section>
<div className="field">
<label className="label is-medium">Compte rendu du vote</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.report}
onChange={onSectionAttrChange.bind(null, 'report')}
rows={20}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer le résultat du vote et les éléments de contexte liés à la prise de décision.</p>
</div>
</section>
);
};

View File

@ -9,29 +9,30 @@ import { useDecisionSupportFiles } from '../../gql/queries/dsf';
import { useCreateDecisionSupportFileMutation, useUpdateDecisionSupportFileMutation } from '../../gql/mutations/dsf';
import { OptionsSection } from './OptionsSection';
import { useIsAuthorized } from '../../gql/queries/authorization';
import { TimelinePanel } from './TimelinePanel';
import { DecisionReportSection } from './DecisionReportSection';
import { RoutedTabs } from '../RoutedTabs';
export interface DecisionSupportFilePageProps {
};
export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageProps> = () => {
const { id } = useParams();
const { id } = useParams<any>();
const history = useHistory();
const { decisionSupportFiles } = useDecisionSupportFiles({
variables:{
filter: {
ids: id !== 'new' ? [id] : undefined,
}
}
},
});
const [ state, setState ] = useState({
dsf: newDecisionSupportFile(),
saved: true,
selectedTabIndex: 0,
});
const { isAuthorized } = useIsAuthorized({
variables: {
action: 'update',
@ -46,9 +47,26 @@ export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageP
setState(state => ({ ...state, dsf: { ...state.dsf, ...dsf }}))
}, [ decisionSupportFiles ]);
const selectTab = (tabIndex: number) => {
setState(state => ({ ...state, selectedTabIndex: tabIndex }));
};
const tabs = [
{
name: "Clarifier la proposition",
icon: "fas fa-pen",
route: '/info',
render: () => (<ClarificationSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
},
{
name: "Explorer les options",
icon: "fas fa-search",
route: '/options',
render: () => (<OptionsSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
},
{
name: "Prendre la décision",
icon: "fas fa-person-booth",
route: '/vote',
render: () => (<DecisionReportSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
}
];
const updateDSF = (dsf: DecisionSupportFile) => {
setState(state => {
@ -114,58 +132,33 @@ export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageP
</div>
<div className="level-right">
<div className="level-item buttons">
<button className="button is-medium is-success" disabled={!canSave} onClick={saveDSF}>
<span className="icon"><i className="fa fa-save"></i></span>
<span>Enregistrer</span>
</button>
{
isAuthorized ?
<button className="button is-medium is-success" disabled={!canSave} onClick={saveDSF}>
<span className="icon"><i className="fa fa-save"></i></span>
<span>Enregistrer</span>
</button> :
null
}
</div>
</div>
</div>
</section>
<div className="columns mt-3">
<div className="column is-9">
<div className="tabs is-medium is-toggle">
<ul>
<li className={`has-background-white ${state.selectedTabIndex === 0 ? 'is-active': ''}`}
onClick={selectTab.bind(null, 0)}>
<a>
<span className="icon is-small"><i className="fas fa-pen" aria-hidden="true"></i></span>
<span>Clarifier la proposition</span>
</a>
</li>
<li className={`has-background-white ${state.selectedTabIndex === 1 ? 'is-active': ''}`}
onClick={selectTab.bind(null, 1)}>
<a>
<span className="icon is-small"><i className="fas fa-search" aria-hidden="true"></i></span>
<span>Explorer les options</span>
</a>
</li>
<li className={`has-background-white ${state.selectedTabIndex === 2 ? 'is-active': ''}`}
onClick={selectTab.bind(null, 2)}>
<a>
<span className="icon is-small"><i className="fas fa-person-booth" aria-hidden="true"></i></span>
<span>Prendre la décision</span>
</a>
</li>
</ul>
<div className="column is-8">
<div className="box">
<RoutedTabs baseRoute={`/decisions/${id}`} tabs={tabs} />
</div>
{
state.selectedTabIndex === 0 ?
<ClarificationSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} /> :
null
}
{
state.selectedTabIndex === 1 ?
<OptionsSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} /> :
null
}
</div>
<div className="column is-3">
<div className="column is-4">
<MetadataPanel readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />
<AppendixPanel dsf={state.dsf} />
{/* <AppendixPanel dsf={state.dsf} /> */}
<TimelinePanel dsf={state.dsf} />
</div>
</div>
</div>
</Page>
);
};
};
export default DecisionSupportFilePage;

View File

@ -4,7 +4,7 @@ import { useWorkgroups } from '../../gql/queries/workgroups';
import { useUserProfile } from '../../gql/queries/profile';
import { inWorkgroup } from '../../types/workgroup';
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
import { asDate } from '../../util/date';
import { asDate, formatDate } from '../../util/date';
import { Link } from 'react-router-dom';
export interface MetadataPanelProps extends DecisionSupportFileUpdaterProps {};
@ -81,13 +81,13 @@ export const MetadataPanel: FunctionComponent<MetadataPanelProps> = ({ dsf, upda
<div className="field">
<div className="label">Créé le</div>
<div className="control">
<p>{asDate(dsf.createdAt).toISOString()}</p>
<p>{formatDate(dsf.createdAt)}</p>
</div>
</div>
<div className="field">
<div className="label">Voté le</div>
<div className="control">
<p>{dsf.votedAt ? dsf.votedAt : '--'}</p>
<p>{dsf.votedAt ? formatDate(dsf.votedAt) : '--'}</p>
</div>
</div>
</div>

View File

@ -75,8 +75,7 @@ export const OptionsSection: FunctionComponent<OptionsSectionProps> = ({ dsf, up
return (
<section>
<h4 id="options-section" className="is-size-4 title is-spaced"><a href="#options-section">Explorer les options</a></h4>
<div className="box">
<h4 id="options-section" className="is-size-4 title is-spaced">Explorer les options</h4>
<div className="table-container">
<table className={`table is-bordered is-striped is-hoverable is-fullwidth`}>
<thead>
@ -154,7 +153,6 @@ export const OptionsSection: FunctionComponent<OptionsSectionProps> = ({ dsf, up
</tfoot>
</table>
</div>
</div>
</section>
);
};

View File

@ -0,0 +1,36 @@
import React, { FunctionComponent, useState } from 'react';
import { DecisionSupportFile } from '../../types/decision';
import { Timeline } from '../Timeline';
import { useEvents } from '../../gql/queries/event';
export interface TimelinePanelProps {
dsf: DecisionSupportFile,
};
export const TimelinePanel: FunctionComponent<TimelinePanelProps> = ({ dsf }) => {
const { events } = useEvents({
variables: {
filter: {
objectType: 'dsf',
objectId: dsf.id
}
}
});
return (
<div className="panel">
<div className="level panel-heading mb-0">
<div className="level-left">
<div className="level-item">
Suivi des opérations
</div>
</div>
<div className="level-right">
</div>
</div>
<div className="panel-block">
<Timeline events={events} />
</div>
</div>
);
};

View File

@ -18,4 +18,6 @@ export function HomePage() {
<WelcomeContent />
</Page>
);
}
}
export default HomePage;

View File

@ -1,6 +1,5 @@
import React, { FunctionComponent, useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { WithLoader } from "../WithLoader";
export interface Item {
id: string
@ -88,7 +87,7 @@ export const ItemPanel: FunctionComponent<ItemPanelProps> = (props) => {
</div>
<div className="panel-block">
<p className="control has-icons-left">
<input className="input" type="text" placeholder="Filtrer..." />
<input disabled={true} className="input" type="text" placeholder="Filtrer..." />
<span className="icon is-left">
<i className="fas fa-search" aria-hidden="true"></i>
</span>

View File

@ -12,4 +12,6 @@ export const LogoutPage: FunctionComponent<LogoutPageProps> = () => {
window.location.replace(Config.logoutURL);
}, []);
return null;
};
};
export default LogoutPage;

View File

@ -1,6 +1,5 @@
import React, { Fragment, useState } from 'react';
import logo from '../resources/logo.svg';
import { useSelector } from 'react-redux';
import { Config } from '../config';
import { Link } from 'react-router-dom';
import { useLoggedIn } from '../hooks/useLoggedIn';
@ -32,6 +31,20 @@ export function Navbar() {
</a>
</div>
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
<div className="navbar-start">
{
loggedIn ?
<React.Fragment>
<Link to="/dashboard" className="navbar-item">
<i className="fa fa-columns"></i>&nbsp;Tableau de bord
</Link>
<Link to="/conference" className="navbar-item">
<i className="fa fa-users"></i>&nbsp;Conférence
</Link>
</React.Fragment> :
null
}
</div>
<div className="navbar-end">
<div className="navbar-item">
<div className="buttons">
@ -44,10 +57,11 @@ export function Navbar() {
</span>
<span>Mon profil</span>
</Link>
<Link className="button is-warning" to="/logout">
<Link className="button is-warning is-small" to="/logout">
<span className="icon">
<i className="fas fa-sign-out-alt"></i>
</span>
<span>Déconnexion</span>
</Link>
</Fragment> :
<a className="button is-primary" href={Config.loginURL}>

View File

@ -2,19 +2,16 @@ import React from 'react';
import { Page } from '../Page';
import { UserForm } from '../UserForm';
import { User } from '../../types/user';
import { useUserProfileQuery } from '../../gql/queries/profile';
import { useUserProfile } from '../../gql/queries/profile';
import { useUpdateUserProfileMutation } from '../../gql/mutations/profile';
import { WithLoader } from '../WithLoader';
export function ProfilePage() {
const userProfileQuery = useUserProfileQuery();
const { user, loading } = useUserProfile();
const [ updateProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
const isLoading = updateUserProfileMutation.loading || userProfileQuery.loading;
const { userProfile } = (userProfileQuery.data || {});
const isLoading = updateUserProfileMutation.loading || loading;
const onUserChange = (user: User) => {
if (userProfile.name !== user.name) {
if (user.name !== user.name) {
updateProfile({ variables: {changes: { name: user.name }}});
}
};
@ -25,16 +22,16 @@ export function ProfilePage() {
<section className="section">
<div className="columns">
<div className="column is-6 is-offset-3">
<h2 className="is-size-2 subtitle">Mon profil</h2>
<WithLoader loading={isLoading || !userProfile}>
{
<UserForm onChange={onUserChange} user={userProfile} />
}
</WithLoader>
<div className="box">
<h2 className="is-size-2 subtitle">Mon profil</h2>
{ !isLoading ? <UserForm onChange={onUserChange} user={user} /> : null }
</div>
</div>
</div>
</section>
</div>
</Page>
);
}
}
export default ProfilePage;

View File

@ -0,0 +1,79 @@
import React, { FunctionComponent, ReactNode, useEffect, useState } from 'react';
import { useHistory, useLocation, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
export interface Tab {
route: string
name: string
icon ?: string
render: (tab: Tab) => ReactNode
}
export interface RoutedTabsProps {
tabs: Tab[]
baseRoute?: string
defaultTabIndex?: number
}
export const RoutedTabs: FunctionComponent<RoutedTabsProps> = ({ tabs, baseRoute, defaultTabIndex }) => {
const history = useHistory();
const location = useLocation();
const tabRoute = (route: string): string => {
return `${baseRoute}${route}`;
};
const [ selectedTabIndex, setSelectedTabIndex ] = useState(defaultTabIndex || 0);
const expectedTab = tabs[selectedTabIndex];
const expectedTabRoute = tabRoute(expectedTab.route);
let matchExpectedTabRoute = useRouteMatch(expectedTabRoute);
useEffect(() => {
if (matchExpectedTabRoute) return;
const newTabIndex = tabs.findIndex(t => location.pathname === tabRoute(t.route));
if (newTabIndex !== -1) {
selectTab(newTabIndex);
return;
}
history.push(expectedTabRoute);
}, [matchExpectedTabRoute]);
const selectTab = (tabIndex: number) => {
setSelectedTabIndex(tabIndex);
const newTab = tabs[tabIndex];
history.push(tabRoute(newTab.route));
};
return (
<React.Fragment>
<div className="tabs is-medium is-boxed">
<ul>
{
tabs.map((t: Tab, i: number) => {
return (
<li key={`tab-${i}`} className={`has-background-white ${selectedTabIndex === i ? 'is-active': ''}`}
onClick={selectTab.bind(null, i)}>
<a>
{
t.icon ?
<span className="icon is-small"><i className={t.icon} aria-hidden="true"></i></span> :
null
}
<span>{t.name}</span>
</a>
</li>
);
})
}
</ul>
</div>
{ expectedTab.render(expectedTab) }
</React.Fragment>
);
}

View File

@ -0,0 +1,232 @@
import React, { FunctionComponent } from "react";
import { formatDate } from "../util/date";
import { Event } from "../types/event";
import { Link } from "react-router-dom";
import { WorkgroupLink } from "./WorkgroupLink";
import { DecisionSupportFileLink } from "./DecisionSupportFileLink";
export interface TimelineProps {
events?: Event[]
}
export const Timeline: FunctionComponent<TimelineProps> = ({ events }) => {
events = debounceEvents(events) || [];
return (
<React.Fragment>
<div className="timeline" style={{width: '100%'}}>
{
events.map(evt => {
return (
<div key={evt.id} className="timeline-item">
{renderEventMarker(evt)}
<div className="timeline-content">
<p className="heading">{formatDate(evt.createdAt)}</p>
{renderEventContent(evt)}
</div>
</div>
);
})
}
{
events.length === 0 ?
<p className="has-text-centered is-italic mb-1 mt-1">Aucun évènement.</p> :
null
}
</div>
</React.Fragment>
);
}
function debounceEvents(events: Event[]): Event[] {
const debounced = [];
for(let evt: Event, i = 0; (evt = events[i]); ++i) {
const prev = i > 0 ? events[i-1] : null;
if (!prev) {
debounced.push(evt);
continue;
}
const isSame = evt.objectId === prev.objectId &&
evt.objectType === prev.objectType &&
evt.type === prev.type &&
evt.user.id === prev.user.id
;
if (isSame) continue;
debounced.push(evt);
}
return debounced;
}
const eventMarkerMap = {
"closed": (evt:Event) => (
<div className="timeline-marker is-icon is-danger">
<i className="fa fa-times"></i>
</div>
),
"created": (evt:Event) => (
<div className="timeline-marker is-icon is-success">
<i className="fa fa-plus"></i>
</div>
),
"updated": (evt:Event) => (
<div className="timeline-marker is-icon is-info">
<i className="fa fa-pen"></i>
</div>
),
"title-changed": (evt:Event) => (
<div className="timeline-marker is-icon is-info">
<i className="fa fa-pen"></i>
</div>
),
"status-changed": (evt:Event) => (
<div className="timeline-marker is-icon is-primary">
<i className="fa fa-star"></i>
</div>
),
"joined": (evt:Event) => (
<div className="timeline-marker is-icon is-info">
<i className="fa fa-users"></i>
</div>
),
"leaved": (evt:Event) => (
<div className="timeline-marker is-icon is-warning">
<i className="fas fa-users-slash"></i>
</div>
),
"voted": (evt:Event) => (
<div className="timeline-marker is-icon is-success">
<i className="fas fa-thumbs-up"></i>
</div>
),
}
function renderEventMarker(evt: Event) {
const render = eventMarkerMap[evt.type];
if (!render) return ( <div className="timeline-marker"></div> );
return render(evt);
}
const eventContentMap = {
"created": {
"workgroup": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a créé le groupe de travail `}</span>
"<WorkgroupLink workgroupId={evt.objectId} />".
</React.Fragment>
);
},
"dsf": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a créé le dossier d'aide à la décision `}</span>
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
</React.Fragment>
);
},
},
"title-changed": {
"dsf": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le titre du dossier d'aide à la décision `}</span>
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
</React.Fragment>
)
}
},
"status-changed": {
"dsf": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le statut du dossier d'aide à la décision `}</span>
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
</React.Fragment>
)
}
},
"joined": {
"workgroup": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a rejoint le groupe de travail `}</span>
"<WorkgroupLink workgroupId={evt.objectId} />".
</React.Fragment>
);
},
},
"updated": {
"workgroup": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a mis à jour le groupe de travail `}</span>
"<WorkgroupLink workgroupId={evt.objectId} />".
</React.Fragment>
);
},
"dsf": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le dossier d'aide à la décision `}</span>
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
</React.Fragment>
);
},
},
"leaved": {
"workgroup": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a quitté le groupe de travail `}</span>
"<WorkgroupLink workgroupId={evt.objectId} />".
</React.Fragment>
);
},
},
"closed": {
"dsf": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a clos le dossier d'aide à la décision `}</span>
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
</React.Fragment>
);
},
"workgroup": (evt:Event) => {
return (
<React.Fragment>
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a clos le groupe de travail `}</span>
"<WorkgroupLink workgroupId={evt.objectId} />".
</React.Fragment>
);
},
},
"voted": {
"dsf": (evt:Event) => {
return (
<React.Fragment>
<span>{`Le dossier d'aide à la décision `}</span>
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />"
<span> a é voté.</span>
</React.Fragment>
);
},
},
};
function renderEventContent(evt: Event) {
const eventTypeMap = eventContentMap[evt.type];
const render = eventTypeMap && eventTypeMap[evt.objectType];
if (!eventTypeMap || !render) {
return (
<span className="is-italic">{`Type d'évènement "${evt.type}/${evt.objectType}" inconnu.`}</span>
);
}
return render(evt);
}

View File

@ -0,0 +1,39 @@
import React, { FunctionComponent } from 'react';
import { Config } from '../../config';
import { Page } from '../Page';
export interface UnauthorizedPageProps {
}
export const UnauthorizedPage:FunctionComponent<UnauthorizedPageProps> = () => {
return (
<Page title="Non autorisé">
<div className="container is-fluid">
<section className="section">
<div className="columns">
<div className="column is-6 is-offset-3">
<div className="message is-danger">
<div className="message-header">
<p><i className="fa fa-ban"></i> Non autorisé</p>
</div>
<div className="message-body">
<p>Vous n'êtes pas autorisé à accéder à cette page.</p>
<br />
<p>Votre compte est peut être désactivé, votre adresse courriel ne fait peut être
pas partie des domaines autorisés ou vous n'avez peut être pas les droits nécessaires pour effectuer cette opération.</p>
<div className="has-text-centered mt-5">
<a href={Config.logoutURL} className="is-warning button"><i className="fa fa-sign-out-alt"></i>&nbsp; Forcer la déconnexion</a>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</Page>
);
}
export default UnauthorizedPage;

View File

@ -1,5 +1,6 @@
import React, { useState, ChangeEvent, useEffect } from 'react';
import { User } from '../types/user';
import { formatDate } from '../util/date';
export interface UserFormProps {
user: User
@ -62,13 +63,13 @@ export function UserForm({ user, onChange }: UserFormProps) {
<div className="field">
<label className="label">Date de dernière connexion</label>
<div className="control">
<p className="input is-static">{state.user.connectedAt}</p>
<p className="input is-static">{formatDate(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>
<p className="input is-static">{formatDate(state.user.createdAt)}</p>
</div>
</div>
<div className="buttons is-right">

View File

@ -1,18 +0,0 @@
import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react';
export interface WithLoaderProps {
loading?: boolean|boolean[]
}
export const WithLoader: FunctionComponent<WithLoaderProps> = ({ loading, children }) => {
const isLoading = Array.isArray(loading) ? loading.some(l => l) : loading;
return (
<Fragment>
{
isLoading ?
<div>Chargement</div> :
children
}
</Fragment>
)
}

View File

@ -0,0 +1,24 @@
import React, { FunctionComponent } from "react";
import { Link } from "react-router-dom";
import { useWorkgroups } from "../gql/queries/workgroups";
export interface WorkgroupLinkProps {
workgroupId: number
}
export const WorkgroupLink: FunctionComponent<WorkgroupLinkProps> = ({ workgroupId }) => {
const { workgroups } = useWorkgroups({
fetchPolicy: "cache-first",
variables: {
filter: {
ids: [workgroupId]
}
}
});
const workgroupName = workgroups.length > 0 ? workgroups[0].name : `#${workgroupId}`;
return (
<Link to={`/workgroups/${workgroupId}`}>{workgroupName}</Link>
);
};

View File

@ -0,0 +1,48 @@
import React, { FunctionComponent } from 'react';
import { Link } from 'react-router-dom';
import { useDecisionSupportFiles } from '../../gql/queries/dsf';
import { DecisionSupportFile } from '../../types/decision';
import { User } from '../../types/user';
import { DecisionSupportFileLink } from '../DecisionSupportFileLink';
import { WorkgroupLink } from '../WorkgroupLink';
export interface DecisionSupportFilePanelProps {
workgroupId: string
}
export const DecisionSupportFilePanel: FunctionComponent<DecisionSupportFilePanelProps> = ({ workgroupId }) => {
const { decisionSupportFiles } = useDecisionSupportFiles({
variables: {
filter: {
workgroups: [workgroupId],
}
}
})
return (
<nav className="panel">
<p className="panel-heading">
Dossiers d'aide à la décision
</p>
{
decisionSupportFiles.map((dsf: DecisionSupportFile) => {
return (
<Link to={`/decisions/${dsf.id}`} key={`dsf-${dsf.id}`} className="panel-block">
<span className="panel-icon">
<i className="fas fa-file" aria-hidden="true"></i>
</span>
<span>{dsf.title}</span>
</Link>
);
})
}
{
decisionSupportFiles.length === 0 ?
<a className="panel-block has-text-centered is-block">
<p className="is-italic">Aucun dossier pour l'instant.</p>
</a> :
null
}
</nav>
);
}

View File

@ -1,6 +1,7 @@
import React, { useState, ChangeEvent, useEffect } from 'react';
import { Workgroup } from '../../types/workgroup';
import { useIsAuthorized } from '../../gql/queries/authorization';
import { formatDate } from '../../util/date';
export interface InfoFormProps {
workgroup: Workgroup
@ -80,7 +81,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
<div className="field">
<label className="label">Date de création</label>
<div className="control">
<p className="input is-static">{state.workgroup.createdAt}</p>
<p className="input is-static">{formatDate(state.workgroup.createdAt)}</p>
</div>
</div>:
null
@ -90,7 +91,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
<div className="field">
<label className="label">Date de clôture</label>
<div className="control">
<p className="input is-static">{state.workgroup.closedAt}</p>
<p className="input is-static">{formatDate(state.workgroup.closedAt)}</p>
</div>
</div>:
null

View File

@ -1,8 +1,6 @@
import React, { FunctionComponent } from 'react';
import { User } from '../../types/user';
import { Workgroup } from '../../types/workgroup';
import { InfoForm } from './InfoForm';
import { WithLoader } from '../WithLoader';
import { useUpdateWorkgroupMutation, useCreateWorkgroupMutation } from '../../gql/mutations/workgroups';
import { useHistory } from 'react-router';
@ -43,9 +41,7 @@ export const InfoPanel: FunctionComponent<InfoPanelProps> = ({ workgroup }) => {
Informations
</p>
<div className="panel-block">
<WithLoader loading={isLoading}>
<InfoForm workgroup={workgroup} onChange={onWorkgroupChange} />
</WithLoader>
{ !isLoading ? <InfoForm workgroup={workgroup} onChange={onWorkgroupChange} /> : null }
</div>
</nav>
);

View File

@ -0,0 +1,37 @@
import React, { FunctionComponent, useState } from 'react';
import { DecisionSupportFile } from '../../types/decision';
import { Timeline } from '../Timeline';
import { useEvents } from '../../gql/queries/event';
import { Workgroup } from '../../types/workgroup';
export interface TimelinePanelProps {
workgroup: Workgroup,
};
export const TimelinePanel: FunctionComponent<TimelinePanelProps> = ({ workgroup }) => {
const { events } = useEvents({
variables: {
filter: {
objectType: 'workgroup',
objectId: workgroup.id
}
}
});
return (
<div className="panel">
<div className="level panel-heading mb-0">
<div className="level-left">
<div className="level-item">
Suivi des opérations
</div>
</div>
<div className="level-right">
</div>
</div>
<div className="panel-block">
<Timeline events={events} />
</div>
</div>
);
};

View File

@ -1,17 +1,18 @@
import React, { useEffect, useState, Fragment } from 'react';
import { Page } from '../Page';
import { WithLoader } from '../WithLoader';
import { useParams } from 'react-router';
import { useWorkgroupsQuery, useWorkgroups } from '../../gql/queries/workgroups';
import { useUserProfileQuery, useUserProfile } from '../../gql/queries/profile';
import { useWorkgroups } from '../../gql/queries/workgroups';
import { useUserProfile } from '../../gql/queries/profile';
import { MembersPanel } from './MembersPanel';
import { User } from '../../types/user';
import { InfoPanel } from './InfoPanel';
import { Workgroup } from '../../types/workgroup';
import { useJoinWorkgroupMutation, useLeaveWorkgroupMutation, useCloseWorkgroupMutation } from '../../gql/mutations/workgroups';
import { TimelinePanel } from './TimelinePanel';
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
export function WorkgroupPage() {
const { id } = useParams();
const { id } = useParams<any>();
const { workgroups } = useWorkgroups({
variables:{
filter: {
@ -134,15 +135,21 @@ export function WorkgroupPage() {
</div>
</div>
<div className="columns">
<div className="column">
<div className="column is-4">
<InfoPanel workgroup={state.workgroup as Workgroup} />
</div>
<div className="column">
<div className="column is-4">
<MembersPanel users={state.workgroup.members as User[]} />
<DecisionSupportFilePanel workgroupId={state.workgroup.id} />
</div>
<div className="column is-4">
<TimelinePanel workgroup={state.workgroup} />
</div>
</div>
</section>
</div>
</Page>
);
}
}
export default WorkgroupPage;

View File

@ -3,6 +3,8 @@ export const Config = {
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"),
conferenceHeartbeatInterval: get<number>("conferenceHeartbeatInterval", 10000),
frontendBaseURL: get<string>("frontendBaseURL", window.location.protocol + '//' + window.location.host + '/'),
};
function get<T>(key: string, defaultValue: T):T {

View File

@ -1,4 +1,4 @@
import { gql, useQuery } from '@apollo/client';
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
import { useGraphQLData } from './helper';
export const QUERY_IS_AUTHORIZED = gql`
@ -7,11 +7,17 @@ export const QUERY_IS_AUTHORIZED = gql`
}
`;
export function useIsAuthorizedQuery(options = {}) {
export function useIsAuthorizedQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
options = Object.assign({
fetchPolicy: 'cache-and-network'
}, options);
return useQuery(QUERY_IS_AUTHORIZED, options);
}
export function useIsAuthorized(options = {}, defaultValue = false) {
export function useIsAuthorized<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}, defaultValue = false) {
options = Object.assign({
fetchPolicy: 'cache-and-network'
}, options);
const { data, loading, error } = useGraphQLData<boolean>(
QUERY_IS_AUTHORIZED, 'isAuthorized', defaultValue, options
);

View File

@ -1,4 +1,4 @@
import { gql, useQuery } from '@apollo/client';
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
import { DecisionSupportFile } from '../../types/decision';
import { useGraphQLData } from './helper';
@ -25,11 +25,11 @@ export const QUERY_DECISION_SUPPORT_FILES = gql`
}
`;
export function useDecisionSupportFilesQuery(options = {}) {
export function useDecisionSupportFilesQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
return useQuery(QUERY_DECISION_SUPPORT_FILES, options);
}
export function useDecisionSupportFiles(options = {}) {
export function useDecisionSupportFiles<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
const { data, loading, error } = useGraphQLData<DecisionSupportFile[]>(
QUERY_DECISION_SUPPORT_FILES, 'decisionSupportFiles', [], options
);

View File

@ -0,0 +1,31 @@
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
import { useGraphQLData } from './helper';
import { Event } from '../../types/event';
export const QUERY_EVENTS = gql`
query events($filter: EventFilter) {
events(filter: $filter) {
id
user {
id
name
email
}
type
objectType
objectId
createdAt
}
}
`;
export function useEventsQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
return useQuery(QUERY_EVENTS, options);
}
export function useEvents<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
const { data, loading, error } = useGraphQLData<Event[]>(
QUERY_EVENTS, 'events', [], options
);
return { events: data, loading, error };
}

View File

@ -1,7 +1,7 @@
import { useQuery, DocumentNode } from "@apollo/client";
import { useQuery, DocumentNode, QueryHookOptions } from "@apollo/client";
import { useState, useEffect } from "react";
export function useGraphQLData<T>(q: DocumentNode, key: string, defaultValue: T, options = {}) {
export function useGraphQLData<T, A = any, R = Record<string, any>>(q: DocumentNode, key: string, defaultValue: T, options: QueryHookOptions<A, R> = {}) {
const query = useQuery(q, options);
const [ data, setData ] = useState<T>(defaultValue);
useEffect(() => {

View File

@ -1,4 +1,4 @@
import { gql, useQuery } from '@apollo/client';
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
import { User } from '../../types/user';
import { useState, useEffect } from 'react';
import { useGraphQLData } from './helper';
@ -14,13 +14,13 @@ query userProfile {
}
}`;
export function useUserProfileQuery() {
return useQuery(QUERY_USER_PROFILE);
export function useUserProfileQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
return useQuery(QUERY_USER_PROFILE, options);
}
export function useUserProfile() {
export function useUserProfile<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
const { data, loading, error } = useGraphQLData<User>(
QUERY_USER_PROFILE, 'userProfile', {id: '', email: ''}
QUERY_USER_PROFILE, 'userProfile', {id: '', email: ''}, options
);
return { user: data, loading, error };
}

View File

@ -1,4 +1,4 @@
import { gql, useQuery } from '@apollo/client';
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
import { Workgroup } from '../../types/workgroup';
import { useGraphQLData } from './helper';
@ -18,11 +18,11 @@ export const QUERY_WORKGROUP = gql`
}
`;
export function useWorkgroupsQuery(options = {}) {
export function useWorkgroupsQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
return useQuery(QUERY_WORKGROUP, options);
}
export function useWorkgroups(options = {}) {
export function useWorkgroups<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
const { data, loading, error } = useGraphQLData<Workgroup[]>(
QUERY_WORKGROUP, 'workgroups', [],
options

View File

@ -0,0 +1,90 @@
import * as Y from 'yjs'
import { WebrtcProvider, } from 'y-webrtc'
import { useEffect, useRef, useState } from 'react'
import { uuidV4 } from '../util/uuid';
const UUIDKey = 'conference-uuid';
let uuid = localStorage.getItem(UUIDKey);
if (!uuid) {
uuid = uuidV4();
localStorage.setItem(UUIDKey, uuid);
}
export function useConference() {
const docRef = useRef(new Y.Doc());
const [ state, setState ] = useState({
data: {
emails: {},
nicknames: {},
statuses: {},
peers: {},
},
uuid,
});
const setData = (key: string, value: any) => {
setState(state => ({...state, data: { ...state.data, [key]: value }}));
}
useEffect(() => {
const doc = docRef.current;
const roomName = `${window.location.protocol}//${window.location.host}/daddy/conference`;
const provider = new WebrtcProvider(roomName, docRef.current);
const peers = doc.getMap('peers');
peers.observe(evt => setData('peers', evt.currentTarget.toJSON()));
const nicknames = doc.getMap('nicknames');
nicknames.observe(evt => setData('nicknames', evt.currentTarget.toJSON()));
const emails = doc.getMap('emails');
emails.observe(evt => setData('emails', evt.currentTarget.toJSON()));
const statuses = doc.getMap('statuses');
statuses.observe(evt => setData('statuses', evt.currentTarget.toJSON()));
return () => {
provider.destroy();
docRef.current.destroy();
};
}, []);
return {
data: state.data,
uuid: state.uuid,
setStatus: (status: string) => {
const doc = docRef.current;
const statuses = doc.getMap('statuses');
statuses.set(state.uuid, status);
},
ping: () => {
const doc = docRef.current;
const peers = doc.getMap('peers');
peers.set(state.uuid, (new Date()).toJSON());
},
setNickname: (nickname: string) => {
console.log('setNickname', nickname);
const doc = docRef.current;
const nicknames = doc.getMap('nicknames');
nicknames.set(state.uuid, nickname);
},
setEmail: (email: string) => {
console.log('setEmail', email);
const doc = docRef.current;
const emails = doc.getMap('emails');
emails.set(state.uuid, email);
},
forget: (uuid: string) => {
const doc = docRef.current;
const peers = doc.getMap('peers');
peers.delete(uuid);
},
};
}

View File

@ -9,13 +9,12 @@ export const useLoggedIn = () => {
};
export function saveLoggedIn(loggedIn: boolean) {
console.log("saveLoggedIn", JSON.stringify(loggedIn))
window.sessionStorage.setItem(LOGGED_IN_KEY, JSON.stringify(loggedIn));
window.localStorage.setItem(LOGGED_IN_KEY, JSON.stringify(loggedIn));
}
export function getSavedLoggedIn(): boolean {
try {
const loggedIn = JSON.parse(window.sessionStorage.getItem(LOGGED_IN_KEY));
const loggedIn = JSON.parse(window.localStorage.getItem(LOGGED_IN_KEY));
return !!loggedIn;
} catch(err) {
return false;

View File

@ -1,3 +1,8 @@
import { Config } from './config';
declare var __webpack_public_path__: string;
__webpack_public_path__ = Config.frontendBaseURL;
import './sass/_all.scss';
import React from 'react';
import ReactDOM from 'react-dom';
@ -8,7 +13,7 @@ import '@fortawesome/fontawesome-free/js/solid'
import '@fortawesome/fontawesome-free/js/regular'
import '@fortawesome/fontawesome-free/js/brands'
import './resources/favicon.png';
import { ApolloProvider } from '@apollo/client';
ReactDOM.render(
<App />,

View File

@ -1,3 +1,5 @@
@import 'bulma/bulma.sass';
@import 'bulma-timeline/dist/css/bulma-timeline.sass';
@import '_bulma-timeline.scss';
@import '_base.scss';
@import '_loader.scss';

View File

@ -0,0 +1,12 @@
.timeline {
.timeline-item {
.timeline-marker {
&.is-icon {
> svg {
color: $white;
font-size: $timeline-icon-size !important;
}
}
}
}
}

View File

@ -1,44 +1,16 @@
.loader-container {
display: flex;
width: 100%;
justify-content: center;
height: 100%;
align-items: center;
}
.app-loader {
@extend body;
.lds-ripple {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
transform: scale(2);
}
.lds-ripple div {
position: absolute;
border: 4px solid $grey;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 72px;
height: 72px;
opacity: 0;
}
}
display: flex;
position: absolute;
z-index: 10000;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100vh;
width: 100vw;
align-items: center;
justify-content: center;
flex-direction: column;
}

11
client/src/types/event.ts Normal file
View File

@ -0,0 +1,11 @@
import { User } from "./user";
export interface Event {
id: string
createdAt: Date
updatedAt: Date
user: User
objectType: string
objectId: number
type: string
}

View File

@ -44,6 +44,19 @@ export function createClient(setLoggedIn: (boolean) => void) {
errorLink,
retryLink
]),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
}
});
}

View File

@ -1,4 +1,16 @@
export function asDate(d: string|Date): Date {
if (typeof d === 'string') return new Date(d);
return d;
}
const intl = Intl.DateTimeFormat(navigator.language, {
weekday: 'long',
month: 'short',
day: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric',
});
export function formatDate(d: Date|string): string {
d = asDate(d);
return intl.format(d);
}

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"module": "es6",
"module": "es2020",
"lib": ["dom", "es6"],
"moduleResolution": "node",
"jsx": "react",

View File

@ -11,7 +11,7 @@ const env = process.env;
module.exports = {
mode: `${env.NODE_ENV ? env.NODE_ENV : 'production'}`,
entry: './src/index.tsx',
devtool: 'inline-source-map',
devtool: env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map',
output: {
filename: '[name].[contenthash].js',
path: path.join(__dirname, 'dist')

View File

@ -5,6 +5,7 @@ import (
"net/http"
"time"
"forge.cadoles.com/Cadoles/daddy/internal/mail"
"forge.cadoles.com/Cadoles/daddy/internal/model"
"forge.cadoles.com/Cadoles/daddy/internal/voter"
@ -78,8 +79,9 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
// Define default cookie options
sessionStore.SessionOpts.Path = "/"
sessionStore.SessionOpts.HttpOnly = true
sessionStore.SessionOpts.Secure = conf.HTTP.CookieSecure
sessionStore.SessionOpts.MaxAge = conf.HTTP.CookieMaxAge
sessionStore.SessionOpts.SameSite = http.SameSiteStrictMode
sessionStore.SessionOpts.SameSite = http.SameSiteLaxMode
ctn.Provide(
session.ServiceName,
@ -108,5 +110,11 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
model.NewWorkgroupVoter(),
))
ctn.Provide(mail.ServiceName, mail.ServiceProvider(
mail.WithServer(conf.SMTP.Host, conf.SMTP.Port),
mail.WithCredentials(conf.SMTP.User, conf.SMTP.Password),
mail.WithTLS(conf.SMTP.UseStartTLS, conf.SMTP.InsecureSkipVerify),
))
return ctn, nil
}

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/Cadoles/daddy/internal/config"
"forge.cadoles.com/Cadoles/daddy/internal/route"
"github.com/getsentry/sentry-go"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"gitlab.com/wpetit/goweb/middleware/container"
@ -17,6 +18,7 @@ import (
"os"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
@ -129,6 +131,34 @@ func main() {
logger.Debug(ctx, "setting log level", logger.F("level", conf.Log.Level.String()))
logger.SetLevel(conf.Log.Level)
useSentry := conf.Sentry.DSN != ""
if useSentry {
var sentryEnv string
if conf.Sentry.Environment == "" {
sentryEnv, _ = os.Hostname()
} else {
sentryEnv = conf.Sentry.Environment
}
err := sentry.Init(sentry.ClientOptions{
Dsn: conf.Sentry.DSN,
Debug: conf.Debug,
SampleRate: conf.Sentry.ServerSampleRate,
Release: ProjectVersion + "-" + GitRef,
Environment: sentryEnv,
})
if err != nil {
logger.Fatal(
ctx,
"could initialize sentry",
logger.E(err),
)
}
defer sentry.Flush(conf.Sentry.ServerFlushTimeout)
}
// Create service container
ctn, err := getServiceContainer(ctx, conf)
if err != nil {
@ -153,12 +183,22 @@ func main() {
os.Exit(0)
}
go runTaskScheduler(ctx, conf)
r := chi.NewRouter()
// Define base middlewares
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
if useSentry {
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
r.Use(sentryMiddleware.Handle)
}
// Expose service container on router
r.Use(container.ServiceContainer(ctn))

View File

@ -81,6 +81,7 @@ var initialModels = []interface{}{
&model.User{},
&model.Workgroup{},
&model.DecisionSupportFile{},
&model.Event{},
}
func m000initialSchema() orm.Migration {

85
cmd/server/scheduler.go Normal file
View File

@ -0,0 +1,85 @@
package main
import (
"context"
"fmt"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/Cadoles/daddy/internal/config"
"forge.cadoles.com/Cadoles/daddy/internal/task"
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
)
type cronLogger struct {
ctx context.Context
}
func (l *cronLogger) Info(msg string, keysAndValues ...interface{}) {
fields := l.createFields(keysAndValues)
logger.Info(l.ctx, msg, fields...)
}
func (l *cronLogger) Error(err error, msg string, keysAndValues ...interface{}) {
fields := l.createFields(keysAndValues)
fields = append(fields, logger.E(err))
logger.Error(l.ctx, msg, fields...)
}
func (l *cronLogger) createFields(keysAndValues ...interface{}) []logger.Field {
fields := make([]logger.Field, 0)
var key string
for _, v := range keysAndValues {
children, ok := v.([]interface{})
if !ok {
continue
}
for i, vv := range children {
if i%2 == 0 {
key = fmt.Sprintf("%v", vv)
continue
}
fields = append(fields, logger.F(key, vv))
}
}
return fields
}
func runTaskScheduler(ctx context.Context, conf *config.Config) {
c := cron.New(
cron.WithLogger(&cronLogger{ctx}),
)
tasks := map[string]task.Task{
conf.Task.Newsletter.CronSpec: task.NewNewsletter(
ctx,
conf.Task.Newsletter.TimeRange,
conf.Task.Newsletter.BaseURL,
conf.Task.Newsletter.ContentTemplate,
conf.Task.Newsletter.SubjectTemplate,
conf.SMTP.SenderAddress,
),
}
for spec, task := range tasks {
if _, err := c.AddFunc(spec, task.Run); err != nil {
logger.Fatal(
ctx,
"could not schedule task",
logger.F("task", task.Name()),
logger.E(errors.WithStack(err)),
)
return
}
}
c.Start()
}

21
go.mod
View File

@ -3,22 +3,37 @@ module forge.cadoles.com/Cadoles/daddy
go 1.14
require (
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20200619080035-4bbf7b016032
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013125038-8d8d1519a52d
forge.cadoles.com/wpetit/hydra-passwordless v0.0.0-20200908094025-38ac4422dddc // indirect
github.com/99designs/gqlgen v0.11.3
github.com/alecthomas/chroma v0.8.1 // indirect
github.com/antonmedv/expr v1.8.8
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/dlclark/regexp2 v1.4.0 // indirect
github.com/getsentry/sentry-go v0.7.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.2.0
github.com/gorilla/websocket v1.4.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/lithammer/dedent v1.1.0
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
github.com/robfig/cron v1.2.0
github.com/robfig/cron/v3 v3.0.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
go.opencensus.io v0.22.5 // indirect
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee // indirect
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/sys v0.0.0-20201013081832-0aaa2718063a // indirect
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v2 v2.2.8
)

356
go.sum
View File

@ -9,33 +9,72 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj
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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
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=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013085949-5d5592098f13 h1:gZCo9pX3I3A0xVvXkbhdTDBbW44CypCxjiP5LtNV5bo=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013085949-5d5592098f13/go.mod h1:phGAWHUGKNZj044478BvRg0jk049uK1IiX2Amh8krAk=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013111944-d43b43b636ed h1:7dTCXOGxvAulu9vnOjpt2cTgsuxMHX4FH795/JJgo08=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013111944-d43b43b636ed/go.mod h1:phGAWHUGKNZj044478BvRg0jk049uK1IiX2Amh8krAk=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013125038-8d8d1519a52d h1:o+Ppy/MyT5UgbtUYI2J1YqS3iuThxOuNFenYoPgKZKk=
forge.cadoles.com/wpetit/goweb-oidc v0.0.0-20201013125038-8d8d1519a52d/go.mod h1:phGAWHUGKNZj044478BvRg0jk049uK1IiX2Amh8krAk=
forge.cadoles.com/wpetit/hydra-passwordless v0.0.0-20200908094025-38ac4422dddc h1:9gc/1qizPtK6/iMVlizknWUFii75ntl2xSUV/FSC92Y=
forge.cadoles.com/wpetit/hydra-passwordless v0.0.0-20200908094025-38ac4422dddc/go.mod h1:nANHORi270d5jDXjeJ7B3pMgK9R4J0/17p1IIc+rhOk=
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/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
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/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
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/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
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/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
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/chroma v0.8.1 h1:ym20sbvyC6RXz45u4qDglcgr8E313oPROshcuCHqiEE=
github.com/alecthomas/chroma v0.8.1/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
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 v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
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=
@ -48,17 +87,28 @@ github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9Pq
github.com/antonmedv/expr v1.8.8 h1:uVwIkIBNO2yn4vY2u2DQUqXTmv9jEEMCEcHa19G5weY=
github.com/antonmedv/expr v1.8.8/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
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/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
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-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
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=
@ -67,6 +117,8 @@ github.com/cortesi/moddwatch v0.0.0-20200427000745-d26468c93cf0 h1:7tjBO+RH4BoxJ
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 v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
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=
@ -82,31 +134,60 @@ github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0
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/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
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/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
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/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/getsentry/sentry-go v0.7.0 h1:MR2yfR4vFfv/2+iBuSnkdQwVg7N9cJzihZ6KJu7srwQ=
github.com/getsentry/sentry-go v0.7.0/go.mod h1:pLFpD2Y5RHIKF9Bw3KH6/68DeN2K/XBJd8awjdPnUwg=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
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-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
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/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
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=
@ -117,32 +198,65 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
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/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
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.1/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/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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/csrf v1.6.2/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
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=
@ -152,10 +266,21 @@ github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYb
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/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
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=
@ -216,8 +341,22 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk=
github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U=
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
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=
@ -228,6 +367,8 @@ 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/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
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=
@ -235,9 +376,12 @@ 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/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
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=
@ -249,6 +393,8 @@ github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+v
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-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/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=
@ -264,15 +410,31 @@ github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/
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/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
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/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
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=
@ -282,11 +444,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e h1:BLqxdwZ6j771IpSCRx7s/GJjXHUE00Hmu7/YegCGdzA=
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
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=
@ -294,8 +462,11 @@ 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 v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
@ -310,6 +481,14 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
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=
@ -320,16 +499,35 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
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/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
@ -339,6 +537,10 @@ 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.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
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=
@ -347,6 +549,7 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY
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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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=
@ -362,11 +565,18 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
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/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/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/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
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=
@ -376,45 +586,77 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
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/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190327091125-710a502c58a2/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-20190628185345-da137c7871d7/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-20190827160401-ba9fcec4b297/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-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/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/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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-20180909124046-d0be0721c37e/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-20181205085412-a5c9d58dba9a/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=
@ -426,21 +668,41 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
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-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/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-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/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/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201013081832-0aaa2718063a h1:bhXnJ7fn2SiL+C8iOWPfNBJKDTjUByftpPW7b9CX94U=
golang.org/x/sys v0.0.0-20201013081832-0aaa2718063a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
@ -449,13 +711,17 @@ 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/time v0.0.0-20191024005414-555d28b269f0/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-20181221001348-537d06c36207/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-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/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=
@ -471,26 +737,67 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
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-20191113191852-77e3bb0ad9e7/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/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-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
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=
@ -499,39 +806,88 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn
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-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
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/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
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/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
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=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
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=

View File

@ -5,6 +5,8 @@ import (
"io/ioutil"
"time"
"github.com/lithammer/dedent"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
@ -19,6 +21,9 @@ type Config struct {
OIDC OIDCConfig `yaml:"oidc"`
Database DatabaseConfig `yaml:"database"`
Auth AuthConfig `yaml:"auth"`
SMTP SMTPConfig `yaml:"smtp"`
Task TaskConfig `yaml:"task"`
Sentry SentryConfig `ymal:"sentry"`
}
// NewFromFile retrieves the configuration from the given file
@ -42,6 +47,7 @@ type HTTPConfig struct {
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"`
CookieSecure bool `yaml:"cookieSecure" env:"HTTP_COOKIE_SECURE"`
TemplateDir string `yaml:"templateDir" env:"HTTP_TEMPLATE_DIR"`
PublicDir string `yaml:"publicDir" env:"HTTP_PUBLIC_DIR"`
FrontendURL string `yaml:"frontendURL" env:"HTTP_FRONTEND_URL"`
@ -74,6 +80,37 @@ type AuthConfig struct {
Rules []string `yaml:"rules" env:"AUTH_RULES"`
}
type SMTPConfig struct {
Host string `yaml:"host" env:"SMTP_HOST"`
Port int `yaml:"port" env:"SMTP_PORT"`
UseStartTLS bool `yaml:"useStartTLS" env:"SMTP_USE_START_TLS"`
User string `yaml:"user" env:"SMTP_USER"`
Password string `yaml:"password" env:"SMTP_PASSWORD"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify" env:"SMTP_INSECURE_SKIP_VERIFY"`
SenderAddress string `yaml:"senderAddress" env:"SMTP_SENDER_ADDRESS"`
SenderName string `yaml:"senderName" env:"SMTP_SENDER_NAME"`
}
type TaskConfig struct {
Newsletter NewsletterTaskConfig `yaml:"newsletter"`
}
type NewsletterTaskConfig struct {
CronSpec string `yaml:"cronSpec" env:"TASK_NEWSLETTER_CRON_SPEC"`
TimeRange time.Duration `yaml:"timeRange" env:"TASK_NEWSLETTER_TIME_RANGE"`
BaseURL string `yaml:"baseUrl" env:"TASK_NEWSLETTER_BASE_URL"`
ContentTemplate string `yaml:"contentTemplate" env:"TASK_NEWSLETTER_CONTENT_TEMPLATE"`
SubjectTemplate string `yaml:"subjectTemplate" env:"TASK_NEWSLETTER_SUBJECT_TEMPLATE"`
}
type SentryConfig struct {
DSN string `yaml:"dsn" env:"SENTRY_DSN"`
// Server events sampling rate, see https://docs.sentry.io/platforms/go/configuration/options/
ServerSampleRate float64 `yaml:"serverSampleRate" env:"SENTRY_SERVER_SAMPLE_RATE"`
ServerFlushTimeout time.Duration `yaml:"serverFlushTimeout" env:"SENTRY_SERVER_FLUSH_TIMEOUT"`
Environment string `yaml:"environment" env:"SENTRY_ENVIRONMENT"`
}
func NewDumpDefault() *Config {
config := NewDefault()
return config
@ -112,6 +149,82 @@ func NewDefault() *Config {
"user.Email endsWith 'cadoles.com'",
},
},
SMTP: SMTPConfig{
Host: "localhost",
Port: 2525,
User: "",
Password: "",
SenderAddress: "noreply@localhost",
SenderName: "noreply",
},
Task: TaskConfig{
Newsletter: NewsletterTaskConfig{
CronSpec: "0 9 * * 1",
TimeRange: 24 * 7 * time.Hour,
BaseURL: "http://localhost:8080",
ContentTemplate: dedent.Dedent(`
{{- $root := . -}}
Bonjour{{if .User.Name}} {{ .User.Name }}{{end}},
{{ if not .HasEvents -}}
Aucun évènement notoire ces derniers jours.
{{ else -}}
Voici les évènements de ces derniers jours:
{{- end}}
{{- with .ReadyToVote }}
Dossiers récemment prêts à voter
--------------------------------
{{range . -}}
- "{{ .Title }}" - {{ $root.BaseURL }}/decisions/{{ .ID }} - créé le {{ .CreatedAt.Format "02/01/2006" }}
{{ end }}
{{- end}}
{{- with .Voted }}
Récemment votés
---------------
{{range . -}}
- "{{ .Title }}" - {{ $root.BaseURL }}/decisions/{{ .ID }} - voté le {{ .VotedAt.Format "02/01/2006" }}
{{ end }}
{{- end}}
{{- with .NewDecisionSupportFiles }}
Nouveaux dossiers d'aide à la décision
--------------------------------------
{{range . -}}
- "{{ .Title }}" - {{ $root.BaseURL }}/decisions/{{ .ID }} - créé le {{ .CreatedAt.Format "02/01/2006" }}
{{ end }}
{{- end}}
{{- with .NewWorkgroups}}
Nouveaux groupes de travail
---------------------------
{{range . -}}
- "{{ .Name }}" - {{ $root.BaseURL }}/workgroups/{{ .ID }} - créé le {{ .CreatedAt.Format "02/01/2006" }}
{{ end }}
{{- end}}
Bonne semaine,
Daddy
`),
SubjectTemplate: `[Daddy] Évènements du {{ .From.Format "02/01/2006" }} au {{ .To.Format "02/01/2006" }}`,
},
},
Sentry: SentryConfig{
DSN: "",
ServerSampleRate: 1,
ServerFlushTimeout: 2 * time.Second,
Environment: "",
},
}
}

View File

@ -2,7 +2,7 @@ package graph
import (
"context"
"encoding/json"
"reflect"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"gitlab.com/wpetit/goweb/middleware/container"
@ -12,6 +12,11 @@ import (
)
func handleCreateDecisionSupportFile(ctx context.Context, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
user, db, err := getSessionUser(ctx)
if err != nil {
return nil, errs.WithStack(err)
}
authorized, err := isAuthorized(ctx, &model.DecisionSupportFile{}, model.ActionCreate)
if err != nil {
return nil, errs.WithStack(err)
@ -21,9 +26,6 @@ func handleCreateDecisionSupportFile(ctx context.Context, changes *model.Decisio
return nil, errs.WithStack(ErrForbidden)
}
ctn := container.Must(ctx)
db := orm.Must(ctn).DB()
repo := model.NewDSFRepository(db)
dsf, err := repo.Create(ctx, changes)
@ -31,21 +33,29 @@ func handleCreateDecisionSupportFile(ctx context.Context, changes *model.Decisio
return nil, errs.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if _, err := eventRepo.Add(ctx, user, model.EventTypeCreated, dsf); err != nil {
return nil, errs.WithStack(err)
}
return dsf, nil
}
func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
ctn := container.Must(ctx)
db := orm.Must(ctn).DB()
repo := model.NewDSFRepository(db)
dsf, err := repo.Find(ctx, id)
user, db, err := getSessionUser(ctx)
if err != nil {
return nil, errs.WithStack(err)
}
authorized, err := isAuthorized(ctx, dsf, model.ActionUpdate)
repo := model.NewDSFRepository(db)
prevDsf, err := repo.Find(ctx, id)
if err != nil {
return nil, errs.WithStack(err)
}
authorized, err := isAuthorized(ctx, prevDsf, model.ActionUpdate)
if err != nil {
return nil, errs.WithStack(err)
}
@ -54,11 +64,42 @@ func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *mo
return nil, errs.WithStack(ErrForbidden)
}
dsf, err = repo.Update(ctx, id, changes)
dsf, err := repo.Update(ctx, id, changes)
if err != nil {
return nil, errs.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if changes != nil && changes.Status != nil && prevDsf.Status != *changes.Status {
switch *changes.Status {
case model.StatusVoted:
if _, err := eventRepo.Add(ctx, user, model.EventTypeVoted, dsf); err != nil {
return nil, errs.WithStack(err)
}
case model.StatusClosed:
if _, err := eventRepo.Add(ctx, user, model.EventTypeClosed, dsf); err != nil {
return nil, errs.WithStack(err)
}
default:
if _, err := eventRepo.Add(ctx, user, model.EventTypeStatusChanged, dsf); err != nil {
return nil, errs.WithStack(err)
}
}
}
if changes != nil && changes.Title != nil && prevDsf.Title != *changes.Title {
if _, err := eventRepo.Add(ctx, user, model.EventTypeTitleChanged, dsf); err != nil {
return nil, errs.WithStack(err)
}
}
if changes != nil && !reflect.DeepEqual(prevDsf.Sections, dsf.Sections) {
if _, err := eventRepo.Add(ctx, user, model.EventTypeUpdated, dsf); err != nil {
return nil, errs.WithStack(err)
}
}
return dsf, nil
}
@ -88,13 +129,3 @@ func handleDecisionSupportFiles(ctx context.Context, filter *model.DecisionSuppo
return dsfs, nil
}
func handleSections(ctx context.Context, dsf *model.DecisionSupportFile) (map[string]interface{}, error) {
sections := make(map[string]interface{})
if err := json.Unmarshal(dsf.Sections.RawMessage, &sections); err != nil {
return nil, errs.WithStack(err)
}
return sections, nil
}

View File

@ -0,0 +1,24 @@
package graph
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/model"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
errs "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/middleware/container"
)
func handleEvents(ctx context.Context, filter *model.EventFilter) ([]*model.Event, error) {
ctn := container.Must(ctx)
db := orm.Must(ctn).DB()
repo := model.NewEventRepository(db)
events, err := repo.Search(ctx, filter)
if err != nil {
return nil, errs.WithStack(err)
}
return events, nil
}

View File

@ -18,6 +18,16 @@ type Workgroup {
members: [User]!
}
type Event {
id: ID!
type: String!
createdAt: Time!
updatedAt: Time!
objectId: ID!
objectType: String!
user: User!
}
input WorkgroupsFilter {
ids: [ID]
}
@ -36,6 +46,7 @@ type DecisionSupportFile {
input DecisionSupportFileFilter {
ids: [ID]
workgroups: [ID]
}
input AuthorizationObject {
@ -44,9 +55,19 @@ input AuthorizationObject {
decisionSupportFileId: ID
}
input EventFilter {
objectType: String
objectId: ID
userId: ID
type: String
from: Time
to: Time
}
type Query {
userProfile: User
workgroups(filter: WorkgroupsFilter): [Workgroup]!
decisionSupportFiles(filter: DecisionSupportFileFilter): [DecisionSupportFile]!
events(filter: EventFilter): [Event]!
isAuthorized(action: String!, object: AuthorizationObject!): Boolean!
}

View File

@ -15,8 +15,16 @@ func (r *decisionSupportFileResolver) ID(ctx context.Context, obj *model1.Decisi
return strconv.FormatUint(uint64(obj.ID), 10), nil
}
func (r *decisionSupportFileResolver) Sections(ctx context.Context, obj *model1.DecisionSupportFile) (map[string]interface{}, error) {
return handleSections(ctx, obj)
func (r *eventResolver) ID(ctx context.Context, obj *model1.Event) (string, error) {
return strconv.FormatUint(uint64(obj.ID), 10), nil
}
func (r *eventResolver) Type(ctx context.Context, obj *model1.Event) (string, error) {
return string(obj.Type), nil
}
func (r *eventResolver) ObjectID(ctx context.Context, obj *model1.Event) (string, error) {
return strconv.FormatUint(uint64(obj.ObjectID), 10), nil
}
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
@ -31,6 +39,10 @@ func (r *queryResolver) DecisionSupportFiles(ctx context.Context, filter *model1
return handleDecisionSupportFiles(ctx, filter)
}
func (r *queryResolver) Events(ctx context.Context, filter *model1.EventFilter) ([]*model1.Event, error) {
return handleEvents(ctx, filter)
}
func (r *queryResolver) IsAuthorized(ctx context.Context, action string, object model1.AuthorizationObject) (bool, error) {
return handleIsAuthorized(ctx, action, object)
}
@ -48,6 +60,9 @@ func (r *Resolver) DecisionSupportFile() generated.DecisionSupportFileResolver {
return &decisionSupportFileResolver{r}
}
// Event returns generated.EventResolver implementation.
func (r *Resolver) Event() generated.EventResolver { return &eventResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
@ -58,6 +73,17 @@ func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }
func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} }
type decisionSupportFileResolver struct{ *Resolver }
type eventResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type userResolver struct{ *Resolver }
type workgroupResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *decisionSupportFileResolver) Sections(ctx context.Context, obj *model1.DecisionSupportFile) (map[string]interface{}, error) {
return obj.Sections, nil
}

View File

@ -72,6 +72,12 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor
return nil, errors.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if _, err := eventRepo.Add(ctx, user, model.EventTypeJoined, workgroup); err != nil {
return nil, errors.WithStack(err)
}
return workgroup, nil
}
@ -102,6 +108,12 @@ func handleLeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workg
return nil, errors.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if _, err := eventRepo.Add(ctx, user, model.EventTypeLeaved, workgroup); err != nil {
return nil, errors.WithStack(err)
}
return workgroup, nil
}
@ -115,7 +127,7 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
return nil, errs.WithStack(ErrForbidden)
}
db, err := getDB(ctx)
user, db, err := getSessionUser(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
@ -127,11 +139,17 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
return nil, errors.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if _, err := eventRepo.Add(ctx, user, model.EventTypeCreated, workgroup); err != nil {
return nil, errors.WithStack(err)
}
return workgroup, nil
}
func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
db, err := getDB(ctx)
user, db, err := getSessionUser(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
@ -157,11 +175,17 @@ func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workg
return nil, errors.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if _, err := eventRepo.Add(ctx, user, model.EventTypeClosed, workgroup); err != nil {
return nil, errors.WithStack(err)
}
return workgroup, nil
}
func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
db, err := getDB(ctx)
user, db, err := getSessionUser(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
@ -187,5 +211,11 @@ func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes mode
return nil, errors.WithStack(err)
}
eventRepo := model.NewEventRepository(db)
if _, err := eventRepo.Add(ctx, user, model.EventTypeUpdated, workgroup); err != nil {
return nil, errors.WithStack(err)
}
return workgroup, nil
}

52
internal/mail/mailer.go Normal file
View File

@ -0,0 +1,52 @@
package mail
const (
ContentTypeHTML = "text/html"
ContentTypeText = "text/plain"
)
type Option struct {
Host string
Port int
User string
Password string
InsecureSkipVerify bool
UseStartTLS bool
}
type OptionFunc func(*Option)
type Mailer struct {
opt *Option
}
func WithTLS(useStartTLS, insecureSkipVerify bool) OptionFunc {
return func(opt *Option) {
opt.UseStartTLS = useStartTLS
opt.InsecureSkipVerify = insecureSkipVerify
}
}
func WithServer(host string, port int) OptionFunc {
return func(opt *Option) {
opt.Host = host
opt.Port = port
}
}
func WithCredentials(user, password string) OptionFunc {
return func(opt *Option) {
opt.User = user
opt.Password = password
}
}
func NewMailer(funcs ...OptionFunc) *Mailer {
opt := &Option{}
for _, fn := range funcs {
fn(opt)
}
return &Mailer{opt}
}

11
internal/mail/provider.go Normal file
View File

@ -0,0 +1,11 @@
package mail
import "gitlab.com/wpetit/goweb/service"
func ServiceProvider(opts ...OptionFunc) service.Provider {
mailer := NewMailer(opts...)
return func(ctn *service.Container) (interface{}, error) {
return mailer, nil
}
}

207
internal/mail/send.go Normal file
View File

@ -0,0 +1,207 @@
package mail
import (
"crypto/tls"
"fmt"
"math/rand"
"net/mail"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
gomail "gopkg.in/mail.v2"
)
var (
ErrUnexpectedEmailAddressFormat = errors.New("unexpected email address format")
)
type SendFunc func(*SendOption)
type SendOption struct {
Charset string
AddressHeaders []AddressHeader
Headers []Header
Body Body
AlternativeBodies []Body
}
type AddressHeader struct {
Field string
Address string
Name string
}
type Header struct {
Field string
Values []string
}
type Body struct {
Type string
Content string
PartSetting gomail.PartSetting
}
func WithCharset(charset string) func(*SendOption) {
return func(opt *SendOption) {
opt.Charset = charset
}
}
func WithSender(address string, name string) func(*SendOption) {
return WithAddressHeader("From", address, name)
}
func WithSubject(subject string) func(*SendOption) {
return WithHeader("Subject", subject)
}
func WithAddressHeader(field, address, name string) func(*SendOption) {
return func(opt *SendOption) {
opt.AddressHeaders = append(opt.AddressHeaders, AddressHeader{field, address, name})
}
}
func WithHeader(field string, values ...string) func(*SendOption) {
return func(opt *SendOption) {
opt.Headers = append(opt.Headers, Header{field, values})
}
}
func WithRecipients(addresses ...string) func(*SendOption) {
return WithHeader("To", addresses...)
}
func WithCopies(addresses ...string) func(*SendOption) {
return WithHeader("Cc", addresses...)
}
func WithInvisibleCopies(addresses ...string) func(*SendOption) {
return WithHeader("Cci", addresses...)
}
func WithBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
return func(opt *SendOption) {
if setting == nil {
setting = gomail.SetPartEncoding(gomail.Unencoded)
}
opt.Body = Body{contentType, content, setting}
}
}
func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
return func(opt *SendOption) {
if setting == nil {
setting = gomail.SetPartEncoding(gomail.Unencoded)
}
opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting})
}
}
func (m *Mailer) Send(funcs ...SendFunc) error {
opt := &SendOption{
Charset: "UTF-8",
Body: Body{
Type: "text/plain",
Content: "",
PartSetting: gomail.SetPartEncoding(gomail.Unencoded),
},
AddressHeaders: make([]AddressHeader, 0),
Headers: make([]Header, 0),
AlternativeBodies: make([]Body, 0),
}
for _, f := range funcs {
f(opt)
}
conn, err := m.openConnection()
if err != nil {
return errors.Wrap(err, "could not open connection")
}
defer conn.Close()
message := gomail.NewMessage(gomail.SetCharset(opt.Charset))
for _, h := range opt.AddressHeaders {
message.SetAddressHeader(h.Field, h.Address, h.Name)
}
for _, h := range opt.Headers {
message.SetHeader(h.Field, h.Values...)
}
froms := message.GetHeader("From")
var sendDomain string
if len(froms) > 0 {
sendDomain, err = extractEmailDomain(froms[0])
if err != nil {
return err
}
}
messageID := generateMessageID(sendDomain)
message.SetHeader("Message-Id", messageID)
message.SetBody(opt.Body.Type, opt.Body.Content, opt.Body.PartSetting)
for _, b := range opt.AlternativeBodies {
message.AddAlternative(b.Type, b.Content, b.PartSetting)
}
if err := gomail.Send(conn, message); err != nil {
return errors.Wrap(err, "could not send message")
}
return nil
}
func (m *Mailer) openConnection() (gomail.SendCloser, error) {
dialer := gomail.NewDialer(
m.opt.Host,
m.opt.Port,
m.opt.User,
m.opt.Password,
)
if m.opt.InsecureSkipVerify {
dialer.TLSConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
conn, err := dialer.Dial()
if err != nil {
return nil, errors.Wrap(err, "could not dial smtp server")
}
return conn, nil
}
func extractEmailDomain(email string) (string, error) {
address, err := mail.ParseAddress(email)
if err != nil {
return "", errors.Wrapf(err, "could not parse email address '%s'", email)
}
addressParts := strings.SplitN(address.Address, "@", 2)
if len(addressParts) != 2 { // nolint: gomnd
return "", errors.WithStack(ErrUnexpectedEmailAddressFormat)
}
domain := addressParts[1]
return domain, nil
}
func generateMessageID(domain string) string {
// Based on https://www.jwz.org/doc/mid.html
timestamp := strconv.FormatInt(time.Now().UnixNano(), 36)
random := strconv.FormatInt(rand.Int63(), 36)
return fmt.Sprintf("<%s.%s@%s>", timestamp, random, domain)
}

33
internal/mail/service.go Normal file
View File

@ -0,0 +1,33 @@
package mail
import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/service"
)
const ServiceName service.Name = "mail"
// From retrieves the mail service in the given container
func From(container *service.Container) (*Mailer, error) {
service, err := container.Service(ServiceName)
if err != nil {
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
}
srv, ok := service.(*Mailer)
if !ok {
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
}
return srv, nil
}
// Must retrieves the mail service in the given container or panic otherwise
func Must(container *service.Container) *Mailer {
srv, err := From(container)
if err != nil {
panic(err)
}
return srv
}

View File

@ -1,19 +1,62 @@
package model
import (
"encoding/json"
"time"
"github.com/jinzhu/gorm"
"github.com/jinzhu/gorm/dialects/postgres"
errs "github.com/pkg/errors"
)
const ObjectTypeDecisionSupportFile = "dsf"
const (
StatusDraft = "draft"
StatusReady = "ready"
StatusVoted = "voted"
StatusClosed = "closed"
)
type DecisionSupportFile struct {
gorm.Model
Title string `json:"title"`
Sections postgres.Jsonb `json:"sections"`
Status string `json:"status"`
WorkgroupID uint `json:"-"`
Workgroup *Workgroup `json:"workgroup"`
VotedAt time.Time `json:"votedAt"`
ClosedAt time.Time `json:"closedAt"`
Title string `json:"title"`
SectionsJSON postgres.Jsonb `json:"-" gorm:"column:sections;"`
Sections map[string]interface{} `gorm:"-"`
Status string `json:"status"`
WorkgroupID uint `json:"-"`
Workgroup *Workgroup `json:"workgroup" gorm:"association_autoupdate:false"`
VotedAt time.Time `json:"votedAt"`
ClosedAt time.Time `json:"closedAt"`
}
func (f *DecisionSupportFile) ObjectID() uint {
return f.ID
}
func (f *DecisionSupportFile) ObjectType() string {
return ObjectTypeDecisionSupportFile
}
func (f *DecisionSupportFile) BeforeSave() error {
rawSections, err := json.Marshal(f.Sections)
if err != nil {
return errs.WithStack(err)
}
f.SectionsJSON = postgres.Jsonb{RawMessage: rawSections}
return nil
}
func (f *DecisionSupportFile) AfterFind() (err error) {
sections := make(map[string]interface{})
if err := json.Unmarshal(f.SectionsJSON.RawMessage, &sections); err != nil {
return errs.WithStack(err)
}
f.Sections = sections
return nil
}

View File

@ -2,11 +2,10 @@ package model
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/jinzhu/gorm"
"github.com/jinzhu/gorm/dialects/postgres"
errs "github.com/pkg/errors"
)
@ -69,12 +68,7 @@ func (r *DSFRepository) updateFromChanges(dsf *DecisionSupportFile, changes *Dec
dsf.Workgroup = wg
if changes.Sections != nil {
rawSections, err := json.Marshal(changes.Sections)
if err != nil {
return errs.WithStack(err)
}
dsf.Sections = postgres.Jsonb{RawMessage: rawSections}
dsf.Sections = changes.Sections
}
if changes.Title != nil {
@ -82,6 +76,14 @@ func (r *DSFRepository) updateFromChanges(dsf *DecisionSupportFile, changes *Dec
}
if changes.Status != nil {
if dsf.Status != StatusVoted && *changes.Status == StatusVoted {
dsf.VotedAt = time.Now().UTC()
}
if *changes.Status == StatusClosed {
dsf.VotedAt = time.Time{}
}
dsf.Status = *changes.Status
}
@ -106,6 +108,10 @@ func (r *DSFRepository) Search(ctx context.Context, filter *DecisionSupportFileF
if filter.Ids != nil {
query = query.Where("id in (?)", filter.Ids)
}
if filter.Workgroups != nil {
query = query.Where("workgroup_id in (?)", filter.Workgroups)
}
}
dsfs := make([]*DecisionSupportFile, 0)

View File

@ -31,6 +31,10 @@ func (v *DecisionSupportFileVoter) Vote(ctx context.Context, subject interface{}
case ActionRead:
return voter.Allow, nil
case ActionUpdate:
if dsf.Status == StatusClosed || dsf.Status == StatusVoted {
return voter.Deny, nil
}
if inWorkgroup(user, dsf.Workgroup) {
return voter.Allow, nil
}

32
internal/model/event.go Normal file
View File

@ -0,0 +1,32 @@
package model
import (
"github.com/jinzhu/gorm"
)
type EventType string
const (
EventTypeCreated EventType = "created"
EventTypeUpdated EventType = "updated"
EventTypeLeaved EventType = "leaved"
EventTypeJoined EventType = "joined"
EventTypeClosed EventType = "closed"
EventTypeStatusChanged EventType = "status-changed"
EventTypeTitleChanged EventType = "title-changed"
EventTypeVoted EventType = "voted"
)
type EventObject interface {
ObjectID() uint
ObjectType() string
}
type Event struct {
gorm.Model
UserID uint `json:"-"`
User *User `json:"user" gorm:"association_autoupdate:false"`
ObjectType string `json:"objectType"`
ObjectID uint `json:"objectId"`
Type EventType `json:"type"`
}

View File

@ -0,0 +1,73 @@
package model
import (
"context"
"github.com/jinzhu/gorm"
errs "github.com/pkg/errors"
)
type EventRepository struct {
db *gorm.DB
}
func (r *EventRepository) Add(ctx context.Context, user *User, eventType EventType, obj EventObject) (*Event, error) {
evt := &Event{
Type: eventType,
User: user,
ObjectID: obj.ObjectID(),
ObjectType: obj.ObjectType(),
}
if err := r.db.Save(&evt).Error; err != nil {
return nil, errs.WithStack(err)
}
return evt, nil
}
func (r *EventRepository) Search(ctx context.Context, filter *EventFilter) ([]*Event, error) {
query := r.db.Model(&Event{}).Preload("User")
if filter == nil {
filter = &EventFilter{}
}
if filter.ObjectID != nil {
query = query.Where("object_id = ?", filter.ObjectID)
}
if filter.ObjectType != nil {
query = query.Where("object_type = ?", filter.ObjectType)
}
if filter.UserID != nil {
query = query.Where("user_id = ?", filter.UserID)
}
if filter.Type != nil {
query = query.Where("type = ?", filter.Type)
}
if filter.From != nil {
query = query.Where("created_at >= ?", filter.From)
}
if filter.To != nil {
query = query.Where("created_at <= ?", filter.To)
}
query = query.Order("created_at DESC")
events := make([]*Event, 0)
if err := query.Find(&events).Error; err != nil {
return nil, errs.WithStack(err)
}
return events, nil
}
func NewEventRepository(db *gorm.DB) *EventRepository {
return &EventRepository{db}
}

View File

@ -11,7 +11,7 @@ type User struct {
Name *string `json:"name"`
Email string `json:"email" gorm:"unique;not null"`
ConnectedAt time.Time `json:"connectedAt"`
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"`
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;association_autoupdate:false"`
}
type ProfileChanges struct {

View File

@ -80,6 +80,17 @@ func (r *UserRepository) Find(ctx context.Context, id string) (*User, error) {
return user, nil
}
func (r *UserRepository) All(ctx context.Context) ([]*User, error) {
users := make([]*User, 0)
query := r.db.Model(&User{})
if err := query.Find(&users).Error; err != nil {
return nil, errs.WithStack(err)
}
return users, nil
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db}
}

View File

@ -6,11 +6,21 @@ import (
"github.com/jinzhu/gorm"
)
const ObjectTypeWorkgroup = "workgroup"
type Workgroup struct {
gorm.Model
Name *string `json:"name"`
ClosedAt time.Time `json:"closedAt"`
Members []*User `gorm:"many2many:users_workgroups;"`
Members []*User `gorm:"many2many:users_workgroups;association_autoupdate:false"`
}
func (w *Workgroup) ObjectID() uint {
return w.ID
}
func (w *Workgroup) ObjectType() string {
return ObjectTypeWorkgroup
}
type WorkgroupChanges struct {

View File

@ -106,29 +106,35 @@ func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, wo
}
func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
user := &User{}
err := r.db.First(user, "id = ?", userID).Error
if err != nil {
return nil, errors.Wrap(err, "could not find user")
}
workgroup := &Workgroup{}
workgroup.ID = workgroupID
err = r.db.Model(user).
Association("Workgroups").
Delete(workgroup).
Error
err := r.db.Transaction(func(tx *gorm.DB) error {
user := &User{}
err := tx.First(user, "id = ?", userID).Error
if err != nil {
return errors.Wrap(err, "could not find user")
}
if err != nil {
return nil, errors.Wrap(err, "could not add user to workgroup")
}
err = tx.Model(user).
Association("Workgroups").
Delete(workgroup).
Error
err = r.db.Model(workgroup).
Preload("Members").
First(workgroup, "id = ?", workgroupID).
Error
if err != nil {
return errors.Wrap(err, "could not add user to workgroup")
}
err = tx.Model(workgroup).
Preload("Members").
First(workgroup, "id = ?", workgroupID).
Error
if err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}

View File

@ -1,7 +1,6 @@
package route
import (
"fmt"
"net/http"
"forge.cadoles.com/Cadoles/daddy/internal/auth"
@ -80,11 +79,8 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
}
if !authorized {
message := fmt.Sprintf(
"You are not authorized to access this application. Disconnect by navigating to %s.",
"http://"+r.Host+"/logout",
)
http.Error(w, message, http.StatusForbidden)
redirectURL := conf.HTTP.FrontendURL + "/unauthorized"
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
return
}

View File

@ -1,10 +1,15 @@
package route
import (
"context"
"net/http"
"path"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/getsentry/sentry-go"
"github.com/pkg/errors"
"forge.cadoles.com/Cadoles/daddy/internal/config"
"forge.cadoles.com/Cadoles/daddy/internal/graph"
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
@ -44,6 +49,19 @@ func Mount(r *chi.Mux, config *config.Config) error {
generated.NewExecutableSchema(gqlConfig),
)
gql.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
// Dispatch error to Sentry
if realErr, ok := err.(error); ok {
sentry.CaptureException(realErr)
}
if strErr, ok := err.(string); ok {
sentry.CaptureException(errors.New(strErr))
}
return graphql.DefaultRecover(ctx, err)
})
gql.AddTransport(transport.POST{})
gql.AddTransport(&transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
@ -72,10 +90,18 @@ func Mount(r *chi.Mux, config *config.Config) error {
}
// List of paths handled directly by the client
r.Get("/workgroups/*", serveClientIndex)
r.Get("/profile", serveClientIndex)
r.Get("/dashboard", serveClientIndex)
r.Get("/decisions/*", serveClientIndex)
clientRoutes := []string{
"/workgroups/*",
"/profile",
"/dashboard",
"/decisions/*",
"/unauthorized",
"/conference",
}
for _, cr := range clientRoutes {
r.Get(cr, serveClientIndex)
}
// Serve static files
notFoundHandler := r.NotFoundHandler()

219
internal/task/newsletter.go Normal file
View File

@ -0,0 +1,219 @@
package task
import (
"bytes"
"context"
"fmt"
"text/template"
"time"
"forge.cadoles.com/Cadoles/daddy/internal/mail"
"forge.cadoles.com/Cadoles/daddy/internal/model"
"github.com/pkg/errors"
"forge.cadoles.com/Cadoles/daddy/internal/orm"
"gitlab.com/wpetit/goweb/middleware/container"
"gitlab.com/wpetit/goweb/logger"
)
type Newsletter struct {
ctx context.Context
timeRange time.Duration
baseURL string
contentTemplate string
subjectTemplate string
from string
}
func (t *Newsletter) Name() string {
return "newsletter"
}
func (t *Newsletter) Run() {
ctx := t.ctx
logger.Info(ctx, "preparing newsletter", logger.F("timeRange", t.timeRange))
contentTmpl, err := template.New("").Parse(t.contentTemplate)
if err != nil {
logger.Error(ctx, "could not parse newsletter content template", logger.E(errors.WithStack(err)))
return
}
subjectTmpl, err := template.New("").Parse(t.subjectTemplate)
if err != nil {
logger.Error(ctx, "could not parse newsletter subject template", logger.E(errors.WithStack(err)))
return
}
ctn := container.Must(ctx)
orm := orm.Must(ctn)
db := orm.DB()
mailSrv := mail.Must(ctn)
eventRepo := model.NewEventRepository(db)
to := time.Now()
from := to.Add(-t.timeRange)
events, err := eventRepo.Search(ctx, &model.EventFilter{
From: &from,
To: &to,
})
if err != nil {
logger.Error(ctx, "could not search events", logger.E(errors.WithStack(err)))
return
}
newWorkgroups := make([]*model.Workgroup, 0)
newDecisionSupportFiles := make([]*model.DecisionSupportFile, 0)
readyToVote := make([]*model.DecisionSupportFile, 0)
voted := make([]*model.DecisionSupportFile, 0)
workgroupRepo := model.NewWorkgroupRepository(db)
dsfRepo := model.NewDSFRepository(db)
for _, evt := range events {
switch {
case evt.Type == model.EventTypeCreated && evt.ObjectType == model.ObjectTypeWorkgroup:
workgroup, err := workgroupRepo.Find(ctx, fmt.Sprintf("%d", evt.ObjectID))
if err != nil {
logger.Error(
ctx, "could not find workgroup",
logger.E(errors.WithStack(err)),
logger.F("id", evt.ObjectID),
)
return
}
newWorkgroups = append(newWorkgroups, workgroup)
case evt.Type == model.EventTypeCreated && evt.ObjectType == model.ObjectTypeDecisionSupportFile:
dsf, err := dsfRepo.Find(ctx, fmt.Sprintf("%d", evt.ObjectID))
if err != nil {
logger.Error(
ctx, "could not find decision support file",
logger.E(errors.WithStack(err)),
logger.F("id", evt.ObjectID),
)
return
}
newDecisionSupportFiles = append(newDecisionSupportFiles, dsf)
case evt.Type == model.EventTypeStatusChanged && evt.ObjectType == model.ObjectTypeDecisionSupportFile:
dsf, err := dsfRepo.Find(ctx, fmt.Sprintf("%d", evt.ObjectID))
if err != nil {
logger.Error(
ctx, "could not find decision support file",
logger.E(errors.WithStack(err)),
logger.F("id", evt.ObjectID),
)
return
}
if dsf.Status == model.StatusReady {
readyToVote = append(readyToVote, dsf)
}
if dsf.Status == model.StatusVoted {
voted = append(voted, dsf)
}
}
}
hasEvents := len(newDecisionSupportFiles) > 0 || len(newWorkgroups) > 0
userRepo := model.NewUserRepository(db)
users, err := userRepo.All(ctx)
if err != nil {
logger.Error(ctx, "could not find users", logger.E(errors.WithStack(err)))
return
}
var (
contentBuff bytes.Buffer
subjectBuff bytes.Buffer
)
for _, u := range users {
tmplData := struct {
User *model.User
NewWorkgroups []*model.Workgroup
NewDecisionSupportFiles []*model.DecisionSupportFile
ReadyToVote []*model.DecisionSupportFile
Voted []*model.DecisionSupportFile
BaseURL string
From time.Time
To time.Time
HasEvents bool
}{
User: u,
BaseURL: t.baseURL,
NewWorkgroups: newWorkgroups,
NewDecisionSupportFiles: newDecisionSupportFiles,
ReadyToVote: readyToVote,
Voted: voted,
From: from.Local(),
To: to.Local(),
HasEvents: hasEvents,
}
err = contentTmpl.Execute(&contentBuff, tmplData)
if err != nil {
logger.Error(ctx, "could not execute newsletter content template", logger.E(errors.WithStack(err)))
return
}
err = subjectTmpl.Execute(&subjectBuff, tmplData)
if err != nil {
logger.Error(ctx, "could not execute newsletter subject template", logger.E(errors.WithStack(err)))
return
}
newsletterContent := contentBuff.String()
newsletterSubject := subjectBuff.String()
err := mailSrv.Send(
mail.WithRecipients(u.Email),
mail.WithSubject(newsletterSubject),
mail.WithSender(t.from, ""),
mail.WithBody(mail.ContentTypeText, newsletterContent, nil),
)
if err != nil {
logger.Error(
ctx, "could not send newsletter",
logger.E(errors.WithStack(err)),
logger.F("email", u.Email),
)
return
}
contentBuff.Reset()
subjectBuff.Reset()
}
}
func NewNewsletter(ctx context.Context, timeRange time.Duration, baseURL, contentTemplate, subjectTemplate, from string) *Newsletter {
return &Newsletter{
ctx: ctx,
timeRange: timeRange,
baseURL: baseURL,
contentTemplate: contentTemplate,
subjectTemplate: subjectTemplate,
from: from,
}
}

6
internal/task/task.go Normal file
View File

@ -0,0 +1,6 @@
package task
type Task interface {
Name() string
Run()
}

View File

@ -19,6 +19,7 @@ modd.conf {
prep: make test
}
client/webpack.config.js
{
daemon: cd client && NODE_ENV=development npm run server -- --display=minimal
}