From c850b221a1029eaf0bfd5ef34b1f0fba0c9debdb Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Fri, 9 Jun 2023 01:09:02 -0400
Subject: [PATCH 1/4] =?UTF-8?q?Impl=C3=A9menter=20client=20web=20de=20base?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Déplacer tous les flags vers rootCmd.PersistentFlags()
Ajouter config struct types à models/
Ajouter data/apiclient.go#ApiClient.GetHealth()
Ajouter webCmd avec viper.Unmarshal() pour valeurs de config
Ajouter package web depuis agecem/bottin
---
cmd/api.go | 56 -----------------
cmd/root.go | 98 ++++++++++++++++++++++++++++++
cmd/web.go | 90 +++++++++++++++++++++++++++
data/apiclient.go | 32 ++++++++++
models/models.go | 41 +++++++++++++
web/embed.go | 10 +++
web/templates/index.html | 117 ++++++++++++++++++++++++++++++++++++
web/webhandlers/handlers.go | 43 +++++++++++++
8 files changed, 431 insertions(+), 56 deletions(-)
create mode 100644 cmd/web.go
create mode 100644 web/embed.go
create mode 100644 web/templates/index.html
create mode 100644 web/webhandlers/handlers.go
diff --git a/cmd/api.go b/cmd/api.go
index 7297270..224df3c 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -93,60 +93,4 @@ var apiCmd = &cobra.Command{
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"))
}
diff --git a/cmd/root.go b/cmd/root.go
index 7fa76ca..b75fdbc 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -29,6 +29,104 @@ func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin-agenda.yaml)")
+
+ // web.api.host
+ rootCmd.PersistentFlags().String(
+ "web-api-host", "api",
+ "Remote API server host (config:'web.api.host')")
+ viper.BindPFlag("web.api.host", rootCmd.PersistentFlags().Lookup("web-api-host"))
+
+ // web.api.key
+ rootCmd.PersistentFlags().String(
+ "web-api-key", "bottin-agenda",
+ "Remote API server key (config:'web.api.key')")
+ viper.BindPFlag("web.api.key", rootCmd.PersistentFlags().Lookup("web-api-key"))
+
+ // web.api.protocol
+ rootCmd.PersistentFlags().String(
+ "web-api-protocol", "http",
+ "Remote API server protocol (config:'web.api.protocol')")
+ viper.BindPFlag("web.api.protocol", rootCmd.PersistentFlags().Lookup("web-api-protocol"))
+
+ // web.api.port
+ rootCmd.PersistentFlags().Int(
+ "web-api-port", 1313,
+ "Remote API server port (config:'web.api.port')")
+ viper.BindPFlag("web.api.port", rootCmd.PersistentFlags().Lookup("web-api-port"))
+
+ // web.password
+ rootCmd.PersistentFlags().String(
+ "web-password", "bottin-agenda",
+ "Web client password (config:'web.password')")
+ viper.BindPFlag("web.password", rootCmd.PersistentFlags().Lookup("web-password"))
+
+ // web.port
+ rootCmd.PersistentFlags().Int(
+ "web-port", 2313,
+ "Web client port (config:'web.port')")
+ viper.BindPFlag("web.port", rootCmd.PersistentFlags().Lookup("web-port"))
+
+ // web.user
+ rootCmd.PersistentFlags().String(
+ "web-user", "bottin-agenda",
+ "Web client user (config:'web.user')")
+ viper.BindPFlag("web.user", rootCmd.PersistentFlags().Lookup("web-user"))
+
+ // api.key
+ rootCmd.PersistentFlags().String(
+ "api-key", "bottin-agenda",
+ "API server key. Leave empty for no key auth. (config: 'api.key')")
+ viper.BindPFlag("api.key", rootCmd.PersistentFlags().Lookup("api-key"))
+
+ // api.port
+ rootCmd.PersistentFlags().Int(
+ "api-port", 1313,
+ "API server port (config:'api.port')")
+ viper.BindPFlag("api.port", rootCmd.PersistentFlags().Lookup("api-port"))
+
+ // bottin.api.host
+ rootCmd.PersistentFlags().String(
+ "bottin-api-host", "api",
+ "Remote bottin API server host (config:'bottin.api.host')")
+ viper.BindPFlag("bottin.api.host", rootCmd.PersistentFlags().Lookup("bottin-api-host"))
+
+ // bottin.api.key
+ rootCmd.PersistentFlags().String(
+ "bottin-api-key", "bottin",
+ "Remote bottin API server key (config:'bottin.api.key')")
+ viper.BindPFlag("bottin.api.key", rootCmd.PersistentFlags().Lookup("bottin-api-key"))
+
+ // bottin.api.protocol
+ rootCmd.PersistentFlags().String(
+ "bottin-api-protocol", "http",
+ "Remote bottin API server protocol (config:'bottin.api.protocol')")
+ viper.BindPFlag("bottin.api.protocol", rootCmd.PersistentFlags().Lookup("bottin-api-protocol"))
+
+ // bottin.api.port
+ rootCmd.PersistentFlags().Int(
+ "bottin-api-port", 1312,
+ "Remote bottin API server port (config:'bottin.api.port')")
+ viper.BindPFlag("bottin.api.port", rootCmd.PersistentFlags().Lookup("bottin-api-port"))
+
+ // db.database
+ rootCmd.PersistentFlags().String("db-database", "bottin-agenda", "Postgres database (config:'db.database')")
+ viper.BindPFlag("db.database", rootCmd.PersistentFlags().Lookup("db-database"))
+
+ // db.host
+ rootCmd.PersistentFlags().String("db-host", "db", "Postgres host (config:'db.host')")
+ viper.BindPFlag("db.host", rootCmd.PersistentFlags().Lookup("db-host"))
+
+ // db.password
+ rootCmd.PersistentFlags().String("db-password", "bottin-agenda", "Postgres password (config:'db.password')")
+ viper.BindPFlag("db.password", rootCmd.PersistentFlags().Lookup("db-password"))
+
+ // db.port
+ rootCmd.PersistentFlags().Int("db-port", 5432, "Postgres port (config:'db.port')")
+ viper.BindPFlag("db.port", rootCmd.PersistentFlags().Lookup("db-port"))
+
+ // db.user
+ rootCmd.PersistentFlags().String("db-user", "bottin-agenda", "Postgres user (config:'db.user')")
+ viper.BindPFlag("db.user", rootCmd.PersistentFlags().Lookup("db-user"))
}
// initConfig reads in config file and ENV variables if set.
diff --git a/cmd/web.go b/cmd/web.go
new file mode 100644
index 0000000..45274da
--- /dev/null
+++ b/cmd/web.go
@@ -0,0 +1,90 @@
+package cmd
+
+import (
+ "crypto/subtle"
+ "embed"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+
+ "git.agecem.com/agecem/bottin-agenda/data"
+ "git.agecem.com/agecem/bottin-agenda/models"
+ "git.agecem.com/agecem/bottin-agenda/web"
+ "git.agecem.com/agecem/bottin-agenda/web/webhandlers"
+ "github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+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) {
+ var config models.Config
+
+ err := viper.Unmarshal(&config)
+ if err != nil {
+ log.Fatalf("Error during webCmd#viper.Unmarshal(): %s", err)
+ }
+
+ // Ping API server
+
+ apiClient := data.NewApiClient(config.Web.Api.Key, config.Web.Api.Host, config.Web.Api.Protocol, config.Web.Api.Port)
+
+ healthResult, err := apiClient.GetHealth()
+ if err != nil {
+ log.Fatalf("Error during webCmd#apiClient.GetHealth(): %s", err)
+ }
+
+ log.Println(healthResult)
+
+ 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(config.Web.User)) == 1
+ passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(config.Web.Password)) == 1
+ return usersMatch && passwordsMatch, nil
+ }))
+
+ // Template
+
+ t := &Template{
+ templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
+ }
+
+ e.Renderer = t
+
+ // Routes
+
+ e.GET("/", webhandlers.GetIndex)
+ //e.POST("/transaction", webhandlers.PostTransaction)
+
+ // Execution
+
+ e.Logger.Fatal(e.Start(
+ fmt.Sprintf(":%d", config.Web.Port)))
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(webCmd)
+ templatesFS = web.GetTemplates()
+}
diff --git a/data/apiclient.go b/data/apiclient.go
index 6132a12..f56979f 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -1,9 +1,14 @@
package data
import (
+ "encoding/json"
+ "errors"
"fmt"
"io"
+ "io/ioutil"
"net/http"
+
+ "git.agecem.com/agecem/bottin-agenda/responses"
)
type ApiClient struct {
@@ -58,3 +63,30 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo
return response, nil
}
+
+// GetHealth allows checking for API server health
+func (a *ApiClient) GetHealth() (string, error) {
+ var response responses.GetHealthResponse
+
+ getHealthResponse, err := a.Call(http.MethodGet, "/v3/health", nil, true)
+ if err != nil {
+ return response.Message, err
+ }
+
+ defer getHealthResponse.Body.Close()
+
+ body, err := ioutil.ReadAll(getHealthResponse.Body)
+ if err != nil {
+ return response.Message, err
+ }
+
+ if err := json.Unmarshal(body, &response); err != nil {
+ return response.Message, err
+ }
+
+ if response.Message == "" {
+ return response.Message, errors.New("Could not confirm that API server is up, no response message")
+ }
+
+ return response.Message, nil
+}
diff --git a/models/models.go b/models/models.go
index ac30776..691afc0 100644
--- a/models/models.go
+++ b/models/models.go
@@ -17,3 +17,44 @@ type Transaction struct {
GivenAt *time.Time `db:"given_at" json:"given_at"`
IsPerpetual bool `db:"is_perpetual" json:"is_perpetual"`
}
+
+type Config struct {
+ Web WebConfig `json:"web"`
+ Api ApiConfig `json:"api"`
+ Bottin BottinConfig `json:"bottin"`
+ Db DbConfig `json:"db"`
+}
+
+type ApiConfig struct {
+ Key string `json:"key"`
+ Port int `json:"port"`
+}
+
+type BottinConfig struct {
+ Api struct {
+ Host string `json:"host"`
+ Key string `json:"key"`
+ Protocol string `json:"protocol"`
+ Port int `json:"port"`
+ } `json:"api"`
+}
+
+type DbConfig struct {
+ Database string `json:"database"`
+ Host string `json:"host"`
+ Password string `json:"password"`
+ Port int `json:"port"`
+ User string `json:"user"`
+}
+
+type WebConfig struct {
+ Api struct {
+ Host string `json:"host"`
+ Key string `json:"key"`
+ Port int `json:"port"`
+ Protocol string `json:"protocol"`
+ } `json:"api"`
+ Password string `json:"password"`
+ Port int `json:"port"`
+ User string `json:"user"`
+}
diff --git a/web/embed.go b/web/embed.go
new file mode 100644
index 0000000..465e5ec
--- /dev/null
+++ b/web/embed.go
@@ -0,0 +1,10 @@
+package web
+
+import "embed"
+
+//go:embed templates/*
+var templatesFS embed.FS
+
+func GetTemplates() embed.FS {
+ return templatesFS
+}
diff --git a/web/templates/index.html b/web/templates/index.html
new file mode 100644
index 0000000..af30688
--- /dev/null
+++ b/web/templates/index.html
@@ -0,0 +1,117 @@
+{{ define "index-html" }}
+
+
+
+
+
+ AGECEM | Agenda
+
+
+
+
+
+
+
+ Distribution d'agendas aux membres de l'AGECEM
+
+
+
+ 1) Si l'agenda demandé est perpétuel, cochez 'Perpétuel?:'
+
+ 2) Sélectionnez le champs 'Numéro étudiant:'
+
+ 3) Scannez la carte étudiante d'unE membre
+ -ou-
+ Entrez manuellement le code à 7 chiffres
+
+ 4) Si aucune erreur ne survient, la personne est libre de partir avec son agenda
+
+
+
+
+ {{ .Result }}
+
+
+
+{{ end }}
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
new file mode 100644
index 0000000..dce8399
--- /dev/null
+++ b/web/webhandlers/handlers.go
@@ -0,0 +1,43 @@
+package webhandlers
+
+import (
+ "fmt"
+ "net/http"
+
+ "git.agecem.com/agecem/bottin/v5/data"
+ "github.com/labstack/echo/v4"
+)
+
+func GetIndex(c echo.Context) error {
+ return c.Render(http.StatusOK, "index-html", nil)
+}
+
+func GetMembre(c echo.Context) error {
+ apiClient := data.NewApiClientFromViper()
+
+ membreID := c.QueryParam("membre_id")
+
+ membre, err := 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,
+ })
+}
--
2.45.2
From 2287f00e2926531bbcfd8ef5cce91a642a0c79ce Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Fri, 9 Jun 2023 01:11:55 -0400
Subject: [PATCH 2/4] Retirer webhandlers#GetMembre()
---
web/webhandlers/handlers.go | 32 --------------------------------
1 file changed, 32 deletions(-)
diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go
index dce8399..8376dd8 100644
--- a/web/webhandlers/handlers.go
+++ b/web/webhandlers/handlers.go
@@ -1,43 +1,11 @@
package webhandlers
import (
- "fmt"
"net/http"
- "git.agecem.com/agecem/bottin/v5/data"
"github.com/labstack/echo/v4"
)
func GetIndex(c echo.Context) error {
return c.Render(http.StatusOK, "index-html", nil)
}
-
-func GetMembre(c echo.Context) error {
- apiClient := data.NewApiClientFromViper()
-
- membreID := c.QueryParam("membre_id")
-
- membre, err := 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,
- })
-}
--
2.45.2
From 660d8826e2402b1bfbce3595950c2e5c8149c061 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Fri, 9 Jun 2023 23:52:03 -0400
Subject: [PATCH 3/4] =?UTF-8?q?Impl=C3=A9menter=20POST=20/transaction?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ajouter data#ApiClient.InsertTransactions()
Fix form action
Ajouter séparateur entre description et formulaire
Ajouter webhandlers#PostTransaction et PostTransactionResult
---
cmd/api.go | 1 +
cmd/web.go | 2 +-
data/apiclient.go | 30 +++++++++++++++++
web/templates/index.html | 4 ++-
web/webhandlers/handlers.go | 65 +++++++++++++++++++++++++++++++++++++
5 files changed, 100 insertions(+), 2 deletions(-)
diff --git a/cmd/api.go b/cmd/api.go
index 224df3c..ad702dc 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -25,6 +25,7 @@ var apiCmd = &cobra.Command{
Short: "Démarrer le serveur API",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
+ // TODO migrer à viper.Unmarshal(&models.Config)
apiKey = viper.GetString("api.key")
apiPort = viper.GetInt("api.port")
diff --git a/cmd/web.go b/cmd/web.go
index 45274da..ed1d7b3 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -75,7 +75,7 @@ var webCmd = &cobra.Command{
// Routes
e.GET("/", webhandlers.GetIndex)
- //e.POST("/transaction", webhandlers.PostTransaction)
+ e.POST("/transaction/", webhandlers.PostTransaction)
// Execution
diff --git a/data/apiclient.go b/data/apiclient.go
index f56979f..0971ba3 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -1,6 +1,7 @@
package data
import (
+ "bytes"
"encoding/json"
"errors"
"fmt"
@@ -8,6 +9,7 @@ import (
"io/ioutil"
"net/http"
+ "git.agecem.com/agecem/bottin-agenda/models"
"git.agecem.com/agecem/bottin-agenda/responses"
)
@@ -90,3 +92,31 @@ func (a *ApiClient) GetHealth() (string, error) {
return response.Message, nil
}
+
+func (a *ApiClient) InsertTransactions(transactions []models.Transaction) ([]models.Transaction, error) {
+ var response responses.PostTransactionsResponse
+
+ var buf bytes.Buffer
+ err := json.NewEncoder(&buf).Encode(transactions)
+ if err != nil {
+ return response.Data.Transactions, err
+ }
+
+ postHealthResponse, err := a.Call(http.MethodPost, "/v3/transactions", &buf, true)
+ defer postHealthResponse.Body.Close()
+
+ body, err := ioutil.ReadAll(postHealthResponse.Body)
+ if err != nil {
+ return response.Data.Transactions, err
+ }
+
+ if err := json.Unmarshal(body, &response); err != nil {
+ return response.Data.Transactions, err
+ }
+
+ if len(response.Data.Transactions) == 0 {
+ return response.Data.Transactions, fmt.Errorf(response.Message)
+ }
+
+ return response.Data.Transactions, nil
+}
diff --git a/web/templates/index.html b/web/templates/index.html
index af30688..9750a38 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -94,7 +94,9 @@ button {
4) Si aucune erreur ne survient, la personne est libre de partir avec son agenda
-