Produit minimal viable #1
19 changed files with 635 additions and 85 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.env
|
51
cmd.go
Normal file
51
cmd.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
16
compose.yaml
Normal file
16
compose.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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/'
|
||||||
|
- '/etc/localtime:/etc/localtime:ro'
|
||||||
|
volumes:
|
||||||
|
db-data:
|
85
config.go
85
config.go
|
@ -1,7 +1,90 @@
|
||||||
package main
|
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 {
|
type Config struct {
|
||||||
|
// Bottin holds a client config to contact a bottin API server
|
||||||
|
// Cannot be set using environment variables
|
||||||
Bottin bottin.APIClientConfig
|
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
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
114
db.go
Normal file
114
db.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.agecem.com/bottin/agendas/queries"
|
||||||
|
"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
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if commandTag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("No rows inserted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
9
entity.go
Normal file
9
entity.go
Normal file
|
@ -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"`
|
||||||
|
}
|
51
flag.go
Normal file
51
flag.go
Normal file
|
@ -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
|
||||||
|
}
|
9
go.mod
9
go.mod
|
@ -5,17 +5,20 @@ go 1.23.4
|
||||||
require (
|
require (
|
||||||
codeberg.org/vlbeaudoin/voki/v3 v3.0.1
|
codeberg.org/vlbeaudoin/voki/v3 v3.0.1
|
||||||
git.agecem.com/bottin/bottin/v10 v10.4.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/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 (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect
|
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // 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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
@ -28,8 +31,6 @@ require (
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.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/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
|
8
go.sum
8
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=
|
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 h1:mRZfqnLhGN9Qb+iQvfOvZM5pxXqSlkiXqIMX59zPAS8=
|
||||||
git.agecem.com/bottin/bottin/v10 v10.4.1/go.mod h1:KTwlqY5XdVi9F7cpwy3hxYN1DQm+74tBv7Wc9rfKXuM=
|
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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
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/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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
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/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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
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 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 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/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 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
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/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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
|
|
97
handler.go
97
handler.go
|
@ -11,14 +11,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handling
|
// 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 {
|
return func(c echo.Context) error {
|
||||||
type data struct {
|
type data struct {
|
||||||
Error string
|
Error string
|
||||||
BottinHealthResponse bottin.ReadHealthResponse
|
BottinHealthResponse bottin.ReadHealthResponse
|
||||||
|
IsDBUp bool
|
||||||
|
Result string
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "index", func() (d data) {
|
return c.Render(http.StatusOK, "app", func() (d data) {
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -36,6 +38,16 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFu
|
||||||
}
|
}
|
||||||
d.BottinHealthResponse = bottinHealthResponse
|
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
|
// No errors
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -51,40 +63,81 @@ func UIIndex(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UIReadMembre(ctx context.Context, bottinClient *bottin.APIClient) echo.HandlerFunc {
|
/*
|
||||||
|
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 {
|
return func(c echo.Context) error {
|
||||||
type data struct {
|
type data struct {
|
||||||
Error string
|
Error string
|
||||||
BottinMembreResponse bottin.ReadMembreResponse
|
Result string
|
||||||
|
//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 err := func() error {
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return fmt.Errorf("Impossible de contacter le serveur: %s", ctx.Err())
|
|
||||||
default:
|
|
||||||
// Check client
|
|
||||||
if bottinClient == nil {
|
if bottinClient == nil {
|
||||||
return fmt.Errorf("Impossible de contacter le serveur, le client API est nil")
|
return fmt.Errorf("Cannot operate on nil *bottin.APIClient")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
/*
|
||||||
|
bottinReadHealthResponse, err := bottinClient.ReadHealth(ctx)
|
||||||
// Check membre
|
|
||||||
d.BottinMembreResponse, err = bottinClient.ReadMembre(ctx, c.QueryParam("m"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// No errors
|
d.BottinHealth = bottinReadHealthResponse
|
||||||
return nil
|
*/
|
||||||
}
|
|
||||||
}(); err != nil {
|
|
||||||
// Send error to user
|
|
||||||
d.Error = err.Error()
|
|
||||||
|
|
||||||
// Log error
|
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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
//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,
|
||||||
|
IsPerpetual: isPerpetual,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare result message
|
||||||
|
d.Result = fmt.Sprintf("Membre %s peut recevoir son agenda %s",
|
||||||
|
membreID,
|
||||||
|
func() string {
|
||||||
|
if isPerpetual {
|
||||||
|
return "perpétuel"
|
||||||
|
} else {
|
||||||
|
return "non-perpétuel"
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
d.Error = err.Error()
|
||||||
log.Println("err:", d.Error)
|
log.Println("err:", d.Error)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
61
main.go
61
main.go
|
@ -3,37 +3,17 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"codeberg.org/vlbeaudoin/voki/v3"
|
"codeberg.org/vlbeaudoin/voki/v3"
|
||||||
"git.agecem.com/bottin/bottin/v10/pkg/bottin"
|
"git.agecem.com/bottin/bottin/v10/pkg/bottin"
|
||||||
"golang.org/x/term"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Entry
|
// Entry
|
||||||
|
|
||||||
func init() {}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := Config{}
|
// Start commandline parsing then call `run`
|
||||||
cfg.Bottin.Host = "api.bottin.agecem.com"
|
Execute()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(ctx context.Context, cfg Config) error {
|
func run(ctx context.Context, cfg Config) error {
|
||||||
|
@ -41,23 +21,38 @@ func run(ctx context.Context, cfg Config) error {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
var protocol string
|
|
||||||
|
|
||||||
if cfg.Bottin.TLS.Enabled {
|
|
||||||
protocol = "https"
|
|
||||||
} else {
|
|
||||||
protocol = "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
bottinClient := bottin.APIClient{Caller: voki.New(
|
bottinClient := bottin.APIClient{Caller: voki.New(
|
||||||
http.DefaultClient,
|
http.DefaultClient,
|
||||||
cfg.Bottin.Host,
|
cfg.Bottin.Host,
|
||||||
cfg.Bottin.Key,
|
cfg.Bottin.Key,
|
||||||
cfg.Bottin.Port,
|
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 {
|
// connect to db
|
||||||
|
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
|
||||||
|
}
|
||||||
|
defer dbPool.Close()
|
||||||
|
|
||||||
|
dbClient := DBClient{Pool: dbPool}
|
||||||
|
|
||||||
|
if err := RunServer(ctx, cfg, &bottinClient, &dbClient); err != nil && err != http.ErrServerClosed {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
queries/queries.go
Normal file
10
queries/queries.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package queries
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed schema.sql
|
||||||
|
var sqlSchema string
|
||||||
|
|
||||||
|
func SQLSchema() string {
|
||||||
|
return sqlSchema
|
||||||
|
}
|
7
queries/schema.sql
Normal file
7
queries/schema.sql
Normal file
|
@ -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)
|
||||||
|
);
|
54
server.go
54
server.go
|
@ -2,14 +2,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.agecem.com/bottin/agendas/ui"
|
||||||
"git.agecem.com/bottin/bottin/v10/pkg/bottin"
|
"git.agecem.com/bottin/bottin/v10/pkg/bottin"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"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 {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
|
@ -18,16 +21,57 @@ func RunServer(ctx context.Context, bottinClient *bottin.APIClient) error {
|
||||||
return fmt.Errorf("nil bottin client")
|
return fmt.Errorf("nil bottin client")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbClient == nil {
|
||||||
|
return fmt.Errorf("nil dbClient")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbClient.Init(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
e.Renderer = NewRenderer()
|
r := ui.NewRenderer()
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
return fmt.Errorf("nil renderer")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Renderer = r
|
||||||
|
|
||||||
e.Pre(middleware.AddTrailingSlash())
|
e.Pre(middleware.AddTrailingSlash())
|
||||||
|
|
||||||
e.GET("/", UIIndex(ctx, bottinClient))
|
// 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.GET("/membre/", UIReadMembre(ctx, bottinClient))
|
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
|
||||||
|
passOK := subtle.ConstantTimeCompare([]byte(password), []byte(validPass)) == 1
|
||||||
|
if userOK && passOK {
|
||||||
|
// log successful basic auths username
|
||||||
|
log.Println("login ok for user", username)
|
||||||
|
|
||||||
return e.Start(":3333")
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
e.GET("/", UIIndex(ctx, bottinClient, dbClient))
|
||||||
|
//e.GET("/transaction/", UIReadTransaction
|
||||||
|
e.POST("/transaction/", UICreateTransaction(ctx, cfg, bottinClient, dbClient))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
121
ui/app.html
Normal file
121
ui/app.html
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
{{ define "app" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>
|
||||||
|
AGECEM | Agenda
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
/* Center the form on the page */
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 400px;
|
||||||
|
/* Form outline */
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form li + li {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
/* Uniform size & alignment */
|
||||||
|
display: inline-block;
|
||||||
|
width: 90px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
/* To make sure that all text fields have the same font settings
|
||||||
|
By default, textareas have a monospace font */
|
||||||
|
font: 1em sans-serif;
|
||||||
|
|
||||||
|
/* Uniform text field size */
|
||||||
|
width: 300px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
/* Match form field borders */
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus {
|
||||||
|
/* Additional highlight for focused elements */
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
/* Align multiline text fields with their labels */
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
/* Provide space to type some text */
|
||||||
|
height: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
/* Align buttons with the text fields */
|
||||||
|
padding-left: 90px; /* same size as the label elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
/* This extra margin represent roughly the same space as the space
|
||||||
|
between the labels and their text fields */
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>
|
||||||
|
Distribution d'agendas aux membres de l'AGECEM
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
1) Si l'agenda demandé est perpétuel, cochez 'Perpétuel?:'<br>
|
||||||
|
<br>
|
||||||
|
2) Sélectionnez le champs 'Numéro étudiant:'<br>
|
||||||
|
<br>
|
||||||
|
3) Scannez la carte étudiante d'unE membre<br>
|
||||||
|
-ou-<br>
|
||||||
|
Entrez manuellement le code à 7 chiffres<br>
|
||||||
|
<br>
|
||||||
|
4) Si aucune erreur ne survient, la personne est libre de partir avec son agenda
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<form action="/transaction/" method="post">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<label for="is_perpetual">Perpétuel?:</label>
|
||||||
|
<input type="checkbox" id="is_perpetual" name="is_perpetual"/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label for="membre_id">Numéro étudiant:</label>
|
||||||
|
<input type="text" id="membre_id" name="membre_id" autofocus/>
|
||||||
|
</li>
|
||||||
|
<li class="button">
|
||||||
|
<button type="submit">Scan</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ if .Error }}<p class="result">👎 {{ .Error }}</p>{{ end }}
|
||||||
|
{{ if .Result }}<p class="result">👍 {{ .Result }}</p>{{ end }}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
|
@ -1,10 +1,8 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "embed"
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
//go:embed *.html
|
||||||
var htmlFS embed.FS
|
var htmlFS embed.FS
|
||||||
|
|
||||||
func HTMLFS() embed.FS {
|
|
||||||
return htmlFS
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{{ define "index" }}
|
|
||||||
hello world
|
|
||||||
{{ . }}
|
|
||||||
{{ end }}
|
|
|
@ -1,10 +1,9 @@
|
||||||
package main
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"git.agecem.com/bottin/agendas/ui"
|
|
||||||
"github.com/labstack/echo/v4"
|
"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 {
|
func NewRenderer() *Renderer {
|
||||||
return &Renderer{
|
return &Renderer{
|
||||||
templates: template.Must(template.ParseFS(ui.HTMLFS(), "*html")),
|
templates: template.Must(template.ParseFS(htmlFS, "*html")),
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
{{ define "test" }}
|
|
||||||
{{ . }}
|
|
||||||
{{ end }}
|
|
Loading…
Reference in a new issue