From 947ee56596ccd40545dd95cbcc38e5c585f23b9e Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 15:00:42 -0500 Subject: [PATCH 1/7] wip --- .gitignore | 1 + compose.yaml | 15 +++++++ db.go | 53 ++++++++++++++++++++++ entity.go | 9 ++++ handler.go | 76 ++++++++++++++++++++++++++++++- main.go | 12 ++++- render.go | 23 ---------- server.go | 18 ++++++-- ui/embed.go | 22 ++++++++- ui/index.html | 122 +++++++++++++++++++++++++++++++++++++++++++++++++- 10 files changed, 319 insertions(+), 32 deletions(-) create mode 100644 .gitignore create mode 100644 compose.yaml create mode 100644 db.go create mode 100644 entity.go delete mode 100644 render.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..e33152e --- /dev/null +++ b/compose.yaml @@ -0,0 +1,15 @@ +name: 'agendas-db' +services: + db: + image: 'postgres:16' + environment: + POSTGRES_USER: "${DB_USER:?}" + POSTGRES_PASSWORD: "${DB_PASSWORD:?}" + POSTGRES_DATABASE: "${DB_DATABASE:?}" + restart: 'unless-stopped' + ports: + - '5432:5432' + volumes: + - 'db-data:/var/lib/postgresql/data/' +volumes: + db-data: diff --git a/db.go b/db.go new file mode 100644 index 0000000..4274fd1 --- /dev/null +++ b/db.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type DBClient struct { + Pool *pgxpool.Pool +} + +func (d DBClient) Ping(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if d.Pool == nil { + return fmt.Errorf("db: cannot check health using nil connection pool") + } + + return d.Pool.Ping(ctx) + } +} + +func (d DBClient) Init(ctx context.Context) error { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO Init + return fmt.Errorf("db: Init not implemented") +} + +func (d DBClient) CreateTransaction(ctx context.Context, transaction Transaction) error { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO CreateTransaction + return fmt.Errorf("db: CreateTransaction not implemented") +} + +func (d DBClient) ReadTransaction(ctx context.Context, membreID string, isPerpetual bool) error { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO ReadTransaction + return fmt.Errorf("db: ReadTransaction not implemented") +} + +func (d DBClient) CountTransactions(ctx context.Context) (membresSansAgenda, membresAvecPerpetuel, membresAvecNonPerpetuel, membresAvecTout int, err error) { + //TODO check context is not closed + //TODO check *DB is not nil + //TODO CountTransactions + return 0, 0, 0, 0, fmt.Errorf("db: CountTransactions not implemented") +} diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..3999d7d --- /dev/null +++ b/entity.go @@ -0,0 +1,9 @@ +package main + +import "time" + +type Transaction struct { + MembreID string `db:"membre_id"` + GivenAt *time.Time `db:"given_at"` + IsPerpetual bool `db:"is_perpetual"` +} diff --git a/handler.go b/handler.go index 34f5ee2..a771687 100644 --- a/handler.go +++ b/handler.go @@ -11,11 +11,12 @@ import ( ) // Handling -func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFunc { +func UIIndex(ctx context.Context, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc { return func(c echo.Context) error { type data struct { Error string BottinHealthResponse bottin.ReadHealthResponse + IsDBUp bool } return c.Render(http.StatusOK, "index", func() (d data) { @@ -36,6 +37,16 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFu } d.BottinHealthResponse = bottinHealthResponse + // Check db health + if dbClient == nil { + return fmt.Errorf("Impossible de contacter la base de données, le client DB est nil") + } + + if err := dbClient.Ping(ctx); err != nil { + return err + } + d.IsDBUp = true + // No errors return nil } @@ -91,3 +102,66 @@ func UIReadMembre(ctx context.Context, bottinClient *bottin.APIClient) echo.Hand }()) } } + +/* +UICreateTransaction gère la création des transactions + +TODO: + - Fully implement + - Check context is not closed +*/ +func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc { + return func(c echo.Context) error { + type data struct { + BottinHealth bottin.ReadHealthResponse + Error string + Result string + } + + return c.Render(http.StatusOK, "index", func() (d data) { + if err := func() error { + if bottinClient == nil { + return fmt.Errorf("Cannot operate on nil *bottin.APIClient") + } + + bottinReadHealthResponse, err := bottinClient.ReadHealth(ctx) + if err != nil { + return err + } + + d.BottinHealth = bottinReadHealthResponse + + isPerpetual := c.FormValue("is_perpetual") == "on" + membreID := c.FormValue("membre_id") + + if membreID == "" { + return fmt.Errorf("👎 Aucun numéro étudiant sélectionné. Assurez-vous de cliquer sur la case 'Numéro étudiant:' avant de scanner.") + } + + // dbclient.CreateTransaction + if err := dbClient.CreateTransaction(ctx, Transaction{ + MembreID: membreID, + IsPerpetual: isPerpetual, + }); err != nil { + return err + } + + // Prepare result message + var typeAgenda string + if isPerpetual { + typeAgenda = "perpétuel" + } else { + typeAgenda = "non-perpétuel" + } + + d.Result = fmt.Sprintf("👍 Membre %s peut recevoir son agenda %s", membreID, typeAgenda) + + return fmt.Errorf("UIIndexPOST not fully implemented") + }(); err != nil { + d.Error = err.Error() + log.Println("err:", d.Error) + } + return + }()) + } +} diff --git a/main.go b/main.go index d55e62b..9049128 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "codeberg.org/vlbeaudoin/voki/v3" "git.agecem.com/bottin/bottin/v10/pkg/bottin" + "github.com/jackc/pgx/v5/pgxpool" "golang.org/x/term" ) @@ -57,7 +58,16 @@ func run(ctx context.Context, cfg Config) error { protocol, )} - if err := RunServer(ctx, &bottinClient); err != nil && err != http.ErrServerClosed { + // connect to db + dbPool, err := pgxpool.New(ctx, "postgres://agendas:agendas@localhost:5432/agendas") + if err != nil { + return err + } + defer dbPool.Close() + + dbClient := DBClient{Pool: dbPool} + + if err := RunServer(ctx, cfg, &bottinClient, &dbClient); err != nil && err != http.ErrServerClosed { return err } diff --git a/render.go b/render.go deleted file mode 100644 index a3ebbc6..0000000 --- a/render.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "io" - "text/template" - - "git.agecem.com/bottin/agendas/ui" - "github.com/labstack/echo/v4" -) - -type Renderer struct { - templates *template.Template -} - -func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -func NewRenderer() *Renderer { - return &Renderer{ - templates: template.Must(template.ParseFS(ui.HTMLFS(), "*html")), - } -} diff --git a/server.go b/server.go index 93c2352..199abda 100644 --- a/server.go +++ b/server.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "git.agecem.com/bottin/agendas/ui" "git.agecem.com/bottin/bottin/v10/pkg/bottin" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) -func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { +func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) error { select { case <-ctx.Done(): return ctx.Err() @@ -18,15 +19,24 @@ func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { return fmt.Errorf("nil bottin client") } + if dbClient == nil { + return fmt.Errorf("nil dbClient") + } + e := echo.New() - e.Renderer = NewRenderer() + e.Renderer = ui.NewRenderer() e.Pre(middleware.AddTrailingSlash()) - e.GET("/", UIIndex(ctx, bottinClient)) + //basicauth TODO + //e.Use( - e.GET("/membre/", UIReadMembre(ctx, bottinClient)) + e.GET("/", UIIndex(ctx, bottinClient, dbClient)) + //e.GET("/transaction/", UIReadTransaction + e.POST("/transaction/", UICreateTransaction(ctx, cfg, bottinClient, dbClient)) + + //e.GET("/membre/", UIReadMembre(ctx, bottinClient)) return e.Start(":3333") } diff --git a/ui/embed.go b/ui/embed.go index c10c63b..fa6bb1b 100644 --- a/ui/embed.go +++ b/ui/embed.go @@ -1,6 +1,12 @@ package ui -import "embed" +import ( + "embed" + "io" + "text/template" + + "github.com/labstack/echo/v4" +) //go:embed *.html var htmlFS embed.FS @@ -8,3 +14,17 @@ var htmlFS embed.FS func HTMLFS() embed.FS { return htmlFS } + +type Renderer struct { + templates *template.Template +} + +func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +func NewRenderer() *Renderer { + return &Renderer{ + templates: template.Must(template.ParseFS(HTMLFS(), "*html")), + } +} diff --git a/ui/index.html b/ui/index.html index 7e43017..0583275 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1,4 +1,122 @@ {{ define "index" }} -hello world -{{ . }} + + + + + + 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 +

+ +
+ +
+ +
+ + + + +

{{ . }}

+ + + {{ end }} From cdb16a9b60a91d30c0681918757b25077906981a Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 17:30:37 -0500 Subject: [PATCH 2/7] wip --- cmd.go | 51 ++++++++++++++++++++++++++ config.go | 76 ++++++++++++++++++++++++++++++++++++++- flag.go | 51 ++++++++++++++++++++++++++ go.mod | 7 ++-- go.sum | 8 +++-- main.go | 44 ++++++----------------- server.go | 16 +++++++-- ui/embed.go | 4 --- render.go => ui/render.go | 5 ++- 9 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 cmd.go create mode 100644 flag.go rename render.go => ui/render.go (73%) diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..5977f37 --- /dev/null +++ b/cmd.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/spf13/cobra" +) + +func init() { + // rootCmd + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.bottin.agendas.yaml)") + + if err := BindFlags(rootCmd.PersistentFlags()); err != nil { + log.Fatal(err) + } +} + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "agendas", + //Args: cobra.(0), + Short: "Gestion de distribution d'agendas", + Run: func(c *cobra.Command, args []string) { + log.Println("Starting bottin agendas microservice") + + ctx := context.TODO() + + cfg, err := ParseConfig() + if err != nil { + log.Fatal(err) + } + + if len(args) > 0 { + if args[0] == "debugConfig" { + log.Println("debug: Printing current config") + cfgAsBytes, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + log.Fatal("Cannot marshal config for printing:", err) + } + fmt.Println(string(cfgAsBytes)) + } + } + + if err := run(ctx, cfg); err != nil { + log.Fatal(err) + } + }, +} diff --git a/config.go b/config.go index 3bfde7a..665e1d1 100644 --- a/config.go +++ b/config.go @@ -1,7 +1,81 @@ package main -import "git.agecem.com/bottin/bottin/v10/pkg/bottin" +import ( + "fmt" + "os" + "strings" + + "git.agecem.com/bottin/bottin/v10/pkg/bottin" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) type Config struct { + // Bottin holds a client config to contact a bottin API server + // Cannot be set using environment variables Bottin bottin.APIClientConfig + + // TLS holds options for TLS / SSL / HTTPS + TLS struct { + // Cert holds the public certificate (or path to a file containing one) for user interface TLS + Cert string + + // Key holds the private key (or path to a file containing one) for user interface TLS + Key string + + // Enabled holds the TLS activation state + Enabled bool + } + + // Credentials holds username-password pairs for basic authentication + Credentials map[string]string + + // Port holds the port on which to expose the user interface + Port int +} + +func ParseConfig() (cfg Config, err error) { + return cfg, viper.Unmarshal(&cfg) +} + +var cfgFile string + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".bottin.agendas" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".bottin.agendas") + } + + // Read in env, will find matching viper bindings in flag.go [BindFlags] + viper.SetEnvPrefix("AGENDAS") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} + +func init() { + cobra.OnInitialize(initConfig) +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } } diff --git a/flag.go b/flag.go new file mode 100644 index 0000000..9ff9c91 --- /dev/null +++ b/flag.go @@ -0,0 +1,51 @@ +package main + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + configPort = "Port" + defaultPort = 3333 + flagPort = "port" + + configTLSEnabled = "TLS.Enabled" + defaultTLSEnabled = true + flagTLSEnabled = "tls-enabled" + + configTLSCert = "TLS.Cert" + defaultTLSCert = "" + flagTLSCert = "tls-cert" + + configTLSKey = "TLS.Key" + defaultTLSKey = "" + flagTLSKey = "tls-key" +) + +// BindClientFlags declares client-related flags and config options in the specified *pflag.FlagSet +func BindFlags(set *pflag.FlagSet) error { + // Credentials -> seulement par config + + set.Int(flagPort, defaultPort, "User interface port") + if err := viper.BindPFlag(configPort, set.Lookup(flagPort)); err != nil { + return err + } + + set.Bool(flagTLSEnabled, defaultTLSEnabled, "User interface TLS state") + if err := viper.BindPFlag(configTLSEnabled, set.Lookup(flagTLSEnabled)); err != nil { + return err + } + + set.String(flagTLSKey, defaultTLSKey, "User interface TLS private key (or path to file)") + if err := viper.BindPFlag(configTLSKey, set.Lookup(flagTLSKey)); err != nil { + return err + } + + set.String(flagTLSCert, defaultTLSCert, "User interface TLS certificate (or path to file)") + if err := viper.BindPFlag(configTLSCert, set.Lookup(flagTLSCert)); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 4c65e6e..089ecbd 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,16 @@ require ( codeberg.org/vlbeaudoin/voki/v3 v3.0.1 git.agecem.com/bottin/bottin/v10 v10.4.1 github.com/labstack/echo/v4 v4.13.3 - golang.org/x/term v0.27.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 ) require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect @@ -28,8 +31,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index b0c9def..8f11c0b 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ codeberg.org/vlbeaudoin/voki/v3 v3.0.1 h1:pFjd/ZKsu4eOzRJYViE9F1S3RglSkAuIiqCo9I codeberg.org/vlbeaudoin/voki/v3 v3.0.1/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g= git.agecem.com/bottin/bottin/v10 v10.4.1 h1:mRZfqnLhGN9Qb+iQvfOvZM5pxXqSlkiXqIMX59zPAS8= git.agecem.com/bottin/bottin/v10 v10.4.1/go.mod h1:KTwlqY5XdVi9F7cpwy3hxYN1DQm+74tBv7Wc9rfKXuM= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -16,6 +17,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -48,6 +51,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -58,6 +62,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -96,8 +102,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= diff --git a/main.go b/main.go index d55e62b..d8ea5fa 100644 --- a/main.go +++ b/main.go @@ -2,38 +2,16 @@ package main import ( "context" - "fmt" - "log" "net/http" "codeberg.org/vlbeaudoin/voki/v3" "git.agecem.com/bottin/bottin/v10/pkg/bottin" - "golang.org/x/term" ) // Entry - -func init() {} - func main() { - cfg := Config{} - cfg.Bottin.Host = "api.bottin.agecem.com" - cfg.Bottin.Port = 443 - cfg.Bottin.TLS.Enabled = true - - ctx := context.TODO() - - fmt.Println("bottin password (no echo): ") - password, err := term.ReadPassword(0) - if err != nil { - log.Fatal(err) - } - - cfg.Bottin.Key = string(password) - - if err := run(ctx, cfg); err != nil { - log.Fatal(err) - } + // Start commandline parsing then call `run` + Execute() } func run(ctx context.Context, cfg Config) error { @@ -41,23 +19,21 @@ func run(ctx context.Context, cfg Config) error { case <-ctx.Done(): return ctx.Err() default: - var protocol string - - if cfg.Bottin.TLS.Enabled { - protocol = "https" - } else { - protocol = "http" - } - bottinClient := bottin.APIClient{Caller: voki.New( http.DefaultClient, cfg.Bottin.Host, cfg.Bottin.Key, cfg.Bottin.Port, - protocol, + func() string { + if cfg.Bottin.TLS.Enabled { + return "https" + } else { + return "http" + } + }(), )} - if err := RunServer(ctx, &bottinClient); err != nil && err != http.ErrServerClosed { + if err := RunServer(ctx, cfg, &bottinClient); err != nil && err != http.ErrServerClosed { return err } diff --git a/server.go b/server.go index 93c2352..89fc41e 100644 --- a/server.go +++ b/server.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "git.agecem.com/bottin/agendas/ui" "git.agecem.com/bottin/bottin/v10/pkg/bottin" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) -func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { +func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient) error { select { case <-ctx.Done(): return ctx.Err() @@ -20,14 +21,23 @@ func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error { e := echo.New() - e.Renderer = NewRenderer() + e.Renderer = ui.NewRenderer() e.Pre(middleware.AddTrailingSlash()) + //TODO basic auth + //TODO log successful basic auths username + e.GET("/", UIIndex(ctx, bottinClient)) e.GET("/membre/", UIReadMembre(ctx, bottinClient)) - return e.Start(":3333") + address := fmt.Sprintf(":%d", cfg.Port) + + if cfg.TLS.Enabled { + return e.StartTLS(address, cfg.TLS.Cert, cfg.TLS.Key) + } else { + return e.Start(address) + } } } diff --git a/ui/embed.go b/ui/embed.go index c10c63b..f757d26 100644 --- a/ui/embed.go +++ b/ui/embed.go @@ -4,7 +4,3 @@ import "embed" //go:embed *.html var htmlFS embed.FS - -func HTMLFS() embed.FS { - return htmlFS -} diff --git a/render.go b/ui/render.go similarity index 73% rename from render.go rename to ui/render.go index a3ebbc6..f4c08c3 100644 --- a/render.go +++ b/ui/render.go @@ -1,10 +1,9 @@ -package main +package ui import ( "io" "text/template" - "git.agecem.com/bottin/agendas/ui" "github.com/labstack/echo/v4" ) @@ -18,6 +17,6 @@ func (t *Renderer) Render(w io.Writer, name string, data any, c echo.Context) er func NewRenderer() *Renderer { return &Renderer{ - templates: template.Must(template.ParseFS(ui.HTMLFS(), "*html")), + templates: template.Must(template.ParseFS(htmlFS, "*html")), } } From abda13d07085a9bc79ef61de50526c09246af325 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 18:10:31 -0500 Subject: [PATCH 3/7] wip --- config.go | 9 +++++++++ main.go | 11 ++++++++++- server.go | 32 +++++++++++++++++++++++++++++--- ui/index.html | 6 ++---- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index 665e1d1..f25cd79 100644 --- a/config.go +++ b/config.go @@ -15,6 +15,15 @@ type Config struct { // Cannot be set using environment variables Bottin bottin.APIClientConfig + DB struct { + Database string + Host string + Password string + Port int + SSLMode string + Username string + } + // TLS holds options for TLS / SSL / HTTPS TLS struct { // Cert holds the public certificate (or path to a file containing one) for user interface TLS diff --git a/main.go b/main.go index e08463d..d6a16f2 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "net/http" "codeberg.org/vlbeaudoin/voki/v3" @@ -35,7 +36,15 @@ func run(ctx context.Context, cfg Config) error { )} // connect to db - dbPool, err := pgxpool.New(ctx, "postgres://agendas:agendas@localhost:5432/agendas") + dbPool, err := pgxpool.New(ctx, + fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", + cfg.DB.Username, + cfg.DB.Password, + cfg.DB.Host, + cfg.DB.Port, + cfg.DB.Database, + cfg.DB.SSLMode, + )) if err != nil { return err } diff --git a/server.go b/server.go index 30d77ca..a414744 100644 --- a/server.go +++ b/server.go @@ -2,7 +2,9 @@ package main import ( "context" + "crypto/subtle" "fmt" + "log" "git.agecem.com/bottin/agendas/ui" "git.agecem.com/bottin/bottin/v10/pkg/bottin" @@ -25,12 +27,36 @@ func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, e := echo.New() - e.Renderer = ui.NewRenderer() + r := ui.NewRenderer() + + if r == nil { + return fmt.Errorf("nil renderer") + } + + e.Renderer = r e.Pre(middleware.AddTrailingSlash()) - //TODO basic auth - //TODO log successful basic auths username + // basic auth + if len(cfg.Credentials) == 0 { + return fmt.Errorf("UI requires at least one credential (config key `Credentials` of type map[string]string)") + } + + e.Use(middleware.BasicAuth( + func(username, password string, c echo.Context) (bool, error) { + for validUser, validPass := range cfg.Credentials { + userOK := subtle.ConstantTimeCompare([]byte(username), []byte(validUser)) == 1 + passOK := subtle.ConstantTimeCompare([]byte(password), []byte(validPass)) == 1 + if userOK && passOK { + // log successful basic auths username + log.Println("login ok for user", username) + + return true, nil + } + } + return false, nil + }), + ) e.GET("/", UIIndex(ctx, bottinClient, dbClient)) //e.GET("/transaction/", UIReadTransaction diff --git a/ui/index.html b/ui/index.html index 0583275..bdf4e1c 100644 --- a/ui/index.html +++ b/ui/index.html @@ -112,10 +112,8 @@ button { - - - -

