Bump API à version 2 #2

Merged
vlbeaudoin merged 9 commits from bump/v2 into main 2023-06-03 20:42:35 -04:00
23 changed files with 889 additions and 742 deletions

3
.env Normal file
View file

@ -0,0 +1,3 @@
BOTTINAGENDA_POSTGRES_DATABASE=bottin-agenda
BOTTINAGENDA_POSTGRES_PASSWORD=bottin-agenda
BOTTINAGENDA_POSTGRES_USER=bottin-agenda

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# ---> Go
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.x86_64
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# env
.env
# .swp
*.swp

View file

@ -1,15 +1,25 @@
FROM docker.io/library/golang:1.18 FROM golang:1.20.4 as build
LABEL author="Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>" LABEL author="vlbeaudoin"
LABEL repo="https://git.agecem.com/agecem/bottin-agenda"
WORKDIR /go/src/app 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 . && \ RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin-agenda .
go install -v .
CMD bottin-agenda -h # Alpine
FROM alpine:latest
WORKDIR /app
COPY --from=build /go/src/app/bottin-agenda /usr/bin/bottin-agenda
CMD ["bottin-agenda", "--help"]

22
LICENSE
View file

@ -1,21 +1,9 @@
The MIT License (MIT) MIT License
Copyright © 2022 AGECEM & Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org> Copyright (c) 2023 AGECEM
Permission is hereby granted, free of charge, to any person obtaining a copy 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:
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 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 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.
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.

View file

@ -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 build -t localhost/agecem/bottin-agenda:latest .

View file

