From c421c86f21c7fb9a1f0558d0a440992f070510f6 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 29 May 2023 17:39:36 -0400
Subject: [PATCH 01/99] =?UTF-8?q?Ajouter=20license=20manquante=20=C3=A0=20?=
 =?UTF-8?q?v4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 v4/LICENSE | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/v4/LICENSE b/v4/LICENSE
index e69de29..018068b 100644
--- a/v4/LICENSE
+++ b/v4/LICENSE
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2021 AGECEM
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

From 9a0bf87e7bc27af08f4c6c4b4a2dba55ab296e11 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 29 May 2023 18:19:31 -0400
Subject: [PATCH 02/99] Bump root version to v4

Remove all files from v3

Move all files from v4/ to project root
---
 v4/.cobra.yaml => .cobra.yaml                 |   0
 .dockerignore                                 |   3 -
 Dockerfile                                    |  26 +-
 Makefile                                      |  11 -
 README.md                                     | 108 +---
 bottin/bottin.go                              | 178 ------
 {v4/cmd => cmd}/api.go                        |   0
 cmd/import.go                                 |  53 --
 cmd/root.go                                   |  32 +-
 cmd/server.go                                 | 145 -----
 {v4/cmd => cmd}/web.go                        |   0
 {v4/data => data}/apiclient.go                |   0
 data/data.go                                  | 314 +++++++++--
 v4/docker-compose.yaml => docker-compose.yaml |   4 +-
 docker-compose.yml                            |  17 -
 embed/embed.go                                |  10 -
 embed/html/index.html                         |  29 -
 examples/bottin.yaml                          |  14 -
 examples/membres-test.json                    |   8 -
 go.mod                                        |  46 +-
 go.sum                                        | 517 +++--------------
 {v4/handlers => handlers}/insert.go           |   0
 {v4/handlers => handlers}/read.go             |   0
 {v4/handlers => handlers}/seed.go             |   0
 {v4/handlers => handlers}/update.go           |   0
 {v4/handlers => handlers}/v4.go               |   0
 main.go                                       |  23 +-
 {v4/models => models}/models.go               |   0
 v4/.env                                       |   3 -
 v4/Dockerfile                                 |  25 -
 v4/LICENSE                                    |   9 -
 v4/README.md                                  |  49 --
 v4/cmd/root.go                                |  56 --
 v4/data/data.go                               | 292 ----------
 v4/go.mod                                     |  43 --
 v4/go.sum                                     | 531 ------------------
 v4/main.go                                    |   7 -
 {v4/web => web}/embed.go                      |   0
 {v4/web => web}/templates/index.html          |   0
 {v4/web => web}/webhandlers/handlers.go       |   0
 40 files changed, 423 insertions(+), 2130 deletions(-)
 rename v4/.cobra.yaml => .cobra.yaml (100%)
 delete mode 100644 .dockerignore
 delete mode 100644 Makefile
 delete mode 100644 bottin/bottin.go
 rename {v4/cmd => cmd}/api.go (100%)
 delete mode 100644 cmd/import.go
 delete mode 100644 cmd/server.go
 rename {v4/cmd => cmd}/web.go (100%)
 rename {v4/data => data}/apiclient.go (100%)
 rename v4/docker-compose.yaml => docker-compose.yaml (89%)
 delete mode 100644 docker-compose.yml
 delete mode 100644 embed/embed.go
 delete mode 100644 embed/html/index.html
 delete mode 100644 examples/bottin.yaml
 delete mode 100644 examples/membres-test.json
 rename {v4/handlers => handlers}/insert.go (100%)
 rename {v4/handlers => handlers}/read.go (100%)
 rename {v4/handlers => handlers}/seed.go (100%)
 rename {v4/handlers => handlers}/update.go (100%)
 rename {v4/handlers => handlers}/v4.go (100%)
 rename {v4/models => models}/models.go (100%)
 delete mode 100644 v4/.env
 delete mode 100644 v4/Dockerfile
 delete mode 100644 v4/LICENSE
 delete mode 100644 v4/README.md
 delete mode 100644 v4/cmd/root.go
 delete mode 100644 v4/data/data.go
 delete mode 100644 v4/go.mod
 delete mode 100644 v4/go.sum
 delete mode 100644 v4/main.go
 rename {v4/web => web}/embed.go (100%)
 rename {v4/web => web}/templates/index.html (100%)
 rename {v4/web => web}/webhandlers/handlers.go (100%)

diff --git a/v4/.cobra.yaml b/.cobra.yaml
similarity index 100%
rename from v4/.cobra.yaml
rename to .cobra.yaml
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 084f48f..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,3 +0,0 @@
-db
-Dockerfile
-.dockerignore
diff --git a/Dockerfile b/Dockerfile
index b7e52d5..805727f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,25 @@
-FROM golang:1.19
+FROM golang:1.20.2 as build
 
-LABEL author="Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>"
-LABEL repo="https://git.agecem.com/agecem/bottin"
+LABEL author="vlbeaudoin"
 
 WORKDIR /go/src/app
 
-COPY . .
+COPY go.mod go.sum main.go ./
 
-ENV PATH=/go/src/app:$PATH
+ADD cmd/ cmd/
+ADD data/ data/
+ADD handlers/ handlers/
+ADD models/ models/
+ADD web/ web/
 
-RUN go get -d -v . && \
-    go install -v .
+RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin .
 
-CMD bottin -h
+# Alpine
+
+FROM alpine:latest
+
+WORKDIR /app
+
+COPY --from=build /go/src/app/bottin /usr/bin/bottin
+
+CMD ["bottin", "--help"]
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 3f0e7a5..0000000
--- a/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-# SHELL = /bin/sh
-
-.DEFAULT_GOAL := help
-
-.PHONY: help
-help: ## Show this help
-	@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
-
-.PHONY: build
-build: ## Build une image latest selon ./Dockerfile
-	@docker-compose build
diff --git a/README.md b/README.md
index 9c2dadc..c32ebe5 100644
--- a/README.md
+++ b/README.md
@@ -1,105 +1,49 @@
-# agecem/bottin
+# agecem/bottin/v4
 
-Bottin de la masse étudiante, codé en Go. (Migration d'une application legacy en php)
+Version 4 du bottin de la masse étudiante, en Go
 
-Permet d'utiliser et de (rudimentairement) mettre à jour une base de donnée
-(présentement uniquement `sqlite3`) de noms et de numéros étudiants.
+https://git.agecem.com/agecem/bottin
 
-Sert principalement à vérifier le statut de membre d'une personne en comparant avec son numéro étudiant du cégep.
+## fonctionalités
 
-Base pour les futures applications utilisant des bases de données similaires:
+### Serveur API
 
-  * Scan de présence pour Assemblée Générale (`agecem/bottin-ag`)
+- Insertion de membre et programme
+- Lecture de membre
+- Modification du nom d'usage de membre
 
-  * Distribution d'Agendas en début de session (`agecem/bottin-agendas)
+### Client web
 
-Inclue un `Dockerfile` pour déployer en tant que container.
+- Lecture de membre par requête au serveur API
 
-Inclue un `Makefile` pour l'exécution de tâches communes.
+## usage
 
-## Repo
+Remplir .env avec les infos qui seront utilisées pour déployer le container
 
-[agecem/bottin](https://git.agecem.com/agecem/bottin)
-
-## Utilisation
+(Remplacer `bottin` par quelque chose de plus sécuritaire)
 
 ```sh
-# Voir les options de make
-make help
-
-# Bâtir localement une container image sur docker
-make build
+BOTTIN_POSTGRES_DATABASE=bottin
+BOTTIN_POSTGRES_PASSWORD=bottin
+BOTTIN_POSTGRES_USER=bottin
 ```
 
-## Flags
+Déployer avec docker-compose
 
-L'application utilise un module pour afficher de l'information sur les différents flags existants.
+`$ docker-compose up -d`
 
-```sh
-# Pour voir l'aide de l'application
-bottin -h
-```
+Pour modifier la configuration du serveur API
 
-Exemple de résultat:
+`$ docker-compose exec -it api vi /etc/bottin/api.yaml`
 
-```
-Run the bottin server
+*Y remplir au minimum le champs `api.key` (string)*
 
-Usage:
-  bottin server [flags]
+Pour modifier la configuration du client web
 
-Flags:
-      --db-sqlite-path string     Path to sqlite database (config: 'db.sqlite.path')
-      --db-type string            Database type (config: 'db.type')
-  -h, --help                      help for server
-      --insert-batch-size int     The amount of inserts to do per batch (config: 'import.insert_batch_size') (default 500)
-      --json-insert-path string   The location of a json file containing Membres to insert.
-      --login-password string     The password to login to the web ui. (config: 'login.password') (default "bottin")
-      --login-username string     The username to login to the web ui. (config: 'login.username') (default "bottin")
-      --server-port int           The port on which the web application will server content (config: 'server.port') (default 1312)
+`$ docker-compose exec -it web vi /etc/bottin/web.yaml`
 
-Global Flags:
-      --config string   config file (default is $HOME/.bottin.yaml)
-```
+*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.password` (string)*
 
-## Procédure avec docker-compose
+Redémarrer les containers une fois la configuration modifiée
 
-Voir documentation de docker-compose
-
-`docker-compose -h`
-
-Démarrer container en mode detached
-
-`docker-compose up -d`
-
-Éteindre le container, sans toucher aux volumes
-
-`docker-compose down`
-
-Sur un container qui roule, utiliser `docker-compose run` pour mettre à jour la db (avec `--json-insert-path` ou éventuellement `bottin import`).
-
-`docker-compose run bottin bottin server --json-insert-path examples/membres-test.json --config /etc/bottin/bottin.yaml`
-
-## Exemples
-
-### Sans viper
-
-```sh
-# Servir l'application sur le port 8080, en spécifiant les credentials à utiliser pour y accéder ainsi que le type et chemin d'accès d'une database.
-bottin server --db-type 'sqlite' --db-sqlite-path '/chemin/vers/bottin-database.db' --login-username 'exemple' --login-password 'un_autre_mot_de_passe' --server-port 8080`
-
-# Servir l'application en spécifiant le chemin d'accès d'une database (sqlite3 ici) ainsi qu'une liste de Membres à insérer lors à la base de donnée lors du démarrage.
-#
-# Important: l'importation ne fait qu'un unmarshal du fichier json et aucune vérification supplémentaire, ni même une vérification d'insertion de doublons dans la db. Fonctionalité à utiliser calmement.
-bottin server --db-type 'sqlite' --db-sqlite-path '/chemin/vers/bottin-database.db' --json-insert-path '/chemin/vers/liste_de_membre.json'`
-```
-
-### Avec viper
-
-1. Ajouter config en suivant documentation dans `help, -h, --help`
-
-2. Appeler config avec flag global `--config /chemin/vers/config`.
-
-*Note: L'endroit par défaut de la config est `$HOME/.bottin.yaml`, mais un autre emplacement peut être utilisé si désiré. `Dockerfile` utilise `/etc/bottin/bottin.yaml` comme défaut.*
-
-La seule option qui n'est présentement pas disponible par config file est `--json-insert-path` afin de prévenir des doublons. Explication complète [dans la définition de flag](https://git.agecem.com/agecem/bottin/src/commit/0a3ba633cc2208cbfae1195770afcbdec9ace634/cmd/server.go#L99).
+`$ docker-compose down && docker-compose up -d`
diff --git a/bottin/bottin.go b/bottin/bottin.go
deleted file mode 100644
index cbe943c..0000000
--- a/bottin/bottin.go
+++ /dev/null
@@ -1,178 +0,0 @@
-package bottin
-
-import (
-	"crypto/subtle"
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"log"
-	"net/http"
-
-	"git.agecem.com/agecem/bottin/data"
-	"git.agecem.com/agecem/bottin/embed"
-	"github.com/labstack/echo/v4"
-	"github.com/labstack/echo/v4/middleware"
-	"github.com/spf13/viper"
-)
-
-var (
-	login_username, login_password         string
-	server_port                            int
-	db_host, db_user, db_password, db_name string
-	db_port                                int
-	db_type, db_path                       string
-	json_insert_path                       string
-	insert_batch_size                      int
-	html                                   string
-)
-
-// Funcs
-
-func init() {
-	html = embed.ReadHtml()
-}
-
-// JSON to []*data.Membre
-func obtenirUnmarshalJSON(path string) ([]*data.Membre, error) {
-	content, err := ioutil.ReadFile(path)
-	if err != nil {
-		return nil, err
-	}
-
-	var membres_insert []*data.Membre
-	err_json := json.Unmarshal([]byte(content), &membres_insert)
-	if err_json != nil {
-		return nil, err_json
-	}
-
-	return membres_insert, nil
-}
-
-func membreToJson(membre *data.Membre) ([]byte, error) {
-	membreJson, err := json.Marshal(membre)
-	return membreJson, err
-}
-
-// Import flags from viper
-func UpdateFlags() {
-	db_type = viper.GetString("db.type")
-	db_path = viper.GetString("db.sqlite.path")
-	server_port = viper.GetInt("server.port")
-	login_username = viper.GetString("login.username")
-	login_password = viper.GetString("login.password")
-	insert_batch_size = viper.GetInt("import.insert_batch_size")
-}
-
-// Batch insert par json passé par argument
-func InsertJson(json_insert_path string) {
-
-	if json_insert_path != "" {
-		log.Printf("Trying to import json: %s", json_insert_path)
-		newMembres, err_insert := obtenirUnmarshalJSON(json_insert_path)
-		if newMembres != nil {
-			log.Printf("Membres found, importing...")
-
-			data.InsertMembres(newMembres, insert_batch_size)
-
-			log.Printf("Success?")
-		}
-		if err_insert != nil {
-			log.Fatal(err_insert)
-		}
-	}
-}
-
-// Run echo webserver
-func RunServer() {
-	// Echo instance and group
-	e := echo.New()
-	g := e.Group("")
-
-	// Middlewares
-	// Compatibilité
-	e.Pre(middleware.Rewrite(map[string]string{
-		"/membre/?num_etud=*": "/membre/$1",
-	}))
-	e.Pre(middleware.RemoveTrailingSlash())
-
-	// Authentification de base
-	g.Use(middleware.BasicAuth(basicAuther))
-
-	// Logger - Choose one
-
-	// Verbose logger
-	//g.Use(middleware.Logger())
-
-	// Less verbose logger
-	g.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
-		Format: "${time_rfc3339_nano} method=${method}, uri=${uri}, status=${status}" + "\n",
-	}))
-
-	// Routes
-	registerRoutes(g)
-	registerRoutesv1(g)
-
-	// Start server
-	e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", server_port)))
-}
-
-// Handlers
-func basicAuther(username, password string, context echo.Context) (bool, error) {
-	if subtle.ConstantTimeCompare([]byte(username), []byte(login_username)) == 1 &&
-		subtle.ConstantTimeCompare([]byte(password), []byte(login_password)) == 1 {
-		return true, nil
-	}
-	return false, nil
-}
-
-func registerRoutesv1(g *echo.Group) {
-	g.GET("/v1", showAPISpecs)
-	g.GET("/v1/membre/:num_etud", showMembreJson)
-}
-
-func showMembreJson(c echo.Context) error {
-	num_etud := c.Param("num_etud")
-
-	var membre data.Membre = data.ReadMembre(num_etud)
-
-	return c.JSON(http.StatusOK, membre)
-}
-
-func showAPISpecs(c echo.Context) error {
-	apispec := fmt.Sprintln(`agecem/bottin
-API Specifications
------
-'/v1'                     | GET | Afficher spécifications API
-'/v1/membre/:num_etud'    | GET | Afficher membre avec le numéro étudiant :num_etud, en JSON
-'/membre/:num_etud'       | GET | Afficher membre avec le numéro étudiant :num_etud
-'/'                       | GET | Afficher bottin web
-'/membre'                 | GET | Afficher bottin web
-'/static'                 | GET | DEPRECATED Répertoire des fichiers statics
------`)
-	return c.String(http.StatusOK, apispec)
-}
-
-func registerRoutes(g *echo.Group) {
-	g.GET("/", func(c echo.Context) error {
-		return c.HTML(http.StatusOK, html)
-	})
-
-	// Doublon de l'autre le temps que j'figure out les shits
-	g.GET("/membre", func(c echo.Context) error {
-		return c.HTML(http.StatusOK, html)
-	})
-
-	// Get specific membre
-	g.GET("/membre/:num_etud", func(c echo.Context) error {
-		num_etud := c.Param("num_etud")
-
-		var membre data.Membre = data.ReadMembre(num_etud)
-
-		msgNumEtud := fmt.Sprintf("<p>Numéro d'étudiantE: %s</p>", num_etud)
-		msgNom := fmt.Sprintf("<p>Nom: %s</p>", membre.Nom)
-
-		msg := fmt.Sprintf("%s%s%s", html, msgNumEtud, msgNom)
-
-		return c.HTML(http.StatusOK, fmt.Sprintf(msg))
-	})
-}
diff --git a/v4/cmd/api.go b/cmd/api.go
similarity index 100%
rename from v4/cmd/api.go
rename to cmd/api.go
diff --git a/cmd/import.go b/cmd/import.go
deleted file mode 100644
index df066d6..0000000
--- a/cmd/import.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package cmd
-
-import (
-	"log"
-	"os"
-
-	"git.agecem.com/agecem/bottin/bottin"
-	"git.agecem.com/agecem/bottin/data"
-	"github.com/spf13/cobra"
-)
-
-// importCmd represents the import command
-var importCmd = &cobra.Command{
-	Use:   "import",
-	Short: "Import content into the database",
-	Run: func(cmd *cobra.Command, args []string) {
-
-		if len(args) == 0 {
-			log.Fatal("Not enough arguments, needs at least 1 path")
-		} else {
-			data.OpenDatabase()
-			data.MigrateDatabase()
-			bottin.UpdateFlags()
-
-			var valid_args []string
-
-			for _, arg := range args {
-				_, err := os.Stat(arg)
-
-				if !os.IsNotExist(err) {
-					log.Printf("Adding %s to valid_args", arg)
-					valid_args = append(valid_args, arg)
-				}
-			}
-
-			// Import from valid args
-			if len(valid_args) == 1 {
-				// Do once
-				bottin.InsertJson(valid_args[0])
-
-			} else {
-				// Do multiple times
-				for _, path := range valid_args {
-					bottin.InsertJson(path)
-				}
-			}
-		}
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(importCmd)
-}
diff --git a/cmd/root.go b/cmd/root.go
index 929e353..94350ca 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,24 +1,3 @@
-/*
-Copyright © 2022 AGECEM & Victor Lacasse-Beaudoin
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-*/
 package cmd
 
 import (
@@ -34,12 +13,7 @@ var cfgFile string
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
 	Use:   "bottin",
-	Short: "Bottin de la masse étudiante.",
-	// Uncomment the following line if your bare application
-	// has an action associated with it:
-	//
-	// Run: func(cmd *cobra.Command, args []string) {
-	// },
+	Short: "Application de gestion de distribution d'agendas",
 }
 
 // Execute adds all child commands to the root command and sets flags appropriately.
@@ -54,10 +28,6 @@ func Execute() {
 func init() {
 	cobra.OnInitialize(initConfig)
 
-	// Here you will define your flags and configuration settings.
-	// Cobra supports persistent flags, which, if defined here,
-	// will be global for your application.
-
 	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
 }
 
diff --git a/cmd/server.go b/cmd/server.go
deleted file mode 100644
index bd84a42..0000000
--- a/cmd/server.go
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
-Copyright © 2022 Victor Lacasse-Beaudoin <victor.lacassebeaudoin@gmail.com>
-*/
-package cmd
-
-import (
-	"git.agecem.com/agecem/bottin/bottin"
-	"git.agecem.com/agecem/bottin/data"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-	"log"
-	"time"
-)
-
-var (
-	json_insert_path string
-)
-
-// serverCmd represents the server command
-var serverCmd = &cobra.Command{
-	Use:   "server",
-	Short: "Run the bottin server",
-	Run: func(cmd *cobra.Command, args []string) {
-		// Timer
-		db_retry_timer := time.Duration(viper.GetInt("db.retry-timer"))
-
-		// Open and migrate db
-		for {
-			err := data.OpenDatabase()
-			if err != nil {
-				log.Println(err)
-				time.Sleep(db_retry_timer * time.Second)
-				continue
-			}
-
-			err = data.MigrateDatabase()
-			if err != nil {
-				log.Println(err)
-				time.Sleep(db_retry_timer * time.Second)
-				continue
-			}
-
-			break
-		}
-
-		bottin.UpdateFlags()
-
-		// Import from flag
-		if json_insert_path != "" {
-			bottin.InsertJson(json_insert_path)
-		}
-
-		// Run web app
-		bottin.RunServer()
-	},
-}
-
-func init() {
-	declareFlags()
-	rootCmd.AddCommand(serverCmd)
-}
-
-func declareFlags() {
-	// db.type
-	serverCmd.PersistentFlags().String(
-		"db-type", "",
-		"Database type (config: 'db.type')")
-	viper.BindPFlag(
-		"db.type",
-		serverCmd.PersistentFlags().Lookup("db-type"))
-	serverCmd.MarkPersistentFlagRequired("db.type")
-
-	// db.sqlite.path
-	serverCmd.PersistentFlags().String(
-		"db-sqlite-path", "",
-		"Path to sqlite database (config: 'db.sqlite.path')")
-	viper.BindPFlag(
-		"db.sqlite.path",
-		serverCmd.PersistentFlags().Lookup("db-sqlite-path"))
-
-	// server.port
-	serverCmd.PersistentFlags().Int(
-		"server-port", 1312,
-		"The port on which the web application will server content (config: 'server.port')")
-	viper.BindPFlag(
-		"server.port",
-		serverCmd.PersistentFlags().Lookup("server-port"))
-
-	// server.static_dir
-	serverCmd.PersistentFlags().String(
-		"static-dir", "/var/lib/bottin/static",
-		"The directory containing static assets (config: 'server.static_dir')")
-	viper.BindPFlag(
-		"server.static_dir",
-		serverCmd.PersistentFlags().Lookup("static-dir"))
-
-	// login.username
-	serverCmd.PersistentFlags().String(
-		"login-username", "bottin",
-		"The username to login to the web ui. (config: 'login.username')")
-	viper.BindPFlag(
-		"login.username",
-		serverCmd.PersistentFlags().Lookup("login-username"))
-
-	// login.password
-	serverCmd.PersistentFlags().String(
-		"login-password", "bottin",
-		"The password to login to the web ui. (config: 'login.password')")
-	viper.BindPFlag(
-		"login.password",
-		serverCmd.PersistentFlags().Lookup("login-password"))
-
-	// import.insert-batch-size
-	serverCmd.PersistentFlags().Int(
-		"insert-batch-size", 500,
-		"The amount of inserts to do per batch (config: 'import.insert_batch_size')")
-	viper.BindPFlag(
-		"import.insert_batch_size",
-		serverCmd.PersistentFlags().Lookup("insert-batch-size"))
-
-	// json-insert-path
-	serverCmd.PersistentFlags().StringVar(
-		&json_insert_path, "json-insert-path", "",
-		"The location of a json file containing Membres to insert.")
-	/*
-			// Not using viper for json-insert-path since it would make it too easy
-			// to forget to remove from the config every time, which heavily risks
-			// doubling values.
-			//
-			// It would at least need to check for doubles before importing the file,
-			// for it to be a kind of automatic differential update of the database.
-
-		  viper.BindPFlag(
-				"import.json-insert-path",
-				serverCmd.PersistentFlags().Lookup("json-insert-path"))
-	*/
-
-	// db.retry-timer
-	serverCmd.PersistentFlags().Int(
-		"db-retry-timer", 2,
-		"Time between failed database connection retries, in seconds.")
-	viper.BindPFlag(
-		"db.retry-timer",
-		serverCmd.PersistentFlags().Lookup("db-retry-timer"))
-}
diff --git a/v4/cmd/web.go b/cmd/web.go
similarity index 100%
rename from v4/cmd/web.go
rename to cmd/web.go
diff --git a/v4/data/apiclient.go b/data/apiclient.go
similarity index 100%
rename from v4/data/apiclient.go
rename to data/apiclient.go
diff --git a/data/data.go b/data/data.go
index 3a9ea17..4572dea 100644
--- a/data/data.go
+++ b/data/data.go
@@ -2,77 +2,291 @@ package data
 
 import (
 	"errors"
-	"log"
+	"fmt"
 
-	"github.com/spf13/viper"
-	"gorm.io/driver/sqlite"
-	"gorm.io/gorm"
+	"git.agecem.com/agecem/bottin/v4/models"
+	_ "github.com/jackc/pgx/stdlib"
+	"github.com/jmoiron/sqlx"
 )
 
-var db *gorm.DB
-
-type Membre struct {
-	gorm.Model
-	NumEtud string `mapper:"num_etud" json:"num_etud"`
-	Nom     string `mapper:"nom" json:"nom"`
+// DataClient is a postgres client based on sqlx
+type DataClient struct {
+	PostgresConnection PostgresConnection
+	DB                 sqlx.DB
 }
 
-func OpenDatabase() error {
-	var err error
+type PostgresConnection struct {
+	User     string
+	Password string
+	Database string
+	Host     string
+	Port     int
+	SSL      bool
+}
 
-	var dialector gorm.Dialector
+func NewDataClient(connection PostgresConnection) (*DataClient, error) {
+	client := &DataClient{PostgresConnection: connection}
 
-	switch t := viper.GetString("db.type"); t {
-	case "sqlite":
-		log.Println("Using driver gorm.io/driver/sqlite")
+	connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
+		client.PostgresConnection.User,
+		client.PostgresConnection.Password,
+		client.PostgresConnection.Host,
+		client.PostgresConnection.Port,
+		client.PostgresConnection.Database,
+	)
 
-		db_sqlite_path := viper.GetString("db.sqlite.path")
-
-		if db_sqlite_path == "" {
-			log.Fatal("No valid database file found in `--db-sqlite-path` or `db.sqlite.path`.")
-		}
-
-		log.Println("Using database file:", db_sqlite_path)
-
-		dialector = sqlite.Open(db_sqlite_path)
-	default:
-		log.Fatalf("Unrecognized database driver requested (%s).\n", t)
-	}
-
-	db, err = gorm.Open(dialector, &gorm.Config{})
+	db, err := sqlx.Connect("pgx", connectionString)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	sqlDB, err := db.DB()
+	client.DB = *db
+
+	return client, nil
+}
+
+func (d *DataClient) Seed() (int64, error) {
+	result, err := d.DB.Exec(models.Schema)
 	if err != nil {
-		return err
+		return 0, err
 	}
 
-	return sqlDB.Ping()
+	rows, err := result.RowsAffected()
+	if err != nil {
+		return rows, err
+	}
+
+	return rows, nil
 }
 
-func MigrateDatabase() error {
-	err := db.AutoMigrate(&Membre{})
-	return err
-}
-
-func ReadMembre(num_etud string) Membre {
-	var membre Membre
-	db.First(&membre, "num_etud = ?", num_etud)
-	return membre
-}
-
-func InsertMembres(membres []*Membre, batch_size int) error {
-	if len(membres) == 0 {
-		return errors.New("Cannot insert empty batch of membres.")
+// InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
+func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
+	var rowsInserted int64
+	tx, err := d.DB.Beginx()
+	if err != nil {
+		tx.Rollback()
+		return rowsInserted, err
 	}
 
 	for _, membre := range membres {
-		membre.ID = 0
+		if membre.ID == "" {
+			tx.Rollback()
+			return 0, errors.New("Cannot insert membre with no membre_id")
+		}
+		result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id);", &membre)
+		if err != nil {
+			tx.Rollback()
+			return 0, err
+		}
+
+		rows, err := result.RowsAffected()
+		if err != nil {
+			tx.Rollback()
+			return 0, err
+		}
+
+		rowsInserted += rows
 	}
 
-	db.CreateInBatches(&membres, batch_size)
+	err = tx.Commit()
+	if err != nil {
+		return rowsInserted, err
+	}
+
+	return rowsInserted, nil
+}
+
+func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, error) {
+	var rowsInserted int64
+	tx, err := d.DB.Beginx()
+	if err != nil {
+		tx.Rollback()
+		return rowsInserted, err
+	}
+
+	for _, programme := range programmes {
+		if programme.ID == "" {
+			tx.Rollback()
+			return 0, errors.New("Cannot insert programme with no programme_id")
+		}
+
+		result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre);", &programme)
+		if err != nil {
+			tx.Rollback()
+			return 0, err
+		}
+
+		rows, err := result.RowsAffected()
+		if err != nil {
+			tx.Rollback()
+			return 0, err
+		}
+
+		rowsInserted += rows
+	}
+
+	err = tx.Commit()
+	if err != nil {
+		return rowsInserted, err
+	}
+
+	return rowsInserted, nil
+}
+
+func (d *DataClient) GetMembre(membreID string) (models.Membre, error) {
+	var membre models.Membre
+
+	rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
+	if err != nil {
+		return membre, err
+	}
+
+	for rows.Next() {
+		err := rows.StructScan(&membre)
+		if err != nil {
+			return membre, err
+		}
+	}
+
+	if membre.ID == "" {
+		return membre, fmt.Errorf("No membre by that id was found")
+	}
+
+	return membre, nil
+}
+
+func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
+	result, err := d.DB.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
+	if err != nil {
+		return 0, err
+	}
+
+	rows, err := result.RowsAffected()
+	if err != nil {
+		return rows, err
+	}
+
+	return rows, nil
+}
+
+/*
+func (d *DataClient) Insert(assets []models.Asset) (id int64, err error) {
+	// Check for minimal required info
+	for _, asset := range assets {
+		if asset.Description == "" {
+			err = errors.New("Cannot insert: At least one asset has no `description` set.")
+			return
+		}
+	}
+
+	tx := d.DB.MustBegin()
+
+	for _, asset := range assets {
+		_, err = tx.NamedExec("INSERT INTO assets (description, status, created_at) VALUES (:description, :status, current_timestamp)", asset)
+		if err != nil {
+			return
+		}
+	}
+
+	err = tx.Commit()
+
+	return
+}
+
+func (d *DataClient) List() ([]models.Asset, error) {
+	// Query the database, storing results in a []Person (wrapped in []interface{})
+	assets := []models.Asset{}
+
+	err := d.DB.Select(&assets, "SELECT * FROM assets WHERE deleted_at IS NULL LIMIT 1000")
+	if err != nil {
+		return nil, err
+	}
+
+	return assets, nil
+}
+
+// RecordEvent allows inserting into events when an asset or a tag is modified
+// or deleted.
+func (d *DataClient) RecordEvent(assetID, tagID int64, content string) error {
+	event := models.Event{
+		AssetID: assetID,
+		TagID:   tagID,
+		Content: content,
+	}
+	_, err := d.DB.NamedExec("INSERT INTO events (asset_id, tag_id, at, content) VALUES (:asset_id, :tag_id, current_timestamp, :content);", event)
+	if err != nil {
+		return err
+	}
 
 	return nil
 }
+
+func (d *DataClient) Delete(assetIDs []int64) ([]int64, error) {
+	var rows []int64
+
+	tx := d.DB.MustBegin()
+
+	for _, assetID := range assetIDs {
+		result, err := d.DB.Exec("UPDATE assets SET deleted_at = current_timestamp WHERE id = $1 AND deleted_at IS NULL;", assetID)
+		if err != nil {
+			return rows, err
+		}
+
+		rowsAffected, err := result.RowsAffected()
+		if err != nil {
+			return rows, err
+		}
+
+		if rowsAffected != 0 {
+			rows = append(rows, assetID)
+		}
+	}
+
+	err := tx.Commit()
+	if err != nil {
+		return rows, err
+	}
+
+	for _, assetID := range assetIDs {
+		d.RecordEvent(assetID, -1, fmt.Sprintf("Asset %d deleted.", assetID))
+	}
+
+	return rows, nil
+}
+
+func (d *DataClient) UpdateAssetDescription(assetID int64, description string) (int64, error) {
+	result, err := d.DB.Exec("UPDATE assets SET description = $1 WHERE id = $2", description, assetID)
+	if err != nil {
+		return 0, err
+	}
+
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return 0, err
+	}
+
+	if rowsAffected != 0 {
+		return 0, errors.New("Nothing to do")
+	}
+
+	return rowsAffected, nil
+}
+
+func (d *DataClient) UpdateAssetStatus(assetID int64, status string) (int64, error) {
+	result, err := d.DB.Exec("UPDATE assets SET status = $1 WHERE id = $2", status, assetID)
+	if err != nil {
+		return 0, err
+	}
+
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return 0, err
+	}
+
+	if rowsAffected != 0 {
+		return 0, errors.New("Nothing to do")
+	}
+
+	return rowsAffected, nil
+}
+*/
diff --git a/v4/docker-compose.yaml b/docker-compose.yaml
similarity index 89%
rename from v4/docker-compose.yaml
rename to docker-compose.yaml
index 3c4c744..b6814bc 100644
--- a/v4/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -14,7 +14,7 @@ services:
     depends_on:
       - db
     build: .
-    image: 'git.agecem.com/agecem/bottin/v4:latest'
+    image: 'git.agecem.com/agecem/bottin:latest'
     ports:
       - '1312:1312'
     volumes:
@@ -26,7 +26,7 @@ services:
     depends_on: 
       - api
     build: .
-    image: 'git.agecem.com/agecem/bottin/v4:latest'
+    image: 'git.agecem.com/agecem/bottin:latest'
     ports:
       - '2312:2312'
     volumes:
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index b1ddcf7..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-services:
-  bottin:
-    build: .
-    image: agecem/bottin:latest
-    ports:
-      - "1312:1312" # http
-    volumes:
-      - "bottin-config:/etc/bottin"
-      - "bottin-data:/var/lib/bottin"
-    command: bottin server --config /etc/bottin/bottin.yaml
-    restart: "always"
-volumes:
-  bottin-config:
-  bottin-data:
-networks:
-  default:
-    name: bottin
diff --git a/embed/embed.go b/embed/embed.go
deleted file mode 100644
index 53b166a..0000000
--- a/embed/embed.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package embed
-
-import _ "embed"
-
-//go:embed html/index.html
-var Html_index string
-
-func ReadHtml() string {
-	return Html_index
-}
diff --git a/embed/html/index.html b/embed/html/index.html
deleted file mode 100644
index 816745a..0000000
--- a/embed/html/index.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<html>
-
-  <head>
-    <title>
-      AGECEM | Bottin
-    </title>
-  </head>
-
-  <body>
-    <h2>
-      Bottin de numéros d&#39étudiantEs
-    </h2>
-
-    <h4>
-      Scannez la carte étudiante d&#39unE membre<br>
-      -ou-<br>
-      Entrez manuellement le code à 7 chiffres
-    </h4>
-
-    <form action="/membre/">
-      <label>#
-        <input type="text" name="num_etud" id="num_etud" autofocus>
-      </label>
-      <button formmethod="get" type="submit">Valider</button>
-    </form>
-
-  </body>
-
-</html>
diff --git a/examples/bottin.yaml b/examples/bottin.yaml
deleted file mode 100644
index 892c4bf..0000000
--- a/examples/bottin.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-server:
-  port: 1312
-
-db:
-  type: 'sqlite'
-  sqlite:
-    path: '/var/lib/bottin/bottin.db'
-
-login:
-  username: 'bottin'
-  password: 'bottin'
-
-import:
-  insert_batch_size: 500
diff --git a/examples/membres-test.json b/examples/membres-test.json
deleted file mode 100644
index f007db6..0000000
--- a/examples/membres-test.json
+++ /dev/null
@@ -1,8 +0,0 @@
-[{
-  "num_etud":"0000001",
-  "nom":"User 1"
-},
-{
-  "num_etud":"64",
-  "nom":"Test User 64"
-}]
diff --git a/go.mod b/go.mod
index 18b4438..b6a59fc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,43 @@
-module git.agecem.com/agecem/bottin
+module git.agecem.com/agecem/bottin/v4
 
-go 1.16
+go 1.20
 
 require (
-	github.com/labstack/echo/v4 v4.5.0
-	github.com/spf13/cobra v1.4.0
-	github.com/spf13/viper v1.12.0
-	gorm.io/driver/sqlite v1.1.4
-	gorm.io/gorm v1.21.14
+	github.com/jackc/pgx v3.6.2+incompatible
+	github.com/jmoiron/sqlx v1.3.5
+	github.com/labstack/echo/v4 v4.10.2
+	github.com/spf13/cobra v1.7.0
+	github.com/spf13/viper v1.15.0
+)
+
+require (
+	github.com/cockroachdb/apd v1.1.0 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/gofrs/uuid v4.4.0+incompatible // indirect
+	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
+	github.com/labstack/gommon v0.4.0 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/shopspring/decimal v1.3.1 // indirect
+	github.com/spf13/afero v1.9.3 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/subosito/gotenv v1.4.2 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasttemplate v1.2.2 // indirect
+	golang.org/x/crypto v0.6.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/time v0.3.0 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 92f5e10..03ed3fe 100644
--- a/go.sum
+++ b/go.sum
@@ -17,32 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
 cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
 cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
 cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
-cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
-cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
-cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
 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/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
-cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
-cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
-cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
-cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
 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/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
 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=
@@ -56,87 +38,42 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
-github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
-github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/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=
@@ -144,8 +81,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 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/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -160,10 +95,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 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=
@@ -174,18 +105,11 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 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-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 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/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
 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=
@@ -196,233 +120,115 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2/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/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
-github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
-github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
-github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
-github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
-github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
-github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
-github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
-github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
-github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
-github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
+github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
+github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
+github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/labstack/echo/v4 v4.5.0 h1:JXk6H5PAw9I3GwizqUHhYyS4f45iyGebR/c1xNCeOCY=
-github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
-github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
-github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
-github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
-github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
-github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
+github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
-github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
-github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
-github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
-github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
-github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
-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/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
+github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
-github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
+github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
+github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
+github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
-github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
-github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
+github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
-github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
-github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
+github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
-github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
-go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
-go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
 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/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/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 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=
@@ -446,7 +252,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu
 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/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/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=
@@ -457,10 +262,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 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-20181114220301-adae6a3d119a/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=
@@ -468,11 +271,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 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=
@@ -489,23 +290,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -515,17 +304,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 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=
@@ -536,35 +314,20 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/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-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -575,8 +338,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
 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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-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-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -584,57 +345,30 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 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/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
-golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -648,7 +382,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -673,7 +406,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
 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-20200619180055-7c47624df98f/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/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
@@ -682,20 +414,12 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 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=
@@ -715,26 +439,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
 google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
 google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
-google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
-google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
-google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
-google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
-google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
-google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
-google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
-google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
-google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
 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=
@@ -765,7 +469,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
 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-20200513103714-09dca8ec2884/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=
@@ -778,48 +481,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
-google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 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=
@@ -833,24 +495,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
 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/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 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=
@@ -861,36 +508,17 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
-gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/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.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
-gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
-gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
-gorm.io/gorm v1.21.14 h1:NAR9A/3SoyiPVHouW/rlpMUZvuQZ6Z6UYGz+2tosSQo=
-gorm.io/gorm v1.21.14/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/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=
@@ -901,4 +529,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 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=
-sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/v4/handlers/insert.go b/handlers/insert.go
similarity index 100%
rename from v4/handlers/insert.go
rename to handlers/insert.go
diff --git a/v4/handlers/read.go b/handlers/read.go
similarity index 100%
rename from v4/handlers/read.go
rename to handlers/read.go
diff --git a/v4/handlers/seed.go b/handlers/seed.go
similarity index 100%
rename from v4/handlers/seed.go
rename to handlers/seed.go
diff --git a/v4/handlers/update.go b/handlers/update.go
similarity index 100%
rename from v4/handlers/update.go
rename to handlers/update.go
diff --git a/v4/handlers/v4.go b/handlers/v4.go
similarity index 100%
rename from v4/handlers/v4.go
rename to handlers/v4.go
diff --git a/main.go b/main.go
index e23a67b..6ceb837 100644
--- a/main.go
+++ b/main.go
@@ -1,27 +1,6 @@
-/*
-Copyright © 2022 AGECEM & Victor Lacasse-Beaudoin
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-*/
 package main
 
-import "git.agecem.com/agecem/bottin/cmd"
+import "git.agecem.com/agecem/bottin/v4/cmd"
 
 func main() {
 	cmd.Execute()
diff --git a/v4/models/models.go b/models/models.go
similarity index 100%
rename from v4/models/models.go
rename to models/models.go
diff --git a/v4/.env b/v4/.env
deleted file mode 100644
index 2e8a756..0000000
--- a/v4/.env
+++ /dev/null
@@ -1,3 +0,0 @@
-BOTTIN_POSTGRES_DATABASE=bottin
-BOTTIN_POSTGRES_PASSWORD=bottin
-BOTTIN_POSTGRES_USER=bottin
diff --git a/v4/Dockerfile b/v4/Dockerfile
deleted file mode 100644
index 805727f..0000000
--- a/v4/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM golang:1.20.2 as build
-
-LABEL author="vlbeaudoin"
-
-WORKDIR /go/src/app
-
-COPY go.mod go.sum main.go ./
-
-ADD cmd/ cmd/
-ADD data/ data/
-ADD handlers/ handlers/
-ADD models/ models/
-ADD web/ web/
-
-RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin .
-
-# Alpine
-
-FROM alpine:latest
-
-WORKDIR /app
-
-COPY --from=build /go/src/app/bottin /usr/bin/bottin
-
-CMD ["bottin", "--help"]
diff --git a/v4/LICENSE b/v4/LICENSE
deleted file mode 100644
index 018068b..0000000
--- a/v4/LICENSE
+++ /dev/null
@@ -1,9 +0,0 @@
-MIT License
-
-Copyright (c) 2021 AGECEM
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/v4/README.md b/v4/README.md
deleted file mode 100644
index c32ebe5..0000000
--- a/v4/README.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# agecem/bottin/v4
-
-Version 4 du bottin de la masse étudiante, en Go
-
-https://git.agecem.com/agecem/bottin
-
-## fonctionalités
-
-### Serveur API
-
-- Insertion de membre et programme
-- Lecture de membre
-- Modification du nom d'usage de membre
-
-### Client web
-
-- Lecture de membre par requête au serveur API
-
-## usage
-
-Remplir .env avec les infos qui seront utilisées pour déployer le container
-
-(Remplacer `bottin` par quelque chose de plus sécuritaire)
-
-```sh
-BOTTIN_POSTGRES_DATABASE=bottin
-BOTTIN_POSTGRES_PASSWORD=bottin
-BOTTIN_POSTGRES_USER=bottin
-```
-
-Déployer avec docker-compose
-
-`$ docker-compose up -d`
-
-Pour modifier la configuration du serveur API
-
-`$ docker-compose exec -it api vi /etc/bottin/api.yaml`
-
-*Y remplir au minimum le champs `api.key` (string)*
-
-Pour modifier la configuration du client web
-
-`$ docker-compose exec -it web vi /etc/bottin/web.yaml`
-
-*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.password` (string)*
-
-Redémarrer les containers une fois la configuration modifiée
-
-`$ docker-compose down && docker-compose up -d`
diff --git a/v4/cmd/root.go b/v4/cmd/root.go
deleted file mode 100644
index 94350ca..0000000
--- a/v4/cmd/root.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-var cfgFile string
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "bottin",
-	Short: "Application de gestion de distribution d'agendas",
-}
-
-// Execute adds all child commands to the root command and sets flags appropriately.
-// This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute() {
-	err := rootCmd.Execute()
-	if err != nil {
-		os.Exit(1)
-	}
-}
-
-func init() {
-	cobra.OnInitialize(initConfig)
-
-	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
-}
-
-// initConfig reads in config file and ENV variables if set.
-func initConfig() {
-	if cfgFile != "" {
-		// Use config file from the flag.
-		viper.SetConfigFile(cfgFile)
-	} else {
-		// Find home directory.
-		home, err := os.UserHomeDir()
-		cobra.CheckErr(err)
-
-		// Search config in home directory with name ".bottin" (without extension).
-		viper.AddConfigPath(home)
-		viper.SetConfigType("yaml")
-		viper.SetConfigName(".bottin")
-	}
-
-	viper.AutomaticEnv() // read in environment variables that match
-
-	// If a config file is found, read it in.
-	if err := viper.ReadInConfig(); err == nil {
-		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
-	}
-}
diff --git a/v4/data/data.go b/v4/data/data.go
deleted file mode 100644
index 4572dea..0000000
--- a/v4/data/data.go
+++ /dev/null
@@ -1,292 +0,0 @@
-package data
-
-import (
-	"errors"
-	"fmt"
-
-	"git.agecem.com/agecem/bottin/v4/models"
-	_ "github.com/jackc/pgx/stdlib"
-	"github.com/jmoiron/sqlx"
-)
-
-// DataClient is a postgres client based on sqlx
-type DataClient struct {
-	PostgresConnection PostgresConnection
-	DB                 sqlx.DB
-}
-
-type PostgresConnection struct {
-	User     string
-	Password string
-	Database string
-	Host     string
-	Port     int
-	SSL      bool
-}
-
-func NewDataClient(connection PostgresConnection) (*DataClient, error) {
-	client := &DataClient{PostgresConnection: connection}
-
-	connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
-		client.PostgresConnection.User,
-		client.PostgresConnection.Password,
-		client.PostgresConnection.Host,
-		client.PostgresConnection.Port,
-		client.PostgresConnection.Database,
-	)
-
-	db, err := sqlx.Connect("pgx", connectionString)
-	if err != nil {
-		return nil, err
-	}
-
-	client.DB = *db
-
-	return client, nil
-}
-
-func (d *DataClient) Seed() (int64, error) {
-	result, err := d.DB.Exec(models.Schema)
-	if err != nil {
-		return 0, err
-	}
-
-	rows, err := result.RowsAffected()
-	if err != nil {
-		return rows, err
-	}
-
-	return rows, nil
-}
-
-// InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
-func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
-	var rowsInserted int64
-	tx, err := d.DB.Beginx()
-	if err != nil {
-		tx.Rollback()
-		return rowsInserted, err
-	}
-
-	for _, membre := range membres {
-		if membre.ID == "" {
-			tx.Rollback()
-			return 0, errors.New("Cannot insert membre with no membre_id")
-		}
-		result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id);", &membre)
-		if err != nil {
-			tx.Rollback()
-			return 0, err
-		}
-
-		rows, err := result.RowsAffected()
-		if err != nil {
-			tx.Rollback()
-			return 0, err
-		}
-
-		rowsInserted += rows
-	}
-
-	err = tx.Commit()
-	if err != nil {
-		return rowsInserted, err
-	}
-
-	return rowsInserted, nil
-}
-
-func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, error) {
-	var rowsInserted int64
-	tx, err := d.DB.Beginx()
-	if err != nil {
-		tx.Rollback()
-		return rowsInserted, err
-	}
-
-	for _, programme := range programmes {
-		if programme.ID == "" {
-			tx.Rollback()
-			return 0, errors.New("Cannot insert programme with no programme_id")
-		}
-
-		result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre);", &programme)
-		if err != nil {
-			tx.Rollback()
-			return 0, err
-		}
-
-		rows, err := result.RowsAffected()
-		if err != nil {
-			tx.Rollback()
-			return 0, err
-		}
-
-		rowsInserted += rows
-	}
-
-	err = tx.Commit()
-	if err != nil {
-		return rowsInserted, err
-	}
-
-	return rowsInserted, nil
-}
-
-func (d *DataClient) GetMembre(membreID string) (models.Membre, error) {
-	var membre models.Membre
-
-	rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
-	if err != nil {
-		return membre, err
-	}
-
-	for rows.Next() {
-		err := rows.StructScan(&membre)
-		if err != nil {
-			return membre, err
-		}
-	}
-
-	if membre.ID == "" {
-		return membre, fmt.Errorf("No membre by that id was found")
-	}
-
-	return membre, nil
-}
-
-func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
-	result, err := d.DB.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
-	if err != nil {
-		return 0, err
-	}
-
-	rows, err := result.RowsAffected()
-	if err != nil {
-		return rows, err
-	}
-
-	return rows, nil
-}
-
-/*
-func (d *DataClient) Insert(assets []models.Asset) (id int64, err error) {
-	// Check for minimal required info
-	for _, asset := range assets {
-		if asset.Description == "" {
-			err = errors.New("Cannot insert: At least one asset has no `description` set.")
-			return
-		}
-	}
-
-	tx := d.DB.MustBegin()
-
-	for _, asset := range assets {
-		_, err = tx.NamedExec("INSERT INTO assets (description, status, created_at) VALUES (:description, :status, current_timestamp)", asset)
-		if err != nil {
-			return
-		}
-	}
-
-	err = tx.Commit()
-
-	return
-}
-
-func (d *DataClient) List() ([]models.Asset, error) {
-	// Query the database, storing results in a []Person (wrapped in []interface{})
-	assets := []models.Asset{}
-
-	err := d.DB.Select(&assets, "SELECT * FROM assets WHERE deleted_at IS NULL LIMIT 1000")
-	if err != nil {
-		return nil, err
-	}
-
-	return assets, nil
-}
-
-// RecordEvent allows inserting into events when an asset or a tag is modified
-// or deleted.
-func (d *DataClient) RecordEvent(assetID, tagID int64, content string) error {
-	event := models.Event{
-		AssetID: assetID,
-		TagID:   tagID,
-		Content: content,
-	}
-	_, err := d.DB.NamedExec("INSERT INTO events (asset_id, tag_id, at, content) VALUES (:asset_id, :tag_id, current_timestamp, :content);", event)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (d *DataClient) Delete(assetIDs []int64) ([]int64, error) {
-	var rows []int64
-
-	tx := d.DB.MustBegin()
-
-	for _, assetID := range assetIDs {
-		result, err := d.DB.Exec("UPDATE assets SET deleted_at = current_timestamp WHERE id = $1 AND deleted_at IS NULL;", assetID)
-		if err != nil {
-			return rows, err
-		}
-
-		rowsAffected, err := result.RowsAffected()
-		if err != nil {
-			return rows, err
-		}
-
-		if rowsAffected != 0 {
-			rows = append(rows, assetID)
-		}
-	}
-
-	err := tx.Commit()
-	if err != nil {
-		return rows, err
-	}
-
-	for _, assetID := range assetIDs {
-		d.RecordEvent(assetID, -1, fmt.Sprintf("Asset %d deleted.", assetID))
-	}
-
-	return rows, nil
-}
-
-func (d *DataClient) UpdateAssetDescription(assetID int64, description string) (int64, error) {
-	result, err := d.DB.Exec("UPDATE assets SET description = $1 WHERE id = $2", description, assetID)
-	if err != nil {
-		return 0, err
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return 0, err
-	}
-
-	if rowsAffected != 0 {
-		return 0, errors.New("Nothing to do")
-	}
-
-	return rowsAffected, nil
-}
-
-func (d *DataClient) UpdateAssetStatus(assetID int64, status string) (int64, error) {
-	result, err := d.DB.Exec("UPDATE assets SET status = $1 WHERE id = $2", status, assetID)
-	if err != nil {
-		return 0, err
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return 0, err
-	}
-
-	if rowsAffected != 0 {
-		return 0, errors.New("Nothing to do")
-	}
-
-	return rowsAffected, nil
-}
-*/
diff --git a/v4/go.mod b/v4/go.mod
deleted file mode 100644
index b6a59fc..0000000
--- a/v4/go.mod
+++ /dev/null
@@ -1,43 +0,0 @@
-module git.agecem.com/agecem/bottin/v4
-
-go 1.20
-
-require (
-	github.com/jackc/pgx v3.6.2+incompatible
-	github.com/jmoiron/sqlx v1.3.5
-	github.com/labstack/echo/v4 v4.10.2
-	github.com/spf13/cobra v1.7.0
-	github.com/spf13/viper v1.15.0
-)
-
-require (
-	github.com/cockroachdb/apd v1.1.0 // indirect
-	github.com/fsnotify/fsnotify v1.6.0 // indirect
-	github.com/gofrs/uuid v4.4.0+incompatible // indirect
-	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
-	github.com/labstack/gommon v0.4.0 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
-	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.17 // indirect
-	github.com/mitchellh/mapstructure v1.5.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/shopspring/decimal v1.3.1 // indirect
-	github.com/spf13/afero v1.9.3 // indirect
-	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/jwalterweatherman v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/subosito/gotenv v1.4.2 // indirect
-	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasttemplate v1.2.2 // indirect
-	golang.org/x/crypto v0.6.0 // indirect
-	golang.org/x/net v0.7.0 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
-	golang.org/x/time v0.3.0 // indirect
-	gopkg.in/ini.v1 v1.67.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
-)
diff --git a/v4/go.sum b/v4/go.sum
deleted file mode 100644
index 03ed3fe..0000000
--- a/v4/go.sum
+++ /dev/null
@@ -1,531 +0,0 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.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 v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-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=
-cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-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/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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-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-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
-github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
-github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
-github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-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/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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-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.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-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-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/martian/v3 v3.1.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/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
-github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
-github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
-github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
-github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
-github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
-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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
-github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
-github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
-github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
-github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
-github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
-github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
-github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
-github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
-github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
-github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-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/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/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-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=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/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/lint v0.0.0-20201208152925-83fdc39ff7b5/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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-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-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/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-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/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/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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-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/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-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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-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-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-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/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-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/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-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/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/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=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-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/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/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/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/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/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/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=
-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=
diff --git a/v4/main.go b/v4/main.go
deleted file mode 100644
index 6ceb837..0000000
--- a/v4/main.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package main
-
-import "git.agecem.com/agecem/bottin/v4/cmd"
-
-func main() {
-	cmd.Execute()
-}
diff --git a/v4/web/embed.go b/web/embed.go
similarity index 100%
rename from v4/web/embed.go
rename to web/embed.go
diff --git a/v4/web/templates/index.html b/web/templates/index.html
similarity index 100%
rename from v4/web/templates/index.html
rename to web/templates/index.html
diff --git a/v4/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
similarity index 100%
rename from v4/web/webhandlers/handlers.go
rename to web/webhandlers/handlers.go

From 9782ef77f6c4447a706e916866829d189d49fa38 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 29 May 2023 18:20:00 -0400
Subject: [PATCH 03/99] Update copyright years

---
 LICENSE | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/LICENSE b/LICENSE
index 018068b..b5cc18d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2021 AGECEM
+Copyright (c) 2021-2023 AGECEM
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 

From 098d255190fc5c441269288afe3efe145f80d9df Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 02:35:15 -0400
Subject: [PATCH 04/99] =?UTF-8?q?D=C3=A9placer=20/v4=20->=20/v4/health?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Renommer handler GetV4 pour GetHealth

Ajouter GetHealthResponse pour serialize et deserialize json response
---
 cmd/api.go         |  2 +-
 handlers/health.go | 17 +++++++++++++++++
 handlers/v4.go     | 13 -------------
 3 files changed, 18 insertions(+), 14 deletions(-)
 create mode 100644 handlers/health.go
 delete mode 100644 handlers/v4.go

diff --git a/cmd/api.go b/cmd/api.go
index e44632c..a948b78 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -49,7 +49,7 @@ var apiCmd = &cobra.Command{
 
 		// Routes
 
-		e.GET("/v4/", handlers.GetV4)
+		e.GET("/v4/health", handlers.GetHealth)
 
 		e.POST("/v4/membres/", handlers.PostMembres)
 
diff --git a/handlers/health.go b/handlers/health.go
new file mode 100644
index 0000000..d917738
--- /dev/null
+++ b/handlers/health.go
@@ -0,0 +1,17 @@
+package handlers
+
+import (
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+)
+
+type GetHealthResponse struct {
+	Message string `json:"message"`
+}
+
+func GetHealth(c echo.Context) error {
+	response := GetHealthResponse{"Bottin API v4 is ready"}
+
+	return c.JSON(http.StatusOK, response)
+}
diff --git a/handlers/v4.go b/handlers/v4.go
deleted file mode 100644
index facbdde..0000000
--- a/handlers/v4.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package handlers
-
-import (
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-)
-
-func GetV4(c echo.Context) error {
-	return c.JSON(http.StatusOK, map[string]string{
-		"message": "Bottin API v4 is ready",
-	})
-}

From 4673612e06ef8e6a546181c978972f939bf40a60 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 02:43:37 -0400
Subject: [PATCH 05/99] Ajouter data.NewApiClientFromViper()

---
 data/apiclient.go | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/data/apiclient.go b/data/apiclient.go
index f97684b..9fc1d5a 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 
 	"git.agecem.com/agecem/bottin/v4/models"
+	"github.com/spf13/viper"
 )
 
 type ApiClient struct {
@@ -18,6 +19,15 @@ type ApiClient struct {
 	Protocol string
 }
 
+func NewApiClientFromViper() *ApiClient {
+	apiClientKey := viper.GetString("web.api.key")
+	apiClientHost := viper.GetString("web.api.host")
+	apiClientProtocol := viper.GetString("web.api.protocol")
+	apiClientPort := viper.GetInt("web.api.port")
+
+	return NewApiClient(apiClientKey, apiClientHost, apiClientProtocol, apiClientPort)
+}
+
 func NewApiClient(key, host, protocol string, port int) *ApiClient {
 	return &ApiClient{
 		Key:      key,

From 80a0260021463499204e42e9b2fac5eb00273f1b Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 02:44:11 -0400
Subject: [PATCH 06/99] Ajouter data.NewDataClientFromViper()

---
 data/data.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/data/data.go b/data/data.go
index 4572dea..9601792 100644
--- a/data/data.go
+++ b/data/data.go
@@ -7,6 +7,7 @@ import (
 	"git.agecem.com/agecem/bottin/v4/models"
 	_ "github.com/jackc/pgx/stdlib"
 	"github.com/jmoiron/sqlx"
+	"github.com/spf13/viper"
 )
 
 // DataClient is a postgres client based on sqlx
@@ -24,6 +25,19 @@ type PostgresConnection struct {
 	SSL      bool
 }
 
+func NewDataClientFromViper() (*DataClient, error) {
+	client, err := NewDataClient(
+		PostgresConnection{
+			User:     viper.GetString("db.user"),
+			Password: viper.GetString("db.password"),
+			Host:     viper.GetString("db.host"),
+			Database: viper.GetString("db.database"),
+			Port:     viper.GetInt("db.port"),
+		})
+
+	return client, err
+}
+
 func NewDataClient(connection PostgresConnection) (*DataClient, error) {
 	client := &DataClient{PostgresConnection: connection}
 

From 6d010c5009b3022e968759bb86a1591e63148a23 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 02:46:08 -0400
Subject: [PATCH 07/99] Cleanup webhandlers.GetMembre

Remplacer data.NewApiClient() -> data.NewApiClientFromViper()

Cleanup comments
---
 web/webhandlers/handlers.go | 23 +----------------------
 1 file changed, 1 insertion(+), 22 deletions(-)

diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
index 34724f9..732d3e7 100644
--- a/web/webhandlers/handlers.go
+++ b/web/webhandlers/handlers.go
@@ -6,7 +6,6 @@ import (
 
 	"git.agecem.com/agecem/bottin/v4/data"
 	"github.com/labstack/echo/v4"
-	"github.com/spf13/viper"
 )
 
 func GetIndex(c echo.Context) error {
@@ -14,30 +13,10 @@ func GetIndex(c echo.Context) error {
 }
 
 func GetMembre(c echo.Context) error {
-	apiClientKey := viper.GetString("web.api.key")
-	apiClientHost := viper.GetString("web.api.host")
-	apiClientProtocol := viper.GetString("web.api.protocol")
-	apiClientPort := viper.GetInt("web.api.port")
-
-	/*
-		log.Printf(`
-		    apiClientKey: %s
-		    apiClientHost: %s
-		    apiClientProtocol: %s
-		    apiClientPort: %d`,
-			apiClientKey, apiClientHost, apiClientProtocol, apiClientPort,
-		)
-	*/
-
-	apiClient := data.NewApiClient(apiClientKey, apiClientHost, apiClientProtocol, apiClientPort)
+	apiClient := data.NewApiClientFromViper()
 
 	membreID := c.QueryParam("membre_id")
 
-	/*
-		// TODO
-		log.Printf("Requesting membreID: [%s]", membreID)
-	*/
-
 	membre, err := apiClient.GetMembre(membreID)
 	if err != nil {
 		return c.Render(http.StatusBadRequest, "index-html", struct {

From b4af26d3ddffcc86e07e66e35eece2af5bbc5345 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 03:17:40 -0400
Subject: [PATCH 08/99] Fix /v4/health

---
 cmd/api.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/api.go b/cmd/api.go
index a948b78..5c2ce60 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -49,7 +49,7 @@ var apiCmd = &cobra.Command{
 
 		// Routes
 
-		e.GET("/v4/health", handlers.GetHealth)
+		e.GET("/v4/health/", handlers.GetHealth)
 
 		e.POST("/v4/membres/", handlers.PostMembres)
 

From 7823541f0eb77a0ab8cd1711355f9b953d35a1c2 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 03:17:43 -0400
Subject: [PATCH 09/99] =?UTF-8?q?Ajouter=20ping=20de=20database=20=C3=A0?=
 =?UTF-8?q?=20healthcheck?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 handlers/health.go | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/handlers/health.go b/handlers/health.go
index d917738..49d45b0 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -1,8 +1,10 @@
 package handlers
 
 import (
+	"fmt"
 	"net/http"
 
+	"git.agecem.com/agecem/bottin/v4/data"
 	"github.com/labstack/echo/v4"
 )
 
@@ -11,7 +13,23 @@ type GetHealthResponse struct {
 }
 
 func GetHealth(c echo.Context) error {
-	response := GetHealthResponse{"Bottin API v4 is ready"}
+	response := GetHealthResponse{
+		Message: "Bottin API v4 is ready",
+	}
+
+	dataClient, err := data.NewDataClientFromViper()
+	if err != nil {
+		response.Message = fmt.Sprintf("Error during data.NewDataClientFromViper(): %s", err)
+
+		return c.JSON(http.StatusInternalServerError, response)
+	}
+	defer dataClient.DB.Close()
+
+	if err = dataClient.DB.Ping(); err != nil {
+		response.Message = fmt.Sprintf("Error during dataClient.DB.Ping(): %s", err)
+
+		return c.JSON(http.StatusInternalServerError, response)
+	}
 
 	return c.JSON(http.StatusOK, response)
 }

From 412727bf9c6728260be0c9ceafda9cfaabc7c5c0 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 16:08:08 -0400
Subject: [PATCH 10/99] Fix local imports

---
 cmd/api.go                  | 4 ++--
 cmd/web.go                  | 6 +++---
 data/apiclient.go           | 2 +-
 data/data.go                | 2 +-
 go.mod                      | 2 +-
 handlers/health.go          | 2 +-
 handlers/insert.go          | 4 ++--
 handlers/read.go            | 2 +-
 handlers/seed.go            | 2 +-
 handlers/update.go          | 2 +-
 main.go                     | 2 +-
 web/webhandlers/handlers.go | 2 +-
 12 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/cmd/api.go b/cmd/api.go
index 5c2ce60..f1988b6 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -5,8 +5,8 @@ import (
 	"fmt"
 	"log"
 
-	"git.agecem.com/agecem/bottin/v4/data"
-	"git.agecem.com/agecem/bottin/v4/handlers"
+	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/handlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
diff --git a/cmd/web.go b/cmd/web.go
index 08f6375..adf7de3 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -8,9 +8,9 @@ import (
 	"io"
 	"log"
 
-	"git.agecem.com/agecem/bottin/v4/data"
-	"git.agecem.com/agecem/bottin/v4/web"
-	"git.agecem.com/agecem/bottin/v4/web/webhandlers"
+	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/web"
+	"git.agecem.com/agecem/bottin/web/webhandlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
diff --git a/data/apiclient.go b/data/apiclient.go
index 9fc1d5a..7143204 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -8,7 +8,7 @@ import (
 	"io/ioutil"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/models"
+	"git.agecem.com/agecem/bottin/models"
 	"github.com/spf13/viper"
 )
 
diff --git a/data/data.go b/data/data.go
index 9601792..5494cb1 100644
--- a/data/data.go
+++ b/data/data.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"fmt"
 
-	"git.agecem.com/agecem/bottin/v4/models"
+	"git.agecem.com/agecem/bottin/models"
 	_ "github.com/jackc/pgx/stdlib"
 	"github.com/jmoiron/sqlx"
 	"github.com/spf13/viper"
diff --git a/go.mod b/go.mod
index b6a59fc..5963afa 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.agecem.com/agecem/bottin/v4
+module git.agecem.com/agecem/bottin
 
 go 1.20
 
diff --git a/handlers/health.go b/handlers/health.go
index 49d45b0..99cc498 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/data"
+	"git.agecem.com/agecem/bottin/data"
 	"github.com/labstack/echo/v4"
 )
 
diff --git a/handlers/insert.go b/handlers/insert.go
index 629d6e7..38cad02 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -3,8 +3,8 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/data"
-	"git.agecem.com/agecem/bottin/v4/models"
+	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/models"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/handlers/read.go b/handlers/read.go
index 8136e21..3932ec3 100644
--- a/handlers/read.go
+++ b/handlers/read.go
@@ -3,7 +3,7 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/data"
+	"git.agecem.com/agecem/bottin/data"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/handlers/seed.go b/handlers/seed.go
index 91d5037..237de09 100644
--- a/handlers/seed.go
+++ b/handlers/seed.go
@@ -3,7 +3,7 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/data"
+	"git.agecem.com/agecem/bottin/data"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/handlers/update.go b/handlers/update.go
index 8a8cf0a..6422f61 100644
--- a/handlers/update.go
+++ b/handlers/update.go
@@ -3,7 +3,7 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/data"
+	"git.agecem.com/agecem/bottin/data"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/main.go b/main.go
index 6ceb837..c6efc0b 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,6 @@
 package main
 
-import "git.agecem.com/agecem/bottin/v4/cmd"
+import "git.agecem.com/agecem/bottin/cmd"
 
 func main() {
 	cmd.Execute()
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
index 732d3e7..abfa955 100644
--- a/web/webhandlers/handlers.go
+++ b/web/webhandlers/handlers.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v4/data"
+	"git.agecem.com/agecem/bottin/data"
 	"github.com/labstack/echo/v4"
 )
 

From 883edccf8703628f9dd4723f9ae086f72f3809ef Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 16:16:18 -0400
Subject: [PATCH 11/99] Cleanup dataclient pour cmd/api

Utiliser NewDataClientFromViper
---
 cmd/api.go | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/cmd/api.go b/cmd/api.go
index f1988b6..a7317b1 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -27,14 +27,6 @@ var apiCmd = &cobra.Command{
 		apiKey = viper.GetString("api.key")
 		apiPort = viper.GetInt("api.port")
 
-		connection := data.PostgresConnection{
-			User:     viper.GetString("db.user"),
-			Password: viper.GetString("db.password"),
-			Host:     viper.GetString("db.host"),
-			Database: viper.GetString("db.database"),
-			Port:     viper.GetInt("db.port"),
-		}
-
 		e := echo.New()
 
 		// Middlewares
@@ -63,7 +55,7 @@ var apiCmd = &cobra.Command{
 
 		// Execution
 
-		client, err := data.NewDataClient(connection)
+		client, err := data.NewDataClientFromViper()
 		if err != nil {
 			log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
 		}

From 9ac95672b9381b99e7f02e713fe555b5e221d072 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 16:21:14 -0400
Subject: [PATCH 12/99] Fix ApiClient.GetHealth

Utiliser type handlers.GetHealthResponse

Pointer vers route /v4/health
---
 data/apiclient.go | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/data/apiclient.go b/data/apiclient.go
index 7143204..21ceecc 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 	"net/http"
 
+	"git.agecem.com/agecem/bottin/handlers"
 	"git.agecem.com/agecem/bottin/models"
 	"github.com/spf13/viper"
 )
@@ -79,33 +80,31 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo
 	return response, nil
 }
 
-// GetV4 allows checking for API v4 server health
-func (a *ApiClient) GetV4() (string, error) {
-	var getV4Response struct {
-		Message string `json:"message"`
-	}
+// GetHealth allows checking for API server health
+func (a *ApiClient) GetHealth() (string, error) {
+	var getHealthResponse handlers.GetHealthResponse
 
-	response, err := a.Call(http.MethodGet, "/v4", nil, true)
+	response, err := a.Call(http.MethodGet, "/v4/health", nil, true)
 	if err != nil {
-		return getV4Response.Message, err
+		return getHealthResponse.Message, err
 	}
 
 	defer response.Body.Close()
 
 	body, err := ioutil.ReadAll(response.Body)
 	if err != nil {
-		return getV4Response.Message, err
+		return getHealthResponse.Message, err
 	}
 
-	if err := json.Unmarshal(body, &getV4Response); err != nil {
-		return getV4Response.Message, err
+	if err := json.Unmarshal(body, &getHealthResponse); err != nil {
+		return getHealthResponse.Message, err
 	}
 
-	if getV4Response.Message == "" {
-		return getV4Response.Message, errors.New("Could not confirm that API server is up, no response message")
+	if getHealthResponse.Message == "" {
+		return getHealthResponse.Message, errors.New("Could not confirm that API server is up, no response message")
 	}
 
-	return getV4Response.Message, nil
+	return getHealthResponse.Message, nil
 }
 
 func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {

From 764093f99d2993db8df7ab6773b5b39c539f8928 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 16:33:13 -0400
Subject: [PATCH 13/99] Bump routes v4 -> v5
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Remplacer références GetV4 -> GetHealth

Ajouter package responses

Remplacer handlers.GetHealthResponse -> responses.GetHealth
---
 cmd/api.go          | 12 ++++++------
 cmd/web.go          |  2 +-
 data/apiclient.go   |  8 ++++----
 handlers/health.go  |  9 +++------
 responses/health.go |  6 ++++++
 5 files changed, 20 insertions(+), 17 deletions(-)
 create mode 100644 responses/health.go

diff --git a/cmd/api.go b/cmd/api.go
index a7317b1..1044c34 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -41,17 +41,17 @@ var apiCmd = &cobra.Command{
 
 		// Routes
 
-		e.GET("/v4/health/", handlers.GetHealth)
+		e.GET("/v5/health/", handlers.GetHealth)
 
-		e.POST("/v4/membres/", handlers.PostMembres)
+		e.POST("/v5/membres/", handlers.PostMembres)
 
-		e.GET("/v4/membres/:membre_id/", handlers.ReadMembre)
+		e.GET("/v5/membres/:membre_id/", handlers.ReadMembre)
 
-		e.PUT("/v4/membres/:membre_id/prefered_name/", handlers.PutMembrePreferedName)
+		e.PUT("/v5/membres/:membre_id/prefered_name/", handlers.PutMembrePreferedName)
 
-		e.POST("/v4/programmes/", handlers.PostProgrammes)
+		e.POST("/v5/programmes/", handlers.PostProgrammes)
 
-		e.POST("/v4/seed/", handlers.PostSeed)
+		e.POST("/v5/seed/", handlers.PostSeed)
 
 		// Execution
 
diff --git a/cmd/web.go b/cmd/web.go
index adf7de3..c773b56 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -55,7 +55,7 @@ var webCmd = &cobra.Command{
 
 		apiClient := data.NewApiClient(webApiKey, webApiHost, webApiProtocol, webApiPort)
 
-		pingResult, err := apiClient.GetV4()
+		pingResult, err := apiClient.GetHealth()
 		if err != nil {
 			log.Fatal(err)
 		}
diff --git a/data/apiclient.go b/data/apiclient.go
index 21ceecc..1eef66a 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -8,8 +8,8 @@ import (
 	"io/ioutil"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/handlers"
 	"git.agecem.com/agecem/bottin/models"
+	"git.agecem.com/agecem/bottin/responses"
 	"github.com/spf13/viper"
 )
 
@@ -82,9 +82,9 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo
 
 // GetHealth allows checking for API server health
 func (a *ApiClient) GetHealth() (string, error) {
-	var getHealthResponse handlers.GetHealthResponse
+	var getHealthResponse responses.GetHealth
 
-	response, err := a.Call(http.MethodGet, "/v4/health", nil, true)
+	response, err := a.Call(http.MethodGet, "/v5/health", nil, true)
 	if err != nil {
 		return getHealthResponse.Message, err
 	}
@@ -124,7 +124,7 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 		log.Println("ApiClient.GetMembre received membreID: ", membreID)
 	*/
 
-	response, err := a.Call(http.MethodGet, fmt.Sprintf("/v4/membres/%s", membreID), nil, true)
+	response, err := a.Call(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true)
 	if err != nil {
 		return getMembreResponse.Data.Membre, err
 	}
diff --git a/handlers/health.go b/handlers/health.go
index 99cc498..5292e56 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -5,16 +5,13 @@ import (
 	"net/http"
 
 	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/responses"
 	"github.com/labstack/echo/v4"
 )
 
-type GetHealthResponse struct {
-	Message string `json:"message"`
-}
-
 func GetHealth(c echo.Context) error {
-	response := GetHealthResponse{
-		Message: "Bottin API v4 is ready",
+	response := responses.GetHealth{
+		Message: "Bottin API v5 is ready",
 	}
 
 	dataClient, err := data.NewDataClientFromViper()
diff --git a/responses/health.go b/responses/health.go
new file mode 100644
index 0000000..6d5e663
--- /dev/null
+++ b/responses/health.go
@@ -0,0 +1,6 @@
+package responses
+
+// GetHealth is the response type for handlers.GetHealth
+type GetHealth struct {
+	Message string `json:"message"`
+}

From b45c074a120d6cc869b7cbe31c402ece501c669c Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 16:36:08 -0400
Subject: [PATCH 14/99] =?UTF-8?q?Ajouter=20responses/=20=C3=A0=20build=20s?=
 =?UTF-8?q?tep?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Dockerfile | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Dockerfile b/Dockerfile
index 805727f..23820d5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,7 @@ ADD cmd/ cmd/
 ADD data/ data/
 ADD handlers/ handlers/
 ADD models/ models/
+ADD responses/ responses/
 ADD web/ web/
 
 RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin .

From 607165a021457c0a66a9ce41df9a770071d009b6 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 16:42:29 -0400
Subject: [PATCH 15/99] =?UTF-8?q?Retirer=20r=C3=A9f=C3=A9rences=20=C3=A0?=
 =?UTF-8?q?=20/v4=20de=20README.md?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index c32ebe5..74d3152 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# agecem/bottin/v4
+# agecem/bottin
 
-Version 4 du bottin de la masse étudiante, en Go
+Bottin de la masse étudiante, en Go
 
 https://git.agecem.com/agecem/bottin
 

From 3f8074b2379dedb9a959634ffa175b4f942e36e1 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 17:38:42 -0400
Subject: [PATCH 16/99] =?UTF-8?q?Bump=20go.mod=20=C3=A0=20v5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 go.mod | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 5963afa..708678c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.agecem.com/agecem/bottin
+module git.agecem.com/agecem/bottin/v5
 
 go 1.20
 

From 382ba098509fa4b002c0bb2927159869b8cf5a66 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 2 Jun 2023 17:46:57 -0400
Subject: [PATCH 17/99] Pointer subfolders vers agecem/bottin/v5

---
 cmd/api.go                  | 4 ++--
 cmd/web.go                  | 6 +++---
 data/apiclient.go           | 4 ++--
 data/data.go                | 2 +-
 handlers/health.go          | 4 ++--
 handlers/insert.go          | 4 ++--
 handlers/read.go            | 2 +-
 handlers/seed.go            | 2 +-
 handlers/update.go          | 2 +-
 main.go                     | 2 +-
 web/webhandlers/handlers.go | 2 +-
 11 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/cmd/api.go b/cmd/api.go
index 1044c34..e44ab61 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -5,8 +5,8 @@ import (
 	"fmt"
 	"log"
 
-	"git.agecem.com/agecem/bottin/data"
-	"git.agecem.com/agecem/bottin/handlers"
+	"git.agecem.com/agecem/bottin/v5/data"
+	"git.agecem.com/agecem/bottin/v5/handlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
diff --git a/cmd/web.go b/cmd/web.go
index c773b56..d09477c 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -8,9 +8,9 @@ import (
 	"io"
 	"log"
 
-	"git.agecem.com/agecem/bottin/data"
-	"git.agecem.com/agecem/bottin/web"
-	"git.agecem.com/agecem/bottin/web/webhandlers"
+	"git.agecem.com/agecem/bottin/v5/data"
+	"git.agecem.com/agecem/bottin/v5/web"
+	"git.agecem.com/agecem/bottin/v5/web/webhandlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
diff --git a/data/apiclient.go b/data/apiclient.go
index 1eef66a..585dd33 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -8,8 +8,8 @@ import (
 	"io/ioutil"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/models"
-	"git.agecem.com/agecem/bottin/responses"
+	"git.agecem.com/agecem/bottin/v5/models"
+	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/spf13/viper"
 )
 
diff --git a/data/data.go b/data/data.go
index 5494cb1..1e4d17a 100644
--- a/data/data.go
+++ b/data/data.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"fmt"
 
-	"git.agecem.com/agecem/bottin/models"
+	"git.agecem.com/agecem/bottin/v5/models"
 	_ "github.com/jackc/pgx/stdlib"
 	"github.com/jmoiron/sqlx"
 	"github.com/spf13/viper"
diff --git a/handlers/health.go b/handlers/health.go
index 5292e56..f8baece 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/data"
-	"git.agecem.com/agecem/bottin/responses"
+	"git.agecem.com/agecem/bottin/v5/data"
+	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/labstack/echo/v4"
 )
 
diff --git a/handlers/insert.go b/handlers/insert.go
index 38cad02..0542bc5 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -3,8 +3,8 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/data"
-	"git.agecem.com/agecem/bottin/models"
+	"git.agecem.com/agecem/bottin/v5/data"
+	"git.agecem.com/agecem/bottin/v5/models"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/handlers/read.go b/handlers/read.go
index 3932ec3..c5a074d 100644
--- a/handlers/read.go
+++ b/handlers/read.go
@@ -3,7 +3,7 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/handlers/seed.go b/handlers/seed.go
index 237de09..947707d 100644
--- a/handlers/seed.go
+++ b/handlers/seed.go
@@ -3,7 +3,7 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/handlers/update.go b/handlers/update.go
index 6422f61..a0d9ba2 100644
--- a/handlers/update.go
+++ b/handlers/update.go
@@ -3,7 +3,7 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
 	"github.com/spf13/viper"
 )
diff --git a/main.go b/main.go
index c6efc0b..567ba46 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,6 @@
 package main
 
-import "git.agecem.com/agecem/bottin/cmd"
+import "git.agecem.com/agecem/bottin/v5/cmd"
 
 func main() {
 	cmd.Execute()
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
index abfa955..dce8399 100644
--- a/web/webhandlers/handlers.go
+++ b/web/webhandlers/handlers.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/data"
+	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
 )
 

From 4327176d443a0d5272417d786642f01c23abc383 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Sat, 3 Jun 2023 19:19:50 -0400
Subject: [PATCH 18/99] Remettre barebones v4

---
 v4/README.md |  1 +
 v4/go.mod    | 10 ++++++++++
 2 files changed, 11 insertions(+)
 create mode 100644 v4/README.md
 create mode 100644 v4/go.mod

diff --git a/v4/README.md b/v4/README.md
new file mode 100644
index 0000000..4da14f8
--- /dev/null
+++ b/v4/README.md
@@ -0,0 +1 @@
+deprecated, see git.agecem.com/agecem/bottin or git.agecem.com/agecem/bottin/v5
diff --git a/v4/go.mod b/v4/go.mod
new file mode 100644
index 0000000..2d01404
--- /dev/null
+++ b/v4/go.mod
@@ -0,0 +1,10 @@
+module git.agecem.com/agecem/bottin/v4
+
+go 1.20
+
+//retract (
+//	v4.1.0
+//	v4.0.3
+//	v4.0.2
+//	v4.0.1
+//)

From bcdbe4bd174b5d268f278a352729caba0be51bc3 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Sat, 3 Jun 2023 19:22:17 -0400
Subject: [PATCH 19/99] =?UTF-8?q?Remettre=20license=20=C3=A0=20v4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 v4/LICENSE | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 v4/LICENSE

diff --git a/v4/LICENSE b/v4/LICENSE
new file mode 100644
index 0000000..b5cc18d
--- /dev/null
+++ b/v4/LICENSE
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2021-2023 AGECEM
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

From 1ca46703756b20d2b23c8e527ef221082e057cf1 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Sat, 3 Jun 2023 19:33:56 -0400
Subject: [PATCH 20/99] Simplifier dataclient dans insert handler

---
 handlers/insert.go | 21 ++-------------------
 1 file changed, 2 insertions(+), 19 deletions(-)

diff --git a/handlers/insert.go b/handlers/insert.go
index 0542bc5..b928942 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -6,19 +6,10 @@ import (
 	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/models"
 	"github.com/labstack/echo/v4"
-	"github.com/spf13/viper"
 )
 
 func PostMembres(c echo.Context) error {
-	connection := data.PostgresConnection{
-		User:     viper.GetString("db.user"),
-		Password: viper.GetString("db.password"),
-		Host:     viper.GetString("db.host"),
-		Database: viper.GetString("db.database"),
-		Port:     viper.GetInt("db.port"),
-	}
-
-	client, err := data.NewDataClient(connection)
+	client, err := data.NewDataClientFromViper()
 	if err != nil {
 		return c.JSON(http.StatusInternalServerError, map[string]string{
 			"message": "Could not establish database connection",
@@ -58,15 +49,7 @@ func PostMembres(c echo.Context) error {
 }
 
 func PostProgrammes(c echo.Context) error {
-	connection := data.PostgresConnection{
-		User:     viper.GetString("db.user"),
-		Password: viper.GetString("db.password"),
-		Host:     viper.GetString("db.host"),
-		Database: viper.GetString("db.database"),
-		Port:     viper.GetInt("db.port"),
-	}
-
-	client, err := data.NewDataClient(connection)
+	client, err := data.NewDataClientFromViper()
 	if err != nil {
 		return c.JSON(http.StatusInternalServerError, map[string]string{
 			"message": "Could not establish database connection",

From 3870ef42dd24d1cb27d85ae5cb7577405d4a14ab Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 8 Jun 2023 19:47:25 -0400
Subject: [PATCH 21/99] Defer DB.Close() sur chaque DataClient

---
 cmd/api.go         | 3 +--
 handlers/insert.go | 2 ++
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/cmd/api.go b/cmd/api.go
index e44ab61..4d6a30e 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -59,14 +59,13 @@ var apiCmd = &cobra.Command{
 		if err != nil {
 			log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
 		}
+		defer client.DB.Close()
 
 		err = client.DB.Ping()
 		if err != nil {
 			log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
 		}
 
-		client.DB.Close()
-
 		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
 	},
 }
diff --git a/handlers/insert.go b/handlers/insert.go
index b928942..0fc6238 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -16,6 +16,7 @@ func PostMembres(c echo.Context) error {
 			"error":   err.Error(),
 		})
 	}
+	defer client.DB.Close()
 
 	var membres []models.Membre
 
@@ -56,6 +57,7 @@ func PostProgrammes(c echo.Context) error {
 			"error":   err.Error(),
 		})
 	}
+	defer client.DB.Close()
 
 	var programmes []models.Programme
 

From 55f5ce96b87443da2c32f468521cdb16d7ddba8a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 8 Jun 2023 20:05:05 -0400
Subject: [PATCH 22/99] =?UTF-8?q?Cleanup=20blocs=20de=20commentaires=20d?=
 =?UTF-8?q?=C3=A9suets?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 data/apiclient.go |  16 ------
 data/data.go      | 122 ----------------------------------------------
 handlers/read.go  |  68 --------------------------
 3 files changed, 206 deletions(-)

diff --git a/data/apiclient.go b/data/apiclient.go
index 585dd33..2f1f1ae 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -45,11 +45,6 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo
 		a.Protocol, a.Host, a.Port, route,
 	)
 
-	/*
-		//TODO
-		log.Println("endpoint: ", endpoint)
-	*/
-
 	// Create client
 	client := &http.Client{}
 
@@ -119,11 +114,6 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 		return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher")
 	}
 
-	//TODO
-	/*
-		log.Println("ApiClient.GetMembre received membreID: ", membreID)
-	*/
-
 	response, err := a.Call(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true)
 	if err != nil {
 		return getMembreResponse.Data.Membre, err
@@ -140,12 +130,6 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 		return getMembreResponse.Data.Membre, err
 	}
 
-	/*
-		if getMembreResponse.Message != "Read successful" {
-			return getMembreResponse.Data.Membre, errors.New(getMembreResponse.Message)
-		}
-	*/
-
 	if getMembreResponse.Data.Membre == *new(models.Membre) {
 		return getMembreResponse.Data.Membre, fmt.Errorf("Ce numéro étudiant ne correspond à aucunE membre")
 	}
diff --git a/data/data.go b/data/data.go
index 1e4d17a..9a16342 100644
--- a/data/data.go
+++ b/data/data.go
@@ -182,125 +182,3 @@ func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
 
 	return rows, nil
 }
-
-/*
-func (d *DataClient) Insert(assets []models.Asset) (id int64, err error) {
-	// Check for minimal required info
-	for _, asset := range assets {
-		if asset.Description == "" {
-			err = errors.New("Cannot insert: At least one asset has no `description` set.")
-			return
-		}
-	}
-
-	tx := d.DB.MustBegin()
-
-	for _, asset := range assets {
-		_, err = tx.NamedExec("INSERT INTO assets (description, status, created_at) VALUES (:description, :status, current_timestamp)", asset)
-		if err != nil {
-			return
-		}
-	}
-
-	err = tx.Commit()
-
-	return
-}
-
-func (d *DataClient) List() ([]models.Asset, error) {
-	// Query the database, storing results in a []Person (wrapped in []interface{})
-	assets := []models.Asset{}
-
-	err := d.DB.Select(&assets, "SELECT * FROM assets WHERE deleted_at IS NULL LIMIT 1000")
-	if err != nil {
-		return nil, err
-	}
-
-	return assets, nil
-}
-
-// RecordEvent allows inserting into events when an asset or a tag is modified
-// or deleted.
-func (d *DataClient) RecordEvent(assetID, tagID int64, content string) error {
-	event := models.Event{
-		AssetID: assetID,
-		TagID:   tagID,
-		Content: content,
-	}
-	_, err := d.DB.NamedExec("INSERT INTO events (asset_id, tag_id, at, content) VALUES (:asset_id, :tag_id, current_timestamp, :content);", event)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (d *DataClient) Delete(assetIDs []int64) ([]int64, error) {
-	var rows []int64
-
-	tx := d.DB.MustBegin()
-
-	for _, assetID := range assetIDs {
-		result, err := d.DB.Exec("UPDATE assets SET deleted_at = current_timestamp WHERE id = $1 AND deleted_at IS NULL;", assetID)
-		if err != nil {
-			return rows, err
-		}
-
-		rowsAffected, err := result.RowsAffected()
-		if err != nil {
-			return rows, err
-		}
-
-		if rowsAffected != 0 {
-			rows = append(rows, assetID)
-		}
-	}
-
-	err := tx.Commit()
-	if err != nil {
-		return rows, err
-	}
-
-	for _, assetID := range assetIDs {
-		d.RecordEvent(assetID, -1, fmt.Sprintf("Asset %d deleted.", assetID))
-	}
-
-	return rows, nil
-}
-
-func (d *DataClient) UpdateAssetDescription(assetID int64, description string) (int64, error) {
-	result, err := d.DB.Exec("UPDATE assets SET description = $1 WHERE id = $2", description, assetID)
-	if err != nil {
-		return 0, err
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return 0, err
-	}
-
-	if rowsAffected != 0 {
-		return 0, errors.New("Nothing to do")
-	}
-
-	return rowsAffected, nil
-}
-
-func (d *DataClient) UpdateAssetStatus(assetID int64, status string) (int64, error) {
-	result, err := d.DB.Exec("UPDATE assets SET status = $1 WHERE id = $2", status, assetID)
-	if err != nil {
-		return 0, err
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return 0, err
-	}
-
-	if rowsAffected != 0 {
-		return 0, errors.New("Nothing to do")
-	}
-
-	return rowsAffected, nil
-}
-*/
diff --git a/handlers/read.go b/handlers/read.go
index c5a074d..13a8007 100644
--- a/handlers/read.go
+++ b/handlers/read.go
@@ -47,71 +47,3 @@ func ReadMembre(c echo.Context) error {
 		},
 	})
 }
-
-/*
-func PostMembres(c echo.Context) error {
-	connection := data.PostgresConnection{
-		User:     viper.GetString("db.user"),
-		Password: viper.GetString("db.password"),
-		Host:     viper.GetString("db.host"),
-		Database: viper.GetString("db.database"),
-		Port:     viper.GetInt("db.port"),
-	}
-
-	client, err := data.NewDataClient(connection)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not establish database connection",
-			"error":   err.Error(),
-		})
-	}
-
-	programmes := []models.Programme{
-		{
-			ID:    "foo",
-			Titre: "Foo",
-		},
-		{
-			ID:    "bar",
-			Titre: "Bar",
-		},
-	}
-
-	newProgrammes, err := client.InsertProgrammes(programmes)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not insert programmes",
-			"error":   err.Error(),
-		})
-	}
-
-	membres := []models.Membre{
-		{
-			ID:           "1327163",
-			PreferedName: "victor",
-			ProgrammeID:  "foo",
-		},
-		{
-			ID:           "0000000",
-			PreferedName: "test user",
-			ProgrammeID:  "bar",
-		},
-	}
-
-	newMembres, err := client.InsertMembres(membres)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not insert membres",
-			"error":   err.Error(),
-		})
-	}
-
-	return c.JSON(http.StatusOK, map[string]interface{}{
-		"message": "Insert successful",
-		"data": map[string]interface{}{
-			"membres":    newMembres,
-			"programmes": newProgrammes,
-		},
-	})
-}
-*/

From 2a391ae80d75ac3ef6374b989b71d15a3e4fa0e9 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 29 Jun 2023 17:18:38 -0400
Subject: [PATCH 23/99] =?UTF-8?q?Cr=C3=A9er=20les=20tables=20seulement=20s?=
 =?UTF-8?q?i=20elles=20n'existent=20pas=20d=C3=A9j=C3=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 models/models.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/models/models.go b/models/models.go
index c3f4af8..1589aee 100644
--- a/models/models.go
+++ b/models/models.go
@@ -1,12 +1,12 @@
 package models
 
 const Schema = `
-CREATE TABLE programmes (
+CREATE TABLE IF NOT EXISTS programmes (
     id TEXT PRIMARY KEY,
     titre TEXT
 );
 
-CREATE TABLE membres (
+CREATE TABLE IF NOT EXISTS membres (
     id VARCHAR(7) PRIMARY KEY,
     last_name TEXT,
     first_name TEXT,

From 47670ec979fab56ad61a2c7736a5c25ff9918099 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 29 Jun 2023 18:36:13 -0400
Subject: [PATCH 24/99] =?UTF-8?q?Seed()=20la=20base=20de=20donn=C3=A9es=20?=
 =?UTF-8?q?lors=20de=20apiCmd?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd/api.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/cmd/api.go b/cmd/api.go
index 4d6a30e..9a05014 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -66,6 +66,11 @@ var apiCmd = &cobra.Command{
 			log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
 		}
 
+		_, err = client.Seed()
+		if err != nil {
+			log.Fatalf("Error during client.Seed(): %s", err)
+		}
+
 		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
 	},
 }

From 93334871e9cdb6dd1c0f86cecd9628ac292160b2 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 5 Sep 2023 16:14:54 -0400
Subject: [PATCH 25/99] Ajouter Response et Responder

---
 responses/root.go | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 responses/root.go

diff --git a/responses/root.go b/responses/root.go
new file mode 100644
index 0000000..0f0bcce
--- /dev/null
+++ b/responses/root.go
@@ -0,0 +1,15 @@
+package responses
+
+type Response struct {
+	StatusCode int
+	Message    string
+	Error      string
+}
+
+type Responder interface {
+	Respond() Responder
+}
+
+func (r Response) Respond() Responder {
+	return r
+}

From 6faca0e708107b108cb54f12bdd581c9790ade7a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 5 Sep 2023 16:15:22 -0400
Subject: [PATCH 26/99] Ajouter PostMembres et postProgrammes responses

---
 responses/post.go | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 responses/post.go

diff --git a/responses/post.go b/responses/post.go
new file mode 100644
index 0000000..91eef96
--- /dev/null
+++ b/responses/post.go
@@ -0,0 +1,15 @@
+package responses
+
+type PostMembresResponse struct {
+	Response
+	Data struct {
+		MembresInserted int64
+	}
+}
+
+type PostProgrammesResponse struct {
+	Response
+	Data struct {
+		ProgrammesInserted int64
+	}
+}

From 59eeb7a38a0e2c4513db3b544dcc7f68f3297a4b Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 5 Sep 2023 16:15:40 -0400
Subject: [PATCH 27/99] =?UTF-8?q?Impl=C3=A9menter=20responses=20POST=20pou?=
 =?UTF-8?q?r=20application/json?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 handlers/insert.go | 112 +++++++++++++++++++++++++++------------------
 1 file changed, 68 insertions(+), 44 deletions(-)

diff --git a/handlers/insert.go b/handlers/insert.go
index 0fc6238..7452659 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -5,87 +5,111 @@ import (
 
 	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/models"
+	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/labstack/echo/v4"
 )
 
 func PostMembres(c echo.Context) error {
+	var response responses.PostMembresResponse
 	client, err := data.NewDataClientFromViper()
 	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not establish database connection",
-			"error":   err.Error(),
-		})
+		response.StatusCode = http.StatusInternalServerError
+		response.Message = "Could not establish database connection"
+		response.Error = err.Error()
+
+		return c.JSON(response.StatusCode, response)
 	}
 	defer client.DB.Close()
 
 	var membres []models.Membre
 
-	if err := c.Bind(&membres); err != nil {
-		return c.JSON(http.StatusBadRequest, map[string]string{
-			"message": "Could not bind membres",
-			"error":   err.Error(),
-		})
+	switch c.Request().Header.Get("Content-Type") {
+	case "application/json":
+		if err := c.Bind(&membres); err != nil {
+			response.StatusCode = http.StatusBadRequest
+			response.Message = "Could not bind membres"
+			response.Error = err.Error()
+			return c.JSON(response.StatusCode, response)
+		}
+	case "text/csv":
+		response.StatusCode = http.StatusNotImplemented
+		response.Message = "Not Implemented"
+		return c.JSON(response.StatusCode, response)
+	default:
+		response.StatusCode = http.StatusBadRequest
+		response.Message = "Invalid Content-Type"
+		return c.JSON(response.StatusCode, response)
 	}
 
 	if len(membres) == 0 {
-		return c.JSON(http.StatusOK, map[string]string{
-			"message": "Nothing to do",
-		})
+		response.StatusCode = http.StatusOK
+		response.Message = "Nothing to do"
+		return c.JSON(response.StatusCode, response)
 	}
 
 	newMembres, err := client.InsertMembres(membres)
 	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not insert membres",
-			"error":   err.Error(),
-		})
+		response.StatusCode = http.StatusInternalServerError
+		response.Message = "Could not insert membres"
+		response.Error = err.Error()
+		return c.JSON(response.StatusCode, response)
 	}
 
-	return c.JSON(http.StatusCreated, map[string]interface{}{
-		"message": "Insert successful",
-		"data": map[string]interface{}{
-			"membres": newMembres,
-		},
-	})
+	response.StatusCode = http.StatusCreated
+	response.Message = "Insert successful"
+	response.Data.MembresInserted = newMembres
+	return c.JSON(response.StatusCode, response)
 }
 
 func PostProgrammes(c echo.Context) error {
+	var response responses.PostProgrammesResponse
+
 	client, err := data.NewDataClientFromViper()
 	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not establish database connection",
-			"error":   err.Error(),
-		})
+		response.StatusCode = http.StatusInternalServerError
+		response.Message = "Could not establish database connection"
+		response.Error = err.Error()
+
+		return c.JSON(response.StatusCode, response)
 	}
 	defer client.DB.Close()
 
 	var programmes []models.Programme
 
-	if err := c.Bind(&programmes); err != nil {
-		return c.JSON(http.StatusBadRequest, map[string]string{
-			"message": "Could not bind programmes",
-			"error":   err.Error(),
-		})
+	switch c.Request().Header.Get("Content-Type") {
+	case "application/json":
+		if err := c.Bind(&programmes); err != nil {
+			response.StatusCode = http.StatusBadRequest
+			response.Message = "Could not bind programmes"
+			response.Error = err.Error()
+			return c.JSON(response.StatusCode, response)
+		}
+	case "text/csv":
+		response.StatusCode = http.StatusNotImplemented
+		response.Message = "Not Implemented"
+		return c.JSON(response.StatusCode, response)
+	default:
+		response.StatusCode = http.StatusBadRequest
+		response.Message = "Invalid Content-Type"
+		return c.JSON(response.StatusCode, response)
 	}
 
 	if len(programmes) == 0 {
-		return c.JSON(http.StatusOK, map[string]string{
-			"message": "Nothing to do",
-		})
+		response.StatusCode = http.StatusOK
+		response.Message = "Nothing to do"
+		return c.JSON(response.StatusCode, response)
 	}
 
 	newProgrammes, err := client.InsertProgrammes(programmes)
 	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not insert programmes",
-			"error":   err.Error(),
-		})
+		response.StatusCode = http.StatusInternalServerError
+		response.Message = "Could not insert programmes"
+		response.Error = err.Error()
+		return c.JSON(response.StatusCode, response)
 	}
 
-	return c.JSON(http.StatusCreated, map[string]interface{}{
-		"message": "Insert successful",
-		"data": map[string]interface{}{
-			"programmes": newProgrammes,
-		},
-	})
+	response.StatusCode = http.StatusCreated
+	response.Message = "Insert successful"
+	response.Data.ProgrammesInserted = newProgrammes
+	return c.JSON(response.StatusCode, response)
 }

From 0f563487465ed9243edca9629024c4aefd3461af Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 5 Sep 2023 16:30:15 -0400
Subject: [PATCH 28/99] =?UTF-8?q?Impl=C3=A9menter=20Response=20dans=20GetH?=
 =?UTF-8?q?ealthResponse?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 data/apiclient.go   |  2 +-
 handlers/health.go  | 22 +++++++++++++---------
 responses/health.go |  5 ++---
 3 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/data/apiclient.go b/data/apiclient.go
index 2f1f1ae..3319d72 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -77,7 +77,7 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo
 
 // GetHealth allows checking for API server health
 func (a *ApiClient) GetHealth() (string, error) {
-	var getHealthResponse responses.GetHealth
+	var getHealthResponse responses.GetHealthResponse
 
 	response, err := a.Call(http.MethodGet, "/v5/health", nil, true)
 	if err != nil {
diff --git a/handlers/health.go b/handlers/health.go
index f8baece..d4fe868 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -1,7 +1,6 @@
 package handlers
 
 import (
-	"fmt"
 	"net/http"
 
 	"git.agecem.com/agecem/bottin/v5/data"
@@ -10,23 +9,28 @@ import (
 )
 
 func GetHealth(c echo.Context) error {
-	response := responses.GetHealth{
-		Message: "Bottin API v5 is ready",
-	}
+	var response responses.GetHealthResponse
 
 	dataClient, err := data.NewDataClientFromViper()
 	if err != nil {
-		response.Message = fmt.Sprintf("Error during data.NewDataClientFromViper(): %s", err)
+		response.StatusCode = http.StatusInternalServerError
+		response.Message = "Error during data.NewDataClientFromViper()"
+		response.Error = err.Error()
 
-		return c.JSON(http.StatusInternalServerError, response)
+		return c.JSON(response.StatusCode, response)
 	}
 	defer dataClient.DB.Close()
 
 	if err = dataClient.DB.Ping(); err != nil {
-		response.Message = fmt.Sprintf("Error during dataClient.DB.Ping(): %s", err)
+		response.StatusCode = http.StatusInternalServerError
+		response.Message = "Error during dataClient.DB.Ping()"
+		response.Error = err.Error()
 
-		return c.JSON(http.StatusInternalServerError, response)
+		return c.JSON(response.StatusCode, response)
 	}
 
-	return c.JSON(http.StatusOK, response)
+	response.StatusCode = http.StatusOK
+	response.Message = "Bottin API v5 is ready"
+
+	return c.JSON(response.StatusCode, response)
 }
diff --git a/responses/health.go b/responses/health.go
index 6d5e663..854279d 100644
--- a/responses/health.go
+++ b/responses/health.go
@@ -1,6 +1,5 @@
 package responses
 
-// GetHealth is the response type for handlers.GetHealth
-type GetHealth struct {
-	Message string `json:"message"`
+type GetHealthResponse struct {
+	Response
 }

From 7b9ff49444695b068e396d929ae839be537d8512 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 5 Sep 2023 18:03:28 -0400
Subject: [PATCH 29/99] Permettre upload par CSV

---
 data/data.go       |  4 ++--
 go.mod             |  1 +
 go.sum             |  2 ++
 handlers/insert.go | 46 ++++++++++++++++++++++++++++++++++++++++------
 models/models.go   | 14 +++++++-------
 5 files changed, 52 insertions(+), 15 deletions(-)

diff --git a/data/data.go b/data/data.go
index 9a16342..ec603f7 100644
--- a/data/data.go
+++ b/data/data.go
@@ -87,7 +87,7 @@ func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
 			tx.Rollback()
 			return 0, errors.New("Cannot insert membre with no membre_id")
 		}
-		result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id);", &membre)
+		result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id) ON CONFLICT (id) DO NOTHING;", &membre)
 		if err != nil {
 			tx.Rollback()
 			return 0, err
@@ -124,7 +124,7 @@ func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, err
 			return 0, errors.New("Cannot insert programme with no programme_id")
 		}
 
-		result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre);", &programme)
+		result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre) ON CONFLICT DO NOTHING;", &programme)
 		if err != nil {
 			tx.Rollback()
 			return 0, err
diff --git a/go.mod b/go.mod
index 708678c..5bd54b0 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module git.agecem.com/agecem/bottin/v5
 go 1.20
 
 require (
+	github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
 	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/jmoiron/sqlx v1.3.5
 	github.com/labstack/echo/v4 v4.10.2
diff --git a/go.sum b/go.sum
index 03ed3fe..7fa5e1c 100644
--- a/go.sum
+++ b/go.sum
@@ -66,6 +66,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
+github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
diff --git a/handlers/insert.go b/handlers/insert.go
index 7452659..789beb3 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -1,12 +1,16 @@
 package handlers
 
 import (
+	"encoding/csv"
+	"io"
 	"net/http"
 
 	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/models"
 	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/labstack/echo/v4"
+
+	"github.com/gocarina/gocsv"
 )
 
 func PostMembres(c echo.Context) error {
@@ -32,9 +36,21 @@ func PostMembres(c echo.Context) error {
 			return c.JSON(response.StatusCode, response)
 		}
 	case "text/csv":
-		response.StatusCode = http.StatusNotImplemented
-		response.Message = "Not Implemented"
-		return c.JSON(response.StatusCode, response)
+		body := c.Request().Body
+		if body == nil {
+			response.StatusCode = http.StatusBadRequest
+			response.Message = "Request body is empty"
+			return c.JSON(response.StatusCode, response)
+		}
+		defer body.Close()
+
+		// Parse the CSV data from the request body using gocsv.
+		if err := gocsv.Unmarshal(body, &membres); err != nil {
+			response.StatusCode = http.StatusBadRequest
+			response.Message = "Could not unmarshal into membres"
+			response.Error = err.Error()
+			return c.JSON(response.StatusCode, response)
+		}
 	default:
 		response.StatusCode = http.StatusBadRequest
 		response.Message = "Invalid Content-Type"
@@ -85,9 +101,27 @@ func PostProgrammes(c echo.Context) error {
 			return c.JSON(response.StatusCode, response)
 		}
 	case "text/csv":
-		response.StatusCode = http.StatusNotImplemented
-		response.Message = "Not Implemented"
-		return c.JSON(response.StatusCode, response)
+		body := c.Request().Body
+		if body == nil {
+			response.StatusCode = http.StatusBadRequest
+			response.Message = "Request body is empty"
+			return c.JSON(response.StatusCode, response)
+		}
+		defer body.Close()
+
+		gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
+			r := csv.NewReader(in)
+			r.Comma = ';'
+			return r // Allows use ; as delimiter
+		})
+
+		// Parse the CSV data from the request body using gocsv.
+		if err := gocsv.Unmarshal(body, &programmes); err != nil {
+			response.StatusCode = http.StatusBadRequest
+			response.Message = "Could not unmarshal into programmes"
+			response.Error = err.Error()
+			return c.JSON(response.StatusCode, response)
+		}
 	default:
 		response.StatusCode = http.StatusBadRequest
 		response.Message = "Invalid Content-Type"
diff --git a/models/models.go b/models/models.go
index 1589aee..845d28d 100644
--- a/models/models.go
+++ b/models/models.go
@@ -16,16 +16,16 @@ CREATE TABLE IF NOT EXISTS membres (
 `
 
 type Programme struct {
-	ID    string `db:"id" json:"programme_id"`
-	Titre string `db:"titre" json:"nom_programme"`
+	ID    string `db:"id" json:"programme_id" csv:"programme_id"`
+	Titre string `db:"titre" json:"nom_programme" csv:"nom_programme"`
 }
 
 type Membre struct {
-	ID           string `db:"id" json:"membre_id"`
-	LastName     string `db:"last_name" json:"last_name"`
-	FirstName    string `db:"first_name" json:"first_name"`
-	PreferedName string `db:"prefered_name" json:"prefered_name"`
-	ProgrammeID  string `db:"programme_id" json:"programme_id"`
+	ID           string `db:"id" json:"membre_id" csv:"membre_id"`
+	LastName     string `db:"last_name" json:"last_name" csv:"last_name"`
+	FirstName    string `db:"first_name" json:"first_name" csv:"first_name"`
+	PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"`
+	ProgrammeID  string `db:"programme_id" json:"programme_id" csv:"programme_id"`
 }
 
 type Entry interface {

From eca6672746ae4bb108d28e31c7cff9e1572bdf77 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 22:04:54 -0400
Subject: [PATCH 30/99] Update golang et alpine

---
 Dockerfile | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 23820d5..67e6af6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.20.2 as build
+FROM golang:1.21.1 as build
 
 LABEL author="vlbeaudoin"
 
@@ -17,7 +17,7 @@ RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin .
 
 # Alpine
 
-FROM alpine:latest
+FROM alpine:3.18
 
 WORKDIR /app
 

From 025f9d74cecd5b54ee136503e8fc567a37685686 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 22:05:30 -0400
Subject: [PATCH 31/99] =?UTF-8?q?Migrer=20responses=20=C3=A0=20voki/respon?=
 =?UTF-8?q?se?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 responses/health.go |  4 +++-
 responses/post.go   |  6 ++++--
 responses/root.go   | 15 ---------------
 3 files changed, 7 insertions(+), 18 deletions(-)
 delete mode 100644 responses/root.go

diff --git a/responses/health.go b/responses/health.go
index 854279d..1edf34a 100644
--- a/responses/health.go
+++ b/responses/health.go
@@ -1,5 +1,7 @@
 package responses
 
+import "codeberg.org/vlbeaudoin/voki/response"
+
 type GetHealthResponse struct {
-	Response
+	response.ResponseWithError
 }
diff --git a/responses/post.go b/responses/post.go
index 91eef96..cfb03af 100644
--- a/responses/post.go
+++ b/responses/post.go
@@ -1,14 +1,16 @@
 package responses
 
+import "codeberg.org/vlbeaudoin/voki/response"
+
 type PostMembresResponse struct {
-	Response
+	response.ResponseWithError
 	Data struct {
 		MembresInserted int64
 	}
 }
 
 type PostProgrammesResponse struct {
-	Response
+	response.ResponseWithError
 	Data struct {
 		ProgrammesInserted int64
 	}
diff --git a/responses/root.go b/responses/root.go
deleted file mode 100644
index 0f0bcce..0000000
--- a/responses/root.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package responses
-
-type Response struct {
-	StatusCode int
-	Message    string
-	Error      string
-}
-
-type Responder interface {
-	Respond() Responder
-}
-
-func (r Response) Respond() Responder {
-	return r
-}

From 6dff76d871347506da897aaa7ad100768871a051 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 22:06:26 -0400
Subject: [PATCH 32/99] Move APIClient to voki

---
 data/apiclient.go | 84 +++++------------------------------------------
 1 file changed, 8 insertions(+), 76 deletions(-)

diff --git a/data/apiclient.go b/data/apiclient.go
index 3319d72..973326a 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -1,100 +1,43 @@
 package data
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
-	"io/ioutil"
 	"net/http"
 
+	"codeberg.org/vlbeaudoin/voki"
 	"git.agecem.com/agecem/bottin/v5/models"
 	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/spf13/viper"
 )
 
 type ApiClient struct {
-	Key      string
-	Host     string
-	Port     int
-	Protocol string
+	Voki *voki.Voki
 }
 
-func NewApiClientFromViper() *ApiClient {
+func NewApiClientFromViper(client *http.Client) *ApiClient {
 	apiClientKey := viper.GetString("web.api.key")
 	apiClientHost := viper.GetString("web.api.host")
 	apiClientProtocol := viper.GetString("web.api.protocol")
 	apiClientPort := viper.GetInt("web.api.port")
 
-	return NewApiClient(apiClientKey, apiClientHost, apiClientProtocol, apiClientPort)
+	return NewApiClient(client, apiClientKey, apiClientHost, apiClientProtocol, apiClientPort)
 }
 
-func NewApiClient(key, host, protocol string, port int) *ApiClient {
+func NewApiClient(client *http.Client, key, host, protocol string, port int) *ApiClient {
 	return &ApiClient{
-		Key:      key,
-		Host:     host,
-		Port:     port,
-		Protocol: protocol,
+		Voki: voki.New(client, host, key, port, protocol),
 	}
 }
 
-func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey bool) (*http.Response, error) {
-	var response *http.Response
-
-	endpoint := fmt.Sprintf("%s://%s:%d%s",
-		a.Protocol, a.Host, a.Port, route,
-	)
-
-	// Create client
-	client := &http.Client{}
-
-	// Create request
-	request, err := http.NewRequest(method, endpoint, requestBody)
-	if err != nil {
-		return response, err
-	}
-
-	if useKey {
-		if a.Key == "" {
-			return response, fmt.Errorf("Call to API required a key but none was provided. See --help for instructions on providing an API key.")
-		}
-
-		request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Key))
-	}
-
-	if requestBody != nil {
-		request.Header.Add("Content-Type", "application/json")
-	}
-
-	// Fetch Request
-	response, err = client.Do(request)
-	if err != nil {
-		return response, err
-	}
-
-	return response, nil
-}
-
 // GetHealth allows checking for API server health
 func (a *ApiClient) GetHealth() (string, error) {
 	var getHealthResponse responses.GetHealthResponse
-
-	response, err := a.Call(http.MethodGet, "/v5/health", nil, true)
+	err := a.Voki.Unmarshal(http.MethodGet, "/v5/health", nil, true, &getHealthResponse)
 	if err != nil {
 		return getHealthResponse.Message, err
 	}
 
-	defer response.Body.Close()
-
-	body, err := ioutil.ReadAll(response.Body)
-	if err != nil {
-		return getHealthResponse.Message, err
-	}
-
-	if err := json.Unmarshal(body, &getHealthResponse); err != nil {
-		return getHealthResponse.Message, err
-	}
-
 	if getHealthResponse.Message == "" {
 		return getHealthResponse.Message, errors.New("Could not confirm that API server is up, no response message")
 	}
@@ -114,22 +57,11 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 		return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher")
 	}
 
-	response, err := a.Call(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true)
+	err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true, getMembreResponse)
 	if err != nil {
 		return getMembreResponse.Data.Membre, err
 	}
 
-	defer response.Body.Close()
-
-	body, err := ioutil.ReadAll(response.Body)
-	if err != nil {
-		return getMembreResponse.Data.Membre, err
-	}
-
-	if err := json.Unmarshal(body, &getMembreResponse); err != nil {
-		return getMembreResponse.Data.Membre, err
-	}
-
 	if getMembreResponse.Data.Membre == *new(models.Membre) {
 		return getMembreResponse.Data.Membre, fmt.Errorf("Ce numéro étudiant ne correspond à aucunE membre")
 	}

From ad83bc081a596e029110e91a445d5a5ae310d238 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 22:07:02 -0400
Subject: [PATCH 33/99] Migrate webclient to voki and add webhandlers.Handler

---
 cmd/web.go                  | 12 +++++++++---
 web/webhandlers/handlers.go | 11 +++++++----
 2 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/cmd/web.go b/cmd/web.go
index d09477c..a944d30 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -7,6 +7,7 @@ import (
 	"html/template"
 	"io"
 	"log"
+	"net/http"
 
 	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/web"
@@ -53,7 +54,10 @@ var webCmd = &cobra.Command{
 
 		// Ping API server
 
-		apiClient := data.NewApiClient(webApiKey, webApiHost, webApiProtocol, webApiPort)
+		client := http.DefaultClient
+		defer client.CloseIdleConnections()
+
+		apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
 
 		pingResult, err := apiClient.GetHealth()
 		if err != nil {
@@ -84,8 +88,10 @@ var webCmd = &cobra.Command{
 
 		// Routes
 
-		e.GET("/", webhandlers.GetIndex)
-		e.GET("/membre/", webhandlers.GetMembre)
+		handler := webhandlers.Handler{APIClient: apiClient}
+
+		e.GET("/", handler.GetIndex)
+		e.GET("/membre/", handler.GetMembre)
 
 		// Execution
 
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
index dce8399..96fba50 100644
--- a/web/webhandlers/handlers.go
+++ b/web/webhandlers/handlers.go
@@ -8,16 +8,19 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
-func GetIndex(c echo.Context) error {
+type Handler struct {
+	APIClient *data.ApiClient
+}
+
+func (h *Handler) GetIndex(c echo.Context) error {
 	return c.Render(http.StatusOK, "index-html", nil)
 }
 
-func GetMembre(c echo.Context) error {
-	apiClient := data.NewApiClientFromViper()
+func (h *Handler) GetMembre(c echo.Context) error {
 
 	membreID := c.QueryParam("membre_id")
 
-	membre, err := apiClient.GetMembre(membreID)
+	membre, err := h.APIClient.GetMembre(membreID)
 	if err != nil {
 		return c.Render(http.StatusBadRequest, "index-html", struct {
 			Result string

From b36d36d669a799ed8542289274362e050d005d8d Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 22:07:41 -0400
Subject: [PATCH 34/99] Update go.mod and go.sum

---
 go.mod | 5 ++++-
 go.sum | 7 +++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 5bd54b0..bb11ea5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,11 @@
 module git.agecem.com/agecem/bottin/v5
 
-go 1.20
+go 1.21.0
+
+toolchain go1.21.1
 
 require (
+	codeberg.org/vlbeaudoin/voki v1.3.1
 	github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
 	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/jmoiron/sqlx v1.3.5
diff --git a/go.sum b/go.sum
index 7fa5e1c..153feae 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 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=
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+codeberg.org/vlbeaudoin/voki v1.3.1 h1:TxJj3qmOys0Pbq1dPKnOEXMXKqQLQqrBYd4QqiWWXcw=
+codeberg.org/vlbeaudoin/voki v1.3.1/go.mod h1:5XTLx/KiW/OfiupF3o7PAAAU/UhsPdKSrVMmtHbmkPI=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 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=
@@ -59,6 +61,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -109,6 +112,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 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/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -147,9 +151,11 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
 github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
 github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
@@ -179,6 +185,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=

From aa6f3479f6dad9eea2c8ea4a8d5ea40610b81ddd Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 22:55:40 -0400
Subject: [PATCH 35/99] =?UTF-8?q?R=C3=A9utiliser=20*data.DataClient=20?=
 =?UTF-8?q?=C3=A0=20travers=20API=20handlers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd/api.go           | 34 +++++++++++++++++++---------------
 handlers/handlers.go | 11 +++++++++++
 handlers/health.go   |  2 +-
 handlers/insert.go   | 28 ++++------------------------
 handlers/read.go     | 22 ++--------------------
 handlers/seed.go     | 22 ++--------------------
 handlers/update.go   | 24 +++---------------------
 7 files changed, 42 insertions(+), 101 deletions(-)
 create mode 100644 handlers/handlers.go

diff --git a/cmd/api.go b/cmd/api.go
index 9a05014..6de994f 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -39,21 +39,7 @@ var apiCmd = &cobra.Command{
 			}))
 		}
 
-		// Routes
-
-		e.GET("/v5/health/", handlers.GetHealth)
-
-		e.POST("/v5/membres/", handlers.PostMembres)
-
-		e.GET("/v5/membres/:membre_id/", handlers.ReadMembre)
-
-		e.PUT("/v5/membres/:membre_id/prefered_name/", handlers.PutMembrePreferedName)
-
-		e.POST("/v5/programmes/", handlers.PostProgrammes)
-
-		e.POST("/v5/seed/", handlers.PostSeed)
-
-		// Execution
+		// DataClient
 
 		client, err := data.NewDataClientFromViper()
 		if err != nil {
@@ -71,6 +57,24 @@ var apiCmd = &cobra.Command{
 			log.Fatalf("Error during client.Seed(): %s", err)
 		}
 
+		h := handlers.New(client)
+
+		// Routes
+
+		e.GET("/v5/health/", h.GetHealth)
+
+		e.POST("/v5/membres/", h.PostMembres)
+
+		e.GET("/v5/membres/:membre_id/", h.ReadMembre)
+
+		e.PUT("/v5/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
+
+		e.POST("/v5/programmes/", h.PostProgrammes)
+
+		e.POST("/v5/seed/", h.PostSeed)
+
+		// Execution
+
 		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
 	},
 }
diff --git a/handlers/handlers.go b/handlers/handlers.go
new file mode 100644
index 0000000..7bbb88f
--- /dev/null
+++ b/handlers/handlers.go
@@ -0,0 +1,11 @@
+package handlers
+
+import "git.agecem.com/agecem/bottin/v5/data"
+
+type Handler struct {
+	DataClient *data.DataClient
+}
+
+func New(dataClient *data.DataClient) *Handler {
+	return &Handler{DataClient: dataClient}
+}
diff --git a/handlers/health.go b/handlers/health.go
index d4fe868..9e58673 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -8,7 +8,7 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
-func GetHealth(c echo.Context) error {
+func (h *Handler) GetHealth(c echo.Context) error {
 	var response responses.GetHealthResponse
 
 	dataClient, err := data.NewDataClientFromViper()
diff --git a/handlers/insert.go b/handlers/insert.go
index 789beb3..9a1c79b 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -5,7 +5,6 @@ import (
 	"io"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/models"
 	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/labstack/echo/v4"
@@ -13,17 +12,8 @@ import (
 	"github.com/gocarina/gocsv"
 )
 
-func PostMembres(c echo.Context) error {
+func (h *Handler) PostMembres(c echo.Context) error {
 	var response responses.PostMembresResponse
-	client, err := data.NewDataClientFromViper()
-	if err != nil {
-		response.StatusCode = http.StatusInternalServerError
-		response.Message = "Could not establish database connection"
-		response.Error = err.Error()
-
-		return c.JSON(response.StatusCode, response)
-	}
-	defer client.DB.Close()
 
 	var membres []models.Membre
 
@@ -63,7 +53,7 @@ func PostMembres(c echo.Context) error {
 		return c.JSON(response.StatusCode, response)
 	}
 
-	newMembres, err := client.InsertMembres(membres)
+	newMembres, err := h.DataClient.InsertMembres(membres)
 	if err != nil {
 		response.StatusCode = http.StatusInternalServerError
 		response.Message = "Could not insert membres"
@@ -77,19 +67,9 @@ func PostMembres(c echo.Context) error {
 	return c.JSON(response.StatusCode, response)
 }
 
-func PostProgrammes(c echo.Context) error {
+func (h *Handler) PostProgrammes(c echo.Context) error {
 	var response responses.PostProgrammesResponse
 
-	client, err := data.NewDataClientFromViper()
-	if err != nil {
-		response.StatusCode = http.StatusInternalServerError
-		response.Message = "Could not establish database connection"
-		response.Error = err.Error()
-
-		return c.JSON(response.StatusCode, response)
-	}
-	defer client.DB.Close()
-
 	var programmes []models.Programme
 
 	switch c.Request().Header.Get("Content-Type") {
@@ -134,7 +114,7 @@ func PostProgrammes(c echo.Context) error {
 		return c.JSON(response.StatusCode, response)
 	}
 
-	newProgrammes, err := client.InsertProgrammes(programmes)
+	newProgrammes, err := h.DataClient.InsertProgrammes(programmes)
 	if err != nil {
 		response.StatusCode = http.StatusInternalServerError
 		response.Message = "Could not insert programmes"
diff --git a/handlers/read.go b/handlers/read.go
index 13a8007..5048f25 100644
--- a/handlers/read.go
+++ b/handlers/read.go
@@ -3,31 +3,13 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
-	"github.com/spf13/viper"
 )
 
-func ReadMembre(c echo.Context) error {
-	connection := data.PostgresConnection{
-		User:     viper.GetString("db.user"),
-		Password: viper.GetString("db.password"),
-		Host:     viper.GetString("db.host"),
-		Database: viper.GetString("db.database"),
-		Port:     viper.GetInt("db.port"),
-	}
-
-	client, err := data.NewDataClient(connection)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not establish database connection",
-			"error":   err.Error(),
-		})
-	}
-
+func (h *Handler) ReadMembre(c echo.Context) error {
 	membreID := c.Param("membre_id")
 
-	membre, err := client.GetMembre(membreID)
+	membre, err := h.DataClient.GetMembre(membreID)
 	if err != nil {
 		if err.Error() == "No membre by that id was found" {
 			return c.JSON(http.StatusNotFound, map[string]string{
diff --git a/handlers/seed.go b/handlers/seed.go
index 947707d..27f5958 100644
--- a/handlers/seed.go
+++ b/handlers/seed.go
@@ -3,29 +3,11 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
-	"github.com/spf13/viper"
 )
 
-func PostSeed(c echo.Context) error {
-	connection := data.PostgresConnection{
-		User:     viper.GetString("db.user"),
-		Password: viper.GetString("db.password"),
-		Host:     viper.GetString("db.host"),
-		Database: viper.GetString("db.database"),
-		Port:     viper.GetInt("db.port"),
-	}
-
-	client, err := data.NewDataClient(connection)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not establish database connection",
-			"error":   err.Error(),
-		})
-	}
-
-	rows, err := client.Seed()
+func (h *Handler) PostSeed(c echo.Context) error {
+	rows, err := h.DataClient.Seed()
 	if err != nil {
 		return c.JSON(http.StatusInternalServerError, map[string]string{
 			"message": "Seed failed",
diff --git a/handlers/update.go b/handlers/update.go
index a0d9ba2..8b4bd04 100644
--- a/handlers/update.go
+++ b/handlers/update.go
@@ -3,33 +3,15 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/data"
 	"github.com/labstack/echo/v4"
-	"github.com/spf13/viper"
 )
 
-func PutMembrePreferedName(c echo.Context) error {
-	connection := data.PostgresConnection{
-		User:     viper.GetString("db.user"),
-		Password: viper.GetString("db.password"),
-		Host:     viper.GetString("db.host"),
-		Database: viper.GetString("db.database"),
-		Port:     viper.GetInt("db.port"),
-	}
-
-	client, err := data.NewDataClient(connection)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not establish database connection",
-			"error":   err.Error(),
-		})
-	}
-
+func (h *Handler) PutMembrePreferedName(c echo.Context) error {
 	membreID := c.Param("membre_id")
 
 	var newName string
 
-	err = c.Bind(&newName)
+	err := c.Bind(&newName)
 	if err != nil {
 		return c.JSON(http.StatusBadRequest, map[string]string{
 			"message": "Could not bind newName",
@@ -37,7 +19,7 @@ func PutMembrePreferedName(c echo.Context) error {
 		})
 	}
 
-	rows, err := client.UpdateMembreName(membreID, newName)
+	rows, err := h.DataClient.UpdateMembreName(membreID, newName)
 	if err != nil {
 		return c.JSON(http.StatusInternalServerError, map[string]string{
 			"message": "Could not update membre name",

From 3a421c6d3551ea27ad28da5545779392595a5333 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Mon, 18 Sep 2023 23:17:40 -0500
Subject: [PATCH 36/99] Fix non-pointer destination in voki.Unmarshal

---
 data/apiclient.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/data/apiclient.go b/data/apiclient.go
index 973326a..3ce3694 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -57,7 +57,7 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 		return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher")
 	}
 
-	err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true, getMembreResponse)
+	err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true, &getMembreResponse)
 	if err != nil {
 		return getMembreResponse.Data.Membre, err
 	}

From 6ede2083fa34a483b57d8fcf4c5b4d75c47fe51b Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <victor.lacassebeaudoin@proton.me>
Date: Tue, 19 Sep 2023 19:09:51 -0400
Subject: [PATCH 37/99] Ajouter GET /v5/membres pour lister membres en json

---
 cmd/api.go        |  2 ++
 data/apiclient.go |  4 ++++
 data/data.go      |  4 ++++
 handlers/read.go  | 30 ++++++++++++++++++++++++++++++
 responses/list.go | 13 +++++++++++++
 5 files changed, 53 insertions(+)
 create mode 100644 responses/list.go

diff --git a/cmd/api.go b/cmd/api.go
index 6de994f..0c8f84b 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -65,6 +65,8 @@ var apiCmd = &cobra.Command{
 
 		e.POST("/v5/membres/", h.PostMembres)
 
+		e.GET("/v5/membres/", h.ListMembres)
+
 		e.GET("/v5/membres/:membre_id/", h.ReadMembre)
 
 		e.PUT("/v5/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
diff --git a/data/apiclient.go b/data/apiclient.go
index 3ce3694..d5257cf 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -68,3 +68,7 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 
 	return getMembreResponse.Data.Membre, nil
 }
+
+func (a *ApiClient) ListMembres() (r responses.ListMembresResponse, err error) {
+	return r, a.Voki.Unmarshal(http.MethodGet, "/v5/membres", nil, true, &r)
+}
diff --git a/data/data.go b/data/data.go
index ec603f7..98c293b 100644
--- a/data/data.go
+++ b/data/data.go
@@ -182,3 +182,7 @@ func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
 
 	return rows, nil
 }
+
+func (d *DataClient) GetMembres() (membres []models.Membre, err error) {
+	return membres, d.DB.Select(&membres, "SELECT * FROM membres;")
+}
diff --git a/handlers/read.go b/handlers/read.go
index 5048f25..8fec2b1 100644
--- a/handlers/read.go
+++ b/handlers/read.go
@@ -1,8 +1,10 @@
 package handlers
 
 import (
+	"fmt"
 	"net/http"
 
+	"git.agecem.com/agecem/bottin/v5/responses"
 	"github.com/labstack/echo/v4"
 )
 
@@ -29,3 +31,31 @@ func (h *Handler) ReadMembre(c echo.Context) error {
 		},
 	})
 }
+
+func (h *Handler) ListMembres(c echo.Context) error {
+	var r responses.ListMembresResponse
+
+	membres, err := h.DataClient.GetMembres()
+	if err != nil {
+		r.StatusCode = http.StatusInternalServerError
+		r.Error = err.Error()
+		r.Message = "Error during (*handlers.Handler).DataClient.GetMembres"
+
+		return c.JSON(r.StatusCode, r)
+	}
+
+	r.StatusCode = http.StatusOK
+
+	switch membres := len(membres); membres {
+	case 0:
+		r.Message = "No membres returned from database"
+	case 1:
+		r.Message = "Membre returned from database"
+	default:
+		r.Message = fmt.Sprintf("%d membres returned from database", membres)
+	}
+
+	r.Data.Membres = membres
+
+	return c.JSON(r.StatusCode, r)
+}
diff --git a/responses/list.go b/responses/list.go
new file mode 100644
index 0000000..04df99f
--- /dev/null
+++ b/responses/list.go
@@ -0,0 +1,13 @@
+package responses
+
+import (
+	"codeberg.org/vlbeaudoin/voki/response"
+	"git.agecem.com/agecem/bottin/v5/models"
+)
+
+type ListMembresResponse struct {
+	response.ResponseWithError
+	Data struct {
+		Membres []models.Membre
+	}
+}

From 5f9564d93c2c347307a932a47b0cbb06f5886aa2 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 17 Oct 2023 16:35:51 -0400
Subject: [PATCH 38/99] =?UTF-8?q?Migrer=20d=C3=A9claration=20de=20flags=20?=
 =?UTF-8?q?=C3=A0=20serpents?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd/api.go | 40 ++++++++++++++++++++------------------
 cmd/web.go | 50 +++++++++++++++++++++---------------------------
 go.mod     | 21 ++++++++++----------
 go.sum     | 56 ++++++++++++++++++++++++++++++------------------------
 4 files changed, 85 insertions(+), 82 deletions(-)

diff --git a/cmd/api.go b/cmd/api.go
index 0c8f84b..fb355be 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"log"
 
+	"codeberg.org/vlbeaudoin/serpents"
 	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/handlers"
 	"github.com/labstack/echo/v4"
@@ -85,34 +86,37 @@ func init() {
 	rootCmd.AddCommand(apiCmd)
 
 	// api.key
-	apiCmd.Flags().String(
-		"api-key", "bottin",
-		"API server key. Leave empty for no key auth. (config: 'api.key')")
-	viper.BindPFlag("api.key", apiCmd.Flags().Lookup("api-key"))
+	serpents.String(apiCmd.Flags(),
+		"api.key", "api-key", "bottin",
+		"API server key. Leave empty for no key auth")
 
 	// api.port
-	apiCmd.Flags().Int(
-		"api-port", 1312,
-		"API server port (config:'api.port')")
-	viper.BindPFlag("api.port", apiCmd.Flags().Lookup("api-port"))
+	serpents.Int(apiCmd.Flags(),
+		"api.port", "api-port", 1312,
+		"API server port")
 
 	// db.database
-	apiCmd.Flags().String("db-database", "bottin", "Postgres database (config:'db.database')")
-	viper.BindPFlag("db.database", apiCmd.Flags().Lookup("db-database"))
+	serpents.String(apiCmd.Flags(),
+		"db.database", "db-database", "bottin",
+		"Postgres database")
 
 	// db.host
-	apiCmd.Flags().String("db-host", "db", "Postgres host (config:'db.host')")
-	viper.BindPFlag("db.host", apiCmd.Flags().Lookup("db-host"))
+	serpents.String(apiCmd.Flags(),
+		"db.host", "db-host", "db",
+		"Postgres host")
 
 	// db.password
-	apiCmd.Flags().String("db-password", "bottin", "Postgres password (config:'db.password')")
-	viper.BindPFlag("db.password", apiCmd.Flags().Lookup("db-password"))
+	serpents.String(apiCmd.Flags(),
+		"db.password", "db-password", "bottin",
+		"Postgres password")
 
 	// db.port
-	apiCmd.Flags().Int("db-port", 5432, "Postgres port (config:'db.port')")
-	viper.BindPFlag("db.port", apiCmd.Flags().Lookup("db-port"))
+	serpents.Int(apiCmd.Flags(),
+		"db.port", "db-port", 5432,
+		"Postgres port")
 
 	// db.user
-	apiCmd.Flags().String("db-user", "bottin", "Postgres user (config:'db.user')")
-	viper.BindPFlag("db.user", apiCmd.Flags().Lookup("db-user"))
+	serpents.String(apiCmd.Flags(),
+		"db.user", "db-user", "bottin",
+		"Postgres user")
 }
diff --git a/cmd/web.go b/cmd/web.go
index a944d30..1aa9e31 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -9,6 +9,7 @@ import (
 	"log"
 	"net/http"
 
+	"codeberg.org/vlbeaudoin/serpents"
 	"git.agecem.com/agecem/bottin/v5/data"
 	"git.agecem.com/agecem/bottin/v5/web"
 	"git.agecem.com/agecem/bottin/v5/web/webhandlers"
@@ -105,44 +106,37 @@ func init() {
 	templatesFS = web.GetTemplates()
 
 	// web.api.host
-	webCmd.Flags().String(
-		"web-api-host", "api",
-		"Remote API server host (config:'web.api.host')")
-	viper.BindPFlag("web.api.host", webCmd.Flags().Lookup("web-api-host"))
+	serpents.String(webCmd.Flags(),
+		"web.api.host", "web-api-host", "api",
+		"Remote API server host")
 
 	// web.api.key
-	webCmd.Flags().String(
-		"web-api-key", "bottin",
-		"Remote API server key (config:'web.api.key')")
-	viper.BindPFlag("web.api.key", webCmd.Flags().Lookup("web-api-key"))
+	serpents.String(webCmd.Flags(),
+		"web.api.key", "web-api-key", "bottin",
+		"Remote API server key")
 
 	// web.api.protocol
-	webCmd.Flags().String(
-		"web-api-protocol", "http",
-		"Remote API server protocol (config:'web.api.protocol')")
-	viper.BindPFlag("web.api.protocol", webCmd.Flags().Lookup("web-api-protocol"))
+	serpents.String(webCmd.Flags(),
+		"web.api.protocol", "web-api-protocol", "http",
+		"Remote API server protocol")
 
 	// web.api.port
-	webCmd.Flags().Int(
-		"web-api-port", 1312,
-		"Remote API server port (config:'web.api.port')")
-	viper.BindPFlag("web.api.port", webCmd.Flags().Lookup("web-api-port"))
+	serpents.Int(webCmd.Flags(),
+		"web.api.port", "web-api-port", 1312,
+		"Remote API server port")
 
 	// web.password
-	webCmd.Flags().String(
-		"web-password", "bottin",
-		"Web client password (config:'web.password')")
-	viper.BindPFlag("web.password", webCmd.Flags().Lookup("web-password"))
+	serpents.String(webCmd.Flags(),
+		"web.password", "web-password", "bottin",
+		"Web client password")
 
 	// web.port
-	webCmd.Flags().Int(
-		"web-port", 2312,
-		"Web client port (config:'web.port')")
-	viper.BindPFlag("web.port", webCmd.Flags().Lookup("web-port"))
+	serpents.Int(webCmd.Flags(),
+		"web.port", "web-port", 2312,
+		"Web client port")
 
 	// web.user
-	webCmd.Flags().String(
-		"web-user", "bottin",
-		"Web client user (config:'web.user')")
-	viper.BindPFlag("web.user", webCmd.Flags().Lookup("web-user"))
+	serpents.String(webCmd.Flags(),
+		"web.user", "web-user", "bottin",
+		"Web client user")
 }
diff --git a/go.mod b/go.mod
index bb11ea5..55c8485 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,16 @@
 module git.agecem.com/agecem/bottin/v5
 
-go 1.21.0
-
-toolchain go1.21.1
+go 1.21.1
 
 require (
+	codeberg.org/vlbeaudoin/serpents v1.0.2
 	codeberg.org/vlbeaudoin/voki v1.3.1
 	github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
 	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/jmoiron/sqlx v1.3.5
 	github.com/labstack/echo/v4 v4.10.2
 	github.com/spf13/cobra v1.7.0
-	github.com/spf13/viper v1.15.0
+	github.com/spf13/viper v1.16.0
 )
 
 require (
@@ -27,20 +26,20 @@ require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
-	github.com/spf13/afero v1.9.3 // indirect
-	github.com/spf13/cast v1.5.0 // indirect
+	github.com/spf13/afero v1.9.5 // indirect
+	github.com/spf13/cast v1.5.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.4.2 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
-	golang.org/x/crypto v0.6.0 // indirect
-	golang.org/x/net v0.7.0 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/crypto v0.9.0 // indirect
+	golang.org/x/net v0.10.0 // indirect
+	golang.org/x/sys v0.8.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 153feae..bd111f3 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 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=
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+codeberg.org/vlbeaudoin/serpents v1.0.2 h1:mHuL+RBAMOGeiB5+ew1cRputEAnOIQNJW9o9a5Qjudo=
+codeberg.org/vlbeaudoin/serpents v1.0.2/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
 codeberg.org/vlbeaudoin/voki v1.3.1 h1:TxJj3qmOys0Pbq1dPKnOEXMXKqQLQqrBYd4QqiWWXcw=
 codeberg.org/vlbeaudoin/voki v1.3.1/go.mod h1:5XTLx/KiW/OfiupF3o7PAAAU/UhsPdKSrVMmtHbmkPI=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -60,8 +62,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
 github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -150,8 +152,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -175,8 +177,8 @@ github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRU
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
-github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
@@ -184,23 +186,23 @@ 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
-github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
+github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
+github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
+github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
 github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
-github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
+github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
+github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -210,8 +212,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -235,9 +237,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 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=
@@ -302,8 +304,9 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -355,6 +358,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -362,8 +366,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 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=
@@ -371,8 +375,10 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 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=

From 0bbf46367414eda143f67a2a0b8b6d515a05799a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 28 Dec 2023 14:18:39 -0500
Subject: [PATCH 39/99] chores!: bump `postgres` to `16.1`

Existing databases will prevent this from booting. If you want to stay on
postgres 14, modify `docker-compose.yaml`'s `services.db.image`.

BREAKING: update postgres image major version from 14 to 16
---
 docker-compose.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker-compose.yaml b/docker-compose.yaml
index b6814bc..cad1268 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,7 +1,7 @@
 services:
 
   db:
-    image: 'docker.io/library/postgres:14.8'
+    image: 'docker.io/library/postgres:16.1'
     environment:
       POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
       POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"

From a8dcdd03886065637e8788c01ec549ac7a9913ff Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 5 Jan 2024 14:38:48 -0500
Subject: [PATCH 40/99] =?UTF-8?q?chores!:=20bump=20API=20et=20go=20mod=20?=
 =?UTF-8?q?=C3=A0=20v6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Tag v6.0.0 est sorti mais n'était pas réflété dans le code.

BREAKING: API est maintenant exposé sur `/v6` et non `/v5`
---
 cmd/api.go                  | 18 +++++++++---------
 cmd/web.go                  |  6 +++---
 data/apiclient.go           | 10 +++++-----
 data/data.go                |  2 +-
 go.mod                      |  2 +-
 handlers/handlers.go        |  2 +-
 handlers/health.go          |  6 +++---
 handlers/insert.go          |  4 ++--
 handlers/read.go            |  2 +-
 main.go                     |  2 +-
 responses/list.go           |  2 +-
 web/webhandlers/handlers.go |  2 +-
 12 files changed, 29 insertions(+), 29 deletions(-)

diff --git a/cmd/api.go b/cmd/api.go
index fb355be..b03be9a 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -6,8 +6,8 @@ import (
 	"log"
 
 	"codeberg.org/vlbeaudoin/serpents"
-	"git.agecem.com/agecem/bottin/v5/data"
-	"git.agecem.com/agecem/bottin/v5/handlers"
+	"git.agecem.com/agecem/bottin/v6/data"
+	"git.agecem.com/agecem/bottin/v6/handlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
@@ -62,19 +62,19 @@ var apiCmd = &cobra.Command{
 
 		// Routes
 
-		e.GET("/v5/health/", h.GetHealth)
+		e.GET("/v6/health/", h.GetHealth)
 
-		e.POST("/v5/membres/", h.PostMembres)
+		e.POST("/v6/membres/", h.PostMembres)
 
-		e.GET("/v5/membres/", h.ListMembres)
+		e.GET("/v6/membres/", h.ListMembres)
 
-		e.GET("/v5/membres/:membre_id/", h.ReadMembre)
+		e.GET("/v6/membres/:membre_id/", h.ReadMembre)
 
-		e.PUT("/v5/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
+		e.PUT("/v6/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
 
-		e.POST("/v5/programmes/", h.PostProgrammes)
+		e.POST("/v6/programmes/", h.PostProgrammes)
 
-		e.POST("/v5/seed/", h.PostSeed)
+		e.POST("/v6/seed/", h.PostSeed)
 
 		// Execution
 
diff --git a/cmd/web.go b/cmd/web.go
index 1aa9e31..4d1a556 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -10,9 +10,9 @@ import (
 	"net/http"
 
 	"codeberg.org/vlbeaudoin/serpents"
-	"git.agecem.com/agecem/bottin/v5/data"
-	"git.agecem.com/agecem/bottin/v5/web"
-	"git.agecem.com/agecem/bottin/v5/web/webhandlers"
+	"git.agecem.com/agecem/bottin/v6/data"
+	"git.agecem.com/agecem/bottin/v6/web"
+	"git.agecem.com/agecem/bottin/v6/web/webhandlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
diff --git a/data/apiclient.go b/data/apiclient.go
index d5257cf..a068809 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -6,8 +6,8 @@ import (
 	"net/http"
 
 	"codeberg.org/vlbeaudoin/voki"
-	"git.agecem.com/agecem/bottin/v5/models"
-	"git.agecem.com/agecem/bottin/v5/responses"
+	"git.agecem.com/agecem/bottin/v6/models"
+	"git.agecem.com/agecem/bottin/v6/responses"
 	"github.com/spf13/viper"
 )
 
@@ -33,7 +33,7 @@ func NewApiClient(client *http.Client, key, host, protocol string, port int) *Ap
 // GetHealth allows checking for API server health
 func (a *ApiClient) GetHealth() (string, error) {
 	var getHealthResponse responses.GetHealthResponse
-	err := a.Voki.Unmarshal(http.MethodGet, "/v5/health", nil, true, &getHealthResponse)
+	err := a.Voki.Unmarshal(http.MethodGet, "/v6/health", nil, true, &getHealthResponse)
 	if err != nil {
 		return getHealthResponse.Message, err
 	}
@@ -57,7 +57,7 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 		return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher")
 	}
 
-	err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true, &getMembreResponse)
+	err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v6/membres/%s", membreID), nil, true, &getMembreResponse)
 	if err != nil {
 		return getMembreResponse.Data.Membre, err
 	}
@@ -70,5 +70,5 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 }
 
 func (a *ApiClient) ListMembres() (r responses.ListMembresResponse, err error) {
-	return r, a.Voki.Unmarshal(http.MethodGet, "/v5/membres", nil, true, &r)
+	return r, a.Voki.Unmarshal(http.MethodGet, "/v6/membres", nil, true, &r)
 }
diff --git a/data/data.go b/data/data.go
index 98c293b..c4cc026 100644
--- a/data/data.go
+++ b/data/data.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"fmt"
 
-	"git.agecem.com/agecem/bottin/v5/models"
+	"git.agecem.com/agecem/bottin/v6/models"
 	_ "github.com/jackc/pgx/stdlib"
 	"github.com/jmoiron/sqlx"
 	"github.com/spf13/viper"
diff --git a/go.mod b/go.mod
index 55c8485..014c923 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.agecem.com/agecem/bottin/v5
+module git.agecem.com/agecem/bottin/v6
 
 go 1.21.1
 
diff --git a/handlers/handlers.go b/handlers/handlers.go
index 7bbb88f..0b35b23 100644
--- a/handlers/handlers.go
+++ b/handlers/handlers.go
@@ -1,6 +1,6 @@
 package handlers
 
-import "git.agecem.com/agecem/bottin/v5/data"
+import "git.agecem.com/agecem/bottin/v6/data"
 
 type Handler struct {
 	DataClient *data.DataClient
diff --git a/handlers/health.go b/handlers/health.go
index 9e58673..4923493 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -3,8 +3,8 @@ package handlers
 import (
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/data"
-	"git.agecem.com/agecem/bottin/v5/responses"
+	"git.agecem.com/agecem/bottin/v6/data"
+	"git.agecem.com/agecem/bottin/v6/responses"
 	"github.com/labstack/echo/v4"
 )
 
@@ -30,7 +30,7 @@ func (h *Handler) GetHealth(c echo.Context) error {
 	}
 
 	response.StatusCode = http.StatusOK
-	response.Message = "Bottin API v5 is ready"
+	response.Message = "Bottin API v6 is ready"
 
 	return c.JSON(response.StatusCode, response)
 }
diff --git a/handlers/insert.go b/handlers/insert.go
index 9a1c79b..c4654b9 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -5,8 +5,8 @@ import (
 	"io"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/models"
-	"git.agecem.com/agecem/bottin/v5/responses"
+	"git.agecem.com/agecem/bottin/v6/models"
+	"git.agecem.com/agecem/bottin/v6/responses"
 	"github.com/labstack/echo/v4"
 
 	"github.com/gocarina/gocsv"
diff --git a/handlers/read.go b/handlers/read.go
index 8fec2b1..0a5b1fd 100644
--- a/handlers/read.go
+++ b/handlers/read.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/responses"
+	"git.agecem.com/agecem/bottin/v6/responses"
 	"github.com/labstack/echo/v4"
 )
 
diff --git a/main.go b/main.go
index 567ba46..f0b7d52 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,6 @@
 package main
 
-import "git.agecem.com/agecem/bottin/v5/cmd"
+import "git.agecem.com/agecem/bottin/v6/cmd"
 
 func main() {
 	cmd.Execute()
diff --git a/responses/list.go b/responses/list.go
index 04df99f..4783e90 100644
--- a/responses/list.go
+++ b/responses/list.go
@@ -2,7 +2,7 @@ package responses
 
 import (
 	"codeberg.org/vlbeaudoin/voki/response"
-	"git.agecem.com/agecem/bottin/v5/models"
+	"git.agecem.com/agecem/bottin/v6/models"
 )
 
 type ListMembresResponse struct {
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
index 96fba50..348e43a 100644
--- a/web/webhandlers/handlers.go
+++ b/web/webhandlers/handlers.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"git.agecem.com/agecem/bottin/v5/data"
+	"git.agecem.com/agecem/bottin/v6/data"
 	"github.com/labstack/echo/v4"
 )
 

From b8f05cb2664ed95551246a3a152fe70d9e1ace95 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 5 Jan 2024 14:49:44 -0500
Subject: [PATCH 41/99] fix: Identifier formats `json` et `csv` permis lors
 d'insertion

Clarifier le message d'erreur d'insertion `Invalid Content-Type` pour inclure les
formats permis
---
 handlers/insert.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/handlers/insert.go b/handlers/insert.go
index c4654b9..8039505 100644
--- a/handlers/insert.go
+++ b/handlers/insert.go
@@ -43,7 +43,7 @@ func (h *Handler) PostMembres(c echo.Context) error {
 		}
 	default:
 		response.StatusCode = http.StatusBadRequest
-		response.Message = "Invalid Content-Type"
+		response.Message = "Invalid Content-Type: Please use application/json or text/csv"
 		return c.JSON(response.StatusCode, response)
 	}
 

From 50155ed9cb411bae151b5a1aafc0a8a3867d9f99 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 5 Jan 2024 15:18:21 -0500
Subject: [PATCH 42/99] license: remplacer license pour GNU GPLv2

---
 LICENSE | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 114 insertions(+), 5 deletions(-)

diff --git a/LICENSE b/LICENSE
index b5cc18d..7d6e2c2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,9 +1,118 @@
-MIT License
+GNU GENERAL PUBLIC LICENSE
+Version 2, June 1991
 
-Copyright (c) 2021-2023 AGECEM
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
 
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+Preamble
+
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+
+     a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
+
+     b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
+
+     c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
+
+     a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+     b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+     c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+     one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
+
+     This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+     This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+     You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
+
+     Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
+
+     Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

From f7437d17196bb8e682d96b8127578415aaf942b8 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 14 Feb 2024 14:05:04 -0500
Subject: [PATCH 43/99] feat: Permettre de configurer `api` et `web` par `.env`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

L'ajout à viper de replacer et préfixe `BOTTIN` permet de déployer et
configurer l'application avec seulement docker-compose, en évitant
d'avoir à nécessairement uploader un fichier de config.

Ajoute aussi des explications dans `README.md` sur changements de
procédure
---
 README.md           | 7 +++++++
 cmd/root.go         | 3 +++
 docker-compose.yaml | 9 +++++++++
 3 files changed, 19 insertions(+)

diff --git a/README.md b/README.md
index 74d3152..acda47f 100644
--- a/README.md
+++ b/README.md
@@ -23,15 +23,22 @@ Remplir .env avec les infos qui seront utilisées pour déployer le container
 (Remplacer `bottin` par quelque chose de plus sécuritaire)
 
 ```sh
+BOTTIN_API_KEY=bottin
 BOTTIN_POSTGRES_DATABASE=bottin
 BOTTIN_POSTGRES_PASSWORD=bottin
 BOTTIN_POSTGRES_USER=bottin
+BOTTIN_WEB_PASSWORD=bottin
+BOTTIN_WEB_USER=bottin
 ```
 
 Déployer avec docker-compose
 
 `$ docker-compose up -d`
 
+### Optionnel: configuration par fichiers YAML
+
+*seulement nécessaire si les fichiers `.env` et `docker-compose.yaml` ne contiennent pas toute l'information nécessaire*
+
 Pour modifier la configuration du serveur API
 
 `$ docker-compose exec -it api vi /etc/bottin/api.yaml`
diff --git a/cmd/root.go b/cmd/root.go
index 94350ca..0b7562e 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
@@ -47,6 +48,8 @@ func initConfig() {
 		viper.SetConfigName(".bottin")
 	}
 
+	viper.SetEnvPrefix("BOTTIN")
+	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
 	viper.AutomaticEnv() // read in environment variables that match
 
 	// If a config file is found, read it in.
diff --git a/docker-compose.yaml b/docker-compose.yaml
index cad1268..1f739d8 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -15,6 +15,11 @@ services:
       - db
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
+    environment:
+      BOTTIN_DB_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
+      BOTTIN_DB_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"
+      BOTTIN_DB_USER: "${BOTTIN_POSTGRES_USER}"
+      BOTTIN_API_KEY: "${BOTTIN_API_KEY}"
     ports:
       - '1312:1312'
     volumes:
@@ -27,6 +32,10 @@ services:
       - api
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
+    environment:
+      BOTTIN_WEB_API_KEY: "${BOTTIN_API_KEY}"
+      BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD}"
+      BOTTIN_WEB_USER: "${BOTTIN_WEB_USER}"
     ports:
       - '2312:2312'
     volumes:

From d5399903e4b6f96bda279b4f991ed148af7b69a7 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 14 Feb 2024 14:13:01 -0500
Subject: [PATCH 44/99] =?UTF-8?q?fix:=20defer=20certains=20appels=20=C3=A0?=
 =?UTF-8?q?=20`tx.Rollback`?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Pour `data.InsertMembres` et `data.InsertProgrammes`
---
 data/data.go | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/data/data.go b/data/data.go
index c4cc026..fa8678c 100644
--- a/data/data.go
+++ b/data/data.go
@@ -78,24 +78,21 @@ func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
 	var rowsInserted int64
 	tx, err := d.DB.Beginx()
 	if err != nil {
-		tx.Rollback()
 		return rowsInserted, err
 	}
+	defer tx.Rollback()
 
 	for _, membre := range membres {
 		if membre.ID == "" {
-			tx.Rollback()
 			return 0, errors.New("Cannot insert membre with no membre_id")
 		}
 		result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id) ON CONFLICT (id) DO NOTHING;", &membre)
 		if err != nil {
-			tx.Rollback()
 			return 0, err
 		}
 
 		rows, err := result.RowsAffected()
 		if err != nil {
-			tx.Rollback()
 			return 0, err
 		}
 
@@ -114,25 +111,22 @@ func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, err
 	var rowsInserted int64
 	tx, err := d.DB.Beginx()
 	if err != nil {
-		tx.Rollback()
 		return rowsInserted, err
 	}
+	defer tx.Rollback()
 
 	for _, programme := range programmes {
 		if programme.ID == "" {
-			tx.Rollback()
 			return 0, errors.New("Cannot insert programme with no programme_id")
 		}
 
 		result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre) ON CONFLICT DO NOTHING;", &programme)
 		if err != nil {
-			tx.Rollback()
 			return 0, err
 		}
 
 		rows, err := result.RowsAffected()
 		if err != nil {
-			tx.Rollback()
 			return 0, err
 		}
 

From 81d775e5a6a885ec2a3439e0f20943575d358108 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 14 Feb 2024 16:36:44 -0500
Subject: [PATCH 45/99] fix: escalate getmembreresponse message as error if no
 returned membre

---
 data/apiclient.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/data/apiclient.go b/data/apiclient.go
index a068809..00fb690 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -63,6 +63,10 @@ func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
 	}
 
 	if getMembreResponse.Data.Membre == *new(models.Membre) {
+		if getMembreResponse.Message != "" {
+			return getMembreResponse.Data.Membre, fmt.Errorf(getMembreResponse.Message)
+		}
+
 		return getMembreResponse.Data.Membre, fmt.Errorf("Ce numéro étudiant ne correspond à aucunE membre")
 	}
 

From 4c8e822324a56a48ffd79dcd7ec18e3b2cbb0d99 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 15 Feb 2024 19:26:44 -0500
Subject: [PATCH 46/99] chores: bump `go.mod` dependencies

Execute `go get -u`
---
 go.mod | 44 +++++++++++++++++++++++++-------------------
 go.sum | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 77 insertions(+), 21 deletions(-)

diff --git a/go.mod b/go.mod
index 014c923..e9ff62b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,46 +1,52 @@
 module git.agecem.com/agecem/bottin/v6
 
-go 1.21.1
+go 1.22.0
 
 require (
-	codeberg.org/vlbeaudoin/serpents v1.0.2
-	codeberg.org/vlbeaudoin/voki v1.3.1
-	github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
+	codeberg.org/vlbeaudoin/serpents v1.1.0
+	codeberg.org/vlbeaudoin/voki v1.7.2
+	github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
 	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/jmoiron/sqlx v1.3.5
-	github.com/labstack/echo/v4 v4.10.2
-	github.com/spf13/cobra v1.7.0
-	github.com/spf13/viper v1.16.0
+	github.com/labstack/echo/v4 v4.11.4
+	github.com/spf13/cobra v1.8.0
+	github.com/spf13/viper v1.18.2
 )
 
 require (
 	github.com/cockroachdb/apd v1.1.0 // indirect
-	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
-	github.com/labstack/gommon v0.4.0 // indirect
+	github.com/labstack/gommon v0.4.2 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/sagikazarmark/locafero v0.4.0 // indirect
+	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
-	github.com/spf13/afero v1.9.5 // indirect
-	github.com/spf13/cast v1.5.1 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.11.0 // indirect
+	github.com/spf13/cast v1.6.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/subosito/gotenv v1.4.2 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
-	golang.org/x/crypto v0.9.0 // indirect
-	golang.org/x/net v0.10.0 // indirect
-	golang.org/x/sys v0.8.0 // indirect
-	golang.org/x/text v0.9.0 // indirect
-	golang.org/x/time v0.3.0 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/crypto v0.19.0 // indirect
+	golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
+	golang.org/x/net v0.21.0 // indirect
+	golang.org/x/sys v0.17.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index bd111f3..57e1bc5 100644
--- a/go.sum
+++ b/go.sum
@@ -37,8 +37,10 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
 codeberg.org/vlbeaudoin/serpents v1.0.2 h1:mHuL+RBAMOGeiB5+ew1cRputEAnOIQNJW9o9a5Qjudo=
 codeberg.org/vlbeaudoin/serpents v1.0.2/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
-codeberg.org/vlbeaudoin/voki v1.3.1 h1:TxJj3qmOys0Pbq1dPKnOEXMXKqQLQqrBYd4QqiWWXcw=
-codeberg.org/vlbeaudoin/voki v1.3.1/go.mod h1:5XTLx/KiW/OfiupF3o7PAAAU/UhsPdKSrVMmtHbmkPI=
+codeberg.org/vlbeaudoin/serpents v1.1.0 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA=
+codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
+codeberg.org/vlbeaudoin/voki v1.7.2 h1:9sMuOCuJ0t4hhZgr/rbRswHvyXN4CuizGJaB/Exmd90=
+codeberg.org/vlbeaudoin/voki v1.7.2/go.mod h1:5XTLx/KiW/OfiupF3o7PAAAU/UhsPdKSrVMmtHbmkPI=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 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=
@@ -53,6 +55,7 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
 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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -64,8 +67,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
 github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 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=
@@ -73,6 +79,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
+github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
+github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@@ -160,8 +168,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
 github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
+github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
+github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
 github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
 github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -173,12 +185,16 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
+github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
@@ -189,20 +205,34 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
 github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
 github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
 github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
 github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -214,8 +244,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@@ -231,6 +264,10 @@ 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/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -240,6 +277,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
 golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 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=
@@ -250,6 +289,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 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/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
+golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 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=
@@ -307,6 +348,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -366,8 +409,11 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 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=
@@ -379,11 +425,15 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

From 9367f0f4c0ae9523daeda2b4d8b0defead0ce7ac Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 15 Feb 2024 19:32:50 -0500
Subject: [PATCH 47/99] chores: bump voki to v2.0.3

---
 data/apiclient.go   |   2 +-
 go.mod              |   4 +-
 go.sum              | 495 +-------------------------------------------
 responses/health.go |   4 +-
 responses/list.go   |   4 +-
 responses/post.go   |   6 +-
 6 files changed, 19 insertions(+), 496 deletions(-)

diff --git a/data/apiclient.go b/data/apiclient.go
index 00fb690..da70a18 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"codeberg.org/vlbeaudoin/voki"
+	"codeberg.org/vlbeaudoin/voki/v2"
 	"git.agecem.com/agecem/bottin/v6/models"
 	"git.agecem.com/agecem/bottin/v6/responses"
 	"github.com/spf13/viper"
diff --git a/go.mod b/go.mod
index e9ff62b..bd60d6f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.22.0
 
 require (
 	codeberg.org/vlbeaudoin/serpents v1.1.0
-	codeberg.org/vlbeaudoin/voki v1.7.2
+	codeberg.org/vlbeaudoin/voki/v2 v2.0.3
 	github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
 	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/jmoiron/sqlx v1.3.5
@@ -34,12 +34,10 @@ require (
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.11.0 // indirect
 	github.com/spf13/cast v1.6.0 // indirect
-	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
-	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/crypto v0.19.0 // indirect
 	golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
diff --git a/go.sum b/go.sum
index 57e1bc5..b8fe100 100644
--- a/go.sum
+++ b/go.sum
@@ -1,152 +1,30 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.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 v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-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=
-cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
-codeberg.org/vlbeaudoin/serpents v1.0.2 h1:mHuL+RBAMOGeiB5+ew1cRputEAnOIQNJW9o9a5Qjudo=
-codeberg.org/vlbeaudoin/serpents v1.0.2/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
 codeberg.org/vlbeaudoin/serpents v1.1.0 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA=
 codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
-codeberg.org/vlbeaudoin/voki v1.7.2 h1:9sMuOCuJ0t4hhZgr/rbRswHvyXN4CuizGJaB/Exmd90=
-codeberg.org/vlbeaudoin/voki v1.7.2/go.mod h1:5XTLx/KiW/OfiupF3o7PAAAU/UhsPdKSrVMmtHbmkPI=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-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/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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E=
+codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U=
 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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
-github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-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-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
-github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
 github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-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/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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-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.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-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-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/martian/v3 v3.1.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/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
@@ -155,53 +33,34 @@ github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yI
 github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
 github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
 github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
-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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
-github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
 github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
 github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
-github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
-github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
 github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
-github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
 github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -213,384 +72,50 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
-github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
-github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
 github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
 github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
-github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
-github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
 github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
 github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
 github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
-github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
 github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
 github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
-github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
-github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-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/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/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
-go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-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/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/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/lint v0.0.0-20201208152925-83fdc39ff7b5/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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-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-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/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-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/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/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/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-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/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-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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-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-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-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/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-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/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-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/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/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=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-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/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/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/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/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/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/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=
-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=
diff --git a/responses/health.go b/responses/health.go
index 1edf34a..59c6fe5 100644
--- a/responses/health.go
+++ b/responses/health.go
@@ -1,7 +1,7 @@
 package responses
 
-import "codeberg.org/vlbeaudoin/voki/response"
+import "codeberg.org/vlbeaudoin/voki/v2"
 
 type GetHealthResponse struct {
-	response.ResponseWithError
+	voki.ResponseWithError
 }
diff --git a/responses/list.go b/responses/list.go
index 4783e90..414883b 100644
--- a/responses/list.go
+++ b/responses/list.go
@@ -1,12 +1,12 @@
 package responses
 
 import (
-	"codeberg.org/vlbeaudoin/voki/response"
+	"codeberg.org/vlbeaudoin/voki/v2"
 	"git.agecem.com/agecem/bottin/v6/models"
 )
 
 type ListMembresResponse struct {
-	response.ResponseWithError
+	voki.ResponseWithError
 	Data struct {
 		Membres []models.Membre
 	}
diff --git a/responses/post.go b/responses/post.go
index cfb03af..7f1b7b4 100644
--- a/responses/post.go
+++ b/responses/post.go
@@ -1,16 +1,16 @@
 package responses
 
-import "codeberg.org/vlbeaudoin/voki/response"
+import "codeberg.org/vlbeaudoin/voki/v2"
 
 type PostMembresResponse struct {
-	response.ResponseWithError
+	voki.ResponseWithError
 	Data struct {
 		MembresInserted int64
 	}
 }
 
 type PostProgrammesResponse struct {
-	response.ResponseWithError
+	voki.ResponseWithError
 	Data struct {
 		ProgrammesInserted int64
 	}

From 4a87daae799af4c16f08db944e3b2be78e27240c Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 15 Feb 2024 19:37:06 -0500
Subject: [PATCH 48/99] chores(Dockerfile): bump go version to v1.22.0

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 67e6af6..c83ac91 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.21.1 as build
+FROM golang:1.22.0 as build
 
 LABEL author="vlbeaudoin"
 

From 917aab7e0113ec73f250b284a0b48834b2a1618a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 15 Feb 2024 19:38:02 -0500
Subject: [PATCH 49/99] chores(Dockerfile): bump alpine to 3.19

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index c83ac91..52a9a73 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,7 +17,7 @@ RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin .
 
 # Alpine
 
-FROM alpine:3.18
+FROM alpine:3.19
 
 WORKDIR /app
 

From 6d98375adb95673f18c98fe8df88589e92440862 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 6 Jun 2024 01:40:56 -0400
Subject: [PATCH 50/99] =?UTF-8?q?d=C3=A9but=20de=20r=C3=A9=C3=A9criture=20?=
 =?UTF-8?q?pour=20v7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Dockerfile                              |  15 +--
 cmd/api.go                              | 141 ++++++++++++++++--------
 cmd/root.go                             |   2 +-
 cmd/web.go                              | 135 +++++++++++++++--------
 config.go                               |  99 +++++++++++++++++
 data/apiclient.go                       |  78 -------------
 data/data.go => db.go                   |  48 +++++---
 db_test.go                              |  45 ++++++++
 docker-compose.yaml                     |   2 +-
 models/models.go => entity.go           |  21 +---
 go.mod                                  |  16 +--
 go.sum                                  |  40 +++----
 handlers/handlers.go                    |  11 --
 handlers/health.go                      |  36 ------
 handlers/insert.go                      | 129 ----------------------
 handlers/read.go                        |  61 ----------
 handlers/seed.go                        |  24 ----
 handlers/update.go                      |  42 -------
 main.go                                 |  16 ++-
 responses/post.go => responses.go       |  13 ++-
 responses/health.go                     |   7 --
 responses/list.go                       |  13 ---
 sql/schema.sql                          |  12 ++
 {web/templates => templates}/index.html |   0
 v4/LICENSE                              |   9 --
 v4/README.md                            |   1 -
 v4/go.mod                               |  10 --
 web/embed.go                            |  10 --
 web/webhandlers/handlers.go             |  46 --------
 29 files changed, 421 insertions(+), 661 deletions(-)
 create mode 100644 config.go
 delete mode 100644 data/apiclient.go
 rename data/data.go => db.go (78%)
 create mode 100644 db_test.go
 rename models/models.go => entity.go (61%)
 delete mode 100644 handlers/handlers.go
 delete mode 100644 handlers/health.go
 delete mode 100644 handlers/insert.go
 delete mode 100644 handlers/read.go
 delete mode 100644 handlers/seed.go
 delete mode 100644 handlers/update.go
 rename responses/post.go => responses.go (59%)
 delete mode 100644 responses/health.go
 delete mode 100644 responses/list.go
 create mode 100644 sql/schema.sql
 rename {web/templates => templates}/index.html (100%)
 delete mode 100644 v4/LICENSE
 delete mode 100644 v4/README.md
 delete mode 100644 v4/go.mod
 delete mode 100644 web/embed.go
 delete mode 100644 web/webhandlers/handlers.go

diff --git a/Dockerfile b/Dockerfile
index 52a9a73..7c94ff6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,23 +1,20 @@
-FROM golang:1.22.0 as build
+FROM golang:1.22.3 as build
 
 LABEL author="vlbeaudoin"
 
 WORKDIR /go/src/app
 
-COPY go.mod go.sum main.go ./
+COPY go.mod go.sum db.go entity.go main.go responses.go ./
 
 ADD cmd/ cmd/
-ADD data/ data/
-ADD handlers/ handlers/
-ADD models/ models/
-ADD responses/ responses/
-ADD web/ web/
+ADD templates/ templates/
+ADD sql/ sql/
 
-RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin .
+RUN CGO_ENABLED=0 go build -a -o bottin .
 
 # Alpine
 
-FROM alpine:3.19
+FROM alpine:3.20
 
 WORKDIR /app
 
diff --git a/cmd/api.go b/cmd/api.go
index b03be9a..1e9f6d8 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -5,9 +5,6 @@ import (
 	"fmt"
 	"log"
 
-	"codeberg.org/vlbeaudoin/serpents"
-	"git.agecem.com/agecem/bottin/v6/data"
-	"git.agecem.com/agecem/bottin/v6/handlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
@@ -19,14 +16,51 @@ var (
 	apiKey  string
 )
 
+const (
+	ViperAPIPort       string = "api.port"
+	FlagAPIPort        string = "api-port"
+	DefaultAPIPort     int    = 1312
+	DescriptionAPIPort string = "API server port"
+
+	ViperAPIKey       string = "api.key"
+	FlagAPIKey        string = "api-key"
+	DefaultAPIKey     string = "bottin"
+	DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)"
+
+	ViperDBDatabase       string = "db.database"
+	FlagDBDatabase        string = "db-database"
+	DefaultDBDatabase     string = "bottin"
+	DescriptionDBDatabase string = "Postgres database"
+
+	ViperDBHost       string = "db.host"
+	FlagDBHost        string = "db-host"
+	DefaultDBHost     string = "db"
+	DescriptionDBHost string = "Postgres host"
+
+	ViperDBPassword       string = "db.password"
+	FlagDBPassword        string = "db-password"
+	DefaultDBPassword     string = "bottin"
+	DescriptionDBPassword string = "Postgres password"
+
+	ViperDBPort       string = "db.port"
+	FlagDBPort        string = "db-port"
+	DefaultDBPort     int    = 5432
+	DescriptionDBPort string = "Postgres port"
+
+	ViperDBUser       string = "db.user"
+	FlagDBUser        string = "db-user"
+	DefaultDBUser     string = "bottin"
+	DescriptionDBUser string = "Postgres user"
+)
+
 // apiCmd represents the api command
 var apiCmd = &cobra.Command{
 	Use:   "api",
 	Short: "Démarrer le serveur API",
 	Args:  cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
-		apiKey = viper.GetString("api.key")
-		apiPort = viper.GetInt("api.port")
+		apiKey = viper.GetString(ViperAPIKey)
+		apiPort = viper.GetInt(ViperAPIPort)
 
 		e := echo.New()
 
@@ -41,40 +75,42 @@ var apiCmd = &cobra.Command{
 		}
 
 		// DataClient
+		/*
+			client, err := data.NewDataClientFromViper()
+			if err != nil {
+				log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
+			}
+			defer client.DB.Close()
 
-		client, err := data.NewDataClientFromViper()
-		if err != nil {
-			log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
-		}
-		defer client.DB.Close()
+			err = client.DB.Ping()
+			if err != nil {
+				log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
+			}
 
-		err = client.DB.Ping()
-		if err != nil {
-			log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
-		}
-
-		_, err = client.Seed()
-		if err != nil {
-			log.Fatalf("Error during client.Seed(): %s", err)
-		}
-
-		h := handlers.New(client)
+			_, err = client.Seed()
+			if err != nil {
+				log.Fatalf("Error during client.Seed(): %s", err)
+			}
+		*/
 
 		// Routes
+		/*
+			h := handlers.New(client)
 
-		e.GET("/v6/health/", h.GetHealth)
+			e.GET("/v7/health/", h.GetHealth)
 
-		e.POST("/v6/membres/", h.PostMembres)
+			e.POST("/v7/membres/", h.PostMembres)
 
-		e.GET("/v6/membres/", h.ListMembres)
+			e.GET("/v7/membres/", h.ListMembres)
 
-		e.GET("/v6/membres/:membre_id/", h.ReadMembre)
+			e.GET("/v7/membres/:membre_id/", h.ReadMembre)
 
-		e.PUT("/v6/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
+			e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
 
-		e.POST("/v6/programmes/", h.PostProgrammes)
+			e.POST("/v7/programmes/", h.PostProgrammes)
 
-		e.POST("/v6/seed/", h.PostSeed)
+			e.POST("/v7/seed/", h.PostSeed)
+		*/
 
 		// Execution
 
@@ -86,37 +122,44 @@ func init() {
 	rootCmd.AddCommand(apiCmd)
 
 	// api.key
-	serpents.String(apiCmd.Flags(),
-		"api.key", "api-key", "bottin",
-		"API server key. Leave empty for no key auth")
+	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
+	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
+		log.Fatal(err)
+	}
 
 	// api.port
-	serpents.Int(apiCmd.Flags(),
-		"api.port", "api-port", 1312,
-		"API server port")
+	apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
+	if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
+		log.Fatal(err)
+	}
 
 	// db.database
-	serpents.String(apiCmd.Flags(),
-		"db.database", "db-database", "bottin",
-		"Postgres database")
+	apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
+	if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
+		log.Fatal(err)
+	}
 
 	// db.host
-	serpents.String(apiCmd.Flags(),
-		"db.host", "db-host", "db",
-		"Postgres host")
+	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
+	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
+		log.Fatal(err)
+	}
 
 	// db.password
-	serpents.String(apiCmd.Flags(),
-		"db.password", "db-password", "bottin",
-		"Postgres password")
+	apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
+	if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
+		log.Fatal(err)
+	}
 
 	// db.port
-	serpents.Int(apiCmd.Flags(),
-		"db.port", "db-port", 5432,
-		"Postgres port")
+	apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
+	if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
+		log.Fatal(err)
+	}
 
 	// db.user
-	serpents.String(apiCmd.Flags(),
-		"db.user", "db-user", "bottin",
-		"Postgres user")
+	apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
+	if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
+		log.Fatal(err)
+	}
 }
diff --git a/cmd/root.go b/cmd/root.go
index 0b7562e..26e0636 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -14,7 +14,7 @@ var cfgFile string
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
 	Use:   "bottin",
-	Short: "Application de gestion de distribution d'agendas",
+	Short: "Bottin étudiant de l'AGECEM",
 }
 
 // Execute adds all child commands to the root command and sets flags appropriately.
diff --git a/cmd/web.go b/cmd/web.go
index 4d1a556..2ce2fad 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -7,12 +7,7 @@ import (
 	"html/template"
 	"io"
 	"log"
-	"net/http"
 
-	"codeberg.org/vlbeaudoin/serpents"
-	"git.agecem.com/agecem/bottin/v6/data"
-	"git.agecem.com/agecem/bottin/v6/web"
-	"git.agecem.com/agecem/bottin/v6/web/webhandlers"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
@@ -29,6 +24,43 @@ var (
 	webApiProtocol string
 )
 
+const (
+	viperWebUser       string = "web.user"
+	flagWebUser        string = "web-user"
+	defaultWebUser     string = "bottin"
+	descriptionWebUser string = "Web client basic auth user"
+
+	viperWebPassword       string = "web.password"
+	flagWebPassword        string = "web-password"
+	defaultWebPassword     string = "bottin"
+	descriptionWebPassword string = "Web client basic auth password"
+
+	viperWebPort       string = "web.port"
+	flagWebPort        string = "web-port"
+	defaultWebPort     int    = 2312
+	descriptionWebPort string = "Web client port"
+
+	viperWebAPIHost       string = "api.host"
+	flagWebAPIHost        string = "api-host"
+	defaultWebAPIHost     string = "api"
+	descriptionWebAPIHost string = "Target API server host"
+
+	viperWebAPIKey       string = "api.key"
+	flagWebAPIKey        string = "api-key"
+	defaultWebAPIKey     string = "bottin"
+	descriptionWebAPIKey string = "Target API server key"
+
+	viperWebAPIPort       string = "api.port"
+	flagWebAPIPort        string = "api-port"
+	defaultWebAPIPort     int    = 1312
+	descriptionWebAPIPort string = "Target API server port"
+
+	viperWebAPIProtocol       string = "api.protocol"
+	flagWebAPIProtocol        string = "api-protocol"
+	defaultWebAPIProtocol     string = "http"
+	descriptionWebAPIProtocol string = "Target API server protocol (http/https)"
+)
+
 var templatesFS embed.FS
 
 type Template struct {
@@ -45,27 +77,28 @@ var webCmd = &cobra.Command{
 	Short: "Démarrer le client web",
 	Args:  cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
-		webApiHost = viper.GetString("web.api.host")
-		webApiKey = viper.GetString("web.api.key")
-		webApiPort = viper.GetInt("web.api.port")
-		webApiProtocol = viper.GetString("web.api.protocol")
-		webPassword = viper.GetString("web.password")
-		webPort = viper.GetInt("web.port")
-		webUser = viper.GetString("web.user")
+		webApiHost = viper.GetString(viperWebAPIHost)
+		webApiKey = viper.GetString(viperWebAPIKey)
+		webApiPort = viper.GetInt(viperWebAPIPort)
+		webApiProtocol = viper.GetString(viperWebAPIProtocol)
+		webPassword = viper.GetString(viperWebPassword)
+		webPort = viper.GetInt(viperWebPort)
+		webUser = viper.GetString(viperWebUser)
 
 		// Ping API server
+		/*
+			client := http.DefaultClient
+			defer client.CloseIdleConnections()
 
-		client := http.DefaultClient
-		defer client.CloseIdleConnections()
+			apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
 
-		apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
+			pingResult, err := apiClient.GetHealth()
+			if err != nil {
+				log.Fatal(err)
+			}
 
-		pingResult, err := apiClient.GetHealth()
-		if err != nil {
-			log.Fatal(err)
-		}
-
-		log.Println(pingResult)
+			log.Println(pingResult)
+		*/
 
 		e := echo.New()
 
@@ -88,11 +121,12 @@ var webCmd = &cobra.Command{
 		e.Renderer = t
 
 		// Routes
+		/*
+			handler := webhandlers.Handler{APIClient: apiClient}
 
-		handler := webhandlers.Handler{APIClient: apiClient}
-
-		e.GET("/", handler.GetIndex)
-		e.GET("/membre/", handler.GetMembre)
+			e.GET("/", handler.GetIndex)
+			e.GET("/membre/", handler.GetMembre)
+		*/
 
 		// Execution
 
@@ -103,40 +137,47 @@ var webCmd = &cobra.Command{
 
 func init() {
 	rootCmd.AddCommand(webCmd)
-	templatesFS = web.GetTemplates()
+	//templatesFS = web.GetTemplates()
 
 	// web.api.host
-	serpents.String(webCmd.Flags(),
-		"web.api.host", "web-api-host", "api",
-		"Remote API server host")
+	webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost)
+	if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil {
+		log.Fatal(err)
+	}
 
 	// web.api.key
-	serpents.String(webCmd.Flags(),
-		"web.api.key", "web-api-key", "bottin",
-		"Remote API server key")
+	webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey)
+	if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil {
+		log.Fatal(err)
+	}
 
 	// web.api.protocol
-	serpents.String(webCmd.Flags(),
-		"web.api.protocol", "web-api-protocol", "http",
-		"Remote API server protocol")
+	webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol)
+	if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil {
+		log.Fatal(err)
+	}
 
 	// web.api.port
-	serpents.Int(webCmd.Flags(),
-		"web.api.port", "web-api-port", 1312,
-		"Remote API server port")
+	webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort)
+	if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil {
+		log.Fatal(err)
+	}
 
 	// web.password
-	serpents.String(webCmd.Flags(),
-		"web.password", "web-password", "bottin",
-		"Web client password")
+	webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword)
+	if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil {
+		log.Fatal(err)
+	}
 
 	// web.port
-	serpents.Int(webCmd.Flags(),
-		"web.port", "web-port", 2312,
-		"Web client port")
+	webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort)
+	if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil {
+		log.Fatal(err)
+	}
 
 	// web.user
-	serpents.String(webCmd.Flags(),
-		"web.user", "web-user", "bottin",
-		"Web client user")
+	webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser)
+	if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil {
+		log.Fatal(err)
+	}
 }
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..c0108b4
--- /dev/null
+++ b/config.go
@@ -0,0 +1,99 @@
+package main
+
+const (
+	ViperAPIPort       string = "api.port"
+	FlagAPIPort        string = "api-port"
+	DefaultAPIPort     int    = 1312
+	DescriptionAPIPort string = "API server port"
+
+	ViperAPIKey       string = "api.key"
+	FlagAPIKey        string = "api-key"
+	DefaultAPIKey     string = "bottin"
+	DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)"
+
+	ViperDBDatabase       string = "db.database"
+	FlagDBDatabase        string = "db-database"
+	DefaultDBDatabase     string = "bottin"
+	DescriptionDBDatabase string = "Postgres database"
+
+	ViperDBHost       string = "db.host"
+	FlagDBHost        string = "db-host"
+	DefaultDBHost     string = "db"
+	DescriptionDBHost string = "Postgres host"
+
+	ViperDBPassword       string = "db.password"
+	FlagDBPassword        string = "db-password"
+	DefaultDBPassword     string = "bottin"
+	DescriptionDBPassword string = "Postgres password"
+
+	ViperDBPort       string = "db.port"
+	FlagDBPort        string = "db-port"
+	DefaultDBPort     int    = 5432
+	DescriptionDBPort string = "Postgres port"
+
+	ViperDBUser       string = "db.user"
+	FlagDBUser        string = "db-user"
+	DefaultDBUser     string = "bottin"
+	DescriptionDBUser string = "Postgres user"
+
+	viperWebUser       string = "web.user"
+	flagWebUser        string = "web-user"
+	defaultWebUser     string = "bottin"
+	descriptionWebUser string = "Web client basic auth user"
+
+	viperWebPassword       string = "web.password"
+	flagWebPassword        string = "web-password"
+	defaultWebPassword     string = "bottin"
+	descriptionWebPassword string = "Web client basic auth password"
+
+	viperWebPort       string = "web.port"
+	flagWebPort        string = "web-port"
+	defaultWebPort     int    = 2312
+	descriptionWebPort string = "Web client port"
+
+	viperWebAPIHost       string = "api.host"
+	flagWebAPIHost        string = "api-host"
+	defaultWebAPIHost     string = "api"
+	descriptionWebAPIHost string = "Target API server host"
+
+	viperWebAPIKey       string = "api.key"
+	flagWebAPIKey        string = "api-key"
+	defaultWebAPIKey     string = "bottin"
+	descriptionWebAPIKey string = "Target API server key"
+
+	viperWebAPIPort       string = "api.port"
+	flagWebAPIPort        string = "api-port"
+	defaultWebAPIPort     int    = 1312
+	descriptionWebAPIPort string = "Target API server port"
+
+	viperWebAPIProtocol       string = "api.protocol"
+	flagWebAPIProtocol        string = "api-protocol"
+	defaultWebAPIProtocol     string = "http"
+	descriptionWebAPIProtocol string = "Target API server protocol (http/https)"
+)
+
+type Config struct {
+	API struct {
+		Port int    `yaml:"port"`
+		Key  string `yaml:"key"`
+	} `yaml:"api"`
+	DB struct {
+		Database string `yaml:"database"`
+		Host     string `yaml:"host"`
+		SSLMode  string `yaml:"sslmode"`
+		Password string `yaml:"password"`
+		Port     int    `yaml:"port"`
+		User     string `yaml:"user"`
+	} `yaml:"db"`
+	Web struct {
+		User     string `yaml:"user"`
+		Password string `yaml:"password"`
+		Port     int    `yaml:"port"`
+		API      struct {
+			Host     string `yaml:"host"`
+			Key      string `yaml:"key"`
+			Port     int    `yaml:"port"`
+			Protocol string `yaml:"protocol"`
+		} `yaml:"api"`
+	} `yaml:"web"`
+}
diff --git a/data/apiclient.go b/data/apiclient.go
deleted file mode 100644
index da70a18..0000000
--- a/data/apiclient.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package data
-
-import (
-	"errors"
-	"fmt"
-	"net/http"
-
-	"codeberg.org/vlbeaudoin/voki/v2"
-	"git.agecem.com/agecem/bottin/v6/models"
-	"git.agecem.com/agecem/bottin/v6/responses"
-	"github.com/spf13/viper"
-)
-
-type ApiClient struct {
-	Voki *voki.Voki
-}
-
-func NewApiClientFromViper(client *http.Client) *ApiClient {
-	apiClientKey := viper.GetString("web.api.key")
-	apiClientHost := viper.GetString("web.api.host")
-	apiClientProtocol := viper.GetString("web.api.protocol")
-	apiClientPort := viper.GetInt("web.api.port")
-
-	return NewApiClient(client, apiClientKey, apiClientHost, apiClientProtocol, apiClientPort)
-}
-
-func NewApiClient(client *http.Client, key, host, protocol string, port int) *ApiClient {
-	return &ApiClient{
-		Voki: voki.New(client, host, key, port, protocol),
-	}
-}
-
-// GetHealth allows checking for API server health
-func (a *ApiClient) GetHealth() (string, error) {
-	var getHealthResponse responses.GetHealthResponse
-	err := a.Voki.Unmarshal(http.MethodGet, "/v6/health", nil, true, &getHealthResponse)
-	if err != nil {
-		return getHealthResponse.Message, err
-	}
-
-	if getHealthResponse.Message == "" {
-		return getHealthResponse.Message, errors.New("Could not confirm that API server is up, no response message")
-	}
-
-	return getHealthResponse.Message, nil
-}
-
-func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
-	var getMembreResponse struct {
-		Message string `json:"message"`
-		Data    struct {
-			Membre models.Membre `json:"membre"`
-		} `json:"data"`
-	}
-
-	if membreID == "" {
-		return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher")
-	}
-
-	err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v6/membres/%s", membreID), nil, true, &getMembreResponse)
-	if err != nil {
-		return getMembreResponse.Data.Membre, err
-	}
-
-	if getMembreResponse.Data.Membre == *new(models.Membre) {
-		if getMembreResponse.Message != "" {
-			return getMembreResponse.Data.Membre, fmt.Errorf(getMembreResponse.Message)
-		}
-
-		return getMembreResponse.Data.Membre, fmt.Errorf("Ce numéro étudiant ne correspond à aucunE membre")
-	}
-
-	return getMembreResponse.Data.Membre, nil
-}
-
-func (a *ApiClient) ListMembres() (r responses.ListMembresResponse, err error) {
-	return r, a.Voki.Unmarshal(http.MethodGet, "/v6/membres", nil, true, &r)
-}
diff --git a/data/data.go b/db.go
similarity index 78%
rename from data/data.go
rename to db.go
index fa8678c..fff8885 100644
--- a/data/data.go
+++ b/db.go
@@ -1,15 +1,26 @@
-package data
+package main
 
 import (
-	"errors"
-	"fmt"
+	"context"
+	_ "embed"
 
-	"git.agecem.com/agecem/bottin/v6/models"
-	_ "github.com/jackc/pgx/stdlib"
-	"github.com/jmoiron/sqlx"
-	"github.com/spf13/viper"
+	"github.com/jackc/pgx/v5/pgxpool"
 )
 
+//go:embed sql/schema.sql
+var sqlSchema string
+
+type PostgresClient struct {
+	Ctx  context.Context
+	Pool *pgxpool.Pool
+}
+
+func (db *PostgresClient) CreateOrReplaceSchema() error {
+	_, err := db.Pool.Exec(db.Ctx, sqlSchema)
+	return err
+}
+
+/*
 // DataClient is a postgres client based on sqlx
 type DataClient struct {
 	PostgresConnection PostgresConnection
@@ -28,11 +39,11 @@ type PostgresConnection struct {
 func NewDataClientFromViper() (*DataClient, error) {
 	client, err := NewDataClient(
 		PostgresConnection{
-			User:     viper.GetString("db.user"),
-			Password: viper.GetString("db.password"),
-			Host:     viper.GetString("db.host"),
-			Database: viper.GetString("db.database"),
-			Port:     viper.GetInt("db.port"),
+			User:     viper.GetString(cmd.ViperDBHost),
+			Password: viper.GetString(cmd.ViperDBPassword),
+			Host:     viper.GetString(cmd.ViperDBHost),
+			Database: viper.GetString(cmd.ViperDBDatabase),
+			Port:     viper.GetInt(cmd.ViperDBPort),
 		})
 
 	return client, err
@@ -60,7 +71,7 @@ func NewDataClient(connection PostgresConnection) (*DataClient, error) {
 }
 
 func (d *DataClient) Seed() (int64, error) {
-	result, err := d.DB.Exec(models.Schema)
+	result, err := d.DB.Exec(sqlSchema)
 	if err != nil {
 		return 0, err
 	}
@@ -74,7 +85,7 @@ func (d *DataClient) Seed() (int64, error) {
 }
 
 // InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
-func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
+func (d *DataClient) InsertMembres(membres []Membre) (int64, error) {
 	var rowsInserted int64
 	tx, err := d.DB.Beginx()
 	if err != nil {
@@ -107,7 +118,7 @@ func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
 	return rowsInserted, nil
 }
 
-func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, error) {
+func (d *DataClient) InsertProgrammes(programmes []Programme) (int64, error) {
 	var rowsInserted int64
 	tx, err := d.DB.Beginx()
 	if err != nil {
@@ -141,8 +152,8 @@ func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, err
 	return rowsInserted, nil
 }
 
-func (d *DataClient) GetMembre(membreID string) (models.Membre, error) {
-	var membre models.Membre
+func (d *DataClient) GetMembre(membreID string) (Membre, error) {
+	var membre Membre
 
 	rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
 	if err != nil {
@@ -177,6 +188,7 @@ func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
 	return rows, nil
 }
 
-func (d *DataClient) GetMembres() (membres []models.Membre, err error) {
+func (d *DataClient) GetMembres() (membres []Membre, err error) {
 	return membres, d.DB.Select(&membres, "SELECT * FROM membres;")
 }
+*/
diff --git a/db_test.go b/db_test.go
new file mode 100644
index 0000000..d9d235a
--- /dev/null
+++ b/db_test.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/jackc/pgx/v5/pgxpool"
+)
+
+func TestDB(t *testing.T) {
+	ctx := context.Background()
+
+	//prep
+	pool, err := pgxpool.New(
+		ctx,
+		fmt.Sprintf(
+			"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
+			"bottin",
+			"bottin",
+			"bottin",
+			"localhost",
+			5432,
+			"prefer", //TODO change to "require"
+		))
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer pool.Close()
+
+	db := &PostgresClient{
+		Ctx:  ctx,
+		Pool: pool,
+	}
+
+	//exec
+
+	t.Run("create or replace schema",
+		func(t *testing.T) {
+			if err := db.CreateOrReplaceSchema(); err != nil {
+				t.Error(err)
+			}
+		})
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 1f739d8..9f2e604 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,7 +1,7 @@
 services:
 
   db:
-    image: 'docker.io/library/postgres:16.1'
+    image: 'docker.io/library/postgres:16'
     environment:
       POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
       POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"
diff --git a/models/models.go b/entity.go
similarity index 61%
rename from models/models.go
rename to entity.go
index 845d28d..52e54d0 100644
--- a/models/models.go
+++ b/entity.go
@@ -1,19 +1,4 @@
-package models
-
-const Schema = `
-CREATE TABLE IF NOT EXISTS programmes (
-    id TEXT PRIMARY KEY,
-    titre TEXT
-);
-
-CREATE TABLE IF NOT EXISTS membres (
-    id VARCHAR(7) PRIMARY KEY,
-    last_name TEXT,
-    first_name TEXT,
-    prefered_name TEXT,
-    programme_id TEXT REFERENCES programmes(id)
-);
-`
+package main
 
 type Programme struct {
 	ID    string `db:"id" json:"programme_id" csv:"programme_id"`
@@ -27,7 +12,3 @@ type Membre struct {
 	PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"`
 	ProgrammeID  string `db:"programme_id" json:"programme_id" csv:"programme_id"`
 }
-
-type Entry interface {
-	Programme | Membre
-}
diff --git a/go.mod b/go.mod
index bd60d6f..1952064 100644
--- a/go.mod
+++ b/go.mod
@@ -1,36 +1,31 @@
-module git.agecem.com/agecem/bottin/v6
+module git.agecem.com/agecem/bottin/v7
 
 go 1.22.0
 
 require (
-	codeberg.org/vlbeaudoin/serpents v1.1.0
 	codeberg.org/vlbeaudoin/voki/v2 v2.0.3
-	github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
-	github.com/jackc/pgx v3.6.2+incompatible
-	github.com/jmoiron/sqlx v1.3.5
+	github.com/jackc/pgx/v5 v5.6.0
 	github.com/labstack/echo/v4 v4.11.4
 	github.com/spf13/cobra v1.8.0
 	github.com/spf13/viper v1.18.2
 )
 
 require (
-	github.com/cockroachdb/apd v1.1.0 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
-	github.com/gofrs/uuid v4.4.0+incompatible // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+	github.com/jackc/puddle/v2 v2.2.1 // indirect
 	github.com/labstack/gommon v0.4.2 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/sagikazarmark/locafero v0.4.0 // indirect
 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
-	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.11.0 // indirect
 	github.com/spf13/cast v1.6.0 // indirect
@@ -42,6 +37,7 @@ require (
 	golang.org/x/crypto v0.19.0 // indirect
 	golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
 	golang.org/x/net v0.21.0 // indirect
+	golang.org/x/sync v0.5.0 // indirect
 	golang.org/x/sys v0.17.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
diff --git a/go.sum b/go.sum
index b8fe100..5c5e2b0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,5 @@
-codeberg.org/vlbeaudoin/serpents v1.1.0 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA=
-codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
 codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E=
 codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U=
-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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,12 +9,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
-github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
-github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
-github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
-github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
@@ -27,12 +17,14 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
-github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
-github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
-github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
-github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
-github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
+github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -41,8 +33,6 @@ github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zG
 github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
 github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
-github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -50,14 +40,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
 github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -68,8 +54,6 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
 github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
-github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
-github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@@ -85,6 +69,8 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
@@ -103,6 +89,8 @@ golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5C
 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
@@ -112,8 +100,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/handlers/handlers.go b/handlers/handlers.go
deleted file mode 100644
index 0b35b23..0000000
--- a/handlers/handlers.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package handlers
-
-import "git.agecem.com/agecem/bottin/v6/data"
-
-type Handler struct {
-	DataClient *data.DataClient
-}
-
-func New(dataClient *data.DataClient) *Handler {
-	return &Handler{DataClient: dataClient}
-}
diff --git a/handlers/health.go b/handlers/health.go
deleted file mode 100644
index 4923493..0000000
--- a/handlers/health.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package handlers
-
-import (
-	"net/http"
-
-	"git.agecem.com/agecem/bottin/v6/data"
-	"git.agecem.com/agecem/bottin/v6/responses"
-	"github.com/labstack/echo/v4"
-)
-
-func (h *Handler) GetHealth(c echo.Context) error {
-	var response responses.GetHealthResponse
-
-	dataClient, err := data.NewDataClientFromViper()
-	if err != nil {
-		response.StatusCode = http.StatusInternalServerError
-		response.Message = "Error during data.NewDataClientFromViper()"
-		response.Error = err.Error()
-
-		return c.JSON(response.StatusCode, response)
-	}
-	defer dataClient.DB.Close()
-
-	if err = dataClient.DB.Ping(); err != nil {
-		response.StatusCode = http.StatusInternalServerError
-		response.Message = "Error during dataClient.DB.Ping()"
-		response.Error = err.Error()
-
-		return c.JSON(response.StatusCode, response)
-	}
-
-	response.StatusCode = http.StatusOK
-	response.Message = "Bottin API v6 is ready"
-
-	return c.JSON(response.StatusCode, response)
-}
diff --git a/handlers/insert.go b/handlers/insert.go
deleted file mode 100644
index 8039505..0000000
--- a/handlers/insert.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package handlers
-
-import (
-	"encoding/csv"
-	"io"
-	"net/http"
-
-	"git.agecem.com/agecem/bottin/v6/models"
-	"git.agecem.com/agecem/bottin/v6/responses"
-	"github.com/labstack/echo/v4"
-
-	"github.com/gocarina/gocsv"
-)
-
-func (h *Handler) PostMembres(c echo.Context) error {
-	var response responses.PostMembresResponse
-
-	var membres []models.Membre
-
-	switch c.Request().Header.Get("Content-Type") {
-	case "application/json":
-		if err := c.Bind(&membres); err != nil {
-			response.StatusCode = http.StatusBadRequest
-			response.Message = "Could not bind membres"
-			response.Error = err.Error()
-			return c.JSON(response.StatusCode, response)
-		}
-	case "text/csv":
-		body := c.Request().Body
-		if body == nil {
-			response.StatusCode = http.StatusBadRequest
-			response.Message = "Request body is empty"
-			return c.JSON(response.StatusCode, response)
-		}
-		defer body.Close()
-
-		// Parse the CSV data from the request body using gocsv.
-		if err := gocsv.Unmarshal(body, &membres); err != nil {
-			response.StatusCode = http.StatusBadRequest
-			response.Message = "Could not unmarshal into membres"
-			response.Error = err.Error()
-			return c.JSON(response.StatusCode, response)
-		}
-	default:
-		response.StatusCode = http.StatusBadRequest
-		response.Message = "Invalid Content-Type: Please use application/json or text/csv"
-		return c.JSON(response.StatusCode, response)
-	}
-
-	if len(membres) == 0 {
-		response.StatusCode = http.StatusOK
-		response.Message = "Nothing to do"
-		return c.JSON(response.StatusCode, response)
-	}
-
-	newMembres, err := h.DataClient.InsertMembres(membres)
-	if err != nil {
-		response.StatusCode = http.StatusInternalServerError
-		response.Message = "Could not insert membres"
-		response.Error = err.Error()
-		return c.JSON(response.StatusCode, response)
-	}
-
-	response.StatusCode = http.StatusCreated
-	response.Message = "Insert successful"
-	response.Data.MembresInserted = newMembres
-	return c.JSON(response.StatusCode, response)
-}
-
-func (h *Handler) PostProgrammes(c echo.Context) error {
-	var response responses.PostProgrammesResponse
-
-	var programmes []models.Programme
-
-	switch c.Request().Header.Get("Content-Type") {
-	case "application/json":
-		if err := c.Bind(&programmes); err != nil {
-			response.StatusCode = http.StatusBadRequest
-			response.Message = "Could not bind programmes"
-			response.Error = err.Error()
-			return c.JSON(response.StatusCode, response)
-		}
-	case "text/csv":
-		body := c.Request().Body
-		if body == nil {
-			response.StatusCode = http.StatusBadRequest
-			response.Message = "Request body is empty"
-			return c.JSON(response.StatusCode, response)
-		}
-		defer body.Close()
-
-		gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
-			r := csv.NewReader(in)
-			r.Comma = ';'
-			return r // Allows use ; as delimiter
-		})
-
-		// Parse the CSV data from the request body using gocsv.
-		if err := gocsv.Unmarshal(body, &programmes); err != nil {
-			response.StatusCode = http.StatusBadRequest
-			response.Message = "Could not unmarshal into programmes"
-			response.Error = err.Error()
-			return c.JSON(response.StatusCode, response)
-		}
-	default:
-		response.StatusCode = http.StatusBadRequest
-		response.Message = "Invalid Content-Type"
-		return c.JSON(response.StatusCode, response)
-	}
-
-	if len(programmes) == 0 {
-		response.StatusCode = http.StatusOK
-		response.Message = "Nothing to do"
-		return c.JSON(response.StatusCode, response)
-	}
-
-	newProgrammes, err := h.DataClient.InsertProgrammes(programmes)
-	if err != nil {
-		response.StatusCode = http.StatusInternalServerError
-		response.Message = "Could not insert programmes"
-		response.Error = err.Error()
-		return c.JSON(response.StatusCode, response)
-	}
-
-	response.StatusCode = http.StatusCreated
-	response.Message = "Insert successful"
-	response.Data.ProgrammesInserted = newProgrammes
-	return c.JSON(response.StatusCode, response)
-}
diff --git a/handlers/read.go b/handlers/read.go
deleted file mode 100644
index 0a5b1fd..0000000
--- a/handlers/read.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package handlers
-
-import (
-	"fmt"
-	"net/http"
-
-	"git.agecem.com/agecem/bottin/v6/responses"
-	"github.com/labstack/echo/v4"
-)
-
-func (h *Handler) ReadMembre(c echo.Context) error {
-	membreID := c.Param("membre_id")
-
-	membre, err := h.DataClient.GetMembre(membreID)
-	if err != nil {
-		if err.Error() == "No membre by that id was found" {
-			return c.JSON(http.StatusNotFound, map[string]string{
-				"message": "Not Found",
-			})
-		}
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Unknown error during GetMembre",
-			"error":   err.Error(),
-		})
-	}
-
-	return c.JSON(http.StatusOK, map[string]interface{}{
-		"message": "Read successful",
-		"data": map[string]interface{}{
-			"membre": &membre,
-		},
-	})
-}
-
-func (h *Handler) ListMembres(c echo.Context) error {
-	var r responses.ListMembresResponse
-
-	membres, err := h.DataClient.GetMembres()
-	if err != nil {
-		r.StatusCode = http.StatusInternalServerError
-		r.Error = err.Error()
-		r.Message = "Error during (*handlers.Handler).DataClient.GetMembres"
-
-		return c.JSON(r.StatusCode, r)
-	}
-
-	r.StatusCode = http.StatusOK
-
-	switch membres := len(membres); membres {
-	case 0:
-		r.Message = "No membres returned from database"
-	case 1:
-		r.Message = "Membre returned from database"
-	default:
-		r.Message = fmt.Sprintf("%d membres returned from database", membres)
-	}
-
-	r.Data.Membres = membres
-
-	return c.JSON(r.StatusCode, r)
-}
diff --git a/handlers/seed.go b/handlers/seed.go
deleted file mode 100644
index 27f5958..0000000
--- a/handlers/seed.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package handlers
-
-import (
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-)
-
-func (h *Handler) PostSeed(c echo.Context) error {
-	rows, err := h.DataClient.Seed()
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Seed failed",
-			"error":   err.Error(),
-		})
-	}
-
-	return c.JSON(http.StatusOK, map[string]interface{}{
-		"message": "Seed successful",
-		"data": map[string]interface{}{
-			"rows": rows,
-		},
-	})
-}
diff --git a/handlers/update.go b/handlers/update.go
deleted file mode 100644
index 8b4bd04..0000000
--- a/handlers/update.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package handlers
-
-import (
-	"net/http"
-
-	"github.com/labstack/echo/v4"
-)
-
-func (h *Handler) PutMembrePreferedName(c echo.Context) error {
-	membreID := c.Param("membre_id")
-
-	var newName string
-
-	err := c.Bind(&newName)
-	if err != nil {
-		return c.JSON(http.StatusBadRequest, map[string]string{
-			"message": "Could not bind newName",
-			"error":   err.Error(),
-		})
-	}
-
-	rows, err := h.DataClient.UpdateMembreName(membreID, newName)
-	if err != nil {
-		return c.JSON(http.StatusInternalServerError, map[string]string{
-			"message": "Could not update membre name",
-			"error":   err.Error(),
-		})
-	}
-
-	if rows == 0 {
-		return c.JSON(http.StatusBadRequest, map[string]string{
-			"message": "No update was done, probably no membre by that id",
-		})
-	}
-
-	return c.JSON(http.StatusOK, map[string]interface{}{
-		"message": "Update successful",
-		"data": map[string]interface{}{
-			"rows": rows,
-		},
-	})
-}
diff --git a/main.go b/main.go
index f0b7d52..cb9123b 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,19 @@
 package main
 
-import "git.agecem.com/agecem/bottin/v6/cmd"
+import (
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"os"
+)
 
 func main() {
-	cmd.Execute()
+	if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {
+	return fmt.Errorf("not implemented")
 }
diff --git a/responses/post.go b/responses.go
similarity index 59%
rename from responses/post.go
rename to responses.go
index 7f1b7b4..d8af880 100644
--- a/responses/post.go
+++ b/responses.go
@@ -1,7 +1,18 @@
-package responses
+package main
 
 import "codeberg.org/vlbeaudoin/voki/v2"
 
+type GetHealthResponse struct {
+	voki.ResponseWithError
+}
+
+type ListMembresResponse struct {
+	voki.ResponseWithError
+	Data struct {
+		Membres []Membre
+	}
+}
+
 type PostMembresResponse struct {
 	voki.ResponseWithError
 	Data struct {
diff --git a/responses/health.go b/responses/health.go
deleted file mode 100644
index 59c6fe5..0000000
--- a/responses/health.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package responses
-
-import "codeberg.org/vlbeaudoin/voki/v2"
-
-type GetHealthResponse struct {
-	voki.ResponseWithError
-}
diff --git a/responses/list.go b/responses/list.go
deleted file mode 100644
index 414883b..0000000
--- a/responses/list.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package responses
-
-import (
-	"codeberg.org/vlbeaudoin/voki/v2"
-	"git.agecem.com/agecem/bottin/v6/models"
-)
-
-type ListMembresResponse struct {
-	voki.ResponseWithError
-	Data struct {
-		Membres []models.Membre
-	}
-}
diff --git a/sql/schema.sql b/sql/schema.sql
new file mode 100644
index 0000000..154b983
--- /dev/null
+++ b/sql/schema.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS programmes (
+    id TEXT PRIMARY KEY,
+    titre TEXT
+);
+
+CREATE TABLE IF NOT EXISTS membres (
+    id VARCHAR(7) PRIMARY KEY,
+    last_name TEXT,
+    first_name TEXT,
+    prefered_name TEXT,
+    programme_id TEXT REFERENCES programmes(id)
+);
diff --git a/web/templates/index.html b/templates/index.html
similarity index 100%
rename from web/templates/index.html
rename to templates/index.html
diff --git a/v4/LICENSE b/v4/LICENSE
deleted file mode 100644
index b5cc18d..0000000
--- a/v4/LICENSE
+++ /dev/null
@@ -1,9 +0,0 @@
-MIT License
-
-Copyright (c) 2021-2023 AGECEM
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/v4/README.md b/v4/README.md
deleted file mode 100644
index 4da14f8..0000000
--- a/v4/README.md
+++ /dev/null
@@ -1 +0,0 @@
-deprecated, see git.agecem.com/agecem/bottin or git.agecem.com/agecem/bottin/v5
diff --git a/v4/go.mod b/v4/go.mod
deleted file mode 100644
index 2d01404..0000000
--- a/v4/go.mod
+++ /dev/null
@@ -1,10 +0,0 @@
-module git.agecem.com/agecem/bottin/v4
-
-go 1.20
-
-//retract (
-//	v4.1.0
-//	v4.0.3
-//	v4.0.2
-//	v4.0.1
-//)
diff --git a/web/embed.go b/web/embed.go
deleted file mode 100644
index 465e5ec..0000000
--- a/web/embed.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package web
-
-import "embed"
-
-//go:embed templates/*
-var templatesFS embed.FS
-
-func GetTemplates() embed.FS {
-	return templatesFS
-}
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
deleted file mode 100644
index 348e43a..0000000
--- a/web/webhandlers/handlers.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package webhandlers
-
-import (
-	"fmt"
-	"net/http"
-
-	"git.agecem.com/agecem/bottin/v6/data"
-	"github.com/labstack/echo/v4"
-)
-
-type Handler struct {
-	APIClient *data.ApiClient
-}
-
-func (h *Handler) GetIndex(c echo.Context) error {
-	return c.Render(http.StatusOK, "index-html", nil)
-}
-
-func (h *Handler) GetMembre(c echo.Context) error {
-
-	membreID := c.QueryParam("membre_id")
-
-	membre, err := h.APIClient.GetMembre(membreID)
-	if err != nil {
-		return c.Render(http.StatusBadRequest, "index-html", struct {
-			Result string
-		}{
-			Result: fmt.Sprintln("👎", err.Error()),
-		})
-	}
-
-	membreResult := fmt.Sprintf(`👍 
-    Membre trouvéE: [%s]`, membre.ID)
-
-	if membre.PreferedName != "" {
-		membreResult = fmt.Sprintf("%s    -> %s", membreResult, membre.PreferedName)
-	} else {
-		membreResult = fmt.Sprintf("%s    -> %s, %s", membreResult, membre.LastName, membre.FirstName)
-	}
-
-	return c.Render(http.StatusOK, "index-html", struct {
-		Result string
-	}{
-		Result: membreResult,
-	})
-}

From b67955ab283bc119aa64aea82ddb460273de65cd Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 6 Jun 2024 16:28:14 -0400
Subject: [PATCH 51/99] wip: merge cmd package into main package

---
 Dockerfile  |   2 +-
 cmd.go      | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 cmd/api.go  | 165 ---------------------------
 cmd/root.go |  59 ----------
 cmd/web.go  | 183 ------------------------------
 db_test.go  |   9 +-
 main.go     |  14 ++-
 7 files changed, 330 insertions(+), 416 deletions(-)
 create mode 100644 cmd.go
 delete mode 100644 cmd/api.go
 delete mode 100644 cmd/root.go
 delete mode 100644 cmd/web.go

diff --git a/Dockerfile b/Dockerfile
index 7c94ff6..361fe34 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,8 +7,8 @@ WORKDIR /go/src/app
 COPY go.mod go.sum db.go entity.go main.go responses.go ./
 
 ADD cmd/ cmd/
-ADD templates/ templates/
 ADD sql/ sql/
+ADD templates/ templates/
 
 RUN CGO_ENABLED=0 go build -a -o bottin .
 
diff --git a/cmd.go b/cmd.go
new file mode 100644
index 0000000..413e1a2
--- /dev/null
+++ b/cmd.go
@@ -0,0 +1,314 @@
+package main
+
+import (
+	"crypto/subtle"
+	"embed"
+	"fmt"
+	"html/template"
+	"io"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/echo/v4/middleware"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+var (
+	apiPort int
+	apiKey  string
+)
+
+// apiCmd represents the api command
+var apiCmd = &cobra.Command{
+	Use:   "api",
+	Short: "Démarrer le serveur API",
+	Args:  cobra.ExactArgs(0),
+	Run: func(cmd *cobra.Command, args []string) {
+		apiKey = viper.GetString(ViperAPIKey)
+		apiPort = viper.GetInt(ViperAPIPort)
+
+		e := echo.New()
+
+		// Middlewares
+
+		e.Pre(middleware.AddTrailingSlash())
+
+		if apiKey != "" {
+			e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
+				return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil
+			}))
+		}
+
+		// DataClient
+		/*
+			client, err := data.NewDataClientFromViper()
+			if err != nil {
+				log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
+			}
+			defer client.DB.Close()
+
+			err = client.DB.Ping()
+			if err != nil {
+				log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
+			}
+
+			_, err = client.Seed()
+			if err != nil {
+				log.Fatalf("Error during client.Seed(): %s", err)
+			}
+		*/
+
+		// Routes
+		/*
+			h := handlers.New(client)
+
+			e.GET("/v7/health/", h.GetHealth)
+
+			e.POST("/v7/membres/", h.PostMembres)
+
+			e.GET("/v7/membres/", h.ListMembres)
+
+			e.GET("/v7/membres/:membre_id/", h.ReadMembre)
+
+			e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
+
+			e.POST("/v7/programmes/", h.PostProgrammes)
+
+			e.POST("/v7/seed/", h.PostSeed)
+		*/
+
+		// Execution
+
+		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(apiCmd)
+
+	// api.key
+	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
+	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
+		log.Fatal(err)
+	}
+
+	// api.port
+	apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
+	if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.database
+	apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
+	if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.host
+	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
+	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.password
+	apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
+	if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.port
+	apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
+	if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.user
+	apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
+	if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
+		log.Fatal(err)
+	}
+}
+
+var cfgFile string
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "bottin",
+	Short: "Bottin étudiant de l'AGECEM",
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	cobra.OnInitialize(initConfig)
+
+	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
+}
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+	if cfgFile != "" {
+		// Use config file from the flag.
+		viper.SetConfigFile(cfgFile)
+	} else {
+		// Find home directory.
+		home, err := os.UserHomeDir()
+		cobra.CheckErr(err)
+
+		// Search config in home directory with name ".bottin" (without extension).
+		viper.AddConfigPath(home)
+		viper.SetConfigType("yaml")
+		viper.SetConfigName(".bottin")
+	}
+
+	viper.SetEnvPrefix("BOTTIN")
+	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+	viper.AutomaticEnv() // read in environment variables that match
+
+	// If a config file is found, read it in.
+	if err := viper.ReadInConfig(); err == nil {
+		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
+	}
+}
+
+var (
+	webUser        string
+	webPassword    string
+	webPort        int
+	webApiHost     string
+	webApiKey      string
+	webApiPort     int
+	webApiProtocol string
+)
+
+//go:embed templates/*
+var templatesFS embed.FS
+
+type Template struct {
+	templates *template.Template
+}
+
+func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
+	return t.templates.ExecuteTemplate(w, name, data)
+}
+
+// webCmd represents the web command
+var webCmd = &cobra.Command{
+	Use:   "web",
+	Short: "Démarrer le client web",
+	Args:  cobra.ExactArgs(0),
+	Run: func(cmd *cobra.Command, args []string) {
+		webApiHost = viper.GetString(viperWebAPIHost)
+		webApiKey = viper.GetString(viperWebAPIKey)
+		webApiPort = viper.GetInt(viperWebAPIPort)
+		webApiProtocol = viper.GetString(viperWebAPIProtocol)
+		webPassword = viper.GetString(viperWebPassword)
+		webPort = viper.GetInt(viperWebPort)
+		webUser = viper.GetString(viperWebUser)
+
+		// Ping API server
+		/*
+			client := http.DefaultClient
+			defer client.CloseIdleConnections()
+
+			apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
+
+			pingResult, err := apiClient.GetHealth()
+			if err != nil {
+				log.Fatal(err)
+			}
+
+			log.Println(pingResult)
+		*/
+
+		e := echo.New()
+
+		// Middlewares
+
+		e.Pre(middleware.AddTrailingSlash())
+
+		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
+			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1
+			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1
+			return usersMatch && passwordsMatch, nil
+		}))
+
+		// Template
+
+		t := &Template{
+			templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
+		}
+
+		e.Renderer = t
+
+		// Routes
+		/*
+			handler := webhandlers.Handler{APIClient: apiClient}
+
+			e.GET("/", handler.GetIndex)
+			e.GET("/membre/", handler.GetMembre)
+		*/
+
+		// Execution
+
+		e.Logger.Fatal(e.Start(
+			fmt.Sprintf(":%d", webPort)))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(webCmd)
+	//templatesFS = web.GetTemplates()
+
+	// web.api.host
+	webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost)
+	if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.api.key
+	webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey)
+	if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.api.protocol
+	webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol)
+	if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.api.port
+	webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort)
+	if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.password
+	webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword)
+	if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.port
+	webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort)
+	if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.user
+	webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser)
+	if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/cmd/api.go b/cmd/api.go
deleted file mode 100644
index 1e9f6d8..0000000
--- a/cmd/api.go
+++ /dev/null
@@ -1,165 +0,0 @@
-package cmd
-
-import (
-	"crypto/subtle"
-	"fmt"
-	"log"
-
-	"github.com/labstack/echo/v4"
-	"github.com/labstack/echo/v4/middleware"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-var (
-	apiPort int
-	apiKey  string
-)
-
-const (
-	ViperAPIPort       string = "api.port"
-	FlagAPIPort        string = "api-port"
-	DefaultAPIPort     int    = 1312
-	DescriptionAPIPort string = "API server port"
-
-	ViperAPIKey       string = "api.key"
-	FlagAPIKey        string = "api-key"
-	DefaultAPIKey     string = "bottin"
-	DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)"
-
-	ViperDBDatabase       string = "db.database"
-	FlagDBDatabase        string = "db-database"
-	DefaultDBDatabase     string = "bottin"
-	DescriptionDBDatabase string = "Postgres database"
-
-	ViperDBHost       string = "db.host"
-	FlagDBHost        string = "db-host"
-	DefaultDBHost     string = "db"
-	DescriptionDBHost string = "Postgres host"
-
-	ViperDBPassword       string = "db.password"
-	FlagDBPassword        string = "db-password"
-	DefaultDBPassword     string = "bottin"
-	DescriptionDBPassword string = "Postgres password"
-
-	ViperDBPort       string = "db.port"
-	FlagDBPort        string = "db-port"
-	DefaultDBPort     int    = 5432
-	DescriptionDBPort string = "Postgres port"
-
-	ViperDBUser       string = "db.user"
-	FlagDBUser        string = "db-user"
-	DefaultDBUser     string = "bottin"
-	DescriptionDBUser string = "Postgres user"
-)
-
-// apiCmd represents the api command
-var apiCmd = &cobra.Command{
-	Use:   "api",
-	Short: "Démarrer le serveur API",
-	Args:  cobra.ExactArgs(0),
-	Run: func(cmd *cobra.Command, args []string) {
-		apiKey = viper.GetString(ViperAPIKey)
-		apiPort = viper.GetInt(ViperAPIPort)
-
-		e := echo.New()
-
-		// Middlewares
-
-		e.Pre(middleware.AddTrailingSlash())
-
-		if apiKey != "" {
-			e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
-				return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil
-			}))
-		}
-
-		// DataClient
-		/*
-			client, err := data.NewDataClientFromViper()
-			if err != nil {
-				log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
-			}
-			defer client.DB.Close()
-
-			err = client.DB.Ping()
-			if err != nil {
-				log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
-			}
-
-			_, err = client.Seed()
-			if err != nil {
-				log.Fatalf("Error during client.Seed(): %s", err)
-			}
-		*/
-
-		// Routes
-		/*
-			h := handlers.New(client)
-
-			e.GET("/v7/health/", h.GetHealth)
-
-			e.POST("/v7/membres/", h.PostMembres)
-
-			e.GET("/v7/membres/", h.ListMembres)
-
-			e.GET("/v7/membres/:membre_id/", h.ReadMembre)
-
-			e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
-
-			e.POST("/v7/programmes/", h.PostProgrammes)
-
-			e.POST("/v7/seed/", h.PostSeed)
-		*/
-
-		// Execution
-
-		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(apiCmd)
-
-	// api.key
-	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
-	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
-		log.Fatal(err)
-	}
-
-	// api.port
-	apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
-	if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.database
-	apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
-	if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.host
-	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
-	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.password
-	apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
-	if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.port
-	apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
-	if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.user
-	apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
-	if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
-		log.Fatal(err)
-	}
-}
diff --git a/cmd/root.go b/cmd/root.go
deleted file mode 100644
index 26e0636..0000000
--- a/cmd/root.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package cmd
-
-import (
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-var cfgFile string
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "bottin",
-	Short: "Bottin étudiant de l'AGECEM",
-}
-
-// Execute adds all child commands to the root command and sets flags appropriately.
-// This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute() {
-	err := rootCmd.Execute()
-	if err != nil {
-		os.Exit(1)
-	}
-}
-
-func init() {
-	cobra.OnInitialize(initConfig)
-
-	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
-}
-
-// initConfig reads in config file and ENV variables if set.
-func initConfig() {
-	if cfgFile != "" {
-		// Use config file from the flag.
-		viper.SetConfigFile(cfgFile)
-	} else {
-		// Find home directory.
-		home, err := os.UserHomeDir()
-		cobra.CheckErr(err)
-
-		// Search config in home directory with name ".bottin" (without extension).
-		viper.AddConfigPath(home)
-		viper.SetConfigType("yaml")
-		viper.SetConfigName(".bottin")
-	}
-
-	viper.SetEnvPrefix("BOTTIN")
-	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
-	viper.AutomaticEnv() // read in environment variables that match
-
-	// If a config file is found, read it in.
-	if err := viper.ReadInConfig(); err == nil {
-		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
-	}
-}
diff --git a/cmd/web.go b/cmd/web.go
deleted file mode 100644
index 2ce2fad..0000000
--- a/cmd/web.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package cmd
-
-import (
-	"crypto/subtle"
-	"embed"
-	"fmt"
-	"html/template"
-	"io"
-	"log"
-
-	"github.com/labstack/echo/v4"
-	"github.com/labstack/echo/v4/middleware"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-var (
-	webUser        string
-	webPassword    string
-	webPort        int
-	webApiHost     string
-	webApiKey      string
-	webApiPort     int
-	webApiProtocol string
-)
-
-const (
-	viperWebUser       string = "web.user"
-	flagWebUser        string = "web-user"
-	defaultWebUser     string = "bottin"
-	descriptionWebUser string = "Web client basic auth user"
-
-	viperWebPassword       string = "web.password"
-	flagWebPassword        string = "web-password"
-	defaultWebPassword     string = "bottin"
-	descriptionWebPassword string = "Web client basic auth password"
-
-	viperWebPort       string = "web.port"
-	flagWebPort        string = "web-port"
-	defaultWebPort     int    = 2312
-	descriptionWebPort string = "Web client port"
-
-	viperWebAPIHost       string = "api.host"
-	flagWebAPIHost        string = "api-host"
-	defaultWebAPIHost     string = "api"
-	descriptionWebAPIHost string = "Target API server host"
-
-	viperWebAPIKey       string = "api.key"
-	flagWebAPIKey        string = "api-key"
-	defaultWebAPIKey     string = "bottin"
-	descriptionWebAPIKey string = "Target API server key"
-
-	viperWebAPIPort       string = "api.port"
-	flagWebAPIPort        string = "api-port"
-	defaultWebAPIPort     int    = 1312
-	descriptionWebAPIPort string = "Target API server port"
-
-	viperWebAPIProtocol       string = "api.protocol"
-	flagWebAPIProtocol        string = "api-protocol"
-	defaultWebAPIProtocol     string = "http"
-	descriptionWebAPIProtocol string = "Target API server protocol (http/https)"
-)
-
-var templatesFS embed.FS
-
-type Template struct {
-	templates *template.Template
-}
-
-func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
-	return t.templates.ExecuteTemplate(w, name, data)
-}
-
-// webCmd represents the web command
-var webCmd = &cobra.Command{
-	Use:   "web",
-	Short: "Démarrer le client web",
-	Args:  cobra.ExactArgs(0),
-	Run: func(cmd *cobra.Command, args []string) {
-		webApiHost = viper.GetString(viperWebAPIHost)
-		webApiKey = viper.GetString(viperWebAPIKey)
-		webApiPort = viper.GetInt(viperWebAPIPort)
-		webApiProtocol = viper.GetString(viperWebAPIProtocol)
-		webPassword = viper.GetString(viperWebPassword)
-		webPort = viper.GetInt(viperWebPort)
-		webUser = viper.GetString(viperWebUser)
-
-		// Ping API server
-		/*
-			client := http.DefaultClient
-			defer client.CloseIdleConnections()
-
-			apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
-
-			pingResult, err := apiClient.GetHealth()
-			if err != nil {
-				log.Fatal(err)
-			}
-
-			log.Println(pingResult)
-		*/
-
-		e := echo.New()
-
-		// Middlewares
-
-		e.Pre(middleware.AddTrailingSlash())
-
-		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
-			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1
-			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1
-			return usersMatch && passwordsMatch, nil
-		}))
-
-		// Template
-
-		t := &Template{
-			templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
-		}
-
-		e.Renderer = t
-
-		// Routes
-		/*
-			handler := webhandlers.Handler{APIClient: apiClient}
-
-			e.GET("/", handler.GetIndex)
-			e.GET("/membre/", handler.GetMembre)
-		*/
-
-		// Execution
-
-		e.Logger.Fatal(e.Start(
-			fmt.Sprintf(":%d", webPort)))
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(webCmd)
-	//templatesFS = web.GetTemplates()
-
-	// web.api.host
-	webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost)
-	if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.key
-	webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey)
-	if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.protocol
-	webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol)
-	if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.port
-	webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort)
-	if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.password
-	webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword)
-	if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.port
-	webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort)
-	if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.user
-	webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser)
-	if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil {
-		log.Fatal(err)
-	}
-}
diff --git a/db_test.go b/db_test.go
index d9d235a..0f508a5 100644
--- a/db_test.go
+++ b/db_test.go
@@ -8,6 +8,9 @@ import (
 	"github.com/jackc/pgx/v5/pgxpool"
 )
 
+// path to a file containing the db password
+var passfile string
+
 func TestDB(t *testing.T) {
 	ctx := context.Background()
 
@@ -17,11 +20,11 @@ func TestDB(t *testing.T) {
 		fmt.Sprintf(
 			"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
 			"bottin",
+			dbPassword,
 			"bottin",
-			"bottin",
-			"localhost",
+			"postgres.agecem.com",
 			5432,
-			"prefer", //TODO change to "require"
+			"require", //TODO change to "require"
 		))
 	if err != nil {
 		t.Error(err)
diff --git a/main.go b/main.go
index cb9123b..58be335 100644
--- a/main.go
+++ b/main.go
@@ -4,14 +4,18 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"log"
-	"os"
 )
 
 func main() {
-	if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
-		log.Fatal(err)
-	}
+	//TODO
+	/*
+		if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
+			log.Fatal(err)
+		}
+	*/
+
+	// Handle the command-line
+	Execute()
 }
 
 func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {

From 0123d9d37c8ceb356fbe059241b53d2812248203 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 6 Jun 2024 17:01:16 -0400
Subject: [PATCH 52/99] wip: integration between cmd.go and config.go

---
 cmd.go     | 48 +++++++++++++++++--------------
 config.go  | 84 ++++++++++++++++++++++++++++++++++++------------------
 db_test.go | 19 ++++++------
 3 files changed, 93 insertions(+), 58 deletions(-)

diff --git a/cmd.go b/cmd.go
index 413e1a2..8800c3d 100644
--- a/cmd.go
+++ b/cmd.go
@@ -107,6 +107,12 @@ func init() {
 		log.Fatal(err)
 	}
 
+	// db.sslmode
+	apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode)
+	if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil {
+		log.Fatal(err)
+	}
+
 	// db.host
 	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
 	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
@@ -208,13 +214,13 @@ var webCmd = &cobra.Command{
 	Short: "Démarrer le client web",
 	Args:  cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
-		webApiHost = viper.GetString(viperWebAPIHost)
-		webApiKey = viper.GetString(viperWebAPIKey)
-		webApiPort = viper.GetInt(viperWebAPIPort)
-		webApiProtocol = viper.GetString(viperWebAPIProtocol)
-		webPassword = viper.GetString(viperWebPassword)
-		webPort = viper.GetInt(viperWebPort)
-		webUser = viper.GetString(viperWebUser)
+		webApiHost = viper.GetString(ViperWebAPIHost)
+		webApiKey = viper.GetString(ViperWebAPIKey)
+		webApiPort = viper.GetInt(ViperWebAPIPort)
+		webApiProtocol = viper.GetString(ViperWebAPIProtocol)
+		webPassword = viper.GetString(ViperWebPassword)
+		webPort = viper.GetInt(ViperWebPort)
+		webUser = viper.GetString(ViperWebUser)
 
 		// Ping API server
 		/*
@@ -271,44 +277,44 @@ func init() {
 	//templatesFS = web.GetTemplates()
 
 	// web.api.host
-	webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost)
-	if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil {
+	webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost)
+	if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil {
 		log.Fatal(err)
 	}
 
 	// web.api.key
-	webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey)
-	if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil {
+	webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey)
+	if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil {
 		log.Fatal(err)
 	}
 
 	// web.api.protocol
-	webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol)
-	if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil {
+	webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol)
+	if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil {
 		log.Fatal(err)
 	}
 
 	// web.api.port
-	webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort)
-	if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil {
+	webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort)
+	if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil {
 		log.Fatal(err)
 	}
 
 	// web.password
-	webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword)
-	if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil {
+	webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword)
+	if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil {
 		log.Fatal(err)
 	}
 
 	// web.port
-	webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort)
-	if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil {
+	webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort)
+	if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil {
 		log.Fatal(err)
 	}
 
 	// web.user
-	webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser)
-	if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil {
+	webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser)
+	if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/config.go b/config.go
index c0108b4..73d6007 100644
--- a/config.go
+++ b/config.go
@@ -1,5 +1,7 @@
 package main
 
+//TODO move flag declarations here
+
 const (
 	ViperAPIPort       string = "api.port"
 	FlagAPIPort        string = "api-port"
@@ -16,6 +18,11 @@ const (
 	DefaultDBDatabase     string = "bottin"
 	DescriptionDBDatabase string = "Postgres database"
 
+	ViperDBSSLMode       string = "db.sslmode"
+	FlagDBSSLMode        string = "db-sslmode"
+	DefaultDBSSLMode     string = "prefer"
+	DescriptionDBSSLMode string = "Postgres sslmode"
+
 	ViperDBHost       string = "db.host"
 	FlagDBHost        string = "db-host"
 	DefaultDBHost     string = "db"
@@ -36,40 +43,40 @@ const (
 	DefaultDBUser     string = "bottin"
 	DescriptionDBUser string = "Postgres user"
 
-	viperWebUser       string = "web.user"
-	flagWebUser        string = "web-user"
-	defaultWebUser     string = "bottin"
-	descriptionWebUser string = "Web client basic auth user"
+	ViperWebUser       string = "web.user"
+	FlagWebUser        string = "web-user"
+	DefaultWebUser     string = "bottin"
+	DescriptionWebUser string = "Web client basic auth user"
 
-	viperWebPassword       string = "web.password"
-	flagWebPassword        string = "web-password"
-	defaultWebPassword     string = "bottin"
-	descriptionWebPassword string = "Web client basic auth password"
+	ViperWebPassword       string = "web.password"
+	FlagWebPassword        string = "web-password"
+	DefaultWebPassword     string = "bottin"
+	DescriptionWebPassword string = "Web client basic auth password"
 
-	viperWebPort       string = "web.port"
-	flagWebPort        string = "web-port"
-	defaultWebPort     int    = 2312
-	descriptionWebPort string = "Web client port"
+	ViperWebPort       string = "web.port"
+	FlagWebPort        string = "web-port"
+	DefaultWebPort     int    = 2312
+	DescriptionWebPort string = "Web client port"
 
-	viperWebAPIHost       string = "api.host"
-	flagWebAPIHost        string = "api-host"
-	defaultWebAPIHost     string = "api"
-	descriptionWebAPIHost string = "Target API server host"
+	ViperWebAPIHost       string = "api.host"
+	FlagWebAPIHost        string = "api-host"
+	DefaultWebAPIHost     string = "api"
+	DescriptionWebAPIHost string = "Target API server host"
 
-	viperWebAPIKey       string = "api.key"
-	flagWebAPIKey        string = "api-key"
-	defaultWebAPIKey     string = "bottin"
-	descriptionWebAPIKey string = "Target API server key"
+	ViperWebAPIKey       string = "api.key"
+	FlagWebAPIKey        string = "api-key"
+	DefaultWebAPIKey     string = "bottin"
+	DescriptionWebAPIKey string = "Target API server key"
 
-	viperWebAPIPort       string = "api.port"
-	flagWebAPIPort        string = "api-port"
-	defaultWebAPIPort     int    = 1312
-	descriptionWebAPIPort string = "Target API server port"
+	ViperWebAPIPort       string = "api.port"
+	FlagWebAPIPort        string = "api-port"
+	DefaultWebAPIPort     int    = 1312
+	DescriptionWebAPIPort string = "Target API server port"
 
-	viperWebAPIProtocol       string = "api.protocol"
-	flagWebAPIProtocol        string = "api-protocol"
-	defaultWebAPIProtocol     string = "http"
-	descriptionWebAPIProtocol string = "Target API server protocol (http/https)"
+	ViperWebAPIProtocol       string = "api.protocol"
+	FlagWebAPIProtocol        string = "api-protocol"
+	DefaultWebAPIProtocol     string = "http"
+	DescriptionWebAPIProtocol string = "Target API server protocol (http/https)"
 )
 
 type Config struct {
@@ -97,3 +104,24 @@ type Config struct {
 		} `yaml:"api"`
 	} `yaml:"web"`
 }
+
+// DefaultConfig returns a Config filled with the default values from the
+// `Default*` constants defined in this file.
+func DefaultConfig() (cfg Config) {
+	cfg.API.Port = DefaultAPIPort
+	cfg.API.Key = DefaultAPIKey
+	cfg.DB.Database = DefaultDBDatabase
+	cfg.DB.Host = DefaultDBHost
+	cfg.DB.SSLMode = DefaultDBSSLMode
+	cfg.DB.Password = DefaultDBPassword
+	cfg.DB.Port = DefaultDBPort
+	cfg.DB.User = DefaultDBUser
+	cfg.Web.User = DefaultWebUser
+	cfg.Web.Password = DefaultWebPassword
+	cfg.Web.Port = DefaultWebPort
+	cfg.Web.API.Host = DefaultWebAPIHost
+	cfg.Web.API.Key = DefaultWebAPIKey
+	cfg.Web.API.Port = DefaultWebAPIPort
+	cfg.Web.API.Protocol = DefaultWebAPIProtocol
+	return
+}
diff --git a/db_test.go b/db_test.go
index 0f508a5..4c3881a 100644
--- a/db_test.go
+++ b/db_test.go
@@ -6,12 +6,13 @@ import (
 	"testing"
 
 	"github.com/jackc/pgx/v5/pgxpool"
+	"github.com/spf13/viper"
 )
 
-// path to a file containing the db password
-var passfile string
-
 func TestDB(t *testing.T) {
+	cfg := DefaultConfig()
+	cfg.DB.Password = viper.GetString(ViperDBPassword)
+
 	ctx := context.Background()
 
 	//prep
@@ -19,12 +20,12 @@ func TestDB(t *testing.T) {
 		ctx,
 		fmt.Sprintf(
 			"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
-			"bottin",
-			dbPassword,
-			"bottin",
-			"postgres.agecem.com",
-			5432,
-			"require", //TODO change to "require"
+			cfg.DB.User,
+			cfg.DB.Password,
+			cfg.DB.Database,
+			cfg.DB.Host,
+			cfg.DB.Port,
+			cfg.DB.SSLMode,
 		))
 	if err != nil {
 		t.Error(err)

From cdd526a6f3e17cbd1fd366ce9cb95ecfd40202ec Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 6 Jun 2024 17:59:58 -0400
Subject: [PATCH 53/99] wip: make apiCmd run and remove db test

---
 client.go      |  1 +
 client_test.go |  1 +
 cmd.go         | 90 ++++++++++++++++++++++++--------------------------
 db_test.go     | 49 ---------------------------
 4 files changed, 46 insertions(+), 95 deletions(-)
 create mode 100644 client.go
 create mode 100644 client_test.go
 delete mode 100644 db_test.go

diff --git a/client.go b/client.go
new file mode 100644
index 0000000..06ab7d0
--- /dev/null
+++ b/client.go
@@ -0,0 +1 @@
+package main
diff --git a/client_test.go b/client_test.go
new file mode 100644
index 0000000..0fee6f5
--- /dev/null
+++ b/client_test.go
@@ -0,0 +1 @@
+package main_test
diff --git a/cmd.go b/cmd.go
index 8800c3d..3e37028 100644
--- a/cmd.go
+++ b/cmd.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"context"
 	"crypto/subtle"
 	"embed"
 	"fmt"
@@ -10,25 +11,23 @@ import (
 	"os"
 	"strings"
 
+	"github.com/jackc/pgx/v5/pgxpool"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 )
 
-var (
-	apiPort int
-	apiKey  string
-)
-
 // apiCmd represents the api command
 var apiCmd = &cobra.Command{
 	Use:   "api",
 	Short: "Démarrer le serveur API",
 	Args:  cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
-		apiKey = viper.GetString(ViperAPIKey)
-		apiPort = viper.GetInt(ViperAPIPort)
+		var cfg Config
+		if err := viper.Unmarshal(&cfg); err != nil {
+			log.Fatal("parse config:", err)
+		}
 
 		e := echo.New()
 
@@ -36,30 +35,43 @@ var apiCmd = &cobra.Command{
 
 		e.Pre(middleware.AddTrailingSlash())
 
-		if apiKey != "" {
+		if cfg.API.Key != "" {
 			e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
-				return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil
+				return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.API.Key)) == 1, nil
 			}))
 		}
 
 		// DataClient
-		/*
-			client, err := data.NewDataClientFromViper()
-			if err != nil {
-				log.Fatalf("Could not establish database connection.\n	Error: %s\n", err)
-			}
-			defer client.DB.Close()
+		ctx := context.Background()
 
-			err = client.DB.Ping()
-			if err != nil {
-				log.Fatalf("Database was supposed to be ready but Ping() failed.\n  Error: %s\n", err)
-			}
+		//prep
+		pool, err := pgxpool.New(
+			ctx,
+			fmt.Sprintf(
+				"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
+				cfg.DB.User,
+				cfg.DB.Password,
+				cfg.DB.Database,
+				cfg.DB.Host,
+				cfg.DB.Port,
+				cfg.DB.SSLMode,
+			))
+		if err != nil {
+			log.Fatal("init pgx pool:", err)
+		}
+		defer pool.Close()
 
-			_, err = client.Seed()
-			if err != nil {
-				log.Fatalf("Error during client.Seed(): %s", err)
-			}
-		*/
+		db := &PostgresClient{
+			Ctx:  ctx,
+			Pool: pool,
+		}
+		if err := db.Pool.Ping(ctx); err != nil {
+			log.Fatal("ping db:", err)
+		}
+
+		if err := db.CreateOrReplaceSchema(); err != nil {
+			log.Fatal("create or replace schema:", err)
+		}
 
 		// Routes
 		/*
@@ -82,7 +94,7 @@ var apiCmd = &cobra.Command{
 
 		// Execution
 
-		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
+		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port)))
 	},
 }
 
@@ -187,16 +199,6 @@ func initConfig() {
 	}
 }
 
-var (
-	webUser        string
-	webPassword    string
-	webPort        int
-	webApiHost     string
-	webApiKey      string
-	webApiPort     int
-	webApiProtocol string
-)
-
 //go:embed templates/*
 var templatesFS embed.FS
 
@@ -214,13 +216,10 @@ var webCmd = &cobra.Command{
 	Short: "Démarrer le client web",
 	Args:  cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
-		webApiHost = viper.GetString(ViperWebAPIHost)
-		webApiKey = viper.GetString(ViperWebAPIKey)
-		webApiPort = viper.GetInt(ViperWebAPIPort)
-		webApiProtocol = viper.GetString(ViperWebAPIProtocol)
-		webPassword = viper.GetString(ViperWebPassword)
-		webPort = viper.GetInt(ViperWebPort)
-		webUser = viper.GetString(ViperWebUser)
+		var cfg Config
+		if err := viper.Unmarshal(&cfg); err != nil {
+			log.Fatal("init config:", err)
+		}
 
 		// Ping API server
 		/*
@@ -244,8 +243,8 @@ var webCmd = &cobra.Command{
 		e.Pre(middleware.AddTrailingSlash())
 
 		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
-			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1
-			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1
+			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Web.User)) == 1
+			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1
 			return usersMatch && passwordsMatch, nil
 		}))
 
@@ -268,13 +267,12 @@ var webCmd = &cobra.Command{
 		// Execution
 
 		e.Logger.Fatal(e.Start(
-			fmt.Sprintf(":%d", webPort)))
+			fmt.Sprintf(":%d", cfg.Web.Port)))
 	},
 }
 
 func init() {
 	rootCmd.AddCommand(webCmd)
-	//templatesFS = web.GetTemplates()
 
 	// web.api.host
 	webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost)
diff --git a/db_test.go b/db_test.go
deleted file mode 100644
index 4c3881a..0000000
--- a/db_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"testing"
-
-	"github.com/jackc/pgx/v5/pgxpool"
-	"github.com/spf13/viper"
-)
-
-func TestDB(t *testing.T) {
-	cfg := DefaultConfig()
-	cfg.DB.Password = viper.GetString(ViperDBPassword)
-
-	ctx := context.Background()
-
-	//prep
-	pool, err := pgxpool.New(
-		ctx,
-		fmt.Sprintf(
-			"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
-			cfg.DB.User,
-			cfg.DB.Password,
-			cfg.DB.Database,
-			cfg.DB.Host,
-			cfg.DB.Port,
-			cfg.DB.SSLMode,
-		))
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	defer pool.Close()
-
-	db := &PostgresClient{
-		Ctx:  ctx,
-		Pool: pool,
-	}
-
-	//exec
-
-	t.Run("create or replace schema",
-		func(t *testing.T) {
-			if err := db.CreateOrReplaceSchema(); err != nil {
-				t.Error(err)
-			}
-		})
-}

From 780d493dc1e5ac431239771a7b7f92f9e115cfa7 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 6 Jun 2024 18:07:30 -0400
Subject: [PATCH 54/99] split cmd
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

cmd.go contient maintenant juste les actual commandes.
Les fonctionalités liées à la configuration sont dans config.go, et les
fonctionalités liées au templating est dans template.go.
---
 cmd.go      | 176 +++++-----------------------------------------------
 config.go   | 143 +++++++++++++++++++++++++++++++++++++++++-
 main.go     |  21 +++----
 template.go |  20 ++++++
 4 files changed, 185 insertions(+), 175 deletions(-)
 create mode 100644 template.go

diff --git a/cmd.go b/cmd.go
index 3e37028..859f49a 100644
--- a/cmd.go
+++ b/cmd.go
@@ -3,13 +3,10 @@ package main
 import (
 	"context"
 	"crypto/subtle"
-	"embed"
 	"fmt"
 	"html/template"
-	"io"
 	"log"
 	"os"
-	"strings"
 
 	"github.com/jackc/pgx/v5/pgxpool"
 	"github.com/labstack/echo/v4"
@@ -18,6 +15,21 @@ import (
 	"github.com/spf13/viper"
 )
 
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "bottin",
+	Short: "Bottin étudiant de l'AGECEM",
+}
+
+// execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
 // apiCmd represents the api command
 var apiCmd = &cobra.Command{
 	Use:   "api",
@@ -98,118 +110,6 @@ var apiCmd = &cobra.Command{
 	},
 }
 
-func init() {
-	rootCmd.AddCommand(apiCmd)
-
-	// api.key
-	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
-	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
-		log.Fatal(err)
-	}
-
-	// api.port
-	apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
-	if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.database
-	apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
-	if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.sslmode
-	apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode)
-	if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.host
-	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
-	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.password
-	apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
-	if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.port
-	apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
-	if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.user
-	apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
-	if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
-		log.Fatal(err)
-	}
-}
-
-var cfgFile string
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "bottin",
-	Short: "Bottin étudiant de l'AGECEM",
-}
-
-// Execute adds all child commands to the root command and sets flags appropriately.
-// This is called by main.main(). It only needs to happen once to the rootCmd.
-func Execute() {
-	err := rootCmd.Execute()
-	if err != nil {
-		os.Exit(1)
-	}
-}
-
-func init() {
-	cobra.OnInitialize(initConfig)
-
-	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
-}
-
-// initConfig reads in config file and ENV variables if set.
-func initConfig() {
-	if cfgFile != "" {
-		// Use config file from the flag.
-		viper.SetConfigFile(cfgFile)
-	} else {
-		// Find home directory.
-		home, err := os.UserHomeDir()
-		cobra.CheckErr(err)
-
-		// Search config in home directory with name ".bottin" (without extension).
-		viper.AddConfigPath(home)
-		viper.SetConfigType("yaml")
-		viper.SetConfigName(".bottin")
-	}
-
-	viper.SetEnvPrefix("BOTTIN")
-	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
-	viper.AutomaticEnv() // read in environment variables that match
-
-	// If a config file is found, read it in.
-	if err := viper.ReadInConfig(); err == nil {
-		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
-	}
-}
-
-//go:embed templates/*
-var templatesFS embed.FS
-
-type Template struct {
-	templates *template.Template
-}
-
-func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
-	return t.templates.ExecuteTemplate(w, name, data)
-}
-
 // webCmd represents the web command
 var webCmd = &cobra.Command{
 	Use:   "web",
@@ -270,49 +170,3 @@ var webCmd = &cobra.Command{
 			fmt.Sprintf(":%d", cfg.Web.Port)))
 	},
 }
-
-func init() {
-	rootCmd.AddCommand(webCmd)
-
-	// web.api.host
-	webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost)
-	if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.key
-	webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey)
-	if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.protocol
-	webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol)
-	if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.port
-	webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort)
-	if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.password
-	webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword)
-	if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.port
-	webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort)
-	if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.user
-	webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser)
-	if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil {
-		log.Fatal(err)
-	}
-}
diff --git a/config.go b/config.go
index 73d6007..d50e241 100644
--- a/config.go
+++ b/config.go
@@ -1,6 +1,14 @@
 package main
 
-//TODO move flag declarations here
+import (
+	"fmt"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
 
 const (
 	ViperAPIPort       string = "api.port"
@@ -125,3 +133,136 @@ func DefaultConfig() (cfg Config) {
 	cfg.Web.API.Protocol = DefaultWebAPIProtocol
 	return
 }
+
+func init() {
+	// rootCmd
+
+	cobra.OnInitialize(initConfig)
+
+	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
+
+	// apiCmd
+
+	rootCmd.AddCommand(apiCmd)
+
+	// api.key
+	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
+	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
+		log.Fatal(err)
+	}
+
+	// api.port
+	apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
+	if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.database
+	apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
+	if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.sslmode
+	apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode)
+	if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.host
+	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
+	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.password
+	apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
+	if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.port
+	apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
+	if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// db.user
+	apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
+	if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
+		log.Fatal(err)
+	}
+
+	// WebCmd
+	rootCmd.AddCommand(webCmd)
+
+	// web.api.host
+	webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost)
+	if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.api.key
+	webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey)
+	if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.api.protocol
+	webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol)
+	if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.api.port
+	webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort)
+	if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.password
+	webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword)
+	if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.port
+	webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort)
+	if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil {
+		log.Fatal(err)
+	}
+
+	// web.user
+	webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser)
+	if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil {
+		log.Fatal(err)
+	}
+}
+
+var cfgFile string
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+	if cfgFile != "" {
+		// Use config file from the flag.
+		viper.SetConfigFile(cfgFile)
+	} else {
+		// Find home directory.
+		home, err := os.UserHomeDir()
+		cobra.CheckErr(err)
+
+		// Search config in home directory with name ".bottin" (without extension).
+		viper.AddConfigPath(home)
+		viper.SetConfigType("yaml")
+		viper.SetConfigName(".bottin")
+	}
+
+	viper.SetEnvPrefix("BOTTIN")
+	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+	viper.AutomaticEnv() // read in environment variables that match
+
+	// If a config file is found, read it in.
+	if err := viper.ReadInConfig(); err == nil {
+		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
+	}
+}
diff --git a/main.go b/main.go
index 58be335..6f81738 100644
--- a/main.go
+++ b/main.go
@@ -1,23 +1,18 @@
 package main
 
-import (
-	"context"
-	"fmt"
-	"io"
-)
-
 func main() {
-	//TODO
-	/*
-		if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
-			log.Fatal(err)
-		}
+	/* TODO
+	if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
+		log.Fatal(err)
+	}
 	*/
 
-	// Handle the command-line
-	Execute()
+	// Handle the command-line via cobra and viper
+	execute()
 }
 
+/* TODO
 func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {
 	return fmt.Errorf("not implemented")
 }
+*/
diff --git a/template.go b/template.go
new file mode 100644
index 0000000..a5b4980
--- /dev/null
+++ b/template.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"embed"
+	"html/template"
+	"io"
+
+	"github.com/labstack/echo/v4"
+)
+
+//go:embed templates/*
+var templatesFS embed.FS
+
+type Template struct {
+	templates *template.Template
+}
+
+func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
+	return t.templates.ExecuteTemplate(w, name, data)
+}

From 11251042808e44bfa463e8beb893234edd31db0f Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 7 Jun 2024 14:59:49 -0400
Subject: [PATCH 55/99] chores: `go get -u`

---
 go.mod | 24 ++++++++++++------------
 go.sum | 26 ++++++++++++++++++++++++++
 2 files changed, 38 insertions(+), 12 deletions(-)

diff --git a/go.mod b/go.mod
index 1952064..a7833f1 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,11 @@ module git.agecem.com/agecem/bottin/v7
 go 1.22.0
 
 require (
-	codeberg.org/vlbeaudoin/voki/v2 v2.0.3
+	codeberg.org/vlbeaudoin/voki/v2 v2.1.0
 	github.com/jackc/pgx/v5 v5.6.0
-	github.com/labstack/echo/v4 v4.11.4
+	github.com/labstack/echo/v4 v4.12.0
 	github.com/spf13/cobra v1.8.0
-	github.com/spf13/viper v1.18.2
+	github.com/spf13/viper v1.19.0
 )
 
 require (
@@ -16,15 +16,15 @@ require (
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
-	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
 	github.com/jackc/puddle/v2 v2.2.1 // indirect
 	github.com/labstack/gommon v0.4.2 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
-	github.com/sagikazarmark/locafero v0.4.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/sagikazarmark/locafero v0.6.0 // indirect
 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.11.0 // indirect
@@ -34,12 +34,12 @@ require (
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/crypto v0.19.0 // indirect
-	golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
-	golang.org/x/net v0.21.0 // indirect
-	golang.org/x/sync v0.5.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/crypto v0.24.0 // indirect
+	golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
+	golang.org/x/net v0.26.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sys v0.21.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 5c5e2b0..f022429 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E=
 codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U=
+codeberg.org/vlbeaudoin/voki/v2 v2.1.0 h1:pXav77QGMHvMF1RyvkEwK3VKBdQh3ATmgh48TXX0tlU=
+codeberg.org/vlbeaudoin/voki/v2 v2.1.0/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U=
 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -21,6 +23,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
 github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
 github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
@@ -31,6 +35,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
 github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
+github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
+github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
 github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -44,6 +50,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
 github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -52,6 +60,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
 github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
+github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
 github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -66,15 +76,19 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
 github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
+github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
+github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -85,18 +99,30 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
+golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
+golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
 golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

From 1b04237c96f29cd9373631b07ec79e49e9b9edb4 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 7 Jun 2024 15:18:22 -0400
Subject: [PATCH 56/99] =?UTF-8?q?ajouter=20fichiers=20manquants=20=C3=A0?=
 =?UTF-8?q?=20Dockerfile=20build=20step?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Dockerfile | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 361fe34..7c0ad26 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,9 +4,8 @@ LABEL author="vlbeaudoin"
 
 WORKDIR /go/src/app
 
-COPY go.mod go.sum db.go entity.go main.go responses.go ./
+COPY go.mod go.sum cmd.go config.go db.go entity.go main.go responses.go template.go ./
 
-ADD cmd/ cmd/
 ADD sql/ sql/
 ADD templates/ templates/
 

From eca5ffa7fb061af5e320f397fc1cc1cfb71fd743 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 10 Jun 2024 17:25:01 -0400
Subject: [PATCH 57/99] feature(db): Ajouter InsertMembres, InsertProgrammes et
 GetMembres

---
 db.go | 244 +++++++++++++++++++++++++++++-----------------------------
 1 file changed, 120 insertions(+), 124 deletions(-)

diff --git a/db.go b/db.go
index fff8885..ff715d8 100644
--- a/db.go
+++ b/db.go
@@ -3,6 +3,7 @@ package main
 import (
 	"context"
 	_ "embed"
+	"fmt"
 
 	"github.com/jackc/pgx/v5/pgxpool"
 )
@@ -11,6 +12,7 @@ import (
 var sqlSchema string
 
 type PostgresClient struct {
+	//TODO move context out of client
 	Ctx  context.Context
 	Pool *pgxpool.Pool
 }
@@ -20,142 +22,92 @@ func (db *PostgresClient) CreateOrReplaceSchema() error {
 	return err
 }
 
-/*
-// DataClient is a postgres client based on sqlx
-type DataClient struct {
-	PostgresConnection PostgresConnection
-	DB                 sqlx.DB
-}
-
-type PostgresConnection struct {
-	User     string
-	Password string
-	Database string
-	Host     string
-	Port     int
-	SSL      bool
-}
-
-func NewDataClientFromViper() (*DataClient, error) {
-	client, err := NewDataClient(
-		PostgresConnection{
-			User:     viper.GetString(cmd.ViperDBHost),
-			Password: viper.GetString(cmd.ViperDBPassword),
-			Host:     viper.GetString(cmd.ViperDBHost),
-			Database: viper.GetString(cmd.ViperDBDatabase),
-			Port:     viper.GetInt(cmd.ViperDBPort),
-		})
-
-	return client, err
-}
-
-func NewDataClient(connection PostgresConnection) (*DataClient, error) {
-	client := &DataClient{PostgresConnection: connection}
-
-	connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
-		client.PostgresConnection.User,
-		client.PostgresConnection.Password,
-		client.PostgresConnection.Host,
-		client.PostgresConnection.Port,
-		client.PostgresConnection.Database,
-	)
-
-	db, err := sqlx.Connect("pgx", connectionString)
-	if err != nil {
-		return nil, err
-	}
-
-	client.DB = *db
-
-	return client, nil
-}
-
-func (d *DataClient) Seed() (int64, error) {
-	result, err := d.DB.Exec(sqlSchema)
-	if err != nil {
-		return 0, err
-	}
-
-	rows, err := result.RowsAffected()
-	if err != nil {
-		return rows, err
-	}
-
-	return rows, nil
-}
-
 // InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
-func (d *DataClient) InsertMembres(membres []Membre) (int64, error) {
-	var rowsInserted int64
-	tx, err := d.DB.Beginx()
-	if err != nil {
-		return rowsInserted, err
-	}
-	defer tx.Rollback()
-
-	for _, membre := range membres {
-		if membre.ID == "" {
-			return 0, errors.New("Cannot insert membre with no membre_id")
-		}
-		result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id) ON CONFLICT (id) DO NOTHING;", &membre)
+func (d *PostgresClient) InsertMembres(membres []Membre) (inserted int64, err error) {
+	select {
+	case <-d.Ctx.Done():
+		return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+	default:
+		tx, err := d.Pool.Begin(d.Ctx)
 		if err != nil {
+			return inserted, err
+		}
+		defer tx.Rollback(d.Ctx)
+
+		for i, membre := range membres {
+			if membre.ID == "" {
+				return inserted, fmt.Errorf("insertion ligne %d: membre requiert numéro étudiant valide", i)
+			}
+
+			result, err := tx.Exec(d.Ctx, `
+INSERT INTO membres
+  (id, last_name, first_name, prefered_name, programme_id)
+VALUES
+  ($1, $2, $3, $4, $5)
+ON CONFLICT (id) DO NOTHING;`,
+				membre.ID,
+				membre.LastName,
+				membre.FirstName,
+				membre.PreferedName,
+				membre.ProgrammeID,
+			)
+			if err != nil {
+				return 0, err
+			}
+
+			inserted += result.RowsAffected()
+		}
+
+		if err = tx.Commit(d.Ctx); err != nil {
 			return 0, err
 		}
 
-		rows, err := result.RowsAffected()
-		if err != nil {
-			return 0, err
-		}
-
-		rowsInserted += rows
+		return inserted, err
 	}
-
-	err = tx.Commit()
-	if err != nil {
-		return rowsInserted, err
-	}
-
-	return rowsInserted, nil
 }
 
-func (d *DataClient) InsertProgrammes(programmes []Programme) (int64, error) {
-	var rowsInserted int64
-	tx, err := d.DB.Beginx()
-	if err != nil {
-		return rowsInserted, err
-	}
-	defer tx.Rollback()
-
-	for _, programme := range programmes {
-		if programme.ID == "" {
-			return 0, errors.New("Cannot insert programme with no programme_id")
-		}
-
-		result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre) ON CONFLICT DO NOTHING;", &programme)
+func (d *PostgresClient) InsertProgrammes(programmes []Programme) (inserted int64, err error) {
+	select {
+	case <-d.Ctx.Done():
+		return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+	default:
+		tx, err := d.Pool.Begin(d.Ctx)
 		if err != nil {
-			return 0, err
+			return inserted, err
+		}
+		defer tx.Rollback(d.Ctx)
+
+		for _, programme := range programmes {
+			if programme.ID == "" {
+				return 0, fmt.Errorf("Cannot insert programme with no programme_id")
+			}
+
+			result, err := tx.Exec(d.Ctx, `
+INSERT INTO programmes
+(id, titre)
+VALUES ($1, $2) ON CONFLICT DO NOTHING;`,
+				programme.ID,
+				programme.Titre)
+			if err != nil {
+				return 0, err
+			}
+
+			inserted += result.RowsAffected()
 		}
 
-		rows, err := result.RowsAffected()
-		if err != nil {
-			return 0, err
+		if err := tx.Commit(d.Ctx); err != nil {
+			return inserted, err
 		}
 
-		rowsInserted += rows
+		return inserted, err
 	}
-
-	err = tx.Commit()
-	if err != nil {
-		return rowsInserted, err
-	}
-
-	return rowsInserted, nil
 }
 
-func (d *DataClient) GetMembre(membreID string) (Membre, error) {
+/*
+func (d *PostgresClient) GetMembre(membreID string) (Membre, error) {
 	var membre Membre
 
-	rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
+	rows, err := d.Pool.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
 	if err != nil {
 		return membre, err
 	}
@@ -173,9 +125,11 @@ func (d *DataClient) GetMembre(membreID string) (Membre, error) {
 
 	return membre, nil
 }
+*/
 
-func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
-	result, err := d.DB.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
+/*
+func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, error) {
+	result, err := d.Pool.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
 	if err != nil {
 		return 0, err
 	}
@@ -187,8 +141,50 @@ func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
 
 	return rows, nil
 }
-
-func (d *DataClient) GetMembres() (membres []Membre, err error) {
-	return membres, d.DB.Select(&membres, "SELECT * FROM membres;")
-}
 */
+
+func (d *PostgresClient) GetMembres() (membres []Membre, err error) {
+	select {
+	case <-d.Ctx.Done():
+		return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+	default:
+		rows, err := d.Pool.Query(d.Ctx, `
+SELECT
+  membres.id,
+  membres.last_name,
+  membres.first_name,
+  membres.prefered_name,
+  membres.programme_id
+FROM
+  membres
+LIMIT
+  10000
+ORDER BY
+  membres.id;`)
+		if err != nil {
+			return nil, err
+		}
+		defer rows.Close()
+
+		for rows.Next() {
+			var membre Membre
+
+			if err = rows.Scan(
+				&membre.ID,
+				&membre.LastName,
+				&membre.FirstName,
+				&membre.PreferedName,
+				&membre.ProgrammeID,
+			); err != nil {
+				return nil, err
+			}
+
+			membres = append(membres, membre)
+		}
+		if rows.Err() != nil {
+			return membres, rows.Err()
+		}
+
+		return membres, nil
+	}
+}

From be766f593db1f1af072d99f607248967756433f7 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 11 Jun 2024 17:28:20 -0400
Subject: [PATCH 58/99] ajouter API client et tester /api/health

---
 client.go      | 25 +++++++++++++++++++++
 client_test.go | 39 +++++++++++++++++++++++++++++++-
 cmd.go         |  4 +++-
 go.mod         |  3 ++-
 go.sum         | 36 ++++++-----------------------
 request.go     | 40 +++++++++++++++++++++++++++++++++
 response.go    | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++
 responses.go   | 28 -----------------------
 routes.go      | 45 +++++++++++++++++++++++++++++++++++++
 9 files changed, 221 insertions(+), 60 deletions(-)
 create mode 100644 request.go
 create mode 100644 response.go
 delete mode 100644 responses.go
 create mode 100644 routes.go

diff --git a/client.go b/client.go
index 06ab7d0..918fbee 100644
--- a/client.go
+++ b/client.go
@@ -1 +1,26 @@
 package main
+
+import (
+	"fmt"
+
+	"codeberg.org/vlbeaudoin/voki/v3"
+)
+
+type APIClient struct {
+	Voki *voki.Voki
+}
+
+func (c APIClient) GetHealth() (health string, err error) {
+	var request HealthGETRequest
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return "", err
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Message, nil
+}
diff --git a/client_test.go b/client_test.go
index 0fee6f5..2c66470 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1 +1,38 @@
-package main_test
+package main
+
+import (
+	"net/http"
+	"testing"
+
+	"codeberg.org/vlbeaudoin/voki/v3"
+	"github.com/spf13/viper"
+)
+
+func TestAPI(t *testing.T) {
+	var cfg Config
+	if err := viper.Unmarshal(&cfg); err != nil {
+		t.Error(err)
+		return
+	}
+
+	httpClient := http.DefaultClient
+	defer httpClient.CloseIdleConnections()
+
+	vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, "http")
+	apiClient := APIClient{vokiClient}
+
+	t.Run("get API health", func(t *testing.T) {
+		health, err := apiClient.GetHealth()
+		if err != nil {
+			t.Error(err)
+		}
+
+		want := "ok"
+		got := health
+
+		if want != got {
+			t.Errorf("want=%s got=%s", want, got)
+		}
+	})
+
+}
diff --git a/cmd.go b/cmd.go
index 859f49a..4cd12fb 100644
--- a/cmd.go
+++ b/cmd.go
@@ -86,6 +86,9 @@ var apiCmd = &cobra.Command{
 		}
 
 		// Routes
+		if err := addRoutes(e, db); err != nil {
+			log.Fatal("add routes:", err)
+		}
 		/*
 			h := handlers.New(client)
 
@@ -105,7 +108,6 @@ var apiCmd = &cobra.Command{
 		*/
 
 		// Execution
-
 		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port)))
 	},
 }
diff --git a/go.mod b/go.mod
index a7833f1..b6ce20d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,8 @@ module git.agecem.com/agecem/bottin/v7
 go 1.22.0
 
 require (
-	codeberg.org/vlbeaudoin/voki/v2 v2.1.0
+	codeberg.org/vlbeaudoin/pave/v2 v2.0.0
+	codeberg.org/vlbeaudoin/voki/v3 v3.0.0
 	github.com/jackc/pgx/v5 v5.6.0
 	github.com/labstack/echo/v4 v4.12.0
 	github.com/spf13/cobra v1.8.0
diff --git a/go.sum b/go.sum
index f022429..73ace91 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
-codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E=
-codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U=
-codeberg.org/vlbeaudoin/voki/v2 v2.1.0 h1:pXav77QGMHvMF1RyvkEwK3VKBdQh3ATmgh48TXX0tlU=
-codeberg.org/vlbeaudoin/voki/v2 v2.1.0/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U=
+codeberg.org/vlbeaudoin/pave/v2 v2.0.0 h1:hfB5KnqMMu17g5QBWgLvWOsqidrYaohRfu2LflmTrb0=
+codeberg.org/vlbeaudoin/pave/v2 v2.0.0/go.mod h1:TsTfP6IA+3Ph33vLZigeJWS5vgBPgkW1tfs3zFPfycU=
+codeberg.org/vlbeaudoin/voki/v3 v3.0.0 h1:XdF/UTe9YUNj3hYrAyEvdmIMDYLL8SkqTwPkqw1yJ2c=
+codeberg.org/vlbeaudoin/voki/v3 v3.0.0/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g=
 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,16 +13,14 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
@@ -33,8 +31,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
-github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
 github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
 github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
 github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@@ -48,8 +44,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
-github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -58,8 +52,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
-github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
 github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
 github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
@@ -74,8 +66,6 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
 github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
-github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
 github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
 github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -86,8 +76,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -97,30 +87,18 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
diff --git a/request.go b/request.go
new file mode 100644
index 0000000..ecb4346
--- /dev/null
+++ b/request.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"codeberg.org/vlbeaudoin/voki/v3"
+)
+
+var _ voki.Requester[HealthGETResponse] = HealthGETRequest{}
+
+type HealthGETRequest struct{}
+
+func (request HealthGETRequest) Complete() bool { return true }
+
+func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete HealthGET request")
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodGet,
+		"/api/health/",
+		nil,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("%d: %s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
diff --git a/response.go b/response.go
new file mode 100644
index 0000000..55a700d
--- /dev/null
+++ b/response.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"fmt"
+
+	"codeberg.org/vlbeaudoin/voki/v3"
+)
+
+type APIResponse struct {
+	voki.MessageResponse
+	statusCode int
+}
+
+func (R APIResponse) StatusCode() int { return R.statusCode }
+
+func (R *APIResponse) SetStatusCode(code int) error {
+	if code <= 0 {
+		return fmt.Errorf("Cannot set status code to %d", code)
+	}
+	R.statusCode = code
+	return nil
+}
+
+type HealthGETResponse struct {
+	APIResponse
+}
+
+type MembreGETResponse struct {
+	APIResponse
+	Data MembreGETResponseData `json:"data"`
+}
+type MembreGETResponseData struct {
+	Membre Membre `json:"membre"`
+}
+
+type MembresGETResponse struct {
+	APIResponse
+	Data MembresGETResponseData `json:"data"`
+}
+
+type MembresGETResponseData struct {
+	Membres []Membre `json:"membres"`
+}
+
+type MembresPOSTResponse struct {
+	APIResponse
+	Data MembresPOSTResponseData `json:"data"`
+}
+
+type MembresPOSTResponseData struct {
+	MembresInserted int64 `json:"membres_inserted"`
+}
+
+type ProgrammesPOSTResponse struct {
+	APIResponse
+	Data ProgrammesPOSTResponseData `json:"data"`
+}
+
+type ProgrammesPOSTResponseData struct {
+	ProgrammesInserted int64 `json:"programmes_inserted"`
+}
diff --git a/responses.go b/responses.go
deleted file mode 100644
index d8af880..0000000
--- a/responses.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package main
-
-import "codeberg.org/vlbeaudoin/voki/v2"
-
-type GetHealthResponse struct {
-	voki.ResponseWithError
-}
-
-type ListMembresResponse struct {
-	voki.ResponseWithError
-	Data struct {
-		Membres []Membre
-	}
-}
-
-type PostMembresResponse struct {
-	voki.ResponseWithError
-	Data struct {
-		MembresInserted int64
-	}
-}
-
-type PostProgrammesResponse struct {
-	voki.ResponseWithError
-	Data struct {
-		ProgrammesInserted int64
-	}
-}
diff --git a/routes.go b/routes.go
new file mode 100644
index 0000000..ee07763
--- /dev/null
+++ b/routes.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"codeberg.org/vlbeaudoin/pave/v2"
+	"codeberg.org/vlbeaudoin/voki/v3"
+	"github.com/labstack/echo/v4"
+)
+
+func addRoutes(e *echo.Echo, db *PostgresClient) error {
+	_ = db
+
+	apiPath := "/api"
+	apiGroup := e.Group(apiPath)
+	p := pave.New()
+	if err := pave.EchoRegister[HealthGETRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodGet,
+		"/health/",
+		"Get API server health",
+		"HealthGET", func(c echo.Context) error {
+			var request, response = HealthGETRequest{}, HealthGETResponse{}
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete HealthGET request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
+	return nil
+}

From c5339bd45b76a0458e916368421bc36cd241e33d Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 17 Jun 2024 14:06:43 -0400
Subject: [PATCH 59/99] fix(Dockerfile): copier fichiers go manquants vers
 image

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 7c0ad26..6479581 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ LABEL author="vlbeaudoin"
 
 WORKDIR /go/src/app
 
-COPY go.mod go.sum cmd.go config.go db.go entity.go main.go responses.go template.go ./
+COPY go.mod go.sum client.go client_test.go cmd.go config.go db.go entity.go main.go request.go response.go routes.go template.go ./
 
 ADD sql/ sql/
 ADD templates/ templates/

From e1bce94d18e78788ae3be1f3fd0061ef8eb6d103 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 17 Jun 2024 14:07:49 -0400
Subject: [PATCH 60/99] feature: add and test ProgrammesPOST

---
 client.go      | 17 +++++++++++++++++
 client_test.go | 23 +++++++++++++++++++++++
 db.go          |  2 +-
 request.go     | 41 +++++++++++++++++++++++++++++++++++++++++
 routes.go      | 43 +++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 125 insertions(+), 1 deletion(-)

diff --git a/client.go b/client.go
index 918fbee..dd7e1ea 100644
--- a/client.go
+++ b/client.go
@@ -24,3 +24,20 @@ func (c APIClient) GetHealth() (health string, err error) {
 
 	return response.Message, nil
 }
+
+func (c APIClient) InsertProgrammes(programmes ...Programme) (amountInserted int64, err error) {
+	var request ProgrammesPOSTRequest
+	request.Data.Programmes = programmes
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.ProgrammesInserted, nil
+}
diff --git a/client_test.go b/client_test.go
index 2c66470..1f902d2 100644
--- a/client_test.go
+++ b/client_test.go
@@ -8,6 +8,10 @@ import (
 	"github.com/spf13/viper"
 )
 
+func init() {
+	initConfig()
+}
+
 func TestAPI(t *testing.T) {
 	var cfg Config
 	if err := viper.Unmarshal(&cfg); err != nil {
@@ -35,4 +39,23 @@ func TestAPI(t *testing.T) {
 		}
 	})
 
+	//TODO create or replace schema
+	//TODO insert programmes
+	t.Run("insert programmes",
+		func(t *testing.T) {
+			programmes := []Programme{
+				{"404.42", "Cool programme"},
+				{"200.10", "Autre programme"},
+			}
+			t.Log("programmes:", programmes)
+			_, err := apiClient.InsertProgrammes(programmes...)
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	//TODO insert membres
+	//TODO get membre
+	//TODO update membre prefered name
+	//TODO get membres
+
 }
diff --git a/db.go b/db.go
index ff715d8..eda594c 100644
--- a/db.go
+++ b/db.go
@@ -66,7 +66,7 @@ ON CONFLICT (id) DO NOTHING;`,
 	}
 }
 
-func (d *PostgresClient) InsertProgrammes(programmes []Programme) (inserted int64, err error) {
+func (d *PostgresClient) InsertProgrammes(programmes ...Programme) (inserted int64, err error) {
 	select {
 	case <-d.Ctx.Done():
 		return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
diff --git a/request.go b/request.go
index ecb4346..3ff29d7 100644
--- a/request.go
+++ b/request.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -38,3 +39,43 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons
 
 	return
 }
+
+var _ voki.Requester[ProgrammesPOSTResponse] = ProgrammesPOSTRequest{}
+
+type ProgrammesPOSTRequest struct {
+	Data struct {
+		Programmes []Programme
+	}
+}
+
+func (request ProgrammesPOSTRequest) Complete() bool {
+	return len(request.Data.Programmes) != 0
+}
+func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesPOSTResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete ProgrammesPOSTRequest")
+		return
+	}
+
+	var buf bytes.Buffer
+	if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodPost,
+		"/api/programmes/",
+		&buf,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
diff --git a/routes.go b/routes.go
index ee07763..6041358 100644
--- a/routes.go
+++ b/routes.go
@@ -41,5 +41,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		}); err != nil {
 		return err
 	}
+
+	if err := pave.EchoRegister[ProgrammesPOSTRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodPost,
+		"/programmes/",
+		"Get registered programmes",
+		"ProgrammesPOST", func(c echo.Context) error {
+			var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
+
+			if err := c.Bind(&request.Data); err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("parse request body: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete ProgrammesPOST request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			amountInserted, err := db.InsertProgrammes(request.Data.Programmes...)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+			response.Data.ProgrammesInserted = amountInserted
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
 	return nil
 }

From e847f693e057a2652ff019f5fc15d7cdacce9d6f Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 17 Jun 2024 17:25:53 -0400
Subject: [PATCH 61/99] rework: renommer champs dans entities et ajouter
 MembresPOST

- ajouter et tester InsertMembres
- ajouter sql/views.sql
- ajouter view `membres_for_display` -> concat names ou prefered name
- rendre plusieurs champs NOT NULL dans schema
---
 client.go      | 17 +++++++++++++++++
 client_test.go | 23 ++++++++++++++++++++++-
 cmd.go         |  4 ++++
 db.go          | 14 +++++++++++---
 entity.go      |  4 ++--
 request.go     | 41 +++++++++++++++++++++++++++++++++++++++++
 routes.go      | 45 ++++++++++++++++++++++++++++++++++++++++++++-
 sql/schema.sql |  8 ++++----
 sql/views.sql  | 23 +++++++++++++++++++++++
 9 files changed, 168 insertions(+), 11 deletions(-)
 create mode 100644 sql/views.sql

diff --git a/client.go b/client.go
index dd7e1ea..6221ce0 100644
--- a/client.go
+++ b/client.go
@@ -41,3 +41,20 @@ func (c APIClient) InsertProgrammes(programmes ...Programme) (amountInserted int
 
 	return response.Data.ProgrammesInserted, nil
 }
+
+func (c APIClient) InsertMembres(membres ...Membre) (amountInserted int64, err error) {
+	var request MembresPOSTRequest
+	request.Data.Membres = membres
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.MembresInserted, nil
+}
diff --git a/client_test.go b/client_test.go
index 1f902d2..9d1212d 100644
--- a/client_test.go
+++ b/client_test.go
@@ -40,7 +40,6 @@ func TestAPI(t *testing.T) {
 	})
 
 	//TODO create or replace schema
-	//TODO insert programmes
 	t.Run("insert programmes",
 		func(t *testing.T) {
 			programmes := []Programme{
@@ -54,6 +53,28 @@ func TestAPI(t *testing.T) {
 			}
 		})
 	//TODO insert membres
+	t.Run("insert membres",
+		func(t *testing.T) {
+			membres := []Membre{
+				{
+					ID:          "0000000",
+					FirstName:   "Test",
+					LastName:    "User",
+					ProgrammeID: "404.42",
+				},
+				{
+					ID:           "1234567",
+					FirstName:    "Deadname",
+					LastName:     "User",
+					PreferedName: "User, Test-Name",
+					ProgrammeID:  "200.10",
+				},
+			}
+			_, err := apiClient.InsertMembres(membres...)
+			if err != nil {
+				t.Error(err)
+			}
+		})
 	//TODO get membre
 	//TODO update membre prefered name
 	//TODO get membres
diff --git a/cmd.go b/cmd.go
index 4cd12fb..6067795 100644
--- a/cmd.go
+++ b/cmd.go
@@ -85,6 +85,10 @@ var apiCmd = &cobra.Command{
 			log.Fatal("create or replace schema:", err)
 		}
 
+		if err := db.CreateOrReplaceViews(); err != nil {
+			log.Fatal("create or replace views:", err)
+		}
+
 		// Routes
 		if err := addRoutes(e, db); err != nil {
 			log.Fatal("add routes:", err)
diff --git a/db.go b/db.go
index eda594c..c2394e4 100644
--- a/db.go
+++ b/db.go
@@ -11,6 +11,9 @@ import (
 //go:embed sql/schema.sql
 var sqlSchema string
 
+//go:embed sql/views.sql
+var sqlViews string
+
 type PostgresClient struct {
 	//TODO move context out of client
 	Ctx  context.Context
@@ -22,8 +25,13 @@ func (db *PostgresClient) CreateOrReplaceSchema() error {
 	return err
 }
 
+func (db *PostgresClient) CreateOrReplaceViews() error {
+	_, err := db.Pool.Exec(db.Ctx, sqlViews)
+	return err
+}
+
 // InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
-func (d *PostgresClient) InsertMembres(membres []Membre) (inserted int64, err error) {
+func (d *PostgresClient) InsertMembres(membres ...Membre) (inserted int64, err error) {
 	select {
 	case <-d.Ctx.Done():
 		return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
@@ -84,10 +92,10 @@ func (d *PostgresClient) InsertProgrammes(programmes ...Programme) (inserted int
 
 			result, err := tx.Exec(d.Ctx, `
 INSERT INTO programmes
-(id, titre)
+(id, name)
 VALUES ($1, $2) ON CONFLICT DO NOTHING;`,
 				programme.ID,
-				programme.Titre)
+				programme.Name)
 			if err != nil {
 				return 0, err
 			}
diff --git a/entity.go b/entity.go
index 52e54d0..385acde 100644
--- a/entity.go
+++ b/entity.go
@@ -1,8 +1,8 @@
 package main
 
 type Programme struct {
-	ID    string `db:"id" json:"programme_id" csv:"programme_id"`
-	Titre string `db:"titre" json:"nom_programme" csv:"nom_programme"`
+	ID   string `db:"id" json:"programme_id" csv:"programme_id"`
+	Name string `db:"name" json:"nom_programme" csv:"nom_programme"`
 }
 
 type Membre struct {
diff --git a/request.go b/request.go
index 3ff29d7..720561b 100644
--- a/request.go
+++ b/request.go
@@ -79,3 +79,44 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP
 
 	return
 }
+
+var _ voki.Requester[MembresPOSTResponse] = MembresPOSTRequest{}
+
+type MembresPOSTRequest struct {
+	Data struct {
+		Membres []Membre
+	}
+}
+
+func (request MembresPOSTRequest) Complete() bool {
+	return len(request.Data.Membres) != 0
+}
+
+func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete MembresPOSTRequest")
+		return
+	}
+
+	var buf bytes.Buffer
+	if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodPost,
+		"/api/membres/",
+		&buf,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
diff --git a/routes.go b/routes.go
index 6041358..05c62b5 100644
--- a/routes.go
+++ b/routes.go
@@ -48,7 +48,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		apiPath,
 		http.MethodPost,
 		"/programmes/",
-		"Get registered programmes",
+		"Insert programmes",
 		"ProgrammesPOST", func(c echo.Context) error {
 			var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
 
@@ -84,5 +84,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		}); err != nil {
 		return err
 	}
+
+	if err := pave.EchoRegister[MembresPOSTRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodPost,
+		"/membres/",
+		"Insert membres",
+		"MembresPOST", func(c echo.Context) error {
+			var request, response = MembresPOSTRequest{}, MembresPOSTResponse{}
+
+			if err := c.Bind(&request.Data); err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("parse request body: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete MembresPOST request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			amountInserted, err := db.InsertMembres(request.Data.Membres...)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+			response.Data.MembresInserted = amountInserted
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
 	return nil
 }
diff --git a/sql/schema.sql b/sql/schema.sql
index 154b983..6253131 100644
--- a/sql/schema.sql
+++ b/sql/schema.sql
@@ -1,12 +1,12 @@
 CREATE TABLE IF NOT EXISTS programmes (
     id TEXT PRIMARY KEY,
-    titre TEXT
+    name TEXT NOT NULL
 );
 
 CREATE TABLE IF NOT EXISTS membres (
     id VARCHAR(7) PRIMARY KEY,
-    last_name TEXT,
-    first_name TEXT,
+    last_name TEXT NOT NULL,
+    first_name TEXT NOT NULL,
     prefered_name TEXT,
-    programme_id TEXT REFERENCES programmes(id)
+    programme_id TEXT REFERENCES programmes(id) NOT NULL
 );
diff --git a/sql/views.sql b/sql/views.sql
new file mode 100644
index 0000000..ecdde1a
--- /dev/null
+++ b/sql/views.sql
@@ -0,0 +1,23 @@
+-- membres_for_display affiche le numéro étudiant, nom complet OU prefered_name, et titre du programme.
+--
+-- Utilisé par l'application web pour rechercher et afficher les informations des membres
+CREATE OR REPLACE VIEW
+  "membres_for_display"
+AS (
+  SELECT
+    "membres".id,
+    CASE
+      WHEN
+        "membres".prefered_name != '' AND "membres".prefered_name IS NOT NULL
+      THEN
+        "membres".prefered_name
+      ELSE
+        CONCAT("membres".last_name, ', ', "membres".first_name)
+    END AS name,
+    "programmes".id AS programme_id,
+    "programmes".name AS programme_name
+  FROM
+    "membres"
+  INNER JOIN
+    "programmes" ON "programmes".id = "membres".programme_id
+);

From c7c64674c771a49c58233c6d608d14355a508c0b Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 18 Jun 2024 19:44:20 -0400
Subject: [PATCH 62/99] rework: change api prefix to /api/v7/

- add and test GetMembre
- add `IsMembreID(string) bool` function

BREAKING: Rename routes to `/api/v7/...` scheme
---
 client.go      | 17 +++++++++++
 client_test.go | 81 +++++++++++++++++++++++++++++++++++++-------------
 db.go          | 51 +++++++++++++++++++------------
 entity.go      | 16 ++++++++++
 request.go     | 47 +++++++++++++++++++++++++++--
 routes.go      | 45 ++++++++++++++++++++++++++--
 6 files changed, 212 insertions(+), 45 deletions(-)

diff --git a/client.go b/client.go
index 6221ce0..d47a723 100644
--- a/client.go
+++ b/client.go
@@ -58,3 +58,20 @@ func (c APIClient) InsertMembres(membres ...Membre) (amountInserted int64, err e
 
 	return response.Data.MembresInserted, nil
 }
+
+func (c APIClient) GetMembre(membreID string) (membre Membre, err error) {
+	var request MembreGETRequest
+	request.Param.MembreID = membreID
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.Membre, nil
+}
diff --git a/client_test.go b/client_test.go
index 9d1212d..7a847ad 100644
--- a/client_test.go
+++ b/client_test.go
@@ -40,6 +40,7 @@ func TestAPI(t *testing.T) {
 	})
 
 	//TODO create or replace schema
+
 	t.Run("insert programmes",
 		func(t *testing.T) {
 			programmes := []Programme{
@@ -52,31 +53,71 @@ func TestAPI(t *testing.T) {
 				t.Error(err)
 			}
 		})
-	//TODO insert membres
+
+	testMembres := []Membre{
+		{
+			ID:          "0000000",
+			FirstName:   "Test",
+			LastName:    "User",
+			ProgrammeID: "404.42",
+		},
+		{
+			ID:           "1234567",
+			FirstName:    "Deadname",
+			LastName:     "User",
+			PreferedName: "User, Test-Name",
+			ProgrammeID:  "200.10",
+		},
+	}
+
 	t.Run("insert membres",
 		func(t *testing.T) {
-			membres := []Membre{
-				{
-					ID:          "0000000",
-					FirstName:   "Test",
-					LastName:    "User",
-					ProgrammeID: "404.42",
-				},
-				{
-					ID:           "1234567",
-					FirstName:    "Deadname",
-					LastName:     "User",
-					PreferedName: "User, Test-Name",
-					ProgrammeID:  "200.10",
-				},
-			}
-			_, err := apiClient.InsertMembres(membres...)
+			_, err := apiClient.InsertMembres(testMembres...)
 			if err != nil {
 				t.Error(err)
 			}
 		})
-	//TODO get membre
-	//TODO update membre prefered name
-	//TODO get membres
 
+	t.Run("get membre",
+		func(t *testing.T) {
+			membre, err := apiClient.GetMembre(testMembres[0].ID)
+			if err != nil {
+				t.Error(err)
+			}
+
+			want := testMembres[0].LastName
+			got := membre.LastName
+
+			if want != got {
+				t.Errorf("want=%s got=%s", want, got)
+			}
+		})
+
+	t.Run("get invalid membre",
+		func(t *testing.T) {
+			_, err := apiClient.GetMembre("invalid")
+			if err == nil {
+				t.Error("`invalid` should not have been accepted as value to GetMembre, but did")
+			}
+		})
+
+	//TODO update membre prefered name
+	/*
+		t.Run("",
+			func(t *testing.T) {
+				if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil {
+					t.Error(err)
+				}
+			})
+	*/
+	//TODO get membres
+	/*
+		t.Run("get membres, max 50",
+			func(t *testing.T) {
+				membres, err := apiClient.GetMembres(50)
+				if err != nil {
+					t.Error(err)
+				}
+			})
+	*/
 }
diff --git a/db.go b/db.go
index c2394e4..bb401ae 100644
--- a/db.go
+++ b/db.go
@@ -111,29 +111,42 @@ VALUES ($1, $2) ON CONFLICT DO NOTHING;`,
 	}
 }
 
-/*
-func (d *PostgresClient) GetMembre(membreID string) (Membre, error) {
-	var membre Membre
-
-	rows, err := d.Pool.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
-	if err != nil {
-		return membre, err
-	}
-
-	for rows.Next() {
-		err := rows.StructScan(&membre)
-		if err != nil {
-			return membre, err
+func (d *PostgresClient) GetMembre(membreID string) (membre Membre, err error) {
+	select {
+	case <-d.Ctx.Done():
+		err = fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+		return
+	default:
+		if err = d.Pool.QueryRow(d.Ctx, `
+SELECT
+  "membres".id,
+  "membres".last_name,
+  "membres".first_name,
+  "membres".prefered_name,
+  "membres".programme_id
+FROM
+  "membres"
+WHERE
+  "membres".id = $1
+LIMIT
+  1;
+`, membreID).Scan(
+			&membre.ID,
+			&membre.LastName,
+			&membre.FirstName,
+			&membre.PreferedName,
+			&membre.ProgrammeID,
+		); err != nil {
+			return
 		}
-	}
 
-	if membre.ID == "" {
-		return membre, fmt.Errorf("No membre by that id was found")
-	}
+		if membre.ID == "" {
+			return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID)
+		}
 
-	return membre, nil
+		return membre, nil
+	}
 }
-*/
 
 /*
 func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, error) {
diff --git a/entity.go b/entity.go
index 385acde..b919c29 100644
--- a/entity.go
+++ b/entity.go
@@ -1,5 +1,7 @@
 package main
 
+import "unicode"
+
 type Programme struct {
 	ID   string `db:"id" json:"programme_id" csv:"programme_id"`
 	Name string `db:"name" json:"nom_programme" csv:"nom_programme"`
@@ -12,3 +14,17 @@ type Membre struct {
 	PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"`
 	ProgrammeID  string `db:"programme_id" json:"programme_id" csv:"programme_id"`
 }
+
+func IsMembreID(membre_id string) bool {
+	if len(membre_id) != 7 {
+		return false
+	}
+
+	for _, character := range membre_id {
+		if !unicode.IsDigit(character) {
+			return false
+		}
+	}
+
+	return true
+}
diff --git a/request.go b/request.go
index 720561b..277e6f4 100644
--- a/request.go
+++ b/request.go
@@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		"/api/health/",
+		"/api/v7/health/",
 		nil,
 		true,
 	)
@@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPost,
-		"/api/programmes/",
+		"/api/v7/programme/",
 		&buf,
 		true,
 	)
@@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPost,
-		"/api/membres/",
+		"/api/v7/membre/",
 		&buf,
 		true,
 	)
@@ -120,3 +120,44 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes
 
 	return
 }
+
+var _ voki.Requester[MembreGETResponse] = MembreGETRequest{}
+
+type MembreGETRequest struct {
+	Param struct {
+		MembreID string `json:"membre_id" param:"membre_id"`
+	}
+}
+
+func (request MembreGETRequest) Complete() bool {
+	return request.Param.MembreID != ""
+}
+
+func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete MembreGETRequest")
+		return
+	}
+
+	if id := request.Param.MembreID; !IsMembreID(id) {
+		err = fmt.Errorf("MembreID '%s' invalide", id)
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodGet,
+		fmt.Sprintf("/api/v7/membre/%s/", request.Param.MembreID),
+		nil,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
diff --git a/routes.go b/routes.go
index 05c62b5..c5b3083 100644
--- a/routes.go
+++ b/routes.go
@@ -12,7 +12,7 @@ import (
 func addRoutes(e *echo.Echo, db *PostgresClient) error {
 	_ = db
 
-	apiPath := "/api"
+	apiPath := "/api/v7"
 	apiGroup := e.Group(apiPath)
 	p := pave.New()
 	if err := pave.EchoRegister[HealthGETRequest](
@@ -47,7 +47,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		&p,
 		apiPath,
 		http.MethodPost,
-		"/programmes/",
+		"/programme/",
 		"Insert programmes",
 		"ProgrammesPOST", func(c echo.Context) error {
 			var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
@@ -90,7 +90,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		&p,
 		apiPath,
 		http.MethodPost,
-		"/membres/",
+		"/membre/",
 		"Insert membres",
 		"MembresPOST", func(c echo.Context) error {
 			var request, response = MembresPOSTRequest{}, MembresPOSTResponse{}
@@ -127,5 +127,44 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		}); err != nil {
 		return err
 	}
+
+	if err := pave.EchoRegister[MembreGETRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodGet,
+		"/membre/:membre_id/",
+		"Get membre",
+		"MembreGET", func(c echo.Context) error {
+			var request, response = MembreGETRequest{}, MembreGETResponse{}
+
+			request.Param.MembreID = c.Param("membre_id")
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete MembreGET request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			membre, err := db.GetMembre(request.Param.MembreID)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+			response.Data.Membre = membre
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
 	return nil
 }

From 00aebc2ae317e49d1cf4517c61619de19e485835 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 18 Jun 2024 19:47:28 -0400
Subject: [PATCH 63/99] feature: add basic Makefile for integration testing

---
 Makefile | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 Makefile

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..31a6c23
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+## This Makefile uses the help target explained in the following blogpost:
+##
+## https://victoria.dev/blog/how-to-create-a-self-documenting-makefile/
+
+.DEFAULT_GOAL := help
+
+.PHONY: help
+help: ## Show this help
+	@egrep -h '\s##\s' $(MAKEFILE_LIST) | \
+		sort | \
+		awk \
+		'BEGIN  {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: test-integration
+test-integration: ## run integration tests through API client. Config is read from `~/.bottin.yaml`. WARNING: affects data in the database, do not run on production server
+	docker-compose down && docker-compose up -d --build && sleep 2 && go test

From f8b5c720036b5d37bd1415ed4628ef002e08cc90 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 18 Jun 2024 21:21:30 -0400
Subject: [PATCH 64/99] feature: add and test GetMembres

---
 client.go      | 18 ++++++++++++++++++
 client_test.go | 19 ++++++++++---------
 db.go          | 21 +++++++++++----------
 request.go     | 34 ++++++++++++++++++++++++++++++++++
 routes.go      | 44 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 117 insertions(+), 19 deletions(-)

diff --git a/client.go b/client.go
index d47a723..386dbdd 100644
--- a/client.go
+++ b/client.go
@@ -75,3 +75,21 @@ func (c APIClient) GetMembre(membreID string) (membre Membre, err error) {
 
 	return response.Data.Membre, nil
 }
+
+func (c APIClient) GetMembres(limit int) (membres []Membre, err error) {
+	var request MembresGETRequest
+
+	request.Query.Limit = limit
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.Membres, nil
+}
diff --git a/client_test.go b/client_test.go
index 7a847ad..7a7a62d 100644
--- a/client_test.go
+++ b/client_test.go
@@ -111,13 +111,14 @@ func TestAPI(t *testing.T) {
 			})
 	*/
 	//TODO get membres
-	/*
-		t.Run("get membres, max 50",
-			func(t *testing.T) {
-				membres, err := apiClient.GetMembres(50)
-				if err != nil {
-					t.Error(err)
-				}
-			})
-	*/
+	t.Run("get membres, max 50",
+		func(t *testing.T) {
+			membres, err := apiClient.GetMembres(50)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Log(membres)
+		})
+
+	//TODO remove test membres and programmes
 }
diff --git a/db.go b/db.go
index bb401ae..90d24f7 100644
--- a/db.go
+++ b/db.go
@@ -164,24 +164,25 @@ func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, erro
 }
 */
 
-func (d *PostgresClient) GetMembres() (membres []Membre, err error) {
+func (d *PostgresClient) GetMembres(limit int) (membres []Membre, err error) {
 	select {
 	case <-d.Ctx.Done():
 		return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
 	default:
 		rows, err := d.Pool.Query(d.Ctx, `
 SELECT
-  membres.id,
-  membres.last_name,
-  membres.first_name,
-  membres.prefered_name,
-  membres.programme_id
+  "membres".id,
+  "membres".last_name,
+  "membres".first_name,
+  "membres".prefered_name,
+  "membres".programme_id
 FROM
-  membres
-LIMIT
-  10000
+  "membres"
 ORDER BY
-  membres.id;`)
+  "membres".id
+LIMIT
+  $1;
+`, limit)
 		if err != nil {
 			return nil, err
 		}
diff --git a/request.go b/request.go
index 277e6f4..df538b3 100644
--- a/request.go
+++ b/request.go
@@ -161,3 +161,37 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons
 
 	return
 }
+
+var _ voki.Requester[MembresGETResponse] = MembresGETRequest{}
+
+type MembresGETRequest struct {
+	Query struct {
+		Limit int `json:"limit" query:"limit"`
+	}
+}
+
+func (request MembresGETRequest) Complete() bool { return true }
+
+func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete MembresGETRequest")
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodGet,
+		fmt.Sprintf("/api/v7/membre/?limit=%d", request.Query.Limit),
+		nil,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
diff --git a/routes.go b/routes.go
index c5b3083..412141a 100644
--- a/routes.go
+++ b/routes.go
@@ -3,6 +3,7 @@ package main
 import (
 	"fmt"
 	"net/http"
+	"strconv"
 
 	"codeberg.org/vlbeaudoin/pave/v2"
 	"codeberg.org/vlbeaudoin/voki/v3"
@@ -166,5 +167,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		}); err != nil {
 		return err
 	}
+
+	if err := pave.EchoRegister[MembresGETRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodGet,
+		"/membre/",
+		"Get membres",
+		"MembresGET", func(c echo.Context) (err error) {
+			var request, response = MembresGETRequest{}, MembresGETResponse{}
+
+			request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit"))
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("parsing limit: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete MembresGET request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Data.Membres, err = db.GetMembres(request.Query.Limit)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
 	return nil
 }

From f6ffa0337969028d566f92cfa915276296f4b8c6 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 18 Jun 2024 22:51:20 -0400
Subject: [PATCH 65/99] feature: ajouter MembrePreferedNamePUTResponse

---
 response.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/response.go b/response.go
index 55a700d..cf53a80 100644
--- a/response.go
+++ b/response.go
@@ -33,6 +33,10 @@ type MembreGETResponseData struct {
 	Membre Membre `json:"membre"`
 }
 
+type MembrePreferedNamePUTResponse struct {
+	APIResponse
+}
+
 type MembresGETResponse struct {
 	APIResponse
 	Data MembresGETResponseData `json:"data"`

From 4d338f2b03318605e4c9a0c18256b2c82f75eafa Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 18 Jun 2024 22:51:32 -0400
Subject: [PATCH 66/99] feature: ajouter ProgrammesGETResponse et data

---
 response.go | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/response.go b/response.go
index cf53a80..9f0d076 100644
--- a/response.go
+++ b/response.go
@@ -63,3 +63,12 @@ type ProgrammesPOSTResponse struct {
 type ProgrammesPOSTResponseData struct {
 	ProgrammesInserted int64 `json:"programmes_inserted"`
 }
+
+type ProgrammesGETResponse struct {
+	APIResponse
+	Data ProgrammesGETResponseData `json:"data"`
+}
+
+type ProgrammesGETResponseData struct {
+	Programmes []Programme `json:"programmes"`
+}

From 26b3134861d36e8a71ed515acdae9612fd5720f7 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 18 Jun 2024 23:55:55 -0400
Subject: [PATCH 67/99] feature(request): ajouter MembrePreferedNamePUT et
 ProgrammesGET

---
 request.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 78 insertions(+)

diff --git a/request.go b/request.go
index df538b3..ab4642d 100644
--- a/request.go
+++ b/request.go
@@ -195,3 +195,81 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo
 
 	return
 }
+
+var _ voki.Requester[MembrePreferedNamePUTResponse] = MembrePreferedNamePUTRequest{}
+
+type MembrePreferedNamePUTRequest struct {
+	Data struct {
+		PreferedName string `json:"prefered_name"`
+	} `json:"data"`
+	Param struct {
+		MembreID string `json:"membre_id" param:"membre_id"`
+	} `json:"param"`
+}
+
+func (request MembrePreferedNamePUTRequest) Complete() bool {
+	return IsMembreID(request.Param.MembreID) && len(request.Data.PreferedName) != 0
+}
+
+func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response MembrePreferedNamePUTResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete MembrePreferedNamePUTRequest")
+		return
+	}
+
+	var buf bytes.Buffer
+	if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodPut,
+		"/api/v7/membre/%s/",
+		&buf,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
+
+var _ voki.Requester[ProgrammesGETResponse] = ProgrammesGETRequest{}
+
+type ProgrammesGETRequest struct {
+	Query struct {
+		Limit int `json:"limit" query:"limit"`
+	}
+}
+
+func (request ProgrammesGETRequest) Complete() bool { return true }
+
+func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGETResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete ProgrammesGETRequest")
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodGet,
+		fmt.Sprintf("/api/v7/programme/?limit=%d", request.Query.Limit),
+		nil,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}

From 78aafe0ce9953154fd94f4ff38124da8d737081e Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 19 Jun 2024 00:04:19 -0400
Subject: [PATCH 68/99] feature(api): add and test ProgrammesGET

---
 client.go      | 18 ++++++++++++++++++
 client_test.go | 14 +++++++++-----
 db.go          | 41 +++++++++++++++++++++++++++++++++++++++++
 routes.go      | 43 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 111 insertions(+), 5 deletions(-)

diff --git a/client.go b/client.go
index 386dbdd..28ac08c 100644
--- a/client.go
+++ b/client.go
@@ -93,3 +93,21 @@ func (c APIClient) GetMembres(limit int) (membres []Membre, err error) {
 
 	return response.Data.Membres, nil
 }
+
+func (c APIClient) GetProgrammes(limit int) (programmes []Programme, err error) {
+	var request ProgrammesGETRequest
+
+	request.Query.Limit = limit
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.Programmes, nil
+}
diff --git a/client_test.go b/client_test.go
index 7a7a62d..7d3ad78 100644
--- a/client_test.go
+++ b/client_test.go
@@ -39,8 +39,6 @@ func TestAPI(t *testing.T) {
 		}
 	})
 
-	//TODO create or replace schema
-
 	t.Run("insert programmes",
 		func(t *testing.T) {
 			programmes := []Programme{
@@ -70,6 +68,15 @@ func TestAPI(t *testing.T) {
 		},
 	}
 
+	t.Run("get programmes, max 50",
+		func(t *testing.T) {
+			programmes, err := apiClient.GetProgrammes(50)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Log(programmes)
+		})
+
 	t.Run("insert membres",
 		func(t *testing.T) {
 			_, err := apiClient.InsertMembres(testMembres...)
@@ -110,7 +117,6 @@ func TestAPI(t *testing.T) {
 				}
 			})
 	*/
-	//TODO get membres
 	t.Run("get membres, max 50",
 		func(t *testing.T) {
 			membres, err := apiClient.GetMembres(50)
@@ -119,6 +125,4 @@ func TestAPI(t *testing.T) {
 			}
 			t.Log(membres)
 		})
-
-	//TODO remove test membres and programmes
 }
diff --git a/db.go b/db.go
index 90d24f7..3ae7ee0 100644
--- a/db.go
+++ b/db.go
@@ -210,3 +210,44 @@ LIMIT
 		return membres, nil
 	}
 }
+
+func (d *PostgresClient) GetProgrammes(limit int) (programmes []Programme, err error) {
+	select {
+	case <-d.Ctx.Done():
+		return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+	default:
+		rows, err := d.Pool.Query(d.Ctx, `
+SELECT
+  "programmes".id,
+  "programmes".name
+FROM
+  "programmes"
+ORDER BY
+  "programmes".id
+LIMIT
+  $1;
+`, limit)
+		if err != nil {
+			return nil, err
+		}
+		defer rows.Close()
+
+		for rows.Next() {
+			var programme Programme
+
+			if err = rows.Scan(
+				&programme.ID,
+				&programme.Name,
+			); err != nil {
+				return nil, err
+			}
+
+			programmes = append(programmes, programme)
+		}
+		if rows.Err() != nil {
+			return programmes, rows.Err()
+		}
+
+		return programmes, nil
+	}
+}
diff --git a/routes.go b/routes.go
index 412141a..1ec580e 100644
--- a/routes.go
+++ b/routes.go
@@ -210,5 +210,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		}); err != nil {
 		return err
 	}
+
+	if err := pave.EchoRegister[ProgrammesGETRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodGet,
+		"/programme/",
+		"Get programmes",
+		"ProgrammesGET", func(c echo.Context) (err error) {
+			var request, response = ProgrammesGETRequest{}, ProgrammesGETResponse{}
+
+			request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit"))
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("parsing limit: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete ProgrammesGET request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Data.Programmes, err = db.GetProgrammes(request.Query.Limit)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
 	return nil
 }

From e6103c6e6e461605edc2472658daabf95d477e4d Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 19 Jun 2024 00:28:26 -0400
Subject: [PATCH 69/99] feature(api): add and test UpdateMembrePreferedName

---
 client.go      | 22 ++++++++++++++++++++++
 client_test.go | 15 ++++++---------
 db.go          | 21 +++++++++++++++++++++
 request.go     |  2 +-
 routes.go      | 44 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 94 insertions(+), 10 deletions(-)

diff --git a/client.go b/client.go
index 28ac08c..91cdbe7 100644
--- a/client.go
+++ b/client.go
@@ -111,3 +111,25 @@ func (c APIClient) GetProgrammes(limit int) (programmes []Programme, err error)
 
 	return response.Data.Programmes, nil
 }
+
+func (c APIClient) UpdateMembrePreferedName(membreID string, name string) (err error) {
+	var request MembrePreferedNamePUTRequest
+
+	if !IsMembreID(membreID) {
+		return fmt.Errorf("Numéro étudiant '%s' invalide", membreID)
+	}
+	request.Param.MembreID = membreID
+	request.Data.PreferedName = name
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return nil
+}
diff --git a/client_test.go b/client_test.go
index 7d3ad78..1a02a15 100644
--- a/client_test.go
+++ b/client_test.go
@@ -108,15 +108,12 @@ func TestAPI(t *testing.T) {
 			}
 		})
 
-	//TODO update membre prefered name
-	/*
-		t.Run("",
-			func(t *testing.T) {
-				if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil {
-					t.Error(err)
-				}
-			})
-	*/
+	t.Run("update membre prefered name",
+		func(t *testing.T) {
+			if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil {
+				t.Error(err)
+			}
+		})
 	t.Run("get membres, max 50",
 		func(t *testing.T) {
 			membres, err := apiClient.GetMembres(50)
diff --git a/db.go b/db.go
index 3ae7ee0..2608940 100644
--- a/db.go
+++ b/db.go
@@ -251,3 +251,24 @@ LIMIT
 		return programmes, nil
 	}
 }
+
+func (d *PostgresClient) UpdateMembrePreferedName(membreID string, name string) (err error) {
+	select {
+	case <-d.Ctx.Done():
+		return fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+	default:
+		if !IsMembreID(membreID) {
+			return fmt.Errorf("Numéro étudiant '%s' invalide", membreID)
+		}
+
+		_, err = d.Pool.Exec(d.Ctx, `
+UPDATE
+  "membres"
+SET
+  prefered_name = $1
+WHERE
+  "membres".id = $2;
+`, name, membreID)
+	}
+	return
+}
diff --git a/request.go b/request.go
index ab4642d..97f51de 100644
--- a/request.go
+++ b/request.go
@@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPut,
-		"/api/v7/membre/%s/",
+		fmt.Sprintf("/api/v7/membre/%s/prefered_name/", request.Param.MembreID),
 		&buf,
 		true,
 	)
diff --git a/routes.go b/routes.go
index 1ec580e..355dd8f 100644
--- a/routes.go
+++ b/routes.go
@@ -253,5 +253,49 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		}); err != nil {
 		return err
 	}
+
+	if err := pave.EchoRegister[MembrePreferedNamePUTRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodPut,
+		"/membre/:membre_id/prefered_name/",
+		"Update membre prefered name, which is prioritized in the membres_for_display view",
+		"MembrePreferedNamePUT", func(c echo.Context) error {
+			var request, response = MembrePreferedNamePUTRequest{}, MembrePreferedNamePUTResponse{}
+
+			request.Param.MembreID = c.Param("membre_id")
+
+			if err := c.Bind(&request.Data); err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("parse request body: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete MembrePreferedNamePUT request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if err := db.UpdateMembrePreferedName(request.Param.MembreID, request.Data.PreferedName); err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = fmt.Sprintf("Updated membre %s name to %s", request.Param.MembreID, request.Data.PreferedName)
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
+
 	return nil
 }

From e4ff1013d04288a8fbb3710506cebfb3b9b7cd8a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 18:51:38 -0400
Subject: [PATCH 70/99] feature: ajouter et tester GetMembre[s]ForDisplay
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Priorisent le prefered_name ("nom d'usage") et devraient être utilisés
aux endroits où l'affichage est important.
---
 client.go      | 35 +++++++++++++++++++++
 client_test.go | 18 +++++++++++
 db.go          | 80 ++++++++++++++++++++++++++++++++++++++++++++++++
 entity.go      |  8 +++++
 request.go     | 75 +++++++++++++++++++++++++++++++++++++++++++++
 response.go    | 17 +++++++++++
 routes.go      | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 315 insertions(+)

diff --git a/client.go b/client.go
index 91cdbe7..e4958a6 100644
--- a/client.go
+++ b/client.go
@@ -133,3 +133,38 @@ func (c APIClient) UpdateMembrePreferedName(membreID string, name string) (err e
 
 	return nil
 }
+
+func (c APIClient) GetMembreForDisplay(membreID string) (membre MembreForDisplay, err error) {
+	var request MembreDisplayGETRequest
+	request.Param.MembreID = membreID
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.Membre, nil
+}
+
+func (c APIClient) GetMembresForDisplay(limit int) (membres []MembreForDisplay, err error) {
+	var request MembresDisplayGETRequest
+
+	request.Query.Limit = limit
+
+	response, err := request.Request(c.Voki)
+	if err != nil {
+		return
+	}
+
+	if code, message := response.StatusCode(), response.Message; code >= 400 {
+		err = fmt.Errorf("%d: %s", code, message)
+		return
+	}
+
+	return response.Data.Membres, nil
+}
diff --git a/client_test.go b/client_test.go
index 1a02a15..a2aea93 100644
--- a/client_test.go
+++ b/client_test.go
@@ -122,4 +122,22 @@ func TestAPI(t *testing.T) {
 			}
 			t.Log(membres)
 		})
+
+	t.Run("get membre for display",
+		func(t *testing.T) {
+			membre, err := apiClient.GetMembreForDisplay(testMembres[0].ID)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Log(membre)
+		})
+
+	t.Run("get membres for display, max 5",
+		func(t *testing.T) {
+			membres, err := apiClient.GetMembresForDisplay(5)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Log(membres)
+		})
 }
diff --git a/db.go b/db.go
index 2608940..915ddab 100644
--- a/db.go
+++ b/db.go
@@ -272,3 +272,83 @@ WHERE
 	}
 	return
 }
+
+func (d *PostgresClient) GetMembreForDisplay(membreID string) (membre MembreForDisplay, err error) {
+	select {
+	case <-d.Ctx.Done():
+		err = fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+		return
+	default:
+		if err = d.Pool.QueryRow(d.Ctx, `
+SELECT
+  "membres_for_display".id,
+  "membres_for_display".name,
+  "membres_for_display".programme_id,
+  "membres_for_display".programme_name
+FROM
+  "membres_for_display"
+WHERE
+  "membres_for_display".id = $1
+LIMIT
+  1;
+`, membreID).Scan(
+			&membre.ID,
+			&membre.Name,
+			&membre.ProgrammeID,
+			&membre.ProgrammeName,
+		); err != nil {
+			return
+		}
+
+		if membre.ID == "" {
+			return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID)
+		}
+
+		return membre, nil
+	}
+}
+
+func (d *PostgresClient) GetMembresForDisplay(limit int) (membres []MembreForDisplay, err error) {
+	select {
+	case <-d.Ctx.Done():
+		return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
+	default:
+		rows, err := d.Pool.Query(d.Ctx, `
+SELECT
+  "membres_for_display".id,
+  "membres_for_display".name,
+  "membres_for_display".programme_id,
+  "membres_for_display".programme_name
+FROM
+  "membres_for_display"
+ORDER BY
+  "membres_for_display".id
+LIMIT
+  $1;
+`, limit)
+		if err != nil {
+			return nil, err
+		}
+		defer rows.Close()
+
+		for rows.Next() {
+			var membre MembreForDisplay
+
+			if err = rows.Scan(
+				&membre.ID,
+				&membre.Name,
+				&membre.ProgrammeID,
+				&membre.ProgrammeName,
+			); err != nil {
+				return nil, err
+			}
+
+			membres = append(membres, membre)
+		}
+		if rows.Err() != nil {
+			return membres, rows.Err()
+		}
+
+		return membres, nil
+	}
+}
diff --git a/entity.go b/entity.go
index b919c29..672f54c 100644
--- a/entity.go
+++ b/entity.go
@@ -15,6 +15,14 @@ type Membre struct {
 	ProgrammeID  string `db:"programme_id" json:"programme_id" csv:"programme_id"`
 }
 
+// MembreForDisplay maps to the `membres_for_display` view declared in `sql/views.sql`
+type MembreForDisplay struct {
+	ID            string `db:"id" json:"membre_id" csv:"membre_id"`
+	Name          string `db:"name" json:"name" csv:"name"`
+	ProgrammeID   string `db:"programme_id" json:"programme_id" csv:"programme_id"`
+	ProgrammeName string `db:"programme_name" json:"programme_name" csv:"programme_name"`
+}
+
 func IsMembreID(membre_id string) bool {
 	if len(membre_id) != 7 {
 		return false
diff --git a/request.go b/request.go
index 97f51de..8f47f10 100644
--- a/request.go
+++ b/request.go
@@ -273,3 +273,78 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE
 
 	return
 }
+
+var _ voki.Requester[MembresDisplayGETResponse] = MembresDisplayGETRequest{}
+
+type MembresDisplayGETRequest struct {
+	Query struct {
+		Limit int `json:"limit" query:"limit"`
+	}
+}
+
+func (request MembresDisplayGETRequest) Complete() bool { return true }
+
+func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresDisplayGETResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete MembresDisplayGETRequest")
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodGet,
+		fmt.Sprintf("/api/v7/membre/display/?limit=%d", request.Query.Limit),
+		nil,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
+
+var _ voki.Requester[MembreDisplayGETResponse] = MembreDisplayGETRequest{}
+
+type MembreDisplayGETRequest struct {
+	Param struct {
+		MembreID string `json:"membre_id" param:"membre_id"`
+	}
+}
+
+func (request MembreDisplayGETRequest) Complete() bool {
+	return request.Param.MembreID != ""
+}
+
+func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDisplayGETResponse, err error) {
+	if !request.Complete() {
+		err = fmt.Errorf("Incomplete MembreDisplayGETRequest")
+		return
+	}
+
+	if id := request.Param.MembreID; !IsMembreID(id) {
+		err = fmt.Errorf("MembreID '%s' invalide", id)
+		return
+	}
+
+	statusCode, body, err := v.CallAndParse(
+		http.MethodGet,
+		fmt.Sprintf("/api/v7/membre/%s/display/", request.Param.MembreID),
+		nil,
+		true,
+	)
+	if err != nil {
+		err = fmt.Errorf("code=%d err=%s", statusCode, err)
+		return
+	}
+	response.SetStatusCode(statusCode)
+	if err = json.Unmarshal(body, &response); err != nil {
+		return
+	}
+
+	return
+}
diff --git a/response.go b/response.go
index 9f0d076..19818df 100644
--- a/response.go
+++ b/response.go
@@ -46,6 +46,23 @@ type MembresGETResponseData struct {
 	Membres []Membre `json:"membres"`
 }
 
+type MembreDisplayGETResponse struct {
+	APIResponse
+	Data MembreDisplayGETResponseData `json:"data"`
+}
+type MembreDisplayGETResponseData struct {
+	Membre MembreForDisplay `json:"membre_for_display"`
+}
+
+type MembresDisplayGETResponse struct {
+	APIResponse
+	Data MembresDisplayGETResponseData `json:"data"`
+}
+
+type MembresDisplayGETResponseData struct {
+	Membres []MembreForDisplay `json:"membres_for_display"`
+}
+
 type MembresPOSTResponse struct {
 	APIResponse
 	Data MembresPOSTResponseData `json:"data"`
diff --git a/routes.go b/routes.go
index 355dd8f..a5aaed6 100644
--- a/routes.go
+++ b/routes.go
@@ -297,5 +297,87 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		return err
 	}
 
+	if err := pave.EchoRegister[MembresDisplayGETRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodGet,
+		"/membre/display/",
+		"Get membres",
+		"MembresDisplayGET", func(c echo.Context) (err error) {
+			var request, response = MembresDisplayGETRequest{}, MembresDisplayGETResponse{}
+
+			request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit"))
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("parsing limit: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete MembresDisplayGET request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Data.Membres, err = db.GetMembresForDisplay(request.Query.Limit)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
+
+	if err := pave.EchoRegister[MembreDisplayGETRequest](
+		apiGroup,
+		&p,
+		apiPath,
+		http.MethodGet,
+		"/membre/:membre_id/display/",
+		"Get membre",
+		"MembreDisplayGET", func(c echo.Context) error {
+			var request, response = MembreDisplayGETRequest{}, MembreDisplayGETResponse{}
+
+			request.Param.MembreID = c.Param("membre_id")
+
+			if !request.Complete() {
+				var response voki.ResponseBadRequest
+				response.Message = "Incomplete MembreDisplayGET request received"
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			membre, err := db.GetMembreForDisplay(request.Param.MembreID)
+			if err != nil {
+				var response voki.ResponseBadRequest
+				response.Message = fmt.Sprintf("db: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+			response.Data.Membre = membre
+
+			if err := response.SetStatusCode(http.StatusOK); err != nil {
+				var response voki.ResponseInternalServerError
+				response.Message = fmt.Sprintf("handler: %s", err)
+				return c.JSON(response.StatusCode(), response)
+			}
+
+			response.Message = "ok"
+			return c.JSON(response.StatusCode(), response)
+
+		}); err != nil {
+		return err
+	}
+
 	return nil
 }

From 929704c6ff25c9fc0bd4a16f9fbf36f79674dff5 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 19:32:26 -0400
Subject: [PATCH 71/99] =?UTF-8?q?fix(config):=20ajouter=20pr=C3=A9fixe=20w?=
 =?UTF-8?q?eb[.-]=20aux=20options=20config=20web?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 config.go | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/config.go b/config.go
index d50e241..3096960 100644
--- a/config.go
+++ b/config.go
@@ -66,23 +66,23 @@ const (
 	DefaultWebPort     int    = 2312
 	DescriptionWebPort string = "Web client port"
 
-	ViperWebAPIHost       string = "api.host"
-	FlagWebAPIHost        string = "api-host"
+	ViperWebAPIHost       string = "web.api.host"
+	FlagWebAPIHost        string = "web-api-host"
 	DefaultWebAPIHost     string = "api"
 	DescriptionWebAPIHost string = "Target API server host"
 
-	ViperWebAPIKey       string = "api.key"
-	FlagWebAPIKey        string = "api-key"
+	ViperWebAPIKey       string = "web.api.key"
+	FlagWebAPIKey        string = "web-api-key"
 	DefaultWebAPIKey     string = "bottin"
 	DescriptionWebAPIKey string = "Target API server key"
 
-	ViperWebAPIPort       string = "api.port"
-	FlagWebAPIPort        string = "api-port"
+	ViperWebAPIPort       string = "web.api.port"
+	FlagWebAPIPort        string = "web-api-port"
 	DefaultWebAPIPort     int    = 1312
 	DescriptionWebAPIPort string = "Target API server port"
 
-	ViperWebAPIProtocol       string = "api.protocol"
-	FlagWebAPIProtocol        string = "api-protocol"
+	ViperWebAPIProtocol       string = "web.api.protocol"
+	FlagWebAPIProtocol        string = "web-api-protocol"
 	DefaultWebAPIProtocol     string = "http"
 	DescriptionWebAPIProtocol string = "Target API server protocol (http/https)"
 )

From 8cb2014f3bafddb56c0dea3051988a3043a98a8a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 19:34:27 -0400
Subject: [PATCH 72/99] fix(template): expect voki.MessageResponse in input
 object

---
 templates/index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/index.html b/templates/index.html
index 700d9bb..85c857d 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -100,7 +100,7 @@ button {
         </ul>
     </form>
 
-    <p class="result">{{ .Result }}</p>
+    <p class="result">{{ .Message }}</p>
   </body>
 
 </html>

From 7484bafc84252ae468dc094a3ed084640c0ad2eb Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 19:35:07 -0400
Subject: [PATCH 73/99] =?UTF-8?q?fix(web):=20neutraliser=20texte=20avec=20?=
 =?UTF-8?q?middle=20dot=20(=C2=B7)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 templates/index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/index.html b/templates/index.html
index 85c857d..2822147 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -83,7 +83,7 @@ button {
     </h2>
 
     <p>
-      Scannez la carte étudiante d'unE membre<br>
+      Scannez la carte étudiante d'un·e membre<br>
       -ou-<br>
       Entrez manuellement le code à 7 chiffres
     </p>

From 244276905b94253aad868032d388847ef1ffeab4 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 19:36:38 -0400
Subject: [PATCH 74/99] =?UTF-8?q?feature(cmd):=20impl=C3=A9menter=20webCmd?=
 =?UTF-8?q?=20de=20base?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

manque encore le processus de scan mais sinon c'est presque fini
---
 cmd.go | 52 +++++++++++++++++++++++++++-------------------------
 1 file changed, 27 insertions(+), 25 deletions(-)

diff --git a/cmd.go b/cmd.go
index 6067795..dd41f0b 100644
--- a/cmd.go
+++ b/cmd.go
@@ -6,8 +6,10 @@ import (
 	"fmt"
 	"html/template"
 	"log"
+	"net/http"
 	"os"
 
+	"codeberg.org/vlbeaudoin/voki/v3"
 	"github.com/jackc/pgx/v5/pgxpool"
 	"github.com/labstack/echo/v4"
 	"github.com/labstack/echo/v4/middleware"
@@ -122,56 +124,56 @@ var webCmd = &cobra.Command{
 	Short: "Démarrer le client web",
 	Args:  cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
+		// Parse config
 		var cfg Config
 		if err := viper.Unmarshal(&cfg); err != nil {
 			log.Fatal("init config:", err)
 		}
 
-		// Ping API server
-		/*
-			client := http.DefaultClient
-			defer client.CloseIdleConnections()
-
-			apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
-
-			pingResult, err := apiClient.GetHealth()
-			if err != nil {
-				log.Fatal(err)
-			}
-
-			log.Println(pingResult)
-		*/
-
 		e := echo.New()
 
 		// Middlewares
 
+		// Trailing slash
 		e.Pre(middleware.AddTrailingSlash())
 
+		// Auth
 		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
 			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Web.User)) == 1
 			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1
 			return usersMatch && passwordsMatch, nil
 		}))
 
-		// Template
-
-		t := &Template{
+		// Templating
+		e.Renderer = &Template{
 			templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
 		}
 
-		e.Renderer = t
+		// API Client
+		apiClient := APIClient{voki.New(
+			http.DefaultClient,
+			cfg.Web.API.Host,
+			cfg.Web.API.Key,
+			cfg.Web.API.Port,
+			cfg.Web.API.Protocol,
+		)}
+		defer apiClient.Voki.CloseIdleConnections()
 
 		// Routes
-		/*
-			handler := webhandlers.Handler{APIClient: apiClient}
+		e.GET("/", func(c echo.Context) error {
+			pingResult, err := apiClient.GetHealth()
+			if err != nil {
+				log.Fatal(err)
+			}
 
-			e.GET("/", handler.GetIndex)
-			e.GET("/membre/", handler.GetMembre)
-		*/
+			return c.Render(
+				http.StatusOK,
+				"index-html",
+				voki.MessageResponse{Message: pingResult},
+			)
+		})
 
 		// Execution
-
 		e.Logger.Fatal(e.Start(
 			fmt.Sprintf(":%d", cfg.Web.Port)))
 	},

From 0321b1b2a0c9bf2b91e617c95aee61b61dec0781 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 19:54:41 -0400
Subject: [PATCH 75/99] =?UTF-8?q?fix(web):=20correctement=20render=20erreu?=
 =?UTF-8?q?r=20d'acc=C3=A8s=20au=20serveur=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/cmd.go b/cmd.go
index dd41f0b..88f36a5 100644
--- a/cmd.go
+++ b/cmd.go
@@ -163,7 +163,11 @@ var webCmd = &cobra.Command{
 		e.GET("/", func(c echo.Context) error {
 			pingResult, err := apiClient.GetHealth()
 			if err != nil {
-				log.Fatal(err)
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)},
+				)
 			}
 
 			return c.Render(

From 6cc90b1a29cb56e88519689de66c38586702f4dc Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 19:55:12 -0400
Subject: [PATCH 76/99] feature(web): ajouter route /membre/

permet la recherche de membre
---
 cmd.go | 42 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/cmd.go b/cmd.go
index 88f36a5..3bbd59c 100644
--- a/cmd.go
+++ b/cmd.go
@@ -177,6 +177,48 @@ var webCmd = &cobra.Command{
 			)
 		})
 
+		e.GET("/membre/", func(c echo.Context) error {
+			membreID := c.QueryParam("membre_id")
+			switch {
+			case membreID == "":
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: "Veuillez entrer un numéro étudiant à rechercher"},
+				)
+			case !IsMembreID(membreID):
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: fmt.Sprintf("Numéro étudiant '%s' invalide", membreID)},
+				)
+			}
+
+			membre, err := apiClient.GetMembreForDisplay(membreID)
+			if err != nil {
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: fmt.Sprintf("erreur: %s", err)},
+				)
+			}
+
+			return c.Render(
+				http.StatusOK,
+				"index-html",
+				voki.MessageResponse{Message: fmt.Sprintf(`
+Numéro étudiant: %s
+Nom d'usage: %s
+Programme: [%s] %s
+`,
+					membre.ID,
+					membre.Name,
+					membre.ProgrammeID,
+					membre.ProgrammeName,
+				)},
+			)
+		})
+
 		// Execution
 		e.Logger.Fatal(e.Start(
 			fmt.Sprintf(":%d", cfg.Web.Port)))

From 8af11615dda87c67d01090c8eaaea87aef285f50 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 20:16:33 -0400
Subject: [PATCH 77/99] =?UTF-8?q?adjust:=20ajouter=20emojis=20=C3=A0=20cer?=
 =?UTF-8?q?taines=20web=20responses?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/cmd.go b/cmd.go
index 3bbd59c..591bdc8 100644
--- a/cmd.go
+++ b/cmd.go
@@ -184,13 +184,13 @@ var webCmd = &cobra.Command{
 				return c.Render(
 					http.StatusOK,
 					"index-html",
-					voki.MessageResponse{Message: "Veuillez entrer un numéro étudiant à rechercher"},
+					voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"},
 				)
 			case !IsMembreID(membreID):
 				return c.Render(
 					http.StatusOK,
 					"index-html",
-					voki.MessageResponse{Message: fmt.Sprintf("Numéro étudiant '%s' invalide", membreID)},
+					voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)},
 				)
 			}
 
@@ -199,7 +199,7 @@ var webCmd = &cobra.Command{
 				return c.Render(
 					http.StatusOK,
 					"index-html",
-					voki.MessageResponse{Message: fmt.Sprintf("erreur: %s", err)},
+					voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)},
 				)
 			}
 

From 64ddfa96d69820ce8bf2829f63486f14538cbf46 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 20 Jun 2024 20:20:30 -0400
Subject: [PATCH 78/99] =?UTF-8?q?fix:=20franciser=20erreur=20de=20membre?=
 =?UTF-8?q?=20non=20trouv=C3=A9=C2=B7e?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 db.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/db.go b/db.go
index 915ddab..f0b2f4e 100644
--- a/db.go
+++ b/db.go
@@ -5,6 +5,7 @@ import (
 	_ "embed"
 	"fmt"
 
+	"github.com/jackc/pgx/v5"
 	"github.com/jackc/pgx/v5/pgxpool"
 )
 
@@ -297,6 +298,9 @@ LIMIT
 			&membre.ProgrammeID,
 			&membre.ProgrammeName,
 		); err != nil {
+			if err == pgx.ErrNoRows {
+				err = fmt.Errorf("Numéro étudiant valide mais aucun·e membre trouvé·e")
+			}
 			return
 		}
 

From d0de8115473509ff8122a51d7153c8f97bbb3563 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Fri, 21 Jun 2024 18:46:45 -0400
Subject: [PATCH 79/99] chores: update dependencies

---
 go.mod |  4 ++--
 go.sum | 10 +++++-----
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/go.mod b/go.mod
index b6ce20d..a573bf5 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	codeberg.org/vlbeaudoin/voki/v3 v3.0.0
 	github.com/jackc/pgx/v5 v5.6.0
 	github.com/labstack/echo/v4 v4.12.0
-	github.com/spf13/cobra v1.8.0
+	github.com/spf13/cobra v1.8.1
 	github.com/spf13/viper v1.19.0
 )
 
@@ -36,7 +36,7 @@ require (
 	github.com/valyala/fasttemplate v1.2.2 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/crypto v0.24.0 // indirect
-	golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
+	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
 	golang.org/x/net v0.26.0 // indirect
 	golang.org/x/sync v0.7.0 // indirect
 	golang.org/x/sys v0.21.0 // indirect
diff --git a/go.sum b/go.sum
index 73ace91..7d7ac8f 100644
--- a/go.sum
+++ b/go.sum
@@ -2,7 +2,7 @@ codeberg.org/vlbeaudoin/pave/v2 v2.0.0 h1:hfB5KnqMMu17g5QBWgLvWOsqidrYaohRfu2Lfl
 codeberg.org/vlbeaudoin/pave/v2 v2.0.0/go.mod h1:TsTfP6IA+3Ph33vLZigeJWS5vgBPgkW1tfs3zFPfycU=
 codeberg.org/vlbeaudoin/voki/v3 v3.0.0 h1:XdF/UTe9YUNj3hYrAyEvdmIMDYLL8SkqTwPkqw1yJ2c=
 codeberg.org/vlbeaudoin/voki/v3 v3.0.0/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -62,8 +62,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
 github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
 github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
 github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
@@ -89,8 +89,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
-golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
-golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
+golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
+golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=

From 14eb6c5d02c80ec50888824558f9ae41194365bb Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 3 Jul 2024 17:33:56 -0400
Subject: [PATCH 80/99] ajouter examples/example.csv

---
 examples/example.csv | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 examples/example.csv

diff --git a/examples/example.csv b/examples/example.csv
new file mode 100644
index 0000000..fce75ff
--- /dev/null
+++ b/examples/example.csv
@@ -0,0 +1,3 @@
+programme_id;nom_programme;
+000.00;test programme;
+111.11;autre test programme;

From 1f2ba0576a16fd84c0191643092025ce17131102 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 3 Jul 2024 17:34:18 -0400
Subject: [PATCH 81/99] feature: permettre insert par csv
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Ajouter parameter cfg à addRoutes()

Fix empty et default limit sur get requests (set default limit à 1000 hardcoded,
todo move to config)
---
 cmd.go    |   2 +-
 go.mod    |   1 +
 go.sum    |   2 +
 routes.go | 120 +++++++++++++++++++++++++++++++++++++++++++++---------
 4 files changed, 104 insertions(+), 21 deletions(-)

diff --git a/cmd.go b/cmd.go
index 591bdc8..8c5571d 100644
--- a/cmd.go
+++ b/cmd.go
@@ -92,7 +92,7 @@ var apiCmd = &cobra.Command{
 		}
 
 		// Routes
-		if err := addRoutes(e, db); err != nil {
+		if err := addRoutes(e, db, cfg); err != nil {
 			log.Fatal("add routes:", err)
 		}
 		/*
diff --git a/go.mod b/go.mod
index a573bf5..2c52fee 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.22.0
 require (
 	codeberg.org/vlbeaudoin/pave/v2 v2.0.0
 	codeberg.org/vlbeaudoin/voki/v3 v3.0.0
+	github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
 	github.com/jackc/pgx/v5 v5.6.0
 	github.com/labstack/echo/v4 v4.12.0
 	github.com/spf13/cobra v1.8.1
diff --git a/go.sum b/go.sum
index 7d7ac8f..e1ad5b4 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
+github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
diff --git a/routes.go b/routes.go
index a5aaed6..f7eb61c 100644
--- a/routes.go
+++ b/routes.go
@@ -1,16 +1,19 @@
 package main
 
 import (
+	"encoding/csv"
 	"fmt"
+	"io"
 	"net/http"
 	"strconv"
 
 	"codeberg.org/vlbeaudoin/pave/v2"
 	"codeberg.org/vlbeaudoin/voki/v3"
+	"github.com/gocarina/gocsv"
 	"github.com/labstack/echo/v4"
 )
 
-func addRoutes(e *echo.Echo, db *PostgresClient) error {
+func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
 	_ = db
 
 	apiPath := "/api/v7"
@@ -53,9 +56,37 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		"ProgrammesPOST", func(c echo.Context) error {
 			var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
 
-			if err := c.Bind(&request.Data); err != nil {
+			switch contentType := c.Request().Header.Get("Content-Type"); contentType {
+			case "application/json":
+				if err := c.Bind(&request.Data); err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parse request body: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+			case "text/csv":
+				body := c.Request().Body
+				if body == nil {
+					var response voki.ResponseBadRequest
+					response.Message = "empty request body cannot be parsed"
+					return c.JSON(response.StatusCode(), response)
+				}
+				defer body.Close()
+
+				gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
+					r := csv.NewReader(in)
+					r.Comma = ';'
+					return r // Allows use ; as delimiter
+				})
+
+				// Parse CSV data using gocsv
+				if err := gocsv.Unmarshal(body, &request.Data.Programmes); err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parse programmes from csv: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+			default:
 				var response voki.ResponseBadRequest
-				response.Message = fmt.Sprintf("parse request body: %s", err)
+				response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType)
 				return c.JSON(response.StatusCode(), response)
 			}
 
@@ -96,9 +127,37 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		"MembresPOST", func(c echo.Context) error {
 			var request, response = MembresPOSTRequest{}, MembresPOSTResponse{}
 
-			if err := c.Bind(&request.Data); err != nil {
+			switch contentType := c.Request().Header.Get("Content-Type"); contentType {
+			case "application/json":
+				if err := c.Bind(&request.Data); err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parse request body: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+			case "text/csv":
+				body := c.Request().Body
+				if body == nil {
+					var response voki.ResponseBadRequest
+					response.Message = "empty request body cannot be parsed"
+					return c.JSON(response.StatusCode(), response)
+				}
+				defer body.Close()
+
+				gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
+					r := csv.NewReader(in)
+					r.Comma = ';'
+					return r // Allows use ; as delimiter
+				})
+
+				// Parse CSV data using gocsv
+				if err := gocsv.Unmarshal(body, &request.Data.Membres); err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parse membres from csv: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+			default:
 				var response voki.ResponseBadRequest
-				response.Message = fmt.Sprintf("parse request body: %s", err)
+				response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType)
 				return c.JSON(response.StatusCode(), response)
 			}
 
@@ -178,11 +237,18 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		"MembresGET", func(c echo.Context) (err error) {
 			var request, response = MembresGETRequest{}, MembresGETResponse{}
 
-			request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit"))
-			if err != nil {
-				var response voki.ResponseBadRequest
-				response.Message = fmt.Sprintf("parsing limit: %s", err)
-				return c.JSON(response.StatusCode(), response)
+			queryLimit := c.QueryParam("limit")
+			if queryLimit != "" {
+				request.Query.Limit, err = strconv.Atoi(queryLimit)
+				if err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parsing limit: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+
+			} else {
+				//TODO cfg.API.DefaultLimit
+				request.Query.Limit = 1000
 			}
 
 			if !request.Complete() {
@@ -221,11 +287,18 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		"ProgrammesGET", func(c echo.Context) (err error) {
 			var request, response = ProgrammesGETRequest{}, ProgrammesGETResponse{}
 
-			request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit"))
-			if err != nil {
-				var response voki.ResponseBadRequest
-				response.Message = fmt.Sprintf("parsing limit: %s", err)
-				return c.JSON(response.StatusCode(), response)
+			queryLimit := c.QueryParam("limit")
+			if queryLimit != "" {
+				request.Query.Limit, err = strconv.Atoi(queryLimit)
+				if err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parsing limit: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+
+			} else {
+				//TODO cfg.API.DefaultLimit
+				request.Query.Limit = 1000
 			}
 
 			if !request.Complete() {
@@ -307,11 +380,18 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error {
 		"MembresDisplayGET", func(c echo.Context) (err error) {
 			var request, response = MembresDisplayGETRequest{}, MembresDisplayGETResponse{}
 
-			request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit"))
-			if err != nil {
-				var response voki.ResponseBadRequest
-				response.Message = fmt.Sprintf("parsing limit: %s", err)
-				return c.JSON(response.StatusCode(), response)
+			queryLimit := c.QueryParam("limit")
+			if queryLimit != "" {
+				request.Query.Limit, err = strconv.Atoi(queryLimit)
+				if err != nil {
+					var response voki.ResponseBadRequest
+					response.Message = fmt.Sprintf("parsing limit: %s", err)
+					return c.JSON(response.StatusCode(), response)
+				}
+
+			} else {
+				//TODO cfg.API.DefaultLimit
+				request.Query.Limit = 1000
 			}
 
 			if !request.Complete() {

From d80c7675f9ed40d1aaadd844fd69502ca6249fb9 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 3 Jul 2024 17:37:29 -0400
Subject: [PATCH 82/99] fix(routes): unused param cfg

---
 routes.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/routes.go b/routes.go
index f7eb61c..ca7dcb2 100644
--- a/routes.go
+++ b/routes.go
@@ -15,6 +15,7 @@ import (
 
 func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
 	_ = db
+	_ = cfg
 
 	apiPath := "/api/v7"
 	apiGroup := e.Group(apiPath)

From 150782c42f39f2063d999772ce968cc0e6dc491b Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 3 Jul 2024 20:51:43 -0400
Subject: [PATCH 83/99] feature(config): ajouter options TLS

---
 config.go | 45 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/config.go b/config.go
index 3096960..927227c 100644
--- a/config.go
+++ b/config.go
@@ -11,6 +11,21 @@ import (
 )
 
 const (
+	ViperAPITLSEnabled       string = "api.tls.enabled"
+	FlagAPITLSEnabled        string = "api-tls-enabled"
+	DefaultAPITLSEnabled     bool   = false
+	DescriptionAPITLSEnabled string = "Whether to use TLS or not. Requires certificate and private key files."
+
+	ViperAPITLSCertificateFile       string = "api.tls.certificate_file"
+	FlagAPITLSCertificateFile        string = "api-tls-certificate-file"
+	DefaultAPITLSCertificateFile     string = ""
+	DescriptionAPITLSCertificateFile string = "Path to TLS certificate file"
+
+	ViperAPITLSPrivateKeyFile       string = "api.tls.private_key_file"
+	FlagAPITLSPrivateKeyFile        string = "api-tls-private-key-file"
+	DefaultAPITLSPrivateKeyFile     string = ""
+	DescriptionAPITLSPrivateKeyFile string = "Path to TLS private key file"
+
 	ViperAPIPort       string = "api.port"
 	FlagAPIPort        string = "api-port"
 	DefaultAPIPort     int    = 1312
@@ -89,6 +104,15 @@ const (
 
 type Config struct {
 	API struct {
+		TLS struct {
+			Enabled bool `yaml:"enabled"`
+
+			// Path to file containing TLS certificate
+			CertificateFile string `yaml:"certificate_file"`
+
+			// Path to file containing TLS private key
+			PrivateKeyFile string `yaml:"private_key_file"`
+		}
 		Port int    `yaml:"port"`
 		Key  string `yaml:"key"`
 	} `yaml:"api"`
@@ -116,6 +140,9 @@ type Config struct {
 // DefaultConfig returns a Config filled with the default values from the
 // `Default*` constants defined in this file.
 func DefaultConfig() (cfg Config) {
+	cfg.API.TLS.Enabled = DefaultAPITLSEnabled
+	cfg.API.TLS.CertificateFile = DefaultAPITLSCertificateFile
+	cfg.API.TLS.PrivateKeyFile = DefaultAPITLSPrivateKeyFile
 	cfg.API.Port = DefaultAPIPort
 	cfg.API.Key = DefaultAPIKey
 	cfg.DB.Database = DefaultDBDatabase
@@ -145,6 +172,24 @@ func init() {
 
 	rootCmd.AddCommand(apiCmd)
 
+	// api.tls.enabled
+	apiCmd.Flags().Bool(FlagAPITLSEnabled, DefaultAPITLSEnabled, DescriptionAPITLSEnabled)
+	if err := viper.BindPFlag(ViperAPITLSEnabled, apiCmd.Flags().Lookup(FlagAPITLSEnabled)); err != nil {
+		log.Fatal(err)
+	}
+
+	// api.tls.certificate_file
+	apiCmd.Flags().String(FlagAPITLSCertificateFile, DefaultAPITLSCertificateFile, DescriptionAPITLSCertificateFile)
+	if err := viper.BindPFlag(ViperAPITLSCertificateFile, apiCmd.Flags().Lookup(FlagAPITLSCertificateFile)); err != nil {
+		log.Fatal(err)
+	}
+
+	// api.tls.private_key_file
+	apiCmd.Flags().String(FlagAPITLSPrivateKeyFile, DefaultAPITLSPrivateKeyFile, DescriptionAPITLSPrivateKeyFile)
+	if err := viper.BindPFlag(ViperAPITLSPrivateKeyFile, apiCmd.Flags().Lookup(FlagAPITLSPrivateKeyFile)); err != nil {
+		log.Fatal(err)
+	}
+
 	// api.key
 	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
 	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {

From 4ce3d9f60bc3770d40def89d931e81a47682be7a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 3 Jul 2024 20:51:57 -0400
Subject: [PATCH 84/99] feature(api): permettre d'exposer le serveur API par
 https

Requiert `cfg.API.TLS.Enabled = true` et des fichiers valides pour
`cfg.API.TLS.{CertificateFile,PrivateKeyFile}`
---
 cmd.go | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/cmd.go b/cmd.go
index 8c5571d..508af62 100644
--- a/cmd.go
+++ b/cmd.go
@@ -114,7 +114,22 @@ var apiCmd = &cobra.Command{
 		*/
 
 		// Execution
-		e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port)))
+		switch cfg.API.TLS.Enabled {
+		case false:
+			e.Logger.Fatal(
+				e.Start(
+					fmt.Sprintf(":%d", cfg.API.Port),
+				),
+			)
+		case true:
+			e.Logger.Fatal(
+				e.StartTLS(
+					fmt.Sprintf(":%d", cfg.API.Port),
+					cfg.API.TLS.CertificateFile,
+					cfg.API.TLS.PrivateKeyFile,
+				),
+			)
+		}
 	},
 }
 

From a9f16826349aaea3eff018dcb5ca0bc8e2fd0c3f Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 3 Jul 2024 20:53:17 -0400
Subject: [PATCH 85/99] fix(test): ajuster TLS client voki selon config

---
 client_test.go | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/client_test.go b/client_test.go
index a2aea93..3e1f180 100644
--- a/client_test.go
+++ b/client_test.go
@@ -22,7 +22,15 @@ func TestAPI(t *testing.T) {
 	httpClient := http.DefaultClient
 	defer httpClient.CloseIdleConnections()
 
-	vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, "http")
+	var protocol string
+	switch cfg.API.TLS.Enabled {
+	case true:
+		protocol = "https"
+	case false:
+		protocol = "http"
+	}
+
+	vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol)
 	apiClient := APIClient{vokiClient}
 
 	t.Run("get API health", func(t *testing.T) {

From 8c074dd443f5e2d43f00c4910d544d9c9738101d Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Sun, 7 Jul 2024 03:58:15 -0400
Subject: [PATCH 86/99] =?UTF-8?q?fix:=20impl=C3=A9menter=20correctement=20?=
 =?UTF-8?q?tls=20certfile=20et=20keyfile?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

test: ne pas vérifier le certificat avant de l'accepter
---
 client_test.go | 16 ++++++++++++++--
 cmd.go         |  7 +++++--
 config.go      | 38 +++++++++++++++++++-------------------
 3 files changed, 38 insertions(+), 23 deletions(-)

diff --git a/client_test.go b/client_test.go
index 3e1f180..1593e4c 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"crypto/tls"
 	"net/http"
 	"testing"
 
@@ -19,7 +20,18 @@ func TestAPI(t *testing.T) {
 		return
 	}
 
-	httpClient := http.DefaultClient
+	//httpClient := http.DefaultClient
+	//defer httpClient.CloseIdleConnections()
+
+	transport := http.Transport{
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true,
+		},
+	}
+
+	httpClient := http.Client{
+		Transport: &transport,
+	}
 	defer httpClient.CloseIdleConnections()
 
 	var protocol string
@@ -30,7 +42,7 @@ func TestAPI(t *testing.T) {
 		protocol = "http"
 	}
 
-	vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol)
+	vokiClient := voki.New(&httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol)
 	apiClient := APIClient{vokiClient}
 
 	t.Run("get API health", func(t *testing.T) {
diff --git a/cmd.go b/cmd.go
index 508af62..1518476 100644
--- a/cmd.go
+++ b/cmd.go
@@ -122,11 +122,14 @@ var apiCmd = &cobra.Command{
 				),
 			)
 		case true:
+			//TODO
+			log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.API.TLS.Certfile, cfg.API.TLS.Keyfile)
+
 			e.Logger.Fatal(
 				e.StartTLS(
 					fmt.Sprintf(":%d", cfg.API.Port),
-					cfg.API.TLS.CertificateFile,
-					cfg.API.TLS.PrivateKeyFile,
+					cfg.API.TLS.Certfile,
+					cfg.API.TLS.Keyfile,
 				),
 			)
 		}
diff --git a/config.go b/config.go
index 927227c..c13b5a0 100644
--- a/config.go
+++ b/config.go
@@ -16,15 +16,15 @@ const (
 	DefaultAPITLSEnabled     bool   = false
 	DescriptionAPITLSEnabled string = "Whether to use TLS or not. Requires certificate and private key files."
 
-	ViperAPITLSCertificateFile       string = "api.tls.certificate_file"
-	FlagAPITLSCertificateFile        string = "api-tls-certificate-file"
-	DefaultAPITLSCertificateFile     string = ""
-	DescriptionAPITLSCertificateFile string = "Path to TLS certificate file"
+	ViperAPITLSCertfile       string = "api.tls.certfile"
+	FlagAPITLSCertfile        string = "api-tls-certfile"
+	DefaultAPITLSCertfile     string = "/etc/bottin/cert.pem"
+	DescriptionAPITLSCertfile string = "Path to TLS certificate file"
 
-	ViperAPITLSPrivateKeyFile       string = "api.tls.private_key_file"
-	FlagAPITLSPrivateKeyFile        string = "api-tls-private-key-file"
-	DefaultAPITLSPrivateKeyFile     string = ""
-	DescriptionAPITLSPrivateKeyFile string = "Path to TLS private key file"
+	ViperAPITLSKeyfile       string = "api.tls.keyfile"
+	FlagAPITLSKeyfile        string = "api-tls-keyfile"
+	DefaultAPITLSKeyfile     string = "/etc/bottin/key.pem"
+	DescriptionAPITLSKeyFile string = "Path to TLS private key file"
 
 	ViperAPIPort       string = "api.port"
 	FlagAPIPort        string = "api-port"
@@ -108,11 +108,11 @@ type Config struct {
 			Enabled bool `yaml:"enabled"`
 
 			// Path to file containing TLS certificate
-			CertificateFile string `yaml:"certificate_file"`
+			Certfile string `yaml:"certfile"`
 
 			// Path to file containing TLS private key
-			PrivateKeyFile string `yaml:"private_key_file"`
-		}
+			Keyfile string `yaml:"keyfile"`
+		} `yaml:"tls"`
 		Port int    `yaml:"port"`
 		Key  string `yaml:"key"`
 	} `yaml:"api"`
@@ -141,8 +141,8 @@ type Config struct {
 // `Default*` constants defined in this file.
 func DefaultConfig() (cfg Config) {
 	cfg.API.TLS.Enabled = DefaultAPITLSEnabled
-	cfg.API.TLS.CertificateFile = DefaultAPITLSCertificateFile
-	cfg.API.TLS.PrivateKeyFile = DefaultAPITLSPrivateKeyFile
+	cfg.API.TLS.Certfile = DefaultAPITLSCertfile
+	cfg.API.TLS.Keyfile = DefaultAPITLSKeyfile
 	cfg.API.Port = DefaultAPIPort
 	cfg.API.Key = DefaultAPIKey
 	cfg.DB.Database = DefaultDBDatabase
@@ -178,15 +178,15 @@ func init() {
 		log.Fatal(err)
 	}
 
-	// api.tls.certificate_file
-	apiCmd.Flags().String(FlagAPITLSCertificateFile, DefaultAPITLSCertificateFile, DescriptionAPITLSCertificateFile)
-	if err := viper.BindPFlag(ViperAPITLSCertificateFile, apiCmd.Flags().Lookup(FlagAPITLSCertificateFile)); err != nil {
+	// api.tls.certfile
+	apiCmd.Flags().String(FlagAPITLSCertfile, DefaultAPITLSCertfile, DescriptionAPITLSCertfile)
+	if err := viper.BindPFlag(ViperAPITLSCertfile, apiCmd.Flags().Lookup(FlagAPITLSCertfile)); err != nil {
 		log.Fatal(err)
 	}
 
-	// api.tls.private_key_file
-	apiCmd.Flags().String(FlagAPITLSPrivateKeyFile, DefaultAPITLSPrivateKeyFile, DescriptionAPITLSPrivateKeyFile)
-	if err := viper.BindPFlag(ViperAPITLSPrivateKeyFile, apiCmd.Flags().Lookup(FlagAPITLSPrivateKeyFile)); err != nil {
+	// api.tls.keyfile
+	apiCmd.Flags().String(FlagAPITLSKeyfile, DefaultAPITLSKeyfile, DescriptionAPITLSKeyFile)
+	if err := viper.BindPFlag(ViperAPITLSKeyfile, apiCmd.Flags().Lookup(FlagAPITLSKeyfile)); err != nil {
 		log.Fatal(err)
 	}
 

From eb1982898cfb89e5ce74ba7b0c88ec10642d3730 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Mon, 15 Jul 2024 16:52:04 -0400
Subject: [PATCH 87/99] rework: config and cmd

Renamed `web` command to `server ui` (web is still an alias to ui)

Completely changed the config options and flags

Usage of PersistentFlags now allow clearer `--help`

BREAKING: cmd modified
BREAKING: config overhauled
BREAKING: Bump API to v8
---
 README.md           |  21 +-
 client_test.go      |  10 +-
 cmd.go              |  74 ++---
 config.go           | 664 ++++++++++++++++++++++++++------------------
 docker-compose.yaml |  27 +-
 go.mod              |   2 +-
 request.go          |  18 +-
 routes.go           |   5 +-
 8 files changed, 475 insertions(+), 346 deletions(-)

diff --git a/README.md b/README.md
index acda47f..822c4f1 100644
--- a/README.md
+++ b/README.md
@@ -20,17 +20,18 @@ https://git.agecem.com/agecem/bottin
 
 Remplir .env avec les infos qui seront utilisées pour déployer le container
 
-(Remplacer `bottin` par quelque chose de plus sécuritaire)
+Au minimum, il faut ces 3 entrées:
+
+*Remplacer `bottin` par quelque chose de plus sécuritaire*
 
 ```sh
-BOTTIN_API_KEY=bottin
-BOTTIN_POSTGRES_DATABASE=bottin
-BOTTIN_POSTGRES_PASSWORD=bottin
-BOTTIN_POSTGRES_USER=bottin
-BOTTIN_WEB_PASSWORD=bottin
-BOTTIN_WEB_USER=bottin
+BOTTIN_SERVER_DB_DATABASE=bottin
+BOTTIN_SERVER_DB_PASSWORD=bottin
+BOTTIN_SERVER_DB_USER=bottin
 ```
 
+*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options*
+
 Déployer avec docker-compose
 
 `$ docker-compose up -d`
@@ -43,13 +44,13 @@ Pour modifier la configuration du serveur API
 
 `$ docker-compose exec -it api vi /etc/bottin/api.yaml`
 
-*Y remplir au minimum le champs `api.key` (string)*
+*Y remplir au minimum le champs `server.api.key` (string)*
 
 Pour modifier la configuration du client web
 
-`$ docker-compose exec -it web vi /etc/bottin/web.yaml`
+`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml`
 
-*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.password` (string)*
+*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)*
 
 Redémarrer les containers une fois la configuration modifiée
 
diff --git a/client_test.go b/client_test.go
index 1593e4c..261eb20 100644
--- a/client_test.go
+++ b/client_test.go
@@ -34,15 +34,7 @@ func TestAPI(t *testing.T) {
 	}
 	defer httpClient.CloseIdleConnections()
 
-	var protocol string
-	switch cfg.API.TLS.Enabled {
-	case true:
-		protocol = "https"
-	case false:
-		protocol = "http"
-	}
-
-	vokiClient := voki.New(&httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol)
+	vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol)
 	apiClient := APIClient{vokiClient}
 
 	t.Run("get API health", func(t *testing.T) {
diff --git a/cmd.go b/cmd.go
index 1518476..8fbd66b 100644
--- a/cmd.go
+++ b/cmd.go
@@ -32,6 +32,11 @@ func execute() {
 	}
 }
 
+var serverCmd = &cobra.Command{
+	Use:   "server",
+	Short: "Démarrer serveurs (API ou Web UI)",
+}
+
 // apiCmd represents the api command
 var apiCmd = &cobra.Command{
 	Use:   "api",
@@ -49,10 +54,12 @@ var apiCmd = &cobra.Command{
 
 		e.Pre(middleware.AddTrailingSlash())
 
-		if cfg.API.Key != "" {
+		if cfg.Server.API.Key != "" {
 			e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
-				return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.API.Key)) == 1, nil
+				return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil
 			}))
+		} else {
+			log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)")
 		}
 
 		// DataClient
@@ -63,12 +70,12 @@ var apiCmd = &cobra.Command{
 			ctx,
 			fmt.Sprintf(
 				"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
-				cfg.DB.User,
-				cfg.DB.Password,
-				cfg.DB.Database,
-				cfg.DB.Host,
-				cfg.DB.Port,
-				cfg.DB.SSLMode,
+				cfg.Server.API.DB.User,
+				cfg.Server.API.DB.Password,
+				cfg.Server.API.DB.Database,
+				cfg.Server.API.DB.Host,
+				cfg.Server.API.DB.Port,
+				cfg.Server.API.DB.SSLMode,
 			))
 		if err != nil {
 			log.Fatal("init pgx pool:", err)
@@ -98,49 +105,50 @@ var apiCmd = &cobra.Command{
 		/*
 			h := handlers.New(client)
 
-			e.GET("/v7/health/", h.GetHealth)
+			e.GET("/v8/health/", h.GetHealth)
 
-			e.POST("/v7/membres/", h.PostMembres)
+			e.POST("/v8/membres/", h.PostMembres)
 
-			e.GET("/v7/membres/", h.ListMembres)
+			e.GET("/v8/membres/", h.ListMembres)
 
-			e.GET("/v7/membres/:membre_id/", h.ReadMembre)
+			e.GET("/v8/membres/:membre_id/", h.ReadMembre)
 
-			e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
+			e.PUT("/v8/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
 
-			e.POST("/v7/programmes/", h.PostProgrammes)
+			e.POST("/v8/programmes/", h.PostProgrammes)
 
-			e.POST("/v7/seed/", h.PostSeed)
+			e.POST("/v8/seed/", h.PostSeed)
 		*/
 
 		// Execution
-		switch cfg.API.TLS.Enabled {
+		switch cfg.Server.API.TLS.Enabled {
 		case false:
 			e.Logger.Fatal(
 				e.Start(
-					fmt.Sprintf(":%d", cfg.API.Port),
+					fmt.Sprintf(":%d", cfg.Server.API.Port),
 				),
 			)
 		case true:
 			//TODO
-			log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.API.TLS.Certfile, cfg.API.TLS.Keyfile)
+			log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.Server.API.TLS.Certfile, cfg.Server.API.TLS.Keyfile)
 
 			e.Logger.Fatal(
 				e.StartTLS(
-					fmt.Sprintf(":%d", cfg.API.Port),
-					cfg.API.TLS.Certfile,
-					cfg.API.TLS.Keyfile,
+					fmt.Sprintf(":%d", cfg.Server.API.Port),
+					cfg.Server.API.TLS.Certfile,
+					cfg.Server.API.TLS.Keyfile,
 				),
 			)
 		}
 	},
 }
 
-// webCmd represents the web command
-var webCmd = &cobra.Command{
-	Use:   "web",
-	Short: "Démarrer le client web",
-	Args:  cobra.ExactArgs(0),
+// uiCmd represents the ui command
+var uiCmd = &cobra.Command{
+	Use:     "ui",
+	Aliases: []string{"web", "interface"},
+	Short:   "Démarrer l'interface Web UI",
+	Args:    cobra.ExactArgs(0),
 	Run: func(cmd *cobra.Command, args []string) {
 		// Parse config
 		var cfg Config
@@ -157,8 +165,8 @@ var webCmd = &cobra.Command{
 
 		// Auth
 		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
-			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Web.User)) == 1
-			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1
+			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1
+			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1
 			return usersMatch && passwordsMatch, nil
 		}))
 
@@ -170,10 +178,10 @@ var webCmd = &cobra.Command{
 		// API Client
 		apiClient := APIClient{voki.New(
 			http.DefaultClient,
-			cfg.Web.API.Host,
-			cfg.Web.API.Key,
-			cfg.Web.API.Port,
-			cfg.Web.API.Protocol,
+			cfg.Server.UI.API.Host,
+			cfg.Server.UI.API.Key,
+			cfg.Server.UI.API.Port,
+			cfg.Server.UI.API.Protocol,
 		)}
 		defer apiClient.Voki.CloseIdleConnections()
 
@@ -239,6 +247,6 @@ Programme: [%s] %s
 
 		// Execution
 		e.Logger.Fatal(e.Start(
-			fmt.Sprintf(":%d", cfg.Web.Port)))
+			fmt.Sprintf(":%d", cfg.Server.UI.Port)))
 	},
 }
diff --git a/config.go b/config.go
index c13b5a0..01eecd5 100644
--- a/config.go
+++ b/config.go
@@ -10,278 +10,52 @@ import (
 	"github.com/spf13/viper"
 )
 
-const (
-	ViperAPITLSEnabled       string = "api.tls.enabled"
-	FlagAPITLSEnabled        string = "api-tls-enabled"
-	DefaultAPITLSEnabled     bool   = false
-	DescriptionAPITLSEnabled string = "Whether to use TLS or not. Requires certificate and private key files."
-
-	ViperAPITLSCertfile       string = "api.tls.certfile"
-	FlagAPITLSCertfile        string = "api-tls-certfile"
-	DefaultAPITLSCertfile     string = "/etc/bottin/cert.pem"
-	DescriptionAPITLSCertfile string = "Path to TLS certificate file"
-
-	ViperAPITLSKeyfile       string = "api.tls.keyfile"
-	FlagAPITLSKeyfile        string = "api-tls-keyfile"
-	DefaultAPITLSKeyfile     string = "/etc/bottin/key.pem"
-	DescriptionAPITLSKeyFile string = "Path to TLS private key file"
-
-	ViperAPIPort       string = "api.port"
-	FlagAPIPort        string = "api-port"
-	DefaultAPIPort     int    = 1312
-	DescriptionAPIPort string = "API server port"
-
-	ViperAPIKey       string = "api.key"
-	FlagAPIKey        string = "api-key"
-	DefaultAPIKey     string = "bottin"
-	DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)"
-
-	ViperDBDatabase       string = "db.database"
-	FlagDBDatabase        string = "db-database"
-	DefaultDBDatabase     string = "bottin"
-	DescriptionDBDatabase string = "Postgres database"
-
-	ViperDBSSLMode       string = "db.sslmode"
-	FlagDBSSLMode        string = "db-sslmode"
-	DefaultDBSSLMode     string = "prefer"
-	DescriptionDBSSLMode string = "Postgres sslmode"
-
-	ViperDBHost       string = "db.host"
-	FlagDBHost        string = "db-host"
-	DefaultDBHost     string = "db"
-	DescriptionDBHost string = "Postgres host"
-
-	ViperDBPassword       string = "db.password"
-	FlagDBPassword        string = "db-password"
-	DefaultDBPassword     string = "bottin"
-	DescriptionDBPassword string = "Postgres password"
-
-	ViperDBPort       string = "db.port"
-	FlagDBPort        string = "db-port"
-	DefaultDBPort     int    = 5432
-	DescriptionDBPort string = "Postgres port"
-
-	ViperDBUser       string = "db.user"
-	FlagDBUser        string = "db-user"
-	DefaultDBUser     string = "bottin"
-	DescriptionDBUser string = "Postgres user"
-
-	ViperWebUser       string = "web.user"
-	FlagWebUser        string = "web-user"
-	DefaultWebUser     string = "bottin"
-	DescriptionWebUser string = "Web client basic auth user"
-
-	ViperWebPassword       string = "web.password"
-	FlagWebPassword        string = "web-password"
-	DefaultWebPassword     string = "bottin"
-	DescriptionWebPassword string = "Web client basic auth password"
-
-	ViperWebPort       string = "web.port"
-	FlagWebPort        string = "web-port"
-	DefaultWebPort     int    = 2312
-	DescriptionWebPort string = "Web client port"
-
-	ViperWebAPIHost       string = "web.api.host"
-	FlagWebAPIHost        string = "web-api-host"
-	DefaultWebAPIHost     string = "api"
-	DescriptionWebAPIHost string = "Target API server host"
-
-	ViperWebAPIKey       string = "web.api.key"
-	FlagWebAPIKey        string = "web-api-key"
-	DefaultWebAPIKey     string = "bottin"
-	DescriptionWebAPIKey string = "Target API server key"
-
-	ViperWebAPIPort       string = "web.api.port"
-	FlagWebAPIPort        string = "web-api-port"
-	DefaultWebAPIPort     int    = 1312
-	DescriptionWebAPIPort string = "Target API server port"
-
-	ViperWebAPIProtocol       string = "web.api.protocol"
-	FlagWebAPIProtocol        string = "web-api-protocol"
-	DefaultWebAPIProtocol     string = "http"
-	DescriptionWebAPIProtocol string = "Target API server protocol (http/https)"
-)
-
 type Config struct {
-	API struct {
-		TLS struct {
-			Enabled bool `yaml:"enabled"`
-
-			// Path to file containing TLS certificate
-			Certfile string `yaml:"certfile"`
-
-			// Path to file containing TLS private key
-			Keyfile string `yaml:"keyfile"`
-		} `yaml:"tls"`
-		Port int    `yaml:"port"`
-		Key  string `yaml:"key"`
-	} `yaml:"api"`
-	DB struct {
-		Database string `yaml:"database"`
-		Host     string `yaml:"host"`
-		SSLMode  string `yaml:"sslmode"`
-		Password string `yaml:"password"`
-		Port     int    `yaml:"port"`
-		User     string `yaml:"user"`
-	} `yaml:"db"`
-	Web struct {
-		User     string `yaml:"user"`
-		Password string `yaml:"password"`
-		Port     int    `yaml:"port"`
-		API      struct {
+	Client struct {
+		API struct {
 			Host     string `yaml:"host"`
 			Key      string `yaml:"key"`
 			Port     int    `yaml:"port"`
 			Protocol string `yaml:"protocol"`
 		} `yaml:"api"`
-	} `yaml:"web"`
-}
+	} `yaml:"client"`
 
-// DefaultConfig returns a Config filled with the default values from the
-// `Default*` constants defined in this file.
-func DefaultConfig() (cfg Config) {
-	cfg.API.TLS.Enabled = DefaultAPITLSEnabled
-	cfg.API.TLS.Certfile = DefaultAPITLSCertfile
-	cfg.API.TLS.Keyfile = DefaultAPITLSKeyfile
-	cfg.API.Port = DefaultAPIPort
-	cfg.API.Key = DefaultAPIKey
-	cfg.DB.Database = DefaultDBDatabase
-	cfg.DB.Host = DefaultDBHost
-	cfg.DB.SSLMode = DefaultDBSSLMode
-	cfg.DB.Password = DefaultDBPassword
-	cfg.DB.Port = DefaultDBPort
-	cfg.DB.User = DefaultDBUser
-	cfg.Web.User = DefaultWebUser
-	cfg.Web.Password = DefaultWebPassword
-	cfg.Web.Port = DefaultWebPort
-	cfg.Web.API.Host = DefaultWebAPIHost
-	cfg.Web.API.Key = DefaultWebAPIKey
-	cfg.Web.API.Port = DefaultWebAPIPort
-	cfg.Web.API.Protocol = DefaultWebAPIProtocol
-	return
-}
-
-func init() {
-	// rootCmd
-
-	cobra.OnInitialize(initConfig)
-
-	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
-
-	// apiCmd
-
-	rootCmd.AddCommand(apiCmd)
-
-	// api.tls.enabled
-	apiCmd.Flags().Bool(FlagAPITLSEnabled, DefaultAPITLSEnabled, DescriptionAPITLSEnabled)
-	if err := viper.BindPFlag(ViperAPITLSEnabled, apiCmd.Flags().Lookup(FlagAPITLSEnabled)); err != nil {
-		log.Fatal(err)
-	}
-
-	// api.tls.certfile
-	apiCmd.Flags().String(FlagAPITLSCertfile, DefaultAPITLSCertfile, DescriptionAPITLSCertfile)
-	if err := viper.BindPFlag(ViperAPITLSCertfile, apiCmd.Flags().Lookup(FlagAPITLSCertfile)); err != nil {
-		log.Fatal(err)
-	}
-
-	// api.tls.keyfile
-	apiCmd.Flags().String(FlagAPITLSKeyfile, DefaultAPITLSKeyfile, DescriptionAPITLSKeyFile)
-	if err := viper.BindPFlag(ViperAPITLSKeyfile, apiCmd.Flags().Lookup(FlagAPITLSKeyfile)); err != nil {
-		log.Fatal(err)
-	}
-
-	// api.key
-	apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
-	if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
-		log.Fatal(err)
-	}
-
-	// api.port
-	apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
-	if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.database
-	apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
-	if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.sslmode
-	apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode)
-	if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.host
-	apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
-	if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.password
-	apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
-	if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.port
-	apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
-	if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// db.user
-	apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
-	if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
-		log.Fatal(err)
-	}
-
-	// WebCmd
-	rootCmd.AddCommand(webCmd)
-
-	// web.api.host
-	webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost)
-	if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.key
-	webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey)
-	if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.protocol
-	webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol)
-	if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.api.port
-	webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort)
-	if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.password
-	webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword)
-	if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.port
-	webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort)
-	if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil {
-		log.Fatal(err)
-	}
-
-	// web.user
-	webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser)
-	if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil {
-		log.Fatal(err)
-	}
+	Server struct {
+		API struct {
+			DB struct {
+				Database string `yaml:"database"`
+				Host     string `yaml:"host"`
+				Password string `yaml:"password"`
+				Port     int    `yaml:"port"`
+				SSLMode  string `yaml:"sslmode"`
+				User     string `yaml:"user"`
+			} `yaml:"db"`
+			Host string `yaml:"host"`
+			Key  string `yaml:"key"`
+			Port int    `yaml:"port"`
+			TLS  struct {
+				Enabled  bool   `yaml:"enabled"`
+				Certfile string `yaml:"certfile"`
+				Keyfile  string `yaml:"keyfile"`
+			} `yaml:"tls"`
+		} `yaml:"api"`
+		UI struct {
+			API struct {
+				Host     string `yaml:"host"`
+				Key      string `yaml:"key"`
+				Port     int    `yaml:"port"`
+				Protocol string `yaml:"protocol"`
+			} `yaml:"api"`
+			Password string `yaml:"password"`
+			Port     int    `yaml:"port"`
+			TLS      struct {
+				Enabled  bool   `yaml:"enabled"`
+				Certfile string `yaml:"certfile"`
+				Keyfile  string `yaml:"keyfile"`
+			} `yaml:"tls"`
+			User string `yaml:"user"`
+		} `yaml:"ui"`
+	} `yaml:"server"`
 }
 
 var cfgFile string
@@ -311,3 +85,363 @@ func initConfig() {
 		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
 	}
 }
+
+func init() {
+	// rootCmd
+
+	cobra.OnInitialize(initConfig)
+
+	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
+
+	// client.api.host
+	rootCmd.PersistentFlags().String(
+		"client-api-host",
+		"api",
+		"API server host",
+	)
+	if err := viper.BindPFlag(
+		"client.api.host",
+		rootCmd.PersistentFlags().Lookup("client-api-host"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// client.api.key
+	rootCmd.PersistentFlags().String(
+		"client-api-key",
+		"bottin",
+		"API server key",
+	)
+	if err := viper.BindPFlag(
+		"client.api.key",
+		rootCmd.PersistentFlags().Lookup("client-api-key"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// client.api.port
+	rootCmd.PersistentFlags().Int(
+		"client-api-port",
+		1312,
+		"API server port",
+	)
+	if err := viper.BindPFlag(
+		"client.api.port",
+		rootCmd.PersistentFlags().Lookup("client-api-port"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// client.api.protocol
+	rootCmd.PersistentFlags().String(
+		"client-api-protocol",
+		"https",
+		"API server protocol",
+	)
+	if err := viper.BindPFlag(
+		"client.api.protocol",
+		rootCmd.PersistentFlags().Lookup("client-api-protocol"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server
+	rootCmd.AddCommand(serverCmd)
+
+	// server api
+	serverCmd.AddCommand(apiCmd)
+
+	// server api db
+	// server.api.db.database
+	apiCmd.PersistentFlags().String(
+		"server-api-db-database",
+		"bottin",
+		"Postgres database name",
+	)
+	if err := viper.BindPFlag(
+		"server.api.db.database",
+		apiCmd.PersistentFlags().Lookup("server-api-db-database"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.db.host
+	apiCmd.PersistentFlags().String(
+		"server-api-db-host",
+		"db",
+		"Postgres host name",
+	)
+	if err := viper.BindPFlag(
+		"server.api.db.host",
+		apiCmd.PersistentFlags().Lookup("server-api-db-host"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.db.password
+	apiCmd.PersistentFlags().String(
+		"server-api-db-password",
+		"bottin",
+		"Postgres password",
+	)
+	if err := viper.BindPFlag(
+		"server.api.db.password",
+		apiCmd.PersistentFlags().Lookup("server-api-db-password"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.db.port
+	apiCmd.PersistentFlags().Int(
+		"server-api-db-port",
+		5432,
+		"Postgres port",
+	)
+	if err := viper.BindPFlag(
+		"server.api.db.port",
+		apiCmd.PersistentFlags().Lookup("server-api-db-port"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.db.sslmode
+	apiCmd.PersistentFlags().String(
+		"server-api-db-sslmode",
+		"prefer",
+		"Postgres sslmode",
+	)
+	if err := viper.BindPFlag(
+		"server.api.db.sslmode",
+		apiCmd.PersistentFlags().Lookup("server-api-db-sslmode"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.db.user
+	apiCmd.PersistentFlags().String(
+		"server-api-db-user",
+		"bottin",
+		"Postgres user name",
+	)
+	if err := viper.BindPFlag(
+		"server.api.db.user",
+		apiCmd.PersistentFlags().Lookup("server-api-db-user"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.host
+	apiCmd.PersistentFlags().String(
+		"server-api-host",
+		"",
+		"API server hostname or IP to answer on (empty = any)",
+	)
+	if err := viper.BindPFlag(
+		"server.api.host",
+		apiCmd.PersistentFlags().Lookup("server-api-host"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.key
+	apiCmd.PersistentFlags().String(
+		"server-api-key",
+		"bottin",
+		"API server key",
+	)
+	if err := viper.BindPFlag(
+		"server.api.key",
+		apiCmd.PersistentFlags().Lookup("server-api-key"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.port
+	apiCmd.PersistentFlags().Int(
+		"server-api-port",
+		1312,
+		"API server port",
+	)
+	if err := viper.BindPFlag(
+		"server.api.port",
+		apiCmd.PersistentFlags().Lookup("server-api-port"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server api tls
+	// server.api.tls.enabled
+	apiCmd.PersistentFlags().Bool(
+		"server-api-tls-enabled",
+		true,
+		"Use TLS for API server connections (requires certfile and keyfile)",
+	)
+	if err := viper.BindPFlag(
+		"server.api.tls.enabled",
+		apiCmd.PersistentFlags().Lookup("server-api-tls-enabled"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.tls.certfile
+	apiCmd.PersistentFlags().String(
+		"server-api-tls-certfile",
+		"/etc/bottin/cert.pem",
+		"Path to certificate file",
+	)
+	if err := viper.BindPFlag(
+		"server.api.tls.certfile",
+		apiCmd.PersistentFlags().Lookup("server-api-tls-certfile"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.api.tls.keyfile
+	apiCmd.PersistentFlags().String(
+		"server-api-tls-keyfile",
+		"/etc/bottin/key.pem",
+		"Path to private key file",
+	)
+	if err := viper.BindPFlag(
+		"server.api.tls.keyfile",
+		apiCmd.PersistentFlags().Lookup("server-api-tls-keyfile"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server ui
+	serverCmd.AddCommand(uiCmd)
+
+	// server ui api
+
+	// server.ui.api.host
+	uiCmd.PersistentFlags().String(
+		"server-ui-api-host",
+		"api",
+		"Web UI backend API server host name",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.api.host",
+		uiCmd.PersistentFlags().Lookup("server-ui-api-host"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.api.key
+	uiCmd.PersistentFlags().String(
+		"server-ui-api-key",
+		"bottin",
+		"Web UI backend API server key",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.api.key",
+		uiCmd.PersistentFlags().Lookup("server-ui-api-key"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.api.port
+	uiCmd.PersistentFlags().Int(
+		"server-ui-api-port",
+		1312,
+		"Web UI backend API server port",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.api.port",
+		uiCmd.PersistentFlags().Lookup("server-ui-api-port"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.api.protocol
+	uiCmd.PersistentFlags().String(
+		"server-ui-api-protocol",
+		"https",
+		"Web UI backend API server protocol",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.api.protocol",
+		uiCmd.PersistentFlags().Lookup("server-ui-api-protocol"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.password
+	uiCmd.PersistentFlags().String(
+		"server-ui-password",
+		"bottin",
+		"Web UI password",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.password",
+		uiCmd.PersistentFlags().Lookup("server-ui-password"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.port
+	uiCmd.PersistentFlags().Int(
+		"server-ui-port",
+		2312,
+		"Web UI port",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.port",
+		uiCmd.PersistentFlags().Lookup("server-ui-port"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.user
+	uiCmd.PersistentFlags().String(
+		"server-ui-user",
+		"bottin",
+		"Web UI user",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.user",
+		uiCmd.PersistentFlags().Lookup("server-ui-user"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server ui tls
+	// server.ui.tls.enabled
+	uiCmd.PersistentFlags().Bool(
+		"server-ui-tls-enabled",
+		true,
+		"Web UI enable TLS (requires certfile and keyfile)",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.tls.enabled",
+		uiCmd.PersistentFlags().Lookup("server-ui-tls-enabled"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.tls.certfile
+	uiCmd.PersistentFlags().String(
+		"server-ui-tls-certfile",
+		"/etc/bottin/cert.pem",
+		"Path to Web UI TLS certificate file",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.tls.certfile",
+		uiCmd.PersistentFlags().Lookup("server-ui-tls-certfile"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
+	// server.ui.tls.keyfile
+	uiCmd.PersistentFlags().String(
+		"server-ui-tls-keyfile",
+		"/etc/bottin/key.pem",
+		"Path to Web UI TLS private key file",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.tls.keyfile",
+		uiCmd.PersistentFlags().Lookup("server-ui-tls-keyfile"),
+	); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 9f2e604..1156be7 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -3,9 +3,9 @@ services:
   db:
     image: 'docker.io/library/postgres:16'
     environment:
-      POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
-      POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"
-      POSTGRES_USER: "${BOTTIN_POSTGRES_USER}"
+      POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE}"
+      POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD}"
+      POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER}"
     volumes:
       - 'db-data:/var/lib/postgresql/data'
     restart: 'unless-stopped'
@@ -15,33 +15,26 @@ services:
       - db
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
-    environment:
-      BOTTIN_DB_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
-      BOTTIN_DB_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"
-      BOTTIN_DB_USER: "${BOTTIN_POSTGRES_USER}"
-      BOTTIN_API_KEY: "${BOTTIN_API_KEY}"
+    env_file: '.env'
     ports:
       - '1312:1312'
     volumes:
       - 'api-config:/etc/bottin'
     restart: 'unless-stopped'
-    command: ['bottin', '--config', '/etc/bottin/api.yaml', 'api']
+    command: ['bottin', '--config', '/etc/bottin/api.yaml', 'server', 'api']
 
-  web:
+  ui:
     depends_on: 
       - api
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
-    environment:
-      BOTTIN_WEB_API_KEY: "${BOTTIN_API_KEY}"
-      BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD}"
-      BOTTIN_WEB_USER: "${BOTTIN_WEB_USER}"
+    env_file: '.env'
     ports:
       - '2312:2312'
     volumes:
-      - 'web-config:/etc/bottin'
+      - 'ui-config:/etc/bottin'
     restart: 'unless-stopped'
-    command: ['bottin', '--config', '/etc/bottin/web.yaml', 'web']
+    command: ['bottin', '--config', '/etc/bottin/ui.yaml', 'server', 'ui']
   
 #  adminer:
 #    image: adminer
@@ -54,4 +47,4 @@ services:
 volumes:
   db-data:
   api-config:
-  web-config:
+  ui-config:
diff --git a/go.mod b/go.mod
index 2c52fee..489b37d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.agecem.com/agecem/bottin/v7
+module git.agecem.com/agecem/bottin/v8
 
 go 1.22.0
 
diff --git a/request.go b/request.go
index 8f47f10..f1a73b4 100644
--- a/request.go
+++ b/request.go
@@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		"/api/v7/health/",
+		"/api/v8/health/",
 		nil,
 		true,
 	)
@@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPost,
-		"/api/v7/programme/",
+		"/api/v8/programme/",
 		&buf,
 		true,
 	)
@@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPost,
-		"/api/v7/membre/",
+		"/api/v8/membre/",
 		&buf,
 		true,
 	)
@@ -146,7 +146,7 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v7/membre/%s/", request.Param.MembreID),
+		fmt.Sprintf("/api/v8/membre/%s/", request.Param.MembreID),
 		nil,
 		true,
 	)
@@ -180,7 +180,7 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v7/membre/?limit=%d", request.Query.Limit),
+		fmt.Sprintf("/api/v8/membre/?limit=%d", request.Query.Limit),
 		nil,
 		true,
 	)
@@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPut,
-		fmt.Sprintf("/api/v7/membre/%s/prefered_name/", request.Param.MembreID),
+		fmt.Sprintf("/api/v8/membre/%s/prefered_name/", request.Param.MembreID),
 		&buf,
 		true,
 	)
@@ -258,7 +258,7 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v7/programme/?limit=%d", request.Query.Limit),
+		fmt.Sprintf("/api/v8/programme/?limit=%d", request.Query.Limit),
 		nil,
 		true,
 	)
@@ -292,7 +292,7 @@ func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresD
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v7/membre/display/?limit=%d", request.Query.Limit),
+		fmt.Sprintf("/api/v8/membre/display/?limit=%d", request.Query.Limit),
 		nil,
 		true,
 	)
@@ -333,7 +333,7 @@ func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDis
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v7/membre/%s/display/", request.Param.MembreID),
+		fmt.Sprintf("/api/v8/membre/%s/display/", request.Param.MembreID),
 		nil,
 		true,
 	)
diff --git a/routes.go b/routes.go
index ca7dcb2..a631aba 100644
--- a/routes.go
+++ b/routes.go
@@ -17,7 +17,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
 	_ = db
 	_ = cfg
 
-	apiPath := "/api/v7"
+	apiPath := "/api/v8"
 	apiGroup := e.Group(apiPath)
 	p := pave.New()
 	if err := pave.EchoRegister[HealthGETRequest](
@@ -248,7 +248,8 @@ func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
 				}
 
 			} else {
-				//TODO cfg.API.DefaultLimit
+				//TODO cfg.Server.API.DefaultLimit
+				//TODO cfg.Client.API.Limit
 				request.Query.Limit = 1000
 			}
 

From 03c9ad5f3c52a98d65f830cfd51d401457587c2c Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 23 Jul 2024 11:40:40 -0400
Subject: [PATCH 88/99] =?UTF-8?q?fix:=20v=C3=A9rifier=20existence=20de=20c?=
 =?UTF-8?q?ertfile=20et=20keyfile=20pour=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Au lieu de print leur valeur à l'écran
---
 cmd.go | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/cmd.go b/cmd.go
index 8fbd66b..3fe7a06 100644
--- a/cmd.go
+++ b/cmd.go
@@ -129,8 +129,13 @@ var apiCmd = &cobra.Command{
 				),
 			)
 		case true:
-			//TODO
-			log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.Server.API.TLS.Certfile, cfg.Server.API.TLS.Keyfile)
+			if cfg.Server.API.TLS.Certfile == "" {
+				log.Fatal("TLS enabled for API but no certificate file provided")
+			}
+
+			if cfg.Server.API.TLS.Keyfile == "" {
+				log.Fatal("TLS enabled for UI but no private key file provided")
+			}
 
 			e.Logger.Fatal(
 				e.StartTLS(

From 8a9decfe6cd705e22c4f0630056f7992800374d1 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 23 Jul 2024 11:44:41 -0400
Subject: [PATCH 89/99] =?UTF-8?q?fix:=20consid=C3=A9rer=20cfg.Server.API.H?=
 =?UTF-8?q?ost?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Valeur n'avait aucun effet précédemment, permet maintenant de choisir
sur quel hôte le serveur API est rejoignable
---
 cmd.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/cmd.go b/cmd.go
index 3fe7a06..a477a8a 100644
--- a/cmd.go
+++ b/cmd.go
@@ -125,7 +125,7 @@ var apiCmd = &cobra.Command{
 		case false:
 			e.Logger.Fatal(
 				e.Start(
-					fmt.Sprintf(":%d", cfg.Server.API.Port),
+					fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
 				),
 			)
 		case true:
@@ -139,7 +139,7 @@ var apiCmd = &cobra.Command{
 
 			e.Logger.Fatal(
 				e.StartTLS(
-					fmt.Sprintf(":%d", cfg.Server.API.Port),
+					fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
 					cfg.Server.API.TLS.Certfile,
 					cfg.Server.API.TLS.Keyfile,
 				),

From f5aa25a12a4ab7724736d8f4b3fe1b46ba823dfd Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 23 Jul 2024 11:46:37 -0400
Subject: [PATCH 90/99] =?UTF-8?q?feature:=20ajouter=20cfg.Server.UI.Host?=
 =?UTF-8?q?=20et=20impl=C3=A9menter=20UI=20TLS?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd.go    | 24 ++++++++++++++++++++++--
 config.go | 14 ++++++++++++++
 2 files changed, 36 insertions(+), 2 deletions(-)

diff --git a/cmd.go b/cmd.go
index a477a8a..a54cff6 100644
--- a/cmd.go
+++ b/cmd.go
@@ -251,7 +251,27 @@ Programme: [%s] %s
 		})
 
 		// Execution
-		e.Logger.Fatal(e.Start(
-			fmt.Sprintf(":%d", cfg.Server.UI.Port)))
+		switch cfg.Server.UI.TLS.Enabled {
+		case false:
+			e.Logger.Fatal(e.Start(
+				fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port)))
+		case true:
+			if cfg.Server.UI.TLS.Certfile == "" {
+				log.Fatal("TLS enabled for UI but no certificate file provided")
+			}
+
+			if cfg.Server.UI.TLS.Keyfile == "" {
+				log.Fatal("TLS enabled for UI but no private key file provided")
+			}
+
+			e.Logger.Fatal(
+				e.StartTLS(
+					fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port),
+					cfg.Server.UI.TLS.Certfile,
+					cfg.Server.UI.TLS.Keyfile,
+				),
+			)
+		}
+
 	},
 }
diff --git a/config.go b/config.go
index 01eecd5..c611e07 100644
--- a/config.go
+++ b/config.go
@@ -46,6 +46,7 @@ type Config struct {
 				Port     int    `yaml:"port"`
 				Protocol string `yaml:"protocol"`
 			} `yaml:"api"`
+			Host     string `yaml:"host"`
 			Password string `yaml:"password"`
 			Port     int    `yaml:"port"`
 			TLS      struct {
@@ -366,6 +367,19 @@ func init() {
 		log.Fatal(err)
 	}
 
+	// server.ui.host
+	uiCmd.PersistentFlags().String(
+		"server-ui-host",
+		"",
+		"Web UI host",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.host",
+		uiCmd.PersistentFlags().Lookup("server-ui-host"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
 	// server.ui.password
 	uiCmd.PersistentFlags().String(
 		"server-ui-password",

From 7ddf89a859a8ebd23defbe582eb635026cdba790 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 16:42:05 -0400
Subject: [PATCH 91/99] feature(config): add `server.ui.api.tls.skipverify`

---
 config.go | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/config.go b/config.go
index c611e07..cf521a7 100644
--- a/config.go
+++ b/config.go
@@ -45,6 +45,9 @@ type Config struct {
 				Key      string `yaml:"key"`
 				Port     int    `yaml:"port"`
 				Protocol string `yaml:"protocol"`
+				TLS      struct {
+					SkipVerify bool `yaml:"skipverify"`
+				} `yaml:"tls"`
 			} `yaml:"api"`
 			Host     string `yaml:"host"`
 			Password string `yaml:"password"`
@@ -367,6 +370,19 @@ func init() {
 		log.Fatal(err)
 	}
 
+	// server.ui.api.tls.skipverify
+	uiCmd.PersistentFlags().Bool(
+		"server-ui-api-tls-skipverify",
+		false,
+		"Skip API server TLS certificate verification",
+	)
+	if err := viper.BindPFlag(
+		"server.ui.api.tls.skipverify",
+		uiCmd.PersistentFlags().Lookup("server-ui-api-tls-skipverify"),
+	); err != nil {
+		log.Fatal(err)
+	}
+
 	// server.ui.host
 	uiCmd.PersistentFlags().String(
 		"server-ui-host",

From 2b6c631d64b07554f64ead5c6b57b9c46e876202 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 16:42:25 -0400
Subject: [PATCH 92/99] fix(compose): adjust `.env` inject

---
 docker-compose.yaml | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/docker-compose.yaml b/docker-compose.yaml
index 1156be7..4def68e 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -3,9 +3,9 @@ services:
   db:
     image: 'docker.io/library/postgres:16'
     environment:
-      POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE}"
-      POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD}"
-      POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER}"
+      POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}"
+      POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}"
+      POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}"
     volumes:
       - 'db-data:/var/lib/postgresql/data'
     restart: 'unless-stopped'
@@ -15,7 +15,13 @@ services:
       - db
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
-    env_file: '.env'
+    env:
+      BOTTIN_SERVER_API_DB_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}"
+      BOTTIN_SERVER_API_DB_HOST: "${BOTTIN_SERVER_API_DB_HOST:-db}"
+      BOTTIN_SERVER_API_DB_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}"
+      BOTTIN_SERVER_API_DB_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}"
+      #BOTTIN_SERVER_API_HOST: "${BOTTIN_SERVER_API_HOST:}"
+      #BOTTIN_SERVER_API_KEY: "${BOTTIN_SERVER_API_KEY
     ports:
       - '1312:1312'
     volumes:
@@ -28,7 +34,9 @@ services:
       - api
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
-    env_file: '.env'
+    env:
+      BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD:-bottin}"
+      BOTTIN_WEB_USER: "${BOTTIN_WEB_USER:-bottin}"
     ports:
       - '2312:2312'
     volumes:

From 9072f7114ac46551484ab74b79e2a82238b01605 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 16:43:22 -0400
Subject: [PATCH 93/99] =?UTF-8?q?feature(cmd):=20impl=C3=A9menter=20UI=20A?=
 =?UTF-8?q?PI=20TLS=20skip=20verify?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd.go | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/cmd.go b/cmd.go
index a54cff6..e801dd3 100644
--- a/cmd.go
+++ b/cmd.go
@@ -3,6 +3,7 @@ package main
 import (
 	"context"
 	"crypto/subtle"
+	"crypto/tls"
 	"fmt"
 	"html/template"
 	"log"
@@ -181,8 +182,15 @@ var uiCmd = &cobra.Command{
 		}
 
 		// API Client
+		var httpClient = &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{
+					InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify,
+				},
+			},
+		}
 		apiClient := APIClient{voki.New(
-			http.DefaultClient,
+			httpClient,
 			cfg.Server.UI.API.Host,
 			cfg.Server.UI.API.Key,
 			cfg.Server.UI.API.Port,

From bdff81c6b23bbe2eaa40d889fe2ec0b6915a1443 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 16:58:25 -0400
Subject: [PATCH 94/99] =?UTF-8?q?feature(compose):=20directly=20inject=20.?=
 =?UTF-8?q?env=20file=20=C3=A0=20containers=20api=20et=20ui?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 docker-compose.yaml | 18 +++++-------------
 1 file changed, 5 insertions(+), 13 deletions(-)

diff --git a/docker-compose.yaml b/docker-compose.yaml
index 4def68e..5487eb1 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -3,9 +3,9 @@ services:
   db:
     image: 'docker.io/library/postgres:16'
     environment:
-      POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}"
-      POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}"
-      POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}"
+      POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:?}"
+      POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:?}"
+      POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:?}"
     volumes:
       - 'db-data:/var/lib/postgresql/data'
     restart: 'unless-stopped'
@@ -15,13 +15,7 @@ services:
       - db
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
-    env:
-      BOTTIN_SERVER_API_DB_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}"
-      BOTTIN_SERVER_API_DB_HOST: "${BOTTIN_SERVER_API_DB_HOST:-db}"
-      BOTTIN_SERVER_API_DB_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}"
-      BOTTIN_SERVER_API_DB_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}"
-      #BOTTIN_SERVER_API_HOST: "${BOTTIN_SERVER_API_HOST:}"
-      #BOTTIN_SERVER_API_KEY: "${BOTTIN_SERVER_API_KEY
+    env_file: '.env'
     ports:
       - '1312:1312'
     volumes:
@@ -34,9 +28,7 @@ services:
       - api
     build: .
     image: 'git.agecem.com/agecem/bottin:latest'
-    env:
-      BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD:-bottin}"
-      BOTTIN_WEB_USER: "${BOTTIN_WEB_USER:-bottin}"
+    env_file: '.env'
     ports:
       - '2312:2312'
     volumes:

From c9a45f8db868ecc429cda87a5eb7209c7bfeb32d Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 17:01:34 -0400
Subject: [PATCH 95/99] chores(Dockerfile): bump golang -> 1.23.0

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 6479581..8d7516d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.22.3 as build
+FROM golang:1.23.0 as build
 
 LABEL author="vlbeaudoin"
 

From 882553521a8551a2d9c6f578375cc497a1506088 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 17:02:20 -0400
Subject: [PATCH 96/99] chores(Dockerfile): bump alpine -> 3.20.3

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 6479581..462394a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 go build -a -o bottin .
 
 # Alpine
 
-FROM alpine:3.20
+FROM alpine:3.20.3
 
 WORKDIR /app
 

From 0640395fd2ac905f61208ddf9fb7667aae8c59f6 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Tue, 3 Sep 2024 17:06:10 -0400
Subject: [PATCH 97/99] fix(Dockerfile): utiliser version de alpine qui existe
 actually

---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index e08a2f7..18bfd2d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 go build -a -o bottin .
 
 # Alpine
 
-FROM alpine:3.20.3
+FROM alpine:3.20.2
 
 WORKDIR /app
 

From b419a5b26045474e6b9bd4349e5134b247c47c22 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Wed, 18 Sep 2024 19:06:33 -0400
Subject: [PATCH 98/99] =?UTF-8?q?major:=20s=C3=A9parer=20commande=20de=20l?=
 =?UTF-8?q?ibrairie=20importable?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Bump major version à 9

package main déplacé vers cmd/bottin/ pour garder `go install` qui nomme
l'exécutable `bottin`, sans empêcher d'importer le code à l'extérieur du
projet avec pkg/bottin/.

Déplacer fichiers SQL vers queries/

Déplacer fichiers html vers templates/

Ajouter scripts/ avec génération et injection de certificats x509
(https) et les ajouter au Makefile

Ajouter début d'exemple de manifests dans deployments/kubernetes/
---
 .gitignore                                    |   2 +
 Dockerfile                                    |   8 +-
 Makefile                                      |  12 +
 README.md                                     |  58 +--
 cmd.go                                        | 285 ---------------
 config.go => cmd/bottin/main.go               | 344 +++++++++++++++---
 client_test.go => cmd/bottin/main_test.go     |  13 +-
 docker-compose.yaml => compose.yaml           |   5 +-
 deployments/kubernetes/bottin-pod.yaml        |  60 +++
 .../kubernetes/example-bottin-secret.yaml     |  26 ++
 go.mod                                        |   2 +-
 main.go                                       |  18 -
 client.go => pkg/bottin/client.go             |   2 +-
 pkg/bottin/config.go                          |  53 +++
 db.go => pkg/bottin/db.go                     |  13 +-
 entity.go => pkg/bottin/entity.go             |   2 +-
 request.go => pkg/bottin/request.go           |  20 +-
 response.go => pkg/bottin/response.go         |   2 +-
 routes.go => pkg/bottin/routes.go             |   6 +-
 queries/queries.go                            |  14 +
 {sql => queries}/schema.sql                   |   0
 {sql => queries}/views.sql                    |   0
 scripts/compose-inject-x509.sh                |   6 +
 scripts/generate-self-signed-x509.sh          |   2 +
 template.go => templates/templates.go         |  11 +-
 25 files changed, 513 insertions(+), 451 deletions(-)
 delete mode 100644 cmd.go
 rename config.go => cmd/bottin/main.go (56%)
 rename client_test.go => cmd/bottin/main_test.go (91%)
 rename docker-compose.yaml => compose.yaml (95%)
 create mode 100644 deployments/kubernetes/bottin-pod.yaml
 create mode 100644 deployments/kubernetes/example-bottin-secret.yaml
 delete mode 100644 main.go
 rename client.go => pkg/bottin/client.go (99%)
 create mode 100644 pkg/bottin/config.go
 rename db.go => pkg/bottin/db.go (97%)
 rename entity.go => pkg/bottin/entity.go (98%)
 rename request.go => pkg/bottin/request.go (94%)
 rename response.go => pkg/bottin/response.go (99%)
 rename routes.go => pkg/bottin/routes.go (99%)
 create mode 100644 queries/queries.go
 rename {sql => queries}/schema.sql (100%)
 rename {sql => queries}/views.sql (100%)
 create mode 100755 scripts/compose-inject-x509.sh
 create mode 100755 scripts/generate-self-signed-x509.sh
 rename template.go => templates/templates.go (55%)

diff --git a/.gitignore b/.gitignore
index e2f7237..a8c4332 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,8 @@
 # Dependency directories (remove the comment below to include it)
 # vendor/
 
+# cert files
+*.pem
 
 # env
 .env
diff --git a/Dockerfile b/Dockerfile
index 18bfd2d..d24bb50 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,12 +4,14 @@ LABEL author="vlbeaudoin"
 
 WORKDIR /go/src/app
 
-COPY go.mod go.sum client.go client_test.go cmd.go config.go db.go entity.go main.go request.go response.go routes.go template.go ./
+COPY go.mod go.sum LICENSE ./
 
-ADD sql/ sql/
+ADD cmd/ cmd/
+ADD pkg/ pkg/
+ADD queries/ queries/
 ADD templates/ templates/
 
-RUN CGO_ENABLED=0 go build -a -o bottin .
+RUN CGO_ENABLED=0 go build -a -o bottin ./cmd/bottin
 
 # Alpine
 
diff --git a/Makefile b/Makefile
index 31a6c23..827fc0c 100644
--- a/Makefile
+++ b/Makefile
@@ -14,3 +14,15 @@ help: ## Show this help
 .PHONY: test-integration
 test-integration: ## run integration tests through API client. Config is read from `~/.bottin.yaml`. WARNING: affects data in the database, do not run on production server
 	docker-compose down && docker-compose up -d --build && sleep 2 && go test
+
+.PHONY: dev
+dev: generate-self-signed-x509 compose-inject-x509 ## deploy development environment on docker-compose
+	docker-compose up -d
+
+.PHONY: generate-self-signed-x509
+generate-self-signed-x509: ## Générer une paire de clés x509 self-signed pour utilisation avec un serveur de développement
+	./scripts/generate-self-signed-x509.sh
+
+.PHONY: compose-inject-x509
+compose-inject-x509: ## Copie la paire de clés x509 du current directory vers les containers orchestrés par docker-compose
+	./scripts/compose-inject-x509.sh
diff --git a/README.md b/README.md
index 822c4f1..a024676 100644
--- a/README.md
+++ b/README.md
@@ -1,57 +1 @@
-# agecem/bottin
-
-Bottin de la masse étudiante, en Go
-
-https://git.agecem.com/agecem/bottin
-
-## fonctionalités
-
-### Serveur API
-
-- Insertion de membre et programme
-- Lecture de membre
-- Modification du nom d'usage de membre
-
-### Client web
-
-- Lecture de membre par requête au serveur API
-
-## usage
-
-Remplir .env avec les infos qui seront utilisées pour déployer le container
-
-Au minimum, il faut ces 3 entrées:
-
-*Remplacer `bottin` par quelque chose de plus sécuritaire*
-
-```sh
-BOTTIN_SERVER_DB_DATABASE=bottin
-BOTTIN_SERVER_DB_PASSWORD=bottin
-BOTTIN_SERVER_DB_USER=bottin
-```
-
-*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options*
-
-Déployer avec docker-compose
-
-`$ docker-compose up -d`
-
-### Optionnel: configuration par fichiers YAML
-
-*seulement nécessaire si les fichiers `.env` et `docker-compose.yaml` ne contiennent pas toute l'information nécessaire*
-
-Pour modifier la configuration du serveur API
-
-`$ docker-compose exec -it api vi /etc/bottin/api.yaml`
-
-*Y remplir au minimum le champs `server.api.key` (string)*
-
-Pour modifier la configuration du client web
-
-`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml`
-
-*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)*
-
-Redémarrer les containers une fois la configuration modifiée
-
-`$ docker-compose down && docker-compose up -d`
+Requiert un fichier .env ici pour un déploiement avec base de donnée
diff --git a/cmd.go b/cmd.go
deleted file mode 100644
index e801dd3..0000000
--- a/cmd.go
+++ /dev/null
@@ -1,285 +0,0 @@
-package main
-
-import (
-	"context"
-	"crypto/subtle"
-	"crypto/tls"
-	"fmt"
-	"html/template"
-	"log"
-	"net/http"
-	"os"
-
-	"codeberg.org/vlbeaudoin/voki/v3"
-	"github.com/jackc/pgx/v5/pgxpool"
-	"github.com/labstack/echo/v4"
-	"github.com/labstack/echo/v4/middleware"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "bottin",
-	Short: "Bottin étudiant de l'AGECEM",
-}
-
-// execute adds all child commands to the root command and sets flags appropriately.
-// This is called by main.main(). It only needs to happen once to the rootCmd.
-func execute() {
-	err := rootCmd.Execute()
-	if err != nil {
-		os.Exit(1)
-	}
-}
-
-var serverCmd = &cobra.Command{
-	Use:   "server",
-	Short: "Démarrer serveurs (API ou Web UI)",
-}
-
-// apiCmd represents the api command
-var apiCmd = &cobra.Command{
-	Use:   "api",
-	Short: "Démarrer le serveur API",
-	Args:  cobra.ExactArgs(0),
-	Run: func(cmd *cobra.Command, args []string) {
-		var cfg Config
-		if err := viper.Unmarshal(&cfg); err != nil {
-			log.Fatal("parse config:", err)
-		}
-
-		e := echo.New()
-
-		// Middlewares
-
-		e.Pre(middleware.AddTrailingSlash())
-
-		if cfg.Server.API.Key != "" {
-			e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
-				return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil
-			}))
-		} else {
-			log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)")
-		}
-
-		// DataClient
-		ctx := context.Background()
-
-		//prep
-		pool, err := pgxpool.New(
-			ctx,
-			fmt.Sprintf(
-				"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
-				cfg.Server.API.DB.User,
-				cfg.Server.API.DB.Password,
-				cfg.Server.API.DB.Database,
-				cfg.Server.API.DB.Host,
-				cfg.Server.API.DB.Port,
-				cfg.Server.API.DB.SSLMode,
-			))
-		if err != nil {
-			log.Fatal("init pgx pool:", err)
-		}
-		defer pool.Close()
-
-		db := &PostgresClient{
-			Ctx:  ctx,
-			Pool: pool,
-		}
-		if err := db.Pool.Ping(ctx); err != nil {
-			log.Fatal("ping db:", err)
-		}
-
-		if err := db.CreateOrReplaceSchema(); err != nil {
-			log.Fatal("create or replace schema:", err)
-		}
-
-		if err := db.CreateOrReplaceViews(); err != nil {
-			log.Fatal("create or replace views:", err)
-		}
-
-		// Routes
-		if err := addRoutes(e, db, cfg); err != nil {
-			log.Fatal("add routes:", err)
-		}
-		/*
-			h := handlers.New(client)
-
-			e.GET("/v8/health/", h.GetHealth)
-
-			e.POST("/v8/membres/", h.PostMembres)
-
-			e.GET("/v8/membres/", h.ListMembres)
-
-			e.GET("/v8/membres/:membre_id/", h.ReadMembre)
-
-			e.PUT("/v8/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
-
-			e.POST("/v8/programmes/", h.PostProgrammes)
-
-			e.POST("/v8/seed/", h.PostSeed)
-		*/
-
-		// Execution
-		switch cfg.Server.API.TLS.Enabled {
-		case false:
-			e.Logger.Fatal(
-				e.Start(
-					fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
-				),
-			)
-		case true:
-			if cfg.Server.API.TLS.Certfile == "" {
-				log.Fatal("TLS enabled for API but no certificate file provided")
-			}
-
-			if cfg.Server.API.TLS.Keyfile == "" {
-				log.Fatal("TLS enabled for UI but no private key file provided")
-			}
-
-			e.Logger.Fatal(
-				e.StartTLS(
-					fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
-					cfg.Server.API.TLS.Certfile,
-					cfg.Server.API.TLS.Keyfile,
-				),
-			)
-		}
-	},
-}
-
-// uiCmd represents the ui command
-var uiCmd = &cobra.Command{
-	Use:     "ui",
-	Aliases: []string{"web", "interface"},
-	Short:   "Démarrer l'interface Web UI",
-	Args:    cobra.ExactArgs(0),
-	Run: func(cmd *cobra.Command, args []string) {
-		// Parse config
-		var cfg Config
-		if err := viper.Unmarshal(&cfg); err != nil {
-			log.Fatal("init config:", err)
-		}
-
-		e := echo.New()
-
-		// Middlewares
-
-		// Trailing slash
-		e.Pre(middleware.AddTrailingSlash())
-
-		// Auth
-		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
-			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1
-			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1
-			return usersMatch && passwordsMatch, nil
-		}))
-
-		// Templating
-		e.Renderer = &Template{
-			templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
-		}
-
-		// API Client
-		var httpClient = &http.Client{
-			Transport: &http.Transport{
-				TLSClientConfig: &tls.Config{
-					InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify,
-				},
-			},
-		}
-		apiClient := APIClient{voki.New(
-			httpClient,
-			cfg.Server.UI.API.Host,
-			cfg.Server.UI.API.Key,
-			cfg.Server.UI.API.Port,
-			cfg.Server.UI.API.Protocol,
-		)}
-		defer apiClient.Voki.CloseIdleConnections()
-
-		// Routes
-		e.GET("/", func(c echo.Context) error {
-			pingResult, err := apiClient.GetHealth()
-			if err != nil {
-				return c.Render(
-					http.StatusOK,
-					"index-html",
-					voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)},
-				)
-			}
-
-			return c.Render(
-				http.StatusOK,
-				"index-html",
-				voki.MessageResponse{Message: pingResult},
-			)
-		})
-
-		e.GET("/membre/", func(c echo.Context) error {
-			membreID := c.QueryParam("membre_id")
-			switch {
-			case membreID == "":
-				return c.Render(
-					http.StatusOK,
-					"index-html",
-					voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"},
-				)
-			case !IsMembreID(membreID):
-				return c.Render(
-					http.StatusOK,
-					"index-html",
-					voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)},
-				)
-			}
-
-			membre, err := apiClient.GetMembreForDisplay(membreID)
-			if err != nil {
-				return c.Render(
-					http.StatusOK,
-					"index-html",
-					voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)},
-				)
-			}
-
-			return c.Render(
-				http.StatusOK,
-				"index-html",
-				voki.MessageResponse{Message: fmt.Sprintf(`
-Numéro étudiant: %s
-Nom d'usage: %s
-Programme: [%s] %s
-`,
-					membre.ID,
-					membre.Name,
-					membre.ProgrammeID,
-					membre.ProgrammeName,
-				)},
-			)
-		})
-
-		// Execution
-		switch cfg.Server.UI.TLS.Enabled {
-		case false:
-			e.Logger.Fatal(e.Start(
-				fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port)))
-		case true:
-			if cfg.Server.UI.TLS.Certfile == "" {
-				log.Fatal("TLS enabled for UI but no certificate file provided")
-			}
-
-			if cfg.Server.UI.TLS.Keyfile == "" {
-				log.Fatal("TLS enabled for UI but no private key file provided")
-			}
-
-			e.Logger.Fatal(
-				e.StartTLS(
-					fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port),
-					cfg.Server.UI.TLS.Certfile,
-					cfg.Server.UI.TLS.Keyfile,
-				),
-			)
-		}
-
-	},
-}
diff --git a/config.go b/cmd/bottin/main.go
similarity index 56%
rename from config.go
rename to cmd/bottin/main.go
index cf521a7..526a344 100644
--- a/config.go
+++ b/cmd/bottin/main.go
@@ -1,67 +1,25 @@
 package main
 
 import (
+	"context"
+	"crypto/subtle"
+	"crypto/tls"
 	"fmt"
 	"log"
+	"net/http"
 	"os"
 	"strings"
 
+	"codeberg.org/vlbeaudoin/voki/v3"
+	"git.agecem.com/agecem/bottin/v9/pkg/bottin"
+	"git.agecem.com/agecem/bottin/v9/templates"
+	"github.com/jackc/pgx/v5/pgxpool"
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/echo/v4/middleware"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 )
 
-type Config struct {
-	Client struct {
-		API struct {
-			Host     string `yaml:"host"`
-			Key      string `yaml:"key"`
-			Port     int    `yaml:"port"`
-			Protocol string `yaml:"protocol"`
-		} `yaml:"api"`
-	} `yaml:"client"`
-
-	Server struct {
-		API struct {
-			DB struct {
-				Database string `yaml:"database"`
-				Host     string `yaml:"host"`
-				Password string `yaml:"password"`
-				Port     int    `yaml:"port"`
-				SSLMode  string `yaml:"sslmode"`
-				User     string `yaml:"user"`
-			} `yaml:"db"`
-			Host string `yaml:"host"`
-			Key  string `yaml:"key"`
-			Port int    `yaml:"port"`
-			TLS  struct {
-				Enabled  bool   `yaml:"enabled"`
-				Certfile string `yaml:"certfile"`
-				Keyfile  string `yaml:"keyfile"`
-			} `yaml:"tls"`
-		} `yaml:"api"`
-		UI struct {
-			API struct {
-				Host     string `yaml:"host"`
-				Key      string `yaml:"key"`
-				Port     int    `yaml:"port"`
-				Protocol string `yaml:"protocol"`
-				TLS      struct {
-					SkipVerify bool `yaml:"skipverify"`
-				} `yaml:"tls"`
-			} `yaml:"api"`
-			Host     string `yaml:"host"`
-			Password string `yaml:"password"`
-			Port     int    `yaml:"port"`
-			TLS      struct {
-				Enabled  bool   `yaml:"enabled"`
-				Certfile string `yaml:"certfile"`
-				Keyfile  string `yaml:"keyfile"`
-			} `yaml:"tls"`
-			User string `yaml:"user"`
-		} `yaml:"ui"`
-	} `yaml:"server"`
-}
-
 var cfgFile string
 
 // initConfig reads in config file and ENV variables if set.
@@ -90,6 +48,17 @@ func initConfig() {
 	}
 }
 
+func main() {
+	/* TODO
+	if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
+		log.Fatal(err)
+	}
+	*/
+
+	// Handle the command-line via cobra and viper
+	execute()
+}
+
 func init() {
 	// rootCmd
 
@@ -475,3 +444,274 @@ func init() {
 		log.Fatal(err)
 	}
 }
+
+/* TODO
+func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {
+	return fmt.Errorf("not implemented")
+}
+*/
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "bottin",
+	Short: "Bottin étudiant de l'AGECEM",
+}
+
+// execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+var serverCmd = &cobra.Command{
+	Use:   "server",
+	Short: "Démarrer serveurs (API ou Web UI)",
+}
+
+// apiCmd represents the api command
+var apiCmd = &cobra.Command{
+	Use:   "api",
+	Short: "Démarrer le serveur API",
+	Args:  cobra.ExactArgs(0),
+	Run: func(cmd *cobra.Command, args []string) {
+		var cfg bottin.Config
+		if err := viper.Unmarshal(&cfg); err != nil {
+			log.Fatal("parse config:", err)
+		}
+
+		e := echo.New()
+
+		// Middlewares
+
+		e.Pre(middleware.AddTrailingSlash())
+
+		if cfg.Server.API.Key != "" {
+			e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
+				return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil
+			}))
+		} else {
+			log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)")
+		}
+
+		// DataClient
+		ctx := context.Background()
+
+		//prep
+		pool, err := pgxpool.New(
+			ctx,
+			fmt.Sprintf(
+				"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
+				cfg.Server.API.DB.User,
+				cfg.Server.API.DB.Password,
+				cfg.Server.API.DB.Database,
+				cfg.Server.API.DB.Host,
+				cfg.Server.API.DB.Port,
+				cfg.Server.API.DB.SSLMode,
+			))
+		if err != nil {
+			log.Fatal("init pgx pool:", err)
+		}
+		defer pool.Close()
+
+		db := &bottin.PostgresClient{
+			Ctx:  ctx,
+			Pool: pool,
+		}
+		if err := db.Pool.Ping(ctx); err != nil {
+			log.Fatal("ping db:", err)
+		}
+
+		if err := db.CreateOrReplaceSchema(); err != nil {
+			log.Fatal("create or replace schema:", err)
+		}
+
+		if err := db.CreateOrReplaceViews(); err != nil {
+			log.Fatal("create or replace views:", err)
+		}
+
+		// Routes
+		if err := bottin.AddRoutes(e, db, cfg); err != nil {
+			log.Fatal("add routes:", err)
+		}
+		/*
+			h := handlers.New(client)
+
+			e.GET("/v9/health/", h.GetHealth)
+
+			e.POST("/v9/membres/", h.PostMembres)
+
+			e.GET("/v9/membres/", h.ListMembres)
+
+			e.GET("/v9/membres/:membre_id/", h.ReadMembre)
+
+			e.PUT("/v9/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
+
+			e.POST("/v9/programmes/", h.PostProgrammes)
+
+			e.POST("/v9/seed/", h.PostSeed)
+		*/
+
+		// Execution
+		switch cfg.Server.API.TLS.Enabled {
+		case false:
+			e.Logger.Fatal(
+				e.Start(
+					fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
+				),
+			)
+		case true:
+			if cfg.Server.API.TLS.Certfile == "" {
+				log.Fatal("TLS enabled for API but no certificate file provided")
+			}
+
+			if cfg.Server.API.TLS.Keyfile == "" {
+				log.Fatal("TLS enabled for UI but no private key file provided")
+			}
+
+			e.Logger.Fatal(
+				e.StartTLS(
+					fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
+					cfg.Server.API.TLS.Certfile,
+					cfg.Server.API.TLS.Keyfile,
+				),
+			)
+		}
+	},
+}
+
+// uiCmd represents the ui command
+var uiCmd = &cobra.Command{
+	Use:     "ui",
+	Aliases: []string{"web", "interface"},
+	Short:   "Démarrer l'interface Web UI",
+	Args:    cobra.ExactArgs(0),
+	Run: func(cmd *cobra.Command, args []string) {
+		// Parse config
+		var cfg bottin.Config
+		if err := viper.Unmarshal(&cfg); err != nil {
+			log.Fatal("init config:", err)
+		}
+
+		e := echo.New()
+
+		// Middlewares
+
+		// Trailing slash
+		e.Pre(middleware.AddTrailingSlash())
+
+		// Auth
+		e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
+			usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1
+			passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1
+			return usersMatch && passwordsMatch, nil
+		}))
+
+		// Templating
+		e.Renderer = templates.NewTemplate()
+
+		// API Client
+		var httpClient = &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{
+					InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify,
+				},
+			},
+		}
+		apiClient := bottin.APIClient{
+			Voki: voki.New(
+				httpClient,
+				cfg.Server.UI.API.Host,
+				cfg.Server.UI.API.Key,
+				cfg.Server.UI.API.Port,
+				cfg.Server.UI.API.Protocol,
+			)}
+		defer apiClient.Voki.CloseIdleConnections()
+
+		// Routes
+		e.GET("/", func(c echo.Context) error {
+			pingResult, err := apiClient.GetHealth()
+			if err != nil {
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)},
+				)
+			}
+
+			return c.Render(
+				http.StatusOK,
+				"index-html",
+				voki.MessageResponse{Message: pingResult},
+			)
+		})
+
+		e.GET("/membre/", func(c echo.Context) error {
+			membreID := c.QueryParam("membre_id")
+			switch {
+			case membreID == "":
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"},
+				)
+			case !bottin.IsMembreID(membreID):
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)},
+				)
+			}
+
+			membre, err := apiClient.GetMembreForDisplay(membreID)
+			if err != nil {
+				return c.Render(
+					http.StatusOK,
+					"index-html",
+					voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)},
+				)
+			}
+
+			return c.Render(
+				http.StatusOK,
+				"index-html",
+				voki.MessageResponse{Message: fmt.Sprintf(`
+Numéro étudiant: %s
+Nom d'usage: %s
+Programme: [%s] %s
+`,
+					membre.ID,
+					membre.Name,
+					membre.ProgrammeID,
+					membre.ProgrammeName,
+				)},
+			)
+		})
+
+		// Execution
+		switch cfg.Server.UI.TLS.Enabled {
+		case false:
+			e.Logger.Fatal(e.Start(
+				fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port)))
+		case true:
+			if cfg.Server.UI.TLS.Certfile == "" {
+				log.Fatal("TLS enabled for UI but no certificate file provided")
+			}
+
+			if cfg.Server.UI.TLS.Keyfile == "" {
+				log.Fatal("TLS enabled for UI but no private key file provided")
+			}
+
+			e.Logger.Fatal(
+				e.StartTLS(
+					fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port),
+					cfg.Server.UI.TLS.Certfile,
+					cfg.Server.UI.TLS.Keyfile,
+				),
+			)
+		}
+
+	},
+}
diff --git a/client_test.go b/cmd/bottin/main_test.go
similarity index 91%
rename from client_test.go
rename to cmd/bottin/main_test.go
index 261eb20..f8b6685 100644
--- a/client_test.go
+++ b/cmd/bottin/main_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"codeberg.org/vlbeaudoin/voki/v3"
+	"git.agecem.com/agecem/bottin/v9/pkg/bottin"
 	"github.com/spf13/viper"
 )
 
@@ -14,7 +15,7 @@ func init() {
 }
 
 func TestAPI(t *testing.T) {
-	var cfg Config
+	var cfg bottin.Config
 	if err := viper.Unmarshal(&cfg); err != nil {
 		t.Error(err)
 		return
@@ -35,7 +36,7 @@ func TestAPI(t *testing.T) {
 	defer httpClient.CloseIdleConnections()
 
 	vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol)
-	apiClient := APIClient{vokiClient}
+	apiClient := bottin.APIClient{Voki: vokiClient}
 
 	t.Run("get API health", func(t *testing.T) {
 		health, err := apiClient.GetHealth()
@@ -53,9 +54,9 @@ func TestAPI(t *testing.T) {
 
 	t.Run("insert programmes",
 		func(t *testing.T) {
-			programmes := []Programme{
-				{"404.42", "Cool programme"},
-				{"200.10", "Autre programme"},
+			programmes := []bottin.Programme{
+				{ID: "404.42", Name: "Cool programme"},
+				{ID: "200.10", Name: "Autre programme"},
 			}
 			t.Log("programmes:", programmes)
 			_, err := apiClient.InsertProgrammes(programmes...)
@@ -64,7 +65,7 @@ func TestAPI(t *testing.T) {
 			}
 		})
 
-	testMembres := []Membre{
+	testMembres := []bottin.Membre{
 		{
 			ID:          "0000000",
 			FirstName:   "Test",
diff --git a/docker-compose.yaml b/compose.yaml
similarity index 95%
rename from docker-compose.yaml
rename to compose.yaml
index 5487eb1..686312c 100644
--- a/docker-compose.yaml
+++ b/compose.yaml
@@ -1,3 +1,4 @@
+name: 'bottin'
 services:
 
   db:
@@ -13,7 +14,7 @@ services:
   api:
     depends_on:
       - db
-    build: .
+    build: ../..
     image: 'git.agecem.com/agecem/bottin:latest'
     env_file: '.env'
     ports:
@@ -26,7 +27,7 @@ services:
   ui:
     depends_on: 
       - api
-    build: .
+    build: ../..
     image: 'git.agecem.com/agecem/bottin:latest'
     env_file: '.env'
     ports:
diff --git a/deployments/kubernetes/bottin-pod.yaml b/deployments/kubernetes/bottin-pod.yaml
new file mode 100644
index 0000000..72569e4
--- /dev/null
+++ b/deployments/kubernetes/bottin-pod.yaml
@@ -0,0 +1,60 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: bottin-pod
+spec:
+  initContainers:
+    - name: clone
+      image: alpine:3.20
+      command: ['sh', '-c']
+      args:
+        - apk add git &&
+          git clone -- https://git.agecem.com/agecem/bottin /opt/bottin-src
+      volumeMounts:
+        - name: bottin-src
+          mountPath: /opt/bottin-src
+    - name: build
+      image: golang:1.23
+      env:
+        - name: CGO_ENABLED
+          value: '0'
+      command: ['sh', '-c']
+      args:
+        - cd /opt/bottin-src &&
+          go build -a -o /opt/bottin-executable/bottin
+      volumeMounts:
+        - name: bottin-src
+          mountPath: /opt/bottin-src
+        - name: bottin-executable
+          mountPath: /opt/bottin-executable
+  containers:
+    - name: api
+      image: alpine:3.20
+      command: ['sh', '-c']
+      args:
+        - ln -s /opt/bottin-executable/bottin /usr/bin/bottin
+      volumeMounts:
+        - name: bottin-executable
+          mountPath: /opt/bottin-executable
+        - name: bottin-secret
+          readOnly: true
+          mountPath: '/etc/bottin'
+    - name: ui
+      image: alpine:3.20
+      command: ['sh', '-c']
+      args:
+        - bottin --config /etc/bottin/ui.yaml server ui
+      volumeMounts:
+        - name: bottin-executable
+          mountPath: /opt/bottin-executable
+        - name: bottin-secret
+          readOnly: true
+          mountPath: '/etc/bottin'
+  volumes:
+    - name: bottin-src
+      emptyDir: {}
+    - name: bottin-executable
+      emptyDir: {}
+    - name: bottin-secret
+      secret:
+        secretName: bottin-secret
diff --git a/deployments/kubernetes/example-bottin-secret.yaml b/deployments/kubernetes/example-bottin-secret.yaml
new file mode 100644
index 0000000..36bed97
--- /dev/null
+++ b/deployments/kubernetes/example-bottin-secret.yaml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: bottin-secret
+stringData:
+  api.yaml: |
+    bottin:
+      server:
+        api:
+          db:
+            database: 'bottin'
+            host: 'db.example.com'
+            password: 'bottin'
+            sslmode: 'require'
+            user: 'bottin'
+          key: 'bottin'
+  ui.yaml: |
+    bottin:
+      server:
+        ui:
+          api:
+            tls:
+              skipverify: 'true'
+            key: 'bottin'
+          password: 'bottin'
+          user: 'bottin'
diff --git a/go.mod b/go.mod
index 489b37d..e3da402 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.agecem.com/agecem/bottin/v8
+module git.agecem.com/agecem/bottin/v9
 
 go 1.22.0
 
diff --git a/main.go b/main.go
deleted file mode 100644
index 6f81738..0000000
--- a/main.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package main
-
-func main() {
-	/* TODO
-	if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
-		log.Fatal(err)
-	}
-	*/
-
-	// Handle the command-line via cobra and viper
-	execute()
-}
-
-/* TODO
-func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {
-	return fmt.Errorf("not implemented")
-}
-*/
diff --git a/client.go b/pkg/bottin/client.go
similarity index 99%
rename from client.go
rename to pkg/bottin/client.go
index e4958a6..529d9c8 100644
--- a/client.go
+++ b/pkg/bottin/client.go
@@ -1,4 +1,4 @@
-package main
+package bottin
 
 import (
 	"fmt"
diff --git a/pkg/bottin/config.go b/pkg/bottin/config.go
new file mode 100644
index 0000000..b0c4650
--- /dev/null
+++ b/pkg/bottin/config.go
@@ -0,0 +1,53 @@
+package bottin
+
+type Config struct {
+	Client struct {
+		API struct {
+			Host     string `yaml:"host"`
+			Key      string `yaml:"key"`
+			Port     int    `yaml:"port"`
+			Protocol string `yaml:"protocol"`
+		} `yaml:"api"`
+	} `yaml:"client"`
+
+	Server struct {
+		API struct {
+			DB struct {
+				Database string `yaml:"database"`
+				Host     string `yaml:"host"`
+				Password string `yaml:"password"`
+				Port     int    `yaml:"port"`
+				SSLMode  string `yaml:"sslmode"`
+				User     string `yaml:"user"`
+			} `yaml:"db"`
+			Host string `yaml:"host"`
+			Key  string `yaml:"key"`
+			Port int    `yaml:"port"`
+			TLS  struct {
+				Enabled  bool   `yaml:"enabled"`
+				Certfile string `yaml:"certfile"`
+				Keyfile  string `yaml:"keyfile"`
+			} `yaml:"tls"`
+		} `yaml:"api"`
+		UI struct {
+			API struct {
+				Host     string `yaml:"host"`
+				Key      string `yaml:"key"`
+				Port     int    `yaml:"port"`
+				Protocol string `yaml:"protocol"`
+				TLS      struct {
+					SkipVerify bool `yaml:"skipverify"`
+				} `yaml:"tls"`
+			} `yaml:"api"`
+			Host     string `yaml:"host"`
+			Password string `yaml:"password"`
+			Port     int    `yaml:"port"`
+			TLS      struct {
+				Enabled  bool   `yaml:"enabled"`
+				Certfile string `yaml:"certfile"`
+				Keyfile  string `yaml:"keyfile"`
+			} `yaml:"tls"`
+			User string `yaml:"user"`
+		} `yaml:"ui"`
+	} `yaml:"server"`
+}
diff --git a/db.go b/pkg/bottin/db.go
similarity index 97%
rename from db.go
rename to pkg/bottin/db.go
index f0b2f4e..0769fbc 100644
--- a/db.go
+++ b/pkg/bottin/db.go
@@ -1,20 +1,15 @@
-package main
+package bottin
 
 import (
 	"context"
 	_ "embed"
 	"fmt"
 
+	"git.agecem.com/agecem/bottin/v9/queries"
 	"github.com/jackc/pgx/v5"
 	"github.com/jackc/pgx/v5/pgxpool"
 )
 
-//go:embed sql/schema.sql
-var sqlSchema string
-
-//go:embed sql/views.sql
-var sqlViews string
-
 type PostgresClient struct {
 	//TODO move context out of client
 	Ctx  context.Context
@@ -22,12 +17,12 @@ type PostgresClient struct {
 }
 
 func (db *PostgresClient) CreateOrReplaceSchema() error {
-	_, err := db.Pool.Exec(db.Ctx, sqlSchema)
+	_, err := db.Pool.Exec(db.Ctx, queries.SQLSchema())
 	return err
 }
 
 func (db *PostgresClient) CreateOrReplaceViews() error {
-	_, err := db.Pool.Exec(db.Ctx, sqlViews)
+	_, err := db.Pool.Exec(db.Ctx, queries.SQLViews())
 	return err
 }
 
diff --git a/entity.go b/pkg/bottin/entity.go
similarity index 98%
rename from entity.go
rename to pkg/bottin/entity.go
index 672f54c..8279b9e 100644
--- a/entity.go
+++ b/pkg/bottin/entity.go
@@ -1,4 +1,4 @@
-package main
+package bottin
 
 import "unicode"
 
diff --git a/request.go b/pkg/bottin/request.go
similarity index 94%
rename from request.go
rename to pkg/bottin/request.go
index f1a73b4..555ddab 100644
--- a/request.go
+++ b/pkg/bottin/request.go
@@ -1,4 +1,4 @@
-package main
+package bottin
 
 import (
 	"bytes"
@@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		"/api/v8/health/",
+		"/api/v9/health/",
 		nil,
 		true,
 	)
@@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPost,
-		"/api/v8/programme/",
+		"/api/v9/programme/",
 		&buf,
 		true,
 	)
@@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPost,
-		"/api/v8/membre/",
+		"/api/v9/membre/",
 		&buf,
 		true,
 	)
@@ -146,7 +146,7 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v8/membre/%s/", request.Param.MembreID),
+		fmt.Sprintf("/api/v9/membre/%s/", request.Param.MembreID),
 		nil,
 		true,
 	)
@@ -180,7 +180,7 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v8/membre/?limit=%d", request.Query.Limit),
+		fmt.Sprintf("/api/v9/membre/?limit=%d", request.Query.Limit),
 		nil,
 		true,
 	)
@@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodPut,
-		fmt.Sprintf("/api/v8/membre/%s/prefered_name/", request.Param.MembreID),
+		fmt.Sprintf("/api/v9/membre/%s/prefered_name/", request.Param.MembreID),
 		&buf,
 		true,
 	)
@@ -258,7 +258,7 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v8/programme/?limit=%d", request.Query.Limit),
+		fmt.Sprintf("/api/v9/programme/?limit=%d", request.Query.Limit),
 		nil,
 		true,
 	)
@@ -292,7 +292,7 @@ func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresD
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v8/membre/display/?limit=%d", request.Query.Limit),
+		fmt.Sprintf("/api/v9/membre/display/?limit=%d", request.Query.Limit),
 		nil,
 		true,
 	)
@@ -333,7 +333,7 @@ func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDis
 
 	statusCode, body, err := v.CallAndParse(
 		http.MethodGet,
-		fmt.Sprintf("/api/v8/membre/%s/display/", request.Param.MembreID),
+		fmt.Sprintf("/api/v9/membre/%s/display/", request.Param.MembreID),
 		nil,
 		true,
 	)
diff --git a/response.go b/pkg/bottin/response.go
similarity index 99%
rename from response.go
rename to pkg/bottin/response.go
index 19818df..cd034f1 100644
--- a/response.go
+++ b/pkg/bottin/response.go
@@ -1,4 +1,4 @@
-package main
+package bottin
 
 import (
 	"fmt"
diff --git a/routes.go b/pkg/bottin/routes.go
similarity index 99%
rename from routes.go
rename to pkg/bottin/routes.go
index a631aba..dc97041 100644
--- a/routes.go
+++ b/pkg/bottin/routes.go
@@ -1,4 +1,4 @@
-package main
+package bottin
 
 import (
 	"encoding/csv"
@@ -13,11 +13,11 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
-func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
+func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
 	_ = db
 	_ = cfg
 
-	apiPath := "/api/v8"
+	apiPath := "/api/v9"
 	apiGroup := e.Group(apiPath)
 	p := pave.New()
 	if err := pave.EchoRegister[HealthGETRequest](
diff --git a/queries/queries.go b/queries/queries.go
new file mode 100644
index 0000000..0fa44d8
--- /dev/null
+++ b/queries/queries.go
@@ -0,0 +1,14 @@
+package queries
+
+import (
+	_ "embed"
+)
+
+//go:embed schema.sql
+var sqlSchema string
+
+//go:embed views.sql
+var sqlViews string
+
+func SQLSchema() string { return sqlSchema }
+func SQLViews() string  { return sqlViews }
diff --git a/sql/schema.sql b/queries/schema.sql
similarity index 100%
rename from sql/schema.sql
rename to queries/schema.sql
diff --git a/sql/views.sql b/queries/views.sql
similarity index 100%
rename from sql/views.sql
rename to queries/views.sql
diff --git a/scripts/compose-inject-x509.sh b/scripts/compose-inject-x509.sh
new file mode 100755
index 0000000..42ed90c
--- /dev/null
+++ b/scripts/compose-inject-x509.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+docker-compose cp cert.pem api:/etc/bottin/cert.pem
+docker-compose cp key.pem api:/etc/bottin/key.pem
+docker-compose cp cert.pem ui:/etc/bottin/cert.pem
+docker-compose cp key.pem ui:/etc/bottin/key.pem
diff --git a/scripts/generate-self-signed-x509.sh b/scripts/generate-self-signed-x509.sh
new file mode 100755
index 0000000..2e1a5bc
--- /dev/null
+++ b/scripts/generate-self-signed-x509.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
diff --git a/template.go b/templates/templates.go
similarity index 55%
rename from template.go
rename to templates/templates.go
index a5b4980..2539cde 100644
--- a/template.go
+++ b/templates/templates.go
@@ -1,4 +1,4 @@
-package main
+package templates
 
 import (
 	"embed"
@@ -8,7 +8,7 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
-//go:embed templates/*
+//go:embed *.html
 var templatesFS embed.FS
 
 type Template struct {
@@ -18,3 +18,10 @@ type Template struct {
 func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
 	return t.templates.ExecuteTemplate(w, name, data)
 }
+
+// NewTemplate returns a new Template instance with templates embedded from *.html
+func NewTemplate() *Template {
+	return &Template{
+		templates: template.Must(template.ParseFS(templatesFS, "*.html")),
+	}
+}

From 1ad0d614776e110d73b1466843a5b313b2897512 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>
Date: Thu, 19 Sep 2024 14:41:24 -0400
Subject: [PATCH 99/99] docs: rapporter ancien README.md

---
 README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 58 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index a024676..bf96431 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,58 @@
-Requiert un fichier .env ici pour un déploiement avec base de donnée
+# agecem/bottin
+
+Bottin de la masse étudiante, en Go
+
+https://git.agecem.com/agecem/bottin
+
+## fonctionalités
+
+### Serveur API
+
+- Insertion de membre et programme
+- Lecture de membre
+- Modification du nom d'usage de membre
+
+### Client web
+
+- Lecture de membre par requête au serveur API
+
+## usage
+
+Remplir .env avec les infos qui seront utilisées pour déployer le container
+
+Au minimum, il faut ces 3 entrées:
+
+*Remplacer `bottin` par quelque chose de plus sécuritaire*
+
+```sh
+BOTTIN_SERVER_DB_DATABASE=bottin
+BOTTIN_SERVER_DB_PASSWORD=bottin
+BOTTIN_SERVER_DB_USER=bottin
+```
+
+*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options*
+
+Déployer avec docker-compose
+
+`$ docker-compose up -d`
+
+### Optionnel: configuration par fichiers YAML
+
+*seulement nécessaire si les fichiers `.env` et `docker-compose.yaml` ne contiennent pas toute l'information nécessaire*
+
+Pour modifier la configuration du serveur API
+
+`$ docker-compose exec -it api vi /etc/bottin/api.yaml`
+
+*Y remplir au minimum le champs `server.api.key` (string)*
+
+Pour modifier la configuration du client web
+
+`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml`
+
+*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)*
+
+Redémarrer les containers une fois la configuration modifiée
+
+`$ docker-compose down && docker-compose up -d`
+v