{{ . }}

+ {{ if .Error }}

Erreur: {{ .Error }}

{{ end }} +

{{ .Result }}

From 7f3901ddc297e4d364dc7664c00ddc1deb35a572 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 19:13:05 -0500 Subject: [PATCH 4/7] wip --- compose.yaml | 1 + db.go | 11 +++++++-- go.mod | 2 +- handler.go | 60 +++++++++------------------------------------- queries/queries.go | 10 ++++++++ queries/schema.sql | 7 ++++++ server.go | 8 ++++--- ui/index.html | 4 ++-- 8 files changed, 46 insertions(+), 57 deletions(-) create mode 100644 queries/queries.go create mode 100644 queries/schema.sql diff --git a/compose.yaml b/compose.yaml index e33152e..fd5c804 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,5 +11,6 @@ services: - '5432:5432' volumes: - 'db-data:/var/lib/postgresql/data/' + - '/etc/localtime:/etc/localtime:ro' volumes: db-data: diff --git a/db.go b/db.go index 4274fd1..fd243d5 100644 --- a/db.go +++ b/db.go @@ -3,7 +3,9 @@ package main import ( "context" "fmt" + "log" + "git.agecem.com/bottin/agendas/queries" "github.com/jackc/pgx/v5/pgxpool" ) @@ -27,8 +29,13 @@ func (d DBClient) Ping(ctx context.Context) error { func (d DBClient) Init(ctx context.Context) error { //TODO check context is not closed //TODO check *DB is not nil - //TODO Init - return fmt.Errorf("db: Init not implemented") + log.Println("warning: DBClient [Init] not properly checked") + // Init + if _, err := d.Pool.Exec(ctx, queries.SQLSchema()); err != nil { + return err + } + + return nil } func (d DBClient) CreateTransaction(ctx context.Context, transaction Transaction) error { diff --git a/go.mod b/go.mod index 089ecbd..6d97f24 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.4 require ( codeberg.org/vlbeaudoin/voki/v3 v3.0.1 git.agecem.com/bottin/bottin/v10 v10.4.1 + github.com/jackc/pgx/v5 v5.7.1 github.com/labstack/echo/v4 v4.13.3 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 @@ -18,7 +19,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.1 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/handler.go b/handler.go index a771687..801f8dd 100644 --- a/handler.go +++ b/handler.go @@ -17,6 +17,7 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient, dbClient *DBCl Error string BottinHealthResponse bottin.ReadHealthResponse IsDBUp bool + Result string } return c.Render(http.StatusOK, "index", func() (d data) { @@ -62,47 +63,6 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient, dbClient *DBCl } } -func UIReadMembre(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFunc { - return func(c echo.Context) error { - type data struct { - Error string - BottinMembreResponse bottin.ReadMembreResponse - } - - return c.Render(http.StatusOK, "index", func() (d data) { - if err := func() error { - select { - case <-ctx.Done(): - return fmt.Errorf("Impossible de contacter le serveur: %s", ctx.Err()) - default: - // Check client - if bottinClient == nil { - return fmt.Errorf("Impossible de contacter le serveur, le client API est nil") - } - - var err error - - // Check membre - d.BottinMembreResponse, err = bottinClient.ReadMembre(ctx, c.QueryParam("m")) - if err != nil { - return err - } - - // No errors - return nil - } - }(); err != nil { - // Send error to user - d.Error = err.Error() - - // Log error - log.Println("err:", d.Error) - } - return - }()) - } -} - /* UICreateTransaction gère la création des transactions @@ -113,9 +73,9 @@ TODO: func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, dbClient *DBClient) echo.HandlerFunc { return func(c echo.Context) error { type data struct { - BottinHealth bottin.ReadHealthResponse - Error string - Result string + Error string + Result string + //BottinHealth bottin.ReadHealthResponse } return c.Render(http.StatusOK, "index", func() (d data) { @@ -124,12 +84,14 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A return fmt.Errorf("Cannot operate on nil *bottin.APIClient") } - bottinReadHealthResponse, err := bottinClient.ReadHealth(ctx) - if err != nil { - return err - } + /* + bottinReadHealthResponse, err := bottinClient.ReadHealth(ctx) + if err != nil { + return err + } - d.BottinHealth = bottinReadHealthResponse + d.BottinHealth = bottinReadHealthResponse + */ isPerpetual := c.FormValue("is_perpetual") == "on" membreID := c.FormValue("membre_id") diff --git a/queries/queries.go b/queries/queries.go new file mode 100644 index 0000000..3f33b1e --- /dev/null +++ b/queries/queries.go @@ -0,0 +1,10 @@ +package queries + +import _ "embed" + +//go:embed schema.sql +var sqlSchema string + +func SQLSchema() string { + return sqlSchema +} diff --git a/queries/schema.sql b/queries/schema.sql new file mode 100644 index 0000000..3eb2487 --- /dev/null +++ b/queries/schema.sql @@ -0,0 +1,7 @@ +-- Schema +CREATE TABLE IF NOT EXISTS transactions ( + given_at TIMESTAMP DEFAULT current_timestamp, + membre_id VARCHAR(7) NOT NULL CHECK (length(membre_id) > 0), + is_perpetual BOOLEAN NOT NULL, + PRIMARY KEY (membre_id, is_perpetual) +); diff --git a/server.go b/server.go index a414744..a9a4117 100644 --- a/server.go +++ b/server.go @@ -25,6 +25,10 @@ func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, return fmt.Errorf("nil dbClient") } + if err := dbClient.Init(ctx); err != nil { + return err + } + e := echo.New() r := ui.NewRenderer() @@ -42,7 +46,7 @@ func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, return fmt.Errorf("UI requires at least one credential (config key `Credentials` of type map[string]string)") } - e.Use(middleware.BasicAuth( + e.Pre(middleware.BasicAuth( func(username, password string, c echo.Context) (bool, error) { for validUser, validPass := range cfg.Credentials { userOK := subtle.ConstantTimeCompare([]byte(username), []byte(validUser)) == 1 @@ -62,8 +66,6 @@ func RunServer(ctx context.Context, cfg Config, bottinClient *bottin.APIClient, //e.GET("/transaction/", UIReadTransaction e.POST("/transaction/", UICreateTransaction(ctx, cfg, bottinClient, dbClient)) - //e.GET("/membre/", UIReadMembre(ctx, bottinClient)) - address := fmt.Sprintf(":%d", cfg.Port) if cfg.TLS.Enabled { diff --git a/ui/index.html b/ui/index.html index bdf4e1c..a10c75a 100644 --- a/ui/index.html +++ b/ui/index.html @@ -112,8 +112,8 @@ button { - {{ if .Error }}

Erreur: {{ .Error }}

{{ end }} -

{{ .Result }}

+ {{ if .Error }}

Erreur: {{ .Error }}

{{ end }} + {{ if .Result }}

{{ .Result }}

{{ end }} From 294c6221aa02cf552105e27ab73be8c96c906f40 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 19:14:49 -0500 Subject: [PATCH 5/7] rename index template to app --- handler.go | 4 ++-- ui/{index.html => app.html} | 3 ++- ui/test.html | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) rename ui/{index.html => app.html} (99%) delete mode 100644 ui/test.html diff --git a/handler.go b/handler.go index 801f8dd..a5684ab 100644 --- a/handler.go +++ b/handler.go @@ -20,7 +20,7 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient, dbClient *DBCl Result string } - return c.Render(http.StatusOK, "index", func() (d data) { + return c.Render(http.StatusOK, "app", func() (d data) { if err := func() error { select { case <-ctx.Done(): @@ -78,7 +78,7 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A //BottinHealth bottin.ReadHealthResponse } - return c.Render(http.StatusOK, "index", func() (d data) { + return c.Render(http.StatusOK, "app", func() (d data) { if err := func() error { if bottinClient == nil { return fmt.Errorf("Cannot operate on nil *bottin.APIClient") diff --git a/ui/index.html b/ui/app.html similarity index 99% rename from ui/index.html rename to ui/app.html index a10c75a..730837f 100644 --- a/ui/index.html +++ b/ui/app.html @@ -1,4 +1,4 @@ -{{ define "index" }} +{{ define "app" }} @@ -114,6 +114,7 @@ button { {{ if .Error }}

Erreur: {{ .Error }}

{{ end }} {{ if .Result }}

{{ .Result }}

{{ end }} + diff --git a/ui/test.html b/ui/test.html deleted file mode 100644 index 7015968..0000000 --- a/ui/test.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ define "test" }} -{{ . }} -{{ end }} From 1e19ab222ee3d37b95ad6264fbc5e3f3d20e844b Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 19:38:01 -0500 Subject: [PATCH 6/7] wip --- db.go | 68 +++++++++++++++++++++++++++++++++++++++++++----------- handler.go | 35 ++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/db.go b/db.go index fd243d5..99bd339 100644 --- a/db.go +++ b/db.go @@ -39,22 +39,64 @@ func (d DBClient) Init(ctx context.Context) error { } func (d DBClient) CreateTransaction(ctx context.Context, transaction Transaction) error { - //TODO check context is not closed - //TODO check *DB is not nil - //TODO CreateTransaction - return fmt.Errorf("db: CreateTransaction not implemented") + select { + case <-ctx.Done(): + return ctx.Err() + default: + if d.Pool == nil { + return fmt.Errorf("db: cannot check health using nil connection pool") + } + + // CreateTransaction + commandTag, err := d.Pool.Exec(ctx, ` +INSERT INTO "transactions" ( + membre_id, + is_perpetual +) +VALUES ( + $1, + $2 +); +`, transaction.MembreID, transaction.IsPerpetual) + if err != nil { + return err + } + + if commandTag.RowsAffected() == 0 { + return fmt.Errorf("No rows inserted") + } + + return nil + } } -func (d DBClient) ReadTransaction(ctx context.Context, membreID string, isPerpetual bool) error { - //TODO check context is not closed - //TODO check *DB is not nil - //TODO ReadTransaction - return fmt.Errorf("db: ReadTransaction not implemented") +func (d DBClient) ReadTransaction(ctx context.Context, membreID string, isPerpetual bool) (Transaction, error) { + select { + case <-ctx.Done(): + return Transaction{}, ctx.Err() + default: + if d.Pool == nil { + return Transaction{}, fmt.Errorf("db: cannot check health using nil connection pool") + } + + //TODO ReadTransaction + return Transaction{}, fmt.Errorf("db: ReadTransaction not implemented") + } } func (d DBClient) CountTransactions(ctx context.Context) (membresSansAgenda, membresAvecPerpetuel, membresAvecNonPerpetuel, membresAvecTout int, err error) { - //TODO check context is not closed - //TODO check *DB is not nil - //TODO CountTransactions - return 0, 0, 0, 0, fmt.Errorf("db: CountTransactions not implemented") + select { + case <-ctx.Done(): + err = ctx.Err() + return + default: + if d.Pool == nil { + err = fmt.Errorf("db: cannot check health using nil connection pool") + return + } + + //TODO CountTransactions + err = fmt.Errorf("db: CountTransactions not implemented") + return + } } diff --git a/handler.go b/handler.go index a5684ab..97763a5 100644 --- a/handler.go +++ b/handler.go @@ -100,6 +100,21 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A return fmt.Errorf("👎 Aucun numéro étudiant sélectionné. Assurez-vous de cliquer sur la case 'Numéro étudiant:' avant de scanner.") } + //TODO check if membre already received before checking bottin + + // check if membre exists in bottin + membreResponse, err := bottinClient.ReadMembre(ctx, membreID) + if err != nil { + if err.Error() == "400 no rows in result set" { + return fmt.Errorf("👎 Numéro étudiant introuvable %s", membreID) + } + return err + } + + if membreResponse.Data.Membre.ID != membreID { + return fmt.Errorf("👎 Bottin a retourné '%s' en demandant '%s'", membreID, membreResponse.Data.Membre.ID) + } + // dbclient.CreateTransaction if err := dbClient.CreateTransaction(ctx, Transaction{ MembreID: membreID, @@ -109,16 +124,18 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A } // Prepare result message - var typeAgenda string - if isPerpetual { - typeAgenda = "perpétuel" - } else { - typeAgenda = "non-perpétuel" - } + d.Result = fmt.Sprintf("👍 Membre %s peut recevoir son agenda %s", + membreID, + func() string { + if isPerpetual { + return "perpétuel" + } else { + return "non-perpétuel" + } + }(), + ) - d.Result = fmt.Sprintf("👍 Membre %s peut recevoir son agenda %s", membreID, typeAgenda) - - return fmt.Errorf("UIIndexPOST not fully implemented") + return nil }(); err != nil { d.Error = err.Error() log.Println("err:", d.Error) From 12f41c83b99147d793e4fce2fc394c566ab30481 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 30 Dec 2024 19:44:56 -0500 Subject: [PATCH 7/7] wip --- db.go | 12 ++++++++++++ handler.go | 8 ++++---- ui/app.html | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/db.go b/db.go index 99bd339..a822dbc 100644 --- a/db.go +++ b/db.go @@ -59,6 +59,18 @@ VALUES ( ); `, transaction.MembreID, transaction.IsPerpetual) if err != nil { + if err.Error() == `ERROR: duplicate key value violates unique constraint "transactions_pkey" (SQLSTATE 23505)` { + return fmt.Errorf("Membre '%s' a déjà reçu son agenda %s.", + transaction.MembreID, + func() string { + if transaction.IsPerpetual { + return "perpétuel" + } else { + return "non-perpétuel" + } + }(), + ) + } return err } diff --git a/handler.go b/handler.go index 97763a5..d014d98 100644 --- a/handler.go +++ b/handler.go @@ -97,7 +97,7 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A membreID := c.FormValue("membre_id") if membreID == "" { - return fmt.Errorf("👎 Aucun numéro étudiant sélectionné. Assurez-vous de cliquer sur la case 'Numéro étudiant:' avant de scanner.") + return fmt.Errorf("Aucun numéro étudiant sélectionné. Assurez-vous de cliquer sur la case 'Numéro étudiant:' avant de scanner.") } //TODO check if membre already received before checking bottin @@ -106,13 +106,13 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A membreResponse, err := bottinClient.ReadMembre(ctx, membreID) if err != nil { if err.Error() == "400 no rows in result set" { - return fmt.Errorf("👎 Numéro étudiant introuvable %s", membreID) + return fmt.Errorf("Numéro étudiant introuvable %s", membreID) } return err } if membreResponse.Data.Membre.ID != membreID { - return fmt.Errorf("👎 Bottin a retourné '%s' en demandant '%s'", membreID, membreResponse.Data.Membre.ID) + return fmt.Errorf("Bottin a retourné '%s' en demandant '%s'", membreID, membreResponse.Data.Membre.ID) } // dbclient.CreateTransaction @@ -124,7 +124,7 @@ func UICreateTransaction(ctx context.Context, cfg Config, bottinClient *bottin.A } // Prepare result message - d.Result = fmt.Sprintf("👍 Membre %s peut recevoir son agenda %s", + d.Result = fmt.Sprintf("Membre %s peut recevoir son agenda %s", membreID, func() string { if isPerpetual { diff --git a/ui/app.html b/ui/app.html index 730837f..610a07d 100644 --- a/ui/app.html +++ b/ui/app.html @@ -112,8 +112,8 @@ button { - {{ if .Error }}

Erreur: {{ .Error }}

{{ end }} - {{ if .Result }}

{{ .Result }}

{{ end }} + {{ if .Error }}

👎 {{ .Error }}

{{ end }} + {{ if .Result }}

👍 {{ .Result }}

{{ end }}