@ -2,14 +2,6 @@
Application web de distribution d'agendas pour le Centre Multi-Services. Application web de distribution d'agendas pour le Centre Multi-Services.
## Utilisation
Tiens une liste de `data.Membre{}` qui ont reçuEs leur agenda. Utilise une serveur [agecem/bottin](https://git.agecem.com/agecem/bottin)
L'application doit exposer un API specification sur `/v1` pour son utilisation, ainsi que des commandes CLI pour les manipulations de database.
## Prérequis ## Prérequis
Nécessite une installation fonctionnelle et accessible de [agecem/bottin](https://git.agecem.com/agecem/bottin). Nécessite une installation fonctionnelle et accessible de [agecem/bottin](https://git.agecem.com/agecem/bottin).

149
cmd/api.go Normal file
View file

@ -0,0 +1,149 @@
package cmd
import (
"crypto/subtle"
"fmt"
"log"
"git.agecem.com/agecem/bottin-agenda/data"
"git.agecem.com/agecem/bottin-agenda/handlers"
bottindata "git.agecem.com/agecem/bottin/v5/data"
"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("api.key")
apiPort = viper.GetInt("api.port")
bottinApiKey := viper.GetString("bottin.api.key")
bottinApiHost := viper.GetString("bottin.api.host")
bottinApiProtocol := viper.GetString("bottin.api.protocol")
bottinApiPort := viper.GetInt("bottin.api.port")
// Using bottin's API client
bottinConnection := bottindata.NewApiClient(
bottinApiKey,
bottinApiHost,
bottinApiProtocol,
bottinApiPort,
)
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
}))
}
// Routes
e.GET("/v2/health/", handlers.GetHealth)
e.GET("/v2/membres/:membre_id/", handlers.GetMembre)
// Check bottin is ready
bottinHealthResponse, err := bottinConnection.GetHealth()
if err != nil {
log.Fatalf("[bottin] bottinConnection.GetHealth(): %s", err)
}
log.Println("[bottin] ok: ", bottinHealthResponse)
// Check database is ready
dataClient, err := data.NewDataClientFromViper()
if err != nil {
log.Fatalf("[bottin-agenda db] data.NewDataclientFromViper(): %s", err)
}
defer dataClient.DB.Close()
if err := dataClient.DB.Ping(); err != nil {
log.Fatalf("[bottin-agenda db] dataClient.DB.Ping(): %s", err)
} else {
log.Println("[bottin-agenda db] ok")
}
// Execution
e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
},
}
func init() {
rootCmd.AddCommand(apiCmd)
// api.key
apiCmd.Flags().String(
"api-key", "bottin-agenda",
"API server key. Leave empty for no key auth. (config: 'api.key')")
viper.BindPFlag("api.key", apiCmd.Flags().Lookup("api-key"))
// api.port
apiCmd.Flags().Int(
"api-port", 1313,
"API server port (config:'api.port')")
viper.BindPFlag("api.port", apiCmd.Flags().Lookup("api-port"))
// bottin.api.host
apiCmd.Flags().String(
"bottin-api-host", "api",
"Remote bottin API server host (config:'bottin.api.host')")
viper.BindPFlag("bottin.api.host", apiCmd.Flags().Lookup("bottin-api-host"))
// bottin.api.key
apiCmd.Flags().String(
"bottin-api-key", "bottin",
"Remote bottin API server key (config:'bottin.api.key')")
viper.BindPFlag("bottin.api.key", apiCmd.Flags().Lookup("bottin-api-key"))
// bottin.api.protocol
apiCmd.Flags().String(
"bottin-api-protocol", "http",
"Remote bottin API server protocol (config:'bottin.api.protocol')")
viper.BindPFlag("bottin.api.protocol", apiCmd.Flags().Lookup("bottin-api-protocol"))
// bottin.api.port
apiCmd.Flags().Int(
"bottin-api-port", 1312,
"Remote bottin API server port (config:'bottin.api.port')")
viper.BindPFlag("bottin.api.port", apiCmd.Flags().Lookup("bottin-api-port"))
// db.database
apiCmd.Flags().String("db-database", "bottin-agenda", "Postgres database (config:'db.database')")
viper.BindPFlag("db.database", apiCmd.Flags().Lookup("db-database"))
// db.host
apiCmd.Flags().String("db-host", "db", "Postgres host (config:'db.host')")
viper.BindPFlag("db.host", apiCmd.Flags().Lookup("db-host"))
// db.password
apiCmd.Flags().String("db-password", "bottin-agenda", "Postgres password (config:'db.password')")
viper.BindPFlag("db.password", apiCmd.Flags().Lookup("db-password"))
// db.port
apiCmd.Flags().Int("db-port", 5432, "Postgres port (config:'db.port')")
viper.BindPFlag("db.port", apiCmd.Flags().Lookup("db-port"))
// db.user
apiCmd.Flags().String("db-user", "bottin-agenda", "Postgres user (config:'db.user')")
viper.BindPFlag("db.user", apiCmd.Flags().Lookup("db-user"))
}

View file

@ -1,7 +1,7 @@
package cmd package cmd
import ( import (
"log" "fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -13,10 +13,7 @@ var cfgFile string
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "bottin-agenda", Use: "bottin-agenda",
Short: "Application de distribution d'agendas", Short: "Application de gestion de distribution d'agendas",
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
} }
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
@ -31,56 +28,7 @@ func Execute() {
func init() { func init() {
cobra.OnInitialize(initConfig) 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-agenda.yaml)") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin-agenda.yaml)")
// bottin.host
rootCmd.PersistentFlags().String(
"bottin-host", "",
"The host of the bottin instance (config: 'bottin.host')")
viper.BindPFlag(
"bottin.host",
rootCmd.PersistentFlags().Lookup("bottin-host"))
rootCmd.MarkPersistentFlagRequired("bottin.host")
// bottin.protocol
rootCmd.PersistentFlags().String(
"bottin-protocol", "http",
"The protocol of the bottin instance (config: 'bottin.protocol')")
viper.BindPFlag(
"bottin.protocol",
rootCmd.PersistentFlags().Lookup("bottin-protocol"))
rootCmd.MarkPersistentFlagRequired("bottin.protocol")
// bottin.port
rootCmd.PersistentFlags().Int(
"bottin-port", 1312,
"The port to the bottin instance (config: 'bottin.port')")
viper.BindPFlag(
"bottin.port",
rootCmd.PersistentFlags().Lookup("bottin-port"))
rootCmd.MarkPersistentFlagRequired("bottin.port")
// bottin.username
rootCmd.PersistentFlags().String(
"bottin-username", "",
"The username to the bottin instance (config: 'bottin.username')")
viper.BindPFlag(
"bottin.username",
rootCmd.PersistentFlags().Lookup("bottin-username"))
rootCmd.MarkPersistentFlagRequired("bottin.username")
// bottin.password
rootCmd.PersistentFlags().String(
"bottin-password", "",
"The password to the bottin instance (config: 'bottin.password')")
viper.BindPFlag(
"bottin.password",
rootCmd.PersistentFlags().Lookup("bottin-password"))
rootCmd.MarkPersistentFlagRequired("bottin.password")
} }
// initConfig reads in config file and ENV variables if set. // initConfig reads in config file and ENV variables if set.
@ -103,6 +51,6 @@ func initConfig() {
// If a config file is found, read it in. // If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil { if err := viper.ReadInConfig(); err == nil {
log.Printf("Using config file: %s\n", viper.ConfigFileUsed()) fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
} }
} }

