From 9f984865f915e2ac4aecedba0b6c004155036cc7 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 04:04:02 -0400
Subject: [PATCH 01/34] Membre_id doit exister au bottin pour transaction
---
handlers/transaction.go | 40 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/handlers/transaction.go b/handlers/transaction.go
index 9b313d6..ccc77b6 100644
--- a/handlers/transaction.go
+++ b/handlers/transaction.go
@@ -7,7 +7,9 @@ import (
"git.agecem.com/agecem/bottin-agenda/data"
"git.agecem.com/agecem/bottin-agenda/models"
"git.agecem.com/agecem/bottin-agenda/responses"
+ bottindata "git.agecem.com/agecem/bottin/v5/data"
"github.com/labstack/echo/v4"
+ "github.com/spf13/viper"
)
// GetTransactions lists transactions on GET /v2/transactions http/1.1
@@ -73,6 +75,44 @@ func PostTransactions(c echo.Context) error {
return c.JSON(statusCode, response)
}
+ 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
+ bottinApiClient := bottindata.NewApiClient(
+ bottinApiKey,
+ bottinApiHost,
+ bottinApiProtocol,
+ bottinApiPort,
+ )
+
+ // Check if membre_id exists according to bottin
+ for _, transaction := range transactions {
+ if transaction.MembreID == "" {
+ response.Message = fmt.Sprintf("Cannot insert transaction without a membre_id")
+ statusCode = http.StatusBadRequest
+
+ return c.JSON(statusCode, response)
+ }
+ membre, err := bottinApiClient.GetMembre(transaction.MembreID)
+ if err != nil {
+ response.Message = fmt.Sprintf("Error during bottinApiClient.GetMembre(): %s", err)
+
+ return c.JSON(statusCode, response)
+ }
+
+ if membre.ID == "" {
+ response.Message = fmt.Sprintf("Cannot insert transaction for non-existent membre %s", membre.ID)
+ statusCode = http.StatusNotFound
+
+ return c.JSON(statusCode, response)
+ }
+
+ // membre exists, can keep going
+ }
+
// Check for already-existing transactions
for _, transaction := range transactions {
transaction, err := client.GetTransaction(transaction.MembreID, transaction.IsPerpetual)
From 5915e3ca7b44148de1d0730c63ceed57c140ff81 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 20:47:39 -0400
Subject: [PATCH 02/34] Retirer route GET /v2/membres/:membre_id
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Cleanup code désuet (et redondant avec agecem/bottin) en lien avec le
concept de Membre
---
cmd/api.go | 2 --
handlers/membre.go | 56 ------------------------------------------
responses/responses.go | 10 --------
3 files changed, 68 deletions(-)
delete mode 100644 handlers/membre.go
diff --git a/cmd/api.go b/cmd/api.go
index 018fbc1..03b6e30 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -59,8 +59,6 @@ var apiCmd = &cobra.Command{
e.GET("/v2/health/", handlers.GetHealth)
- e.GET("/v2/membres/:membre_id/", handlers.GetMembre)
-
e.GET("/v2/transactions/", handlers.GetTransactions)
e.POST("/v2/transactions/", handlers.PostTransactions)
diff --git a/handlers/membre.go b/handlers/membre.go
deleted file mode 100644
index 67b5215..0000000
--- a/handlers/membre.go
+++ /dev/null
@@ -1,56 +0,0 @@
-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)
-}
diff --git a/responses/responses.go b/responses/responses.go
index fcadcce..7ac2507 100644
--- a/responses/responses.go
+++ b/responses/responses.go
@@ -2,22 +2,12 @@ package responses
import (
"git.agecem.com/agecem/bottin-agenda/models"
- bottinmodels "git.agecem.com/agecem/bottin/v5/models"
)
type PostSeedResponse struct {
Message string `json:"message"`
}
-type GetMembreResponseData struct {
- Membre bottinmodels.Membre `json:"membre"`
-}
-
-type GetMembreResponse struct {
- Message string `json:"message"`
- Data GetMembreResponseData `json:"data"`
-}
-
type PostTransactionsResponseData struct {
Transactions []models.Transaction `json:"transactions"`
}
From d1743d29dedfb1fea5eac119ac5ac580de84c820 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 20:51:23 -0400
Subject: [PATCH 03/34] =?UTF-8?q?Retirer=20code=20comment=C3=A9=20dans=20d?=
=?UTF-8?q?ata/?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Sont des restants des références d'implémentation
---
data/apiclient.go | 37 --------
data/data.go | 234 ----------------------------------------------
2 files changed, 271 deletions(-)
diff --git a/data/apiclient.go b/data/apiclient.go
index f0f1453..013bb33 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -98,40 +98,3 @@ func (a *ApiClient) GetBottinHealth() (string, error) {
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
-}
-*/
diff --git a/data/data.go b/data/data.go
index 42e3a90..2398bd0 100644
--- a/data/data.go
+++ b/data/data.go
@@ -140,237 +140,3 @@ func (d *DataClient) ListTransactions() ([]models.Transaction, error) {
return transactions, 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
-}
-*/
From 8ed2a99b2cbb7249cde3ad69e3a6b4e0dae893cc Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 20:52:16 -0400
Subject: [PATCH 04/34] =?UTF-8?q?Retirer=20code=20comment=C3=A9=20dans=20d?=
=?UTF-8?q?ata/apiClient.go#Call()?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
data/apiclient.go | 5 -----
1 file changed, 5 deletions(-)
diff --git a/data/apiclient.go b/data/apiclient.go
index 013bb33..78ea400 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -32,11 +32,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{}
From fe1caec61e58211ed5d9d6afe56b9554e0564e35 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 21:05:25 -0400
Subject: [PATCH 05/34] Bump all routes /v2 -> /v3
---
cmd/api.go | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/cmd/api.go b/cmd/api.go
index 03b6e30..7297270 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -55,12 +55,12 @@ var apiCmd = &cobra.Command{
// Routes
- e.POST("/v2/seed/", handlers.PostSeed)
+ e.POST("/v3/seed/", handlers.PostSeed)
- e.GET("/v2/health/", handlers.GetHealth)
+ e.GET("/v3/health/", handlers.GetHealth)
- e.GET("/v2/transactions/", handlers.GetTransactions)
- e.POST("/v2/transactions/", handlers.PostTransactions)
+ e.GET("/v3/transactions/", handlers.GetTransactions)
+ e.POST("/v3/transactions/", handlers.PostTransactions)
// Check bottin is ready
From 53c5d8f1c6b2180816da99090b001ec187ba952a Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 21:06:04 -0400
Subject: [PATCH 06/34] [BREAKING] Ajouter response.Data pour GetHealth
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implémenter GetHealthResponse et GetHealthResponseData
---
handlers/health.go | 15 ++++++++++-----
responses/responses.go | 10 ++++++++++
2 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/handlers/health.go b/handlers/health.go
index 47d8177..f193544 100644
--- a/handlers/health.go
+++ b/handlers/health.go
@@ -4,12 +4,16 @@ import (
"net/http"
"git.agecem.com/agecem/bottin-agenda/data"
+ "git.agecem.com/agecem/bottin-agenda/responses"
bottindata "git.agecem.com/agecem/bottin/v5/data"
"github.com/labstack/echo/v4"
"github.com/spf13/viper"
)
func GetHealth(c echo.Context) error {
+ var response responses.GetHealthResponse
+ var statusCode int = http.StatusNotImplemented
+
bottinApiKey := viper.GetString("bottin.api.key")
bottinApiHost := viper.GetString("bottin.api.host")
bottinApiProtocol := viper.GetString("bottin.api.protocol")
@@ -48,9 +52,10 @@ func GetHealth(c echo.Context) error {
}
}
- return c.JSON(http.StatusOK, map[string]string{
- "message": "Bottin-agenda API v2 is ready",
- "bottin": bottinStatus,
- "database": databaseStatus,
- })
+ statusCode = http.StatusOK
+ response.Message = "Bottin-agenda API v3 is ready"
+ response.Data.Bottin = bottinStatus
+ response.Data.Database = databaseStatus
+
+ return c.JSON(statusCode, response)
}
diff --git a/responses/responses.go b/responses/responses.go
index 7ac2507..e54f155 100644
--- a/responses/responses.go
+++ b/responses/responses.go
@@ -4,6 +4,16 @@ import (
"git.agecem.com/agecem/bottin-agenda/models"
)
+type GetHealthResponseData struct {
+ Bottin string `json:"bottin"`
+ Database string `json:"database"`
+}
+
+type GetHealthResponse struct {
+ Message string `json:"message"`
+ Data GetHealthResponseData `json:"data"`
+}
+
type PostSeedResponse struct {
Message string `json:"message"`
}
From 600e22a645d8cf95ad301200cc148ab9813fc586 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 21:08:20 -0400
Subject: [PATCH 07/34] =?UTF-8?q?Retirer=20num=C3=A9ro=20de=20version=20de?=
=?UTF-8?q?=20docstring?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
handlers/transaction.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/handlers/transaction.go b/handlers/transaction.go
index ccc77b6..ecd20d9 100644
--- a/handlers/transaction.go
+++ b/handlers/transaction.go
@@ -12,7 +12,7 @@ import (
"github.com/spf13/viper"
)
-// GetTransactions lists transactions on GET /v2/transactions http/1.1
+// GetTransactions handles the listing of transactions
func GetTransactions(c echo.Context) error {
var statusCode int = http.StatusInternalServerError
var response responses.GetTransactionsResponse
@@ -46,7 +46,7 @@ func GetTransactions(c echo.Context) error {
return c.JSON(statusCode, response)
}
-// PostTransactions creates transactions on POST /v2/transactions http/1.1
+// PostTransactions handles the creation of transactions
func PostTransactions(c echo.Context) error {
var statusCode int = http.StatusInternalServerError
var response responses.PostTransactionsResponse
From f33091f5cfd41cb322116157ec11439d9a35487b Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 21:26:10 -0400
Subject: [PATCH 08/34] Renommer data/data.go -> data/dataclient.go
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Clarifie sa relation par rapport à data/apiclient.go
---
data/{data.go => dataclient.go} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename data/{data.go => dataclient.go} (100%)
diff --git a/data/data.go b/data/dataclient.go
similarity index 100%
rename from data/data.go
rename to data/dataclient.go
From 3da2cb7f5b9634479620ee39d4d250c83bdfc741 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Thu, 8 Jun 2023 21:40:14 -0400
Subject: [PATCH 09/34] =?UTF-8?q?Retirer=20r=C3=A9impl=C3=A9mentation=20de?=
=?UTF-8?q?=20bottin=20health?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
N'était plus utilisé, et le type était redondant avec le response struct
de agecem/bottin/v5/responses#GetHealth
---
data/apiclient.go | 35 -----------------------------------
1 file changed, 35 deletions(-)
diff --git a/data/apiclient.go b/data/apiclient.go
index 78ea400..6132a12 100644
--- a/data/apiclient.go
+++ b/data/apiclient.go
@@ -1,11 +1,8 @@
package data
import (
- "encoding/json"
- "errors"
"fmt"
"io"
- "io/ioutil"
"net/http"
)
@@ -61,35 +58,3 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo
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
-}
From c850b221a1029eaf0bfd5ef34b1f0fba0c9debdb Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Fri, 9 Jun 2023 01:09:02 -0400
Subject: [PATCH 10/34] =?UTF-8?q?Impl=C3=A9menter=20client=20web=20de=20ba?=
=?UTF-8?q?se?=
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,
+ })
+}
From 2287f00e2926531bbcfd8ef5cce91a642a0c79ce Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Fri, 9 Jun 2023 01:11:55 -0400
Subject: [PATCH 11/34] 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,
- })
-}
From 660d8826e2402b1bfbce3595950c2e5c8149c061 Mon Sep 17 00:00:00 2001
From: Victor Lacasse-Beaudoin
Date: Fri, 9 Jun 2023 23:52:03 -0400
Subject: [PATCH 12/34] =?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
-