View file

@ -1,131 +0,0 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"git.agecem.com/agecem/bottin-agenda/data"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
protocol, host, username, password string
port int
check bool
)
// scanCmd represents the scan command
var scanCmd = &cobra.Command{
Use: "scan",
Short: "Check a bottin instance for existing membre",
Run: func(cmd *cobra.Command, args []string) {
args_length := len(args)
if args_length != 1 {
log.Fatalf("[e] Wrong number of arguments, got '%d' needs 1", args_length)
} else {
// Search locally for corresponding Membre
membre, err := readMembre(args[0])
if err != nil {
log.Printf("[e] %s", err)
}
membre_exists := (membre.ID != 0)
if membre_exists {
log.Fatal("[e] Membre is already in the bottin-agenda database")
} else {
log.Println("[i] Membre not already in bottin-agenda, confirming existence with bottin")
// Search in bottin for corresponding Membre
membre_bottin, err := readMembreBottin(args[0])
if err != nil {
log.Fatalf("[e] %s", err)
}
membre_bottin_exists := (membre_bottin.ID != 0)
if membre_bottin_exists {
log.Println("[ok] Membre found in bottin")
if !check {
log.Println("[i] Adding to bottin-agenda")
membre_bottin.CreatedAt = time.Now()
membre_bottin.UpdatedAt = time.Now()
// Add to bottin-agenda database
data.InsertMembre(&membre_bottin)
} else {
log.Fatal("[i] Skipping insert because of --check flag")
}
} else {
log.Fatal("[e] Membre does not exist in bottin database")
}
}
}
},
}
func init() {
rootCmd.AddCommand(scanCmd)
// check
scanCmd.PersistentFlags().BoolVar(
&check, "check", false,
"Do not write anything to database.")
}
func updateFlagsBottin() {
protocol = viper.GetString("bottin.protocol")
host = viper.GetString("bottin.host")
port = viper.GetInt("bottin.port")
username = viper.GetString("bottin.username")
password = viper.GetString("bottin.password")
}
func readMembre(num_etud string) (data.Membre, error) {
data.OpenDatabase()
data.MigrateDatabase()
membre, err := data.ReadMembre(num_etud)
if err != nil {
return membre, err
}
return membre, nil
}
func readMembreBottin(num_etud string) (data.Membre, error) {
updateFlagsBottin()
membre := data.Membre{}
request := fmt.Sprintf("%s://%s:%s@%s:%d/v1/membre/%s", protocol, username, password, host, port, num_etud)
response, err := http.Get(request)
if err != nil {
return membre, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return membre, err
}
err = json.Unmarshal([]byte(body), &membre)
if err != nil {
return membre, err
}
return membre, nil
}

View file

@ -1,259 +0,0 @@
package cmd
import (
"crypto/subtle"
_ "embed"
"errors"
"fmt"
"net/http"
"time"
"git.agecem.com/agecem/bottin-agenda/data"
"git.agecem.com/agecem/bottin-agenda/embed"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
html string
)
// serverCmd represents the server command
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run the bottin-agenda server",
Run: func(cmd *cobra.Command, args []string) {
data.OpenDatabase()
data.MigrateDatabase()
runServer()
},
}
func init() {
declareFlags()
rootCmd.AddCommand(serverCmd)
html = embed.ReadHtml()
}
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", 1313,
"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-agenda/static",
"DEPRECATED 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", "agenda",
"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", "agenda",
"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"))
}
func runServer() {
port := fmt.Sprintf(":%d", viper.GetInt("server.port"))
e := echo.New()
g := e.Group("")
e.Pre(middleware.RemoveTrailingSlash())
g.Use(middleware.BasicAuth(basicAuther))
g.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339_nano} method=${method}, uri=${uri}, status=${status}" + "\n",
}))
// v1 routes
g.GET("/v1", showAPISpec)
g.GET("/v1/scan/:num_etud", showScan)
g.POST("/v1/scan/:num_etud", postScan)
// html routes
registerHTMLRoutes(g)
e.Logger.Fatal(e.Start(port))
}
func basicAuther(username, password string, context echo.Context) (bool, error) {
if subtle.ConstantTimeCompare([]byte(username), []byte(viper.GetString("login.username"))) == 1 &&
subtle.ConstantTimeCompare([]byte(password), []byte(viper.GetString("login.password"))) == 1 {
return true, nil
}
return false, nil
}
func showAPISpec(c echo.Context) error {
return c.String(http.StatusOK, `agecem/bottin-agenda
API Spec
-----
"/v1" | GET | Show API specifications
"/v1/scan/:num_etud" | GET | Show JSON representing a membre with :num_etud
"/v1/scan/:num_etud" | POST | Scan bottin for membre with :num_etud and add to local db if not already present
-----
`)
}
// Handler for "-X GET /v1/scan/:num_etud"
func showScan(c echo.Context) error {
num_etud := c.Param("num_etud")
membre, err := readMembre(num_etud)
if err != nil {
return c.JSON(http.StatusOK, map[string]error{
"message": err,
})
}
return c.JSON(200, membre)
}
// Handler for "-X POST /v1/scan/:num_etud"
func postScan(c echo.Context) error {
num_etud := c.Param("num_etud")
membre, err := scanMembre(num_etud)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": fmt.Sprintf("Erreur lors de l'insertion de Membre '%s'", num_etud),
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": fmt.Sprintf("Membre '%s' scannéE avec succès et peut reçevoir son agenda.", membre.NumEtud),
})
}
// Tries to insert a membre into the local db and return it with any error encountered
func scanMembre(num_etud string) (data.Membre, error) {
membre, err := readMembre(num_etud)
if num_etud == "" {
}
if err != nil {
return membre, err
}
membre_exists := (membre.ID != 0)
membre_bottin, err := readMembreBottin(num_etud)
if err != nil {
return membre, err
}
membre_bottin_exists := (membre_bottin.ID != 0)
if membre_exists {
return membre, errors.New(fmt.Sprintf("Membre '%s' a déjà reçuE son agenda", num_etud))
}
if !membre_bottin_exists {
return membre, errors.New(fmt.Sprintf("Membre '%s' non-trouvéE dans le bottin. Est-ce bien entré?", num_etud))
}
// This should happen if membre is not scanned and is present in bottin
membre_bottin.CreatedAt = time.Now()
membre_bottin.UpdatedAt = time.Now()
err, _ = data.InsertMembre(&membre_bottin)
if err != nil {
return membre_bottin, err
}
return membre_bottin, nil
}
func registerHTMLRoutes(g *echo.Group) {
g.GET("/", showScanHTML)
g.POST("/", postScanHTML)
}
func showScanHTML(c echo.Context) error {
num_etud := c.QueryParam("num_etud")
if num_etud == "" {
return c.HTML(http.StatusOK, html)
}
html_filled := html
if num_etud != "" {
html_filled = fmt.Sprintf(`%s
Numéro étudiant: %s`, html, num_etud)
}
return c.HTML(http.StatusOK, html_filled)
}
func postScanHTML(c echo.Context) error {
num_etud := c.FormValue("num_etud")
if num_etud == "" {
return c.HTML(http.StatusOK, html)
}
membre, err := scanMembre(num_etud)
if err != nil {
return c.HTML(http.StatusInternalServerError, fmt.Sprintf(`%s
Erreur: %s`, html, err))
}
html_filled := html
if num_etud != "" {
if membre.ID == 0 {
html_filled = fmt.Sprintf(`%s
Erreur lors de l'insertion de membre %s. Veuillez SVP signaler cette erreur.`, html, num_etud)
} else {
html_filled = fmt.Sprintf(`%s
Membre '%s' scannéE avec succès et peut recevoir son agenda.`, html, num_etud)
}
}
return c.HTML(http.StatusOK, html_filled)
}

137
data/apiclient.go Normal file
View file

@ -0,0 +1,137 @@
package data
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
)
type ApiClient struct {
Key string
Host string
Port int
Protocol string
}
func NewApiClient(key, host, protocol string, port int) *ApiClient {
return &ApiClient{
Key: key,
Host: host,
Port: port,
Protocol: 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,
)
/*
//TODO
log.Println("endpoint: ", endpoint)
*/
// 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
}
// BottinHealthResponse is the response type for GetBottinHealth
type BottinHealthResponse struct {
Message string `json:"message"`
}
// GetHealth allows checking for API server health
func (a *ApiClient) GetBottinHealth() (string, error) {
var healthResponse BottinHealthResponse
response, err := a.Call(http.MethodGet, "/v4/health", nil, true)
if err != nil {
return healthResponse.Message, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return healthResponse.Message, err
}
if err := json.Unmarshal(body, &healthResponse); err != nil {
return healthResponse.Message, err
}
if healthResponse.Message == "" {
return healthResponse.Message, errors.New("Could not confirm that API server is up, no response message")
}
return healthResponse.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")
}
response, err := a.Call(http.MethodGet, fmt.Sprintf("/v4/membres/%s", membreID), nil, true)
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")
}
return getMembreResponse.Data.Membre, nil
}
*/

View file

@ -1,97 +1,306 @@
package data package data
import ( import (
"errors" "fmt"
"log"
"git.agecem.com/agecem/bottin-agenda/models"
_ "github.com/jackc/pgx/stdlib"
"github.com/jmoiron/sqlx"
"github.com/spf13/viper" "github.com/spf13/viper"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
) )
var db *gorm.DB // DataClient is a postgres client based on sqlx
type DataClient struct {
type Membre struct { PostgresConnection PostgresConnection
gorm.Model DB sqlx.DB
NumEtud string `mapper:"num_etud" json:"num_etud"`
Nom string `mapper:"-" json:"-"`
} }
func OpenDatabase() error { type PostgresConnection struct {
var err error User string
Password string
Database string
Host string
Port int
SSL bool
}
var dialector gorm.Dialector func NewDataClientFromViper() (*DataClient, error) {
client, err := NewDataClient(PostgresConnection{
User: viper.GetString("db.user"),
Password: viper.GetString("db.password"),
Host: viper.GetString("db.host"),
Port: viper.GetInt("db.port"),
Database: viper.GetString("db.database"),
})
switch t := viper.GetString("db.type"); t { return client, err
case "sqlite": }
log.Println("Using driver gorm.io/driver/sqlite")
db_sqlite_path := viper.GetString("db.sqlite.path") func NewDataClient(connection PostgresConnection) (*DataClient, error) {
client := &DataClient{PostgresConnection: connection}
if db_sqlite_path == "" { connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
log.Fatal("No valid database file found in `--db-sqlite-path` or `db.sqlite.path`.") 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
} }
log.Println("Using database file:", db_sqlite_path) rows, err := result.RowsAffected()
if err != nil {
tx.Rollback()
return 0, err
}
dialector = sqlite.Open(db_sqlite_path) rowsInserted += rows
default:
log.Fatalf("Unrecognized database driver requested (%s).\n", t)
} }
db, err = gorm.Open(dialector, &gorm.Config{}) err = tx.Commit()
if err != nil { if err != nil {
return err return rowsInserted, err
} }
sqlDB, err := db.DB() return rowsInserted, nil
if err != nil {
return err
}
return sqlDB.Ping()
} }
func MigrateDatabase() error { func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, error) {
err := db.AutoMigrate(&Membre{}) var rowsInserted int64
return err 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 ReadMembre(num_etud string) (Membre, error) { func (d *DataClient) GetMembre(membreID string) (models.Membre, error) {
var membre Membre var membre models.Membre
result := db.Limit(1).Find(&membre, "num_etud = ?", num_etud)
if result.Error != nil { rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
return membre, result.Error 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 return membre, nil
} }
func InsertMembre(membre *Membre) (error, uint) { func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
// Reset ID before insertion result, err := d.DB.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
membre.ID = 0 if err != nil {
return 0, err
// Insert membre into database
result := db.Create(&membre)
if result.Error != nil {
return result.Error, 0
} }
return nil, membre.ID 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 InsertMembres(membres []*Membre, batch_size int) error { func (d *DataClient) List() ([]models.Asset, error) {
if len(membres) == 0 { // Query the database, storing results in a []Person (wrapped in []interface{})
return errors.New("Cannot insert empty batch of membres.") assets := []models.Asset{}
err := d.DB.Select(&assets, "SELECT * FROM assets WHERE deleted_at IS NULL LIMIT 1000")
if err != nil {
return nil, err
} }
for _, membre := range membres { return assets, nil
membre.ID = 0 }
}
db.CreateInBatches(&membres, batch_size) // 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 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
}
*/

50
docker-compose.yaml Normal file
View file

@ -0,0 +1,50 @@
services:
db:
image: 'docker.io/library/postgres:14.8'
environment:
POSTGRES_DATABASE: "${BOTTINAGENDA_POSTGRES_DATABASE}"
POSTGRES_PASSWORD: "${BOTTINAGENDA_POSTGRES_PASSWORD}"
POSTGRES_USER: "${BOTTINAGENDA_POSTGRES_USER}"
ports:
- '5433:5432'
volumes:
- 'db-data:/var/lib/postgresql/data'
restart: 'unless-stopped'
api:
depends_on:
- db
build: .
image: 'git.agecem.com/agecem/bottin-agenda:latest'
ports:
- '1313:1313'
volumes:
- 'api-config:/etc/bottin-agenda'
restart: 'unless-stopped'
command: ['bottin-agenda', '--config', '/etc/bottin-agenda/api.yaml', 'api']
web:
depends_on:
- api
build: .
image: 'git.agecem.com/agecem/bottin-agenda:latest'
ports:
- '2313:2313'
volumes:
- 'web-config:/etc/bottin-agenda'
restart: 'unless-stopped'
command: ['bottin-agenda', '--config', '/etc/bottin-agenda/web.yaml', 'web']
# adminer:
# image: adminer
# restart: always
# ports:
# - 8088:8080
# depends_on:
# - db
volumes:
db-data:
api-config:
web-config:

View file

@ -1,17 +0,0 @@
version: "3.9"
services:
bottin-agenda:
image: localhost/agecem/bottin-agenda:latest
ports:
- "1313:1313" # http
volumes:
- "bottin-agenda-config:/etc/bottin-agenda"
- "bottin-agenda-data:/var/lib/bottin-agenda"
command: bottin-agenda server --config /etc/bottin-agenda/bottin-agenda.yaml
restart: "always"
volumes:
bottin-agenda-config:
bottin-agenda-data:
networks:
default:
name: bottin

View file

@ -1,10 +0,0 @@
package embed
import _ "embed"
//go:embed html/index.html
var Html_index string
func ReadHtml() string {
return Html_index
}

View file

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>
AGECEM | Agendas
</title>
</head>
<body>
<h2>
Distribution d'agendas
</h2>
<h4>
Scannez la carte étudiante d&#39unE membre<br>
-ou-<br>
Entrez manuellement le code à 7 chiffres
</h4>
<form action="/">
<label for="num_etud">#
<input type="text" name="num_etud" id="num_etud" placeholder="Numéro étudiant"autofocus>
</label>
<button formmethod="post" type="submit">Scan</button>
</form>
</body>
</html>

View file

@ -1,61 +0,0 @@
# bottin instance configurations
bottin:
# protocol
#
# possible values:
#
# - http
# - https
protocol: 'http'
# bottin instance hostname
host: 'localhost'
# bottin instance port
port: '1312'
# bottin instance username
username: 'bottin'
# bottin instance password
password: 'bottin'
# database configurations
db:
# database type
#
# possible values:
#
# - sqlite
type: 'sqlite'
# sqlite configurations
sqlite:
# sqlite database path
#
# doesn't need to exist, but directory must be writable
# for bottin-agenda to create it.
path: '/var/lib/bottin-agenda/bottin-agenda.db'
# data import
import:
# batch size for database imports
insert_batch_size: 500
# authentication
login:
# credentials
username: 'agenda'
password: 'agenda'
# bottin-agenda server options
server:
# bottin-agenda instance port
port: '1313'

53
go.mod
View file

@ -1,43 +1,42 @@
module git.agecem.com/agecem/bottin-agenda module git.agecem.com/agecem/bottin-agenda
go 1.18 go 1.20
require ( require (
github.com/labstack/echo/v4 v4.7.2 git.agecem.com/agecem/bottin/v5 v5.0.4
github.com/spf13/cobra v1.4.0 github.com/jackc/pgx v3.6.2+incompatible
github.com/spf13/viper v1.12.0 github.com/jmoiron/sqlx v1.3.5
gorm.io/driver/sqlite v1.3.4 github.com/labstack/echo/v4 v4.10.2
gorm.io/gorm v1.23.6 github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
) )
require ( require (
github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/labstack/gommon v0.4.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/labstack/gommon v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.12 // indirect github.com/mattn/go-sqlite3 v1.14.12 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/afero v1.8.2 // indirect github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect golang.org/x/time v0.3.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

125
go.sum
View file

@ -36,6 +36,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 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= 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= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.agecem.com/agecem/bottin/v5 v5.0.4 h1:19xQjnc9cZU1vBtk9mjhQ8QDg4nlpYLShmkbUeptVbM=
git.agecem.com/agecem/bottin/v5 v5.0.4/go.mod h1:OGqwTvEtIEOg/AHZgaJPzsMCy94dWygq89rMJlCFLuU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -46,7 +48,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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-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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -57,11 +60,14 @@ 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/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/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 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 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 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-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-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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 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-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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -100,7 +106,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/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.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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 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 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.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.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -125,13 +131,13 @@ 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/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-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/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.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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.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/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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -141,25 +147,29 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 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.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 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/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -168,31 +178,37 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 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 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 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.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 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 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.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.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/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/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
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.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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -210,8 +226,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-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-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.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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-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-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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -276,8 +292,8 @@ 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-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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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-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-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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -334,9 +350,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-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-20211103235746-7861aae1554b/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-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.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -344,13 +361,13 @@ 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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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-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-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-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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-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-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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -491,23 +508,17 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 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.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 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-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/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.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.2/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-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-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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.3.4 h1:NnFOPVfzi4CPsJPH4wXr6rMkPb4ElHEqKMvrsx9c9Fk=
gorm.io/driver/sqlite v1.3.4/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0=
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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-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-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

56
handlers/health.go Normal file
View file

@ -0,0 +1,56 @@
package handlers
import (
"net/http"
"git.agecem.com/agecem/bottin-agenda/data"
bottindata "git.agecem.com/agecem/bottin/v5/data"
"github.com/labstack/echo/v4"
"github.com/spf13/viper"
)
func GetHealth(c echo.Context) error {
bottinApiKey := viper.GetString("bottin.api.key")
bottinApiHost := viper.GetString("bottin.api.host")
bottinApiProtocol := viper.GetString("bottin.api.protocol")
bottinApiPort := viper.GetInt("bottin.api.port")
bottinConnection := bottindata.NewApiClient(
bottinApiKey,
bottinApiHost,
bottinApiProtocol,
bottinApiPort,
)
var bottinStatus string
healthResponse, err := bottinConnection.GetHealth()
if err != nil {
bottinStatus = err.Error()
} else {
bottinStatus = healthResponse
}
var databaseStatus string
// Check database is ready
dataClient, err := data.NewDataClientFromViper()
if err != nil {
databaseStatus = err.Error()
} else {
defer dataClient.DB.Close()
if err := dataClient.DB.Ping(); err != nil {
databaseStatus = err.Error()
} else {
databaseStatus = "Bottin-agenda database is ready"
}
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Bottin-agenda API v2 is ready",
"bottin": bottinStatus,
"database": databaseStatus,
})
}

56
handlers/membre.go Normal file
View file

@ -0,0 +1,56 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/spf13/viper"
"git.agecem.com/agecem/bottin-agenda/responses"
bottindata "git.agecem.com/agecem/bottin/v5/data"
)
func GetMembre(c echo.Context) error {
bottinApiKey := viper.GetString("bottin.api.key")
bottinApiHost := viper.GetString("bottin.api.host")
bottinApiProtocol := viper.GetString("bottin.api.protocol")
bottinApiPort := viper.GetInt("bottin.api.port")
// Using bottin's API client
bottinConnection := bottindata.NewApiClient(
bottinApiKey,
bottinApiHost,
bottinApiProtocol,
bottinApiPort,
)
membreID := c.Param("membre_id")
getMembreResponse := responses.GetMembreResponse{}
membre, err := bottinConnection.GetMembre(membreID)
if err != nil {
getMembreResponse.Message = err.Error()
var statusCode int
switch err.Error() {
case "Veuillez fournir un numéro étudiant à rechercher":
statusCode = http.StatusBadRequest
case "Ce numéro étudiant ne correspond à aucunE membre":
statusCode = http.StatusNotFound
default:
statusCode = http.StatusInternalServerError
}
getMembreResponse.Data.Membre = membre
return c.JSON(statusCode, getMembreResponse)
}
getMembreResponse.Data.Membre = membre
getMembreResponse.Message = "Read successful"
return c.JSON(http.StatusOK, getMembreResponse)
}

19
models/models.go Normal file
View file

@ -0,0 +1,19 @@
package models
import "time"
var Schema = `
CREATE TABLE transactions (
id PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
membre_id VARCHAR(7),
given_at TIMESTAMP,
is_perpetual BOOLEAN
);
`
type Transaction struct {
ID string `db:"id" json:"id"`
MembreID string `db:"membre_id" json:"membre_id"`
GivenAt *time.Time `db:"given_at" json:"given_at"`
IsPerpetual bool `db:"is_perpetual" json:"is_perpetual"`
}

10
responses/responses.go Normal file
View file

@ -0,0 +1,10 @@
package responses
import bottinmodels "git.agecem.com/agecem/bottin/v5/models"
type GetMembreResponse struct {
Message string `json:"message"`
Data struct {
Membre bottinmodels.Membre
} `json:"data"`
}