From 150782c42f39f2063d999772ce968cc0e6dc491b Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 3 Jul 2024 20:51:43 -0400 Subject: [PATCH 01/17] feature(config): ajouter options TLS --- config.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/config.go b/config.go index 3096960..927227c 100644 --- a/config.go +++ b/config.go @@ -11,6 +11,21 @@ import ( ) const ( + ViperAPITLSEnabled string = "api.tls.enabled" + FlagAPITLSEnabled string = "api-tls-enabled" + DefaultAPITLSEnabled bool = false + DescriptionAPITLSEnabled string = "Whether to use TLS or not. Requires certificate and private key files." + + ViperAPITLSCertificateFile string = "api.tls.certificate_file" + FlagAPITLSCertificateFile string = "api-tls-certificate-file" + DefaultAPITLSCertificateFile string = "" + DescriptionAPITLSCertificateFile string = "Path to TLS certificate file" + + ViperAPITLSPrivateKeyFile string = "api.tls.private_key_file" + FlagAPITLSPrivateKeyFile string = "api-tls-private-key-file" + DefaultAPITLSPrivateKeyFile string = "" + DescriptionAPITLSPrivateKeyFile string = "Path to TLS private key file" + ViperAPIPort string = "api.port" FlagAPIPort string = "api-port" DefaultAPIPort int = 1312 @@ -89,6 +104,15 @@ const ( type Config struct { API struct { + TLS struct { + Enabled bool `yaml:"enabled"` + + // Path to file containing TLS certificate + CertificateFile string `yaml:"certificate_file"` + + // Path to file containing TLS private key + PrivateKeyFile string `yaml:"private_key_file"` + } Port int `yaml:"port"` Key string `yaml:"key"` } `yaml:"api"` @@ -116,6 +140,9 @@ type Config struct { // DefaultConfig returns a Config filled with the default values from the // `Default*` constants defined in this file. func DefaultConfig() (cfg Config) { + cfg.API.TLS.Enabled = DefaultAPITLSEnabled + cfg.API.TLS.CertificateFile = DefaultAPITLSCertificateFile + cfg.API.TLS.PrivateKeyFile = DefaultAPITLSPrivateKeyFile cfg.API.Port = DefaultAPIPort cfg.API.Key = DefaultAPIKey cfg.DB.Database = DefaultDBDatabase @@ -145,6 +172,24 @@ func init() { rootCmd.AddCommand(apiCmd) + // api.tls.enabled + apiCmd.Flags().Bool(FlagAPITLSEnabled, DefaultAPITLSEnabled, DescriptionAPITLSEnabled) + if err := viper.BindPFlag(ViperAPITLSEnabled, apiCmd.Flags().Lookup(FlagAPITLSEnabled)); err != nil { + log.Fatal(err) + } + + // api.tls.certificate_file + apiCmd.Flags().String(FlagAPITLSCertificateFile, DefaultAPITLSCertificateFile, DescriptionAPITLSCertificateFile) + if err := viper.BindPFlag(ViperAPITLSCertificateFile, apiCmd.Flags().Lookup(FlagAPITLSCertificateFile)); err != nil { + log.Fatal(err) + } + + // api.tls.private_key_file + apiCmd.Flags().String(FlagAPITLSPrivateKeyFile, DefaultAPITLSPrivateKeyFile, DescriptionAPITLSPrivateKeyFile) + if err := viper.BindPFlag(ViperAPITLSPrivateKeyFile, apiCmd.Flags().Lookup(FlagAPITLSPrivateKeyFile)); err != nil { + log.Fatal(err) + } + // api.key apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { From 4ce3d9f60bc3770d40def89d931e81a47682be7a Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 3 Jul 2024 20:51:57 -0400 Subject: [PATCH 02/17] feature(api): permettre d'exposer le serveur API par https Requiert `cfg.API.TLS.Enabled = true` et des fichiers valides pour `cfg.API.TLS.{CertificateFile,PrivateKeyFile}` --- cmd.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd.go b/cmd.go index 8c5571d..508af62 100644 --- a/cmd.go +++ b/cmd.go @@ -114,7 +114,22 @@ var apiCmd = &cobra.Command{ */ // Execution - e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port))) + switch cfg.API.TLS.Enabled { + case false: + e.Logger.Fatal( + e.Start( + fmt.Sprintf(":%d", cfg.API.Port), + ), + ) + case true: + e.Logger.Fatal( + e.StartTLS( + fmt.Sprintf(":%d", cfg.API.Port), + cfg.API.TLS.CertificateFile, + cfg.API.TLS.PrivateKeyFile, + ), + ) + } }, } From a9f16826349aaea3eff018dcb5ca0bc8e2fd0c3f Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 3 Jul 2024 20:53:17 -0400 Subject: [PATCH 03/17] fix(test): ajuster TLS client voki selon config --- client_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client_test.go b/client_test.go index a2aea93..3e1f180 100644 --- a/client_test.go +++ b/client_test.go @@ -22,7 +22,15 @@ func TestAPI(t *testing.T) { httpClient := http.DefaultClient defer httpClient.CloseIdleConnections() - vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, "http") + var protocol string + switch cfg.API.TLS.Enabled { + case true: + protocol = "https" + case false: + protocol = "http" + } + + vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol) apiClient := APIClient{vokiClient} t.Run("get API health", func(t *testing.T) { From 8c074dd443f5e2d43f00c4910d544d9c9738101d Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Sun, 7 Jul 2024 03:58:15 -0400 Subject: [PATCH 04/17] =?UTF-8?q?fix:=20impl=C3=A9menter=20correctement=20?= =?UTF-8?q?tls=20certfile=20et=20keyfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test: ne pas vérifier le certificat avant de l'accepter --- client_test.go | 16 ++++++++++++++-- cmd.go | 7 +++++-- config.go | 38 +++++++++++++++++++------------------- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/client_test.go b/client_test.go index 3e1f180..1593e4c 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "net/http" "testing" @@ -19,7 +20,18 @@ func TestAPI(t *testing.T) { return } - httpClient := http.DefaultClient + //httpClient := http.DefaultClient + //defer httpClient.CloseIdleConnections() + + transport := http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + httpClient := http.Client{ + Transport: &transport, + } defer httpClient.CloseIdleConnections() var protocol string @@ -30,7 +42,7 @@ func TestAPI(t *testing.T) { protocol = "http" } - vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol) + vokiClient := voki.New(&httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol) apiClient := APIClient{vokiClient} t.Run("get API health", func(t *testing.T) { diff --git a/cmd.go b/cmd.go index 508af62..1518476 100644 --- a/cmd.go +++ b/cmd.go @@ -122,11 +122,14 @@ var apiCmd = &cobra.Command{ ), ) case true: + //TODO + log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.API.TLS.Certfile, cfg.API.TLS.Keyfile) + e.Logger.Fatal( e.StartTLS( fmt.Sprintf(":%d", cfg.API.Port), - cfg.API.TLS.CertificateFile, - cfg.API.TLS.PrivateKeyFile, + cfg.API.TLS.Certfile, + cfg.API.TLS.Keyfile, ), ) } diff --git a/config.go b/config.go index 927227c..c13b5a0 100644 --- a/config.go +++ b/config.go @@ -16,15 +16,15 @@ const ( DefaultAPITLSEnabled bool = false DescriptionAPITLSEnabled string = "Whether to use TLS or not. Requires certificate and private key files." - ViperAPITLSCertificateFile string = "api.tls.certificate_file" - FlagAPITLSCertificateFile string = "api-tls-certificate-file" - DefaultAPITLSCertificateFile string = "" - DescriptionAPITLSCertificateFile string = "Path to TLS certificate file" + ViperAPITLSCertfile string = "api.tls.certfile" + FlagAPITLSCertfile string = "api-tls-certfile" + DefaultAPITLSCertfile string = "/etc/bottin/cert.pem" + DescriptionAPITLSCertfile string = "Path to TLS certificate file" - ViperAPITLSPrivateKeyFile string = "api.tls.private_key_file" - FlagAPITLSPrivateKeyFile string = "api-tls-private-key-file" - DefaultAPITLSPrivateKeyFile string = "" - DescriptionAPITLSPrivateKeyFile string = "Path to TLS private key file" + ViperAPITLSKeyfile string = "api.tls.keyfile" + FlagAPITLSKeyfile string = "api-tls-keyfile" + DefaultAPITLSKeyfile string = "/etc/bottin/key.pem" + DescriptionAPITLSKeyFile string = "Path to TLS private key file" ViperAPIPort string = "api.port" FlagAPIPort string = "api-port" @@ -108,11 +108,11 @@ type Config struct { Enabled bool `yaml:"enabled"` // Path to file containing TLS certificate - CertificateFile string `yaml:"certificate_file"` + Certfile string `yaml:"certfile"` // Path to file containing TLS private key - PrivateKeyFile string `yaml:"private_key_file"` - } + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` Port int `yaml:"port"` Key string `yaml:"key"` } `yaml:"api"` @@ -141,8 +141,8 @@ type Config struct { // `Default*` constants defined in this file. func DefaultConfig() (cfg Config) { cfg.API.TLS.Enabled = DefaultAPITLSEnabled - cfg.API.TLS.CertificateFile = DefaultAPITLSCertificateFile - cfg.API.TLS.PrivateKeyFile = DefaultAPITLSPrivateKeyFile + cfg.API.TLS.Certfile = DefaultAPITLSCertfile + cfg.API.TLS.Keyfile = DefaultAPITLSKeyfile cfg.API.Port = DefaultAPIPort cfg.API.Key = DefaultAPIKey cfg.DB.Database = DefaultDBDatabase @@ -178,15 +178,15 @@ func init() { log.Fatal(err) } - // api.tls.certificate_file - apiCmd.Flags().String(FlagAPITLSCertificateFile, DefaultAPITLSCertificateFile, DescriptionAPITLSCertificateFile) - if err := viper.BindPFlag(ViperAPITLSCertificateFile, apiCmd.Flags().Lookup(FlagAPITLSCertificateFile)); err != nil { + // api.tls.certfile + apiCmd.Flags().String(FlagAPITLSCertfile, DefaultAPITLSCertfile, DescriptionAPITLSCertfile) + if err := viper.BindPFlag(ViperAPITLSCertfile, apiCmd.Flags().Lookup(FlagAPITLSCertfile)); err != nil { log.Fatal(err) } - // api.tls.private_key_file - apiCmd.Flags().String(FlagAPITLSPrivateKeyFile, DefaultAPITLSPrivateKeyFile, DescriptionAPITLSPrivateKeyFile) - if err := viper.BindPFlag(ViperAPITLSPrivateKeyFile, apiCmd.Flags().Lookup(FlagAPITLSPrivateKeyFile)); err != nil { + // api.tls.keyfile + apiCmd.Flags().String(FlagAPITLSKeyfile, DefaultAPITLSKeyfile, DescriptionAPITLSKeyFile) + if err := viper.BindPFlag(ViperAPITLSKeyfile, apiCmd.Flags().Lookup(FlagAPITLSKeyfile)); err != nil { log.Fatal(err) } From eb1982898cfb89e5ce74ba7b0c88ec10642d3730 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 15 Jul 2024 16:52:04 -0400 Subject: [PATCH 05/17] rework: config and cmd Renamed `web` command to `server ui` (web is still an alias to ui) Completely changed the config options and flags Usage of PersistentFlags now allow clearer `--help` BREAKING: cmd modified BREAKING: config overhauled BREAKING: Bump API to v8 --- README.md | 21 +- client_test.go | 10 +- cmd.go | 74 ++--- config.go | 664 ++++++++++++++++++++++++++------------------ docker-compose.yaml | 27 +- go.mod | 2 +- request.go | 18 +- routes.go | 5 +- 8 files changed, 475 insertions(+), 346 deletions(-) diff --git a/README.md b/README.md index acda47f..822c4f1 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,18 @@ https://git.agecem.com/agecem/bottin Remplir .env avec les infos qui seront utilisées pour déployer le container -(Remplacer `bottin` par quelque chose de plus sécuritaire) +Au minimum, il faut ces 3 entrées: + +*Remplacer `bottin` par quelque chose de plus sécuritaire* ```sh -BOTTIN_API_KEY=bottin -BOTTIN_POSTGRES_DATABASE=bottin -BOTTIN_POSTGRES_PASSWORD=bottin -BOTTIN_POSTGRES_USER=bottin -BOTTIN_WEB_PASSWORD=bottin -BOTTIN_WEB_USER=bottin +BOTTIN_SERVER_DB_DATABASE=bottin +BOTTIN_SERVER_DB_PASSWORD=bottin +BOTTIN_SERVER_DB_USER=bottin ``` +*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options* + Déployer avec docker-compose `$ docker-compose up -d` @@ -43,13 +44,13 @@ Pour modifier la configuration du serveur API `$ docker-compose exec -it api vi /etc/bottin/api.yaml` -*Y remplir au minimum le champs `api.key` (string)* +*Y remplir au minimum le champs `server.api.key` (string)* Pour modifier la configuration du client web -`$ docker-compose exec -it web vi /etc/bottin/web.yaml` +`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml` -*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.password` (string)* +*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)* Redémarrer les containers une fois la configuration modifiée diff --git a/client_test.go b/client_test.go index 1593e4c..261eb20 100644 --- a/client_test.go +++ b/client_test.go @@ -34,15 +34,7 @@ func TestAPI(t *testing.T) { } defer httpClient.CloseIdleConnections() - var protocol string - switch cfg.API.TLS.Enabled { - case true: - protocol = "https" - case false: - protocol = "http" - } - - vokiClient := voki.New(&httpClient, "localhost", cfg.API.Key, cfg.API.Port, protocol) + vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol) apiClient := APIClient{vokiClient} t.Run("get API health", func(t *testing.T) { diff --git a/cmd.go b/cmd.go index 1518476..8fbd66b 100644 --- a/cmd.go +++ b/cmd.go @@ -32,6 +32,11 @@ func execute() { } } +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Démarrer serveurs (API ou Web UI)", +} + // apiCmd represents the api command var apiCmd = &cobra.Command{ Use: "api", @@ -49,10 +54,12 @@ var apiCmd = &cobra.Command{ e.Pre(middleware.AddTrailingSlash()) - if cfg.API.Key != "" { + if cfg.Server.API.Key != "" { e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { - return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.API.Key)) == 1, nil + return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil })) + } else { + log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)") } // DataClient @@ -63,12 +70,12 @@ var apiCmd = &cobra.Command{ ctx, fmt.Sprintf( "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", - cfg.DB.User, - cfg.DB.Password, - cfg.DB.Database, - cfg.DB.Host, - cfg.DB.Port, - cfg.DB.SSLMode, + cfg.Server.API.DB.User, + cfg.Server.API.DB.Password, + cfg.Server.API.DB.Database, + cfg.Server.API.DB.Host, + cfg.Server.API.DB.Port, + cfg.Server.API.DB.SSLMode, )) if err != nil { log.Fatal("init pgx pool:", err) @@ -98,49 +105,50 @@ var apiCmd = &cobra.Command{ /* h := handlers.New(client) - e.GET("/v7/health/", h.GetHealth) + e.GET("/v8/health/", h.GetHealth) - e.POST("/v7/membres/", h.PostMembres) + e.POST("/v8/membres/", h.PostMembres) - e.GET("/v7/membres/", h.ListMembres) + e.GET("/v8/membres/", h.ListMembres) - e.GET("/v7/membres/:membre_id/", h.ReadMembre) + e.GET("/v8/membres/:membre_id/", h.ReadMembre) - e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) + e.PUT("/v8/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) - e.POST("/v7/programmes/", h.PostProgrammes) + e.POST("/v8/programmes/", h.PostProgrammes) - e.POST("/v7/seed/", h.PostSeed) + e.POST("/v8/seed/", h.PostSeed) */ // Execution - switch cfg.API.TLS.Enabled { + switch cfg.Server.API.TLS.Enabled { case false: e.Logger.Fatal( e.Start( - fmt.Sprintf(":%d", cfg.API.Port), + fmt.Sprintf(":%d", cfg.Server.API.Port), ), ) case true: //TODO - log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.API.TLS.Certfile, cfg.API.TLS.Keyfile) + log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.Server.API.TLS.Certfile, cfg.Server.API.TLS.Keyfile) e.Logger.Fatal( e.StartTLS( - fmt.Sprintf(":%d", cfg.API.Port), - cfg.API.TLS.Certfile, - cfg.API.TLS.Keyfile, + fmt.Sprintf(":%d", cfg.Server.API.Port), + cfg.Server.API.TLS.Certfile, + cfg.Server.API.TLS.Keyfile, ), ) } }, } -// webCmd represents the web command -var webCmd = &cobra.Command{ - Use: "web", - Short: "Démarrer le client web", - Args: cobra.ExactArgs(0), +// uiCmd represents the ui command +var uiCmd = &cobra.Command{ + Use: "ui", + Aliases: []string{"web", "interface"}, + Short: "Démarrer l'interface Web UI", + Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { // Parse config var cfg Config @@ -157,8 +165,8 @@ var webCmd = &cobra.Command{ // Auth e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { - usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Web.User)) == 1 - passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1 + usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1 return usersMatch && passwordsMatch, nil })) @@ -170,10 +178,10 @@ var webCmd = &cobra.Command{ // API Client apiClient := APIClient{voki.New( http.DefaultClient, - cfg.Web.API.Host, - cfg.Web.API.Key, - cfg.Web.API.Port, - cfg.Web.API.Protocol, + cfg.Server.UI.API.Host, + cfg.Server.UI.API.Key, + cfg.Server.UI.API.Port, + cfg.Server.UI.API.Protocol, )} defer apiClient.Voki.CloseIdleConnections() @@ -239,6 +247,6 @@ Programme: [%s] %s // Execution e.Logger.Fatal(e.Start( - fmt.Sprintf(":%d", cfg.Web.Port))) + fmt.Sprintf(":%d", cfg.Server.UI.Port))) }, } diff --git a/config.go b/config.go index c13b5a0..01eecd5 100644 --- a/config.go +++ b/config.go @@ -10,278 +10,52 @@ import ( "github.com/spf13/viper" ) -const ( - ViperAPITLSEnabled string = "api.tls.enabled" - FlagAPITLSEnabled string = "api-tls-enabled" - DefaultAPITLSEnabled bool = false - DescriptionAPITLSEnabled string = "Whether to use TLS or not. Requires certificate and private key files." - - ViperAPITLSCertfile string = "api.tls.certfile" - FlagAPITLSCertfile string = "api-tls-certfile" - DefaultAPITLSCertfile string = "/etc/bottin/cert.pem" - DescriptionAPITLSCertfile string = "Path to TLS certificate file" - - ViperAPITLSKeyfile string = "api.tls.keyfile" - FlagAPITLSKeyfile string = "api-tls-keyfile" - DefaultAPITLSKeyfile string = "/etc/bottin/key.pem" - DescriptionAPITLSKeyFile string = "Path to TLS private key file" - - ViperAPIPort string = "api.port" - FlagAPIPort string = "api-port" - DefaultAPIPort int = 1312 - DescriptionAPIPort string = "API server port" - - ViperAPIKey string = "api.key" - FlagAPIKey string = "api-key" - DefaultAPIKey string = "bottin" - DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)" - - ViperDBDatabase string = "db.database" - FlagDBDatabase string = "db-database" - DefaultDBDatabase string = "bottin" - DescriptionDBDatabase string = "Postgres database" - - ViperDBSSLMode string = "db.sslmode" - FlagDBSSLMode string = "db-sslmode" - DefaultDBSSLMode string = "prefer" - DescriptionDBSSLMode string = "Postgres sslmode" - - ViperDBHost string = "db.host" - FlagDBHost string = "db-host" - DefaultDBHost string = "db" - DescriptionDBHost string = "Postgres host" - - ViperDBPassword string = "db.password" - FlagDBPassword string = "db-password" - DefaultDBPassword string = "bottin" - DescriptionDBPassword string = "Postgres password" - - ViperDBPort string = "db.port" - FlagDBPort string = "db-port" - DefaultDBPort int = 5432 - DescriptionDBPort string = "Postgres port" - - ViperDBUser string = "db.user" - FlagDBUser string = "db-user" - DefaultDBUser string = "bottin" - DescriptionDBUser string = "Postgres user" - - ViperWebUser string = "web.user" - FlagWebUser string = "web-user" - DefaultWebUser string = "bottin" - DescriptionWebUser string = "Web client basic auth user" - - ViperWebPassword string = "web.password" - FlagWebPassword string = "web-password" - DefaultWebPassword string = "bottin" - DescriptionWebPassword string = "Web client basic auth password" - - ViperWebPort string = "web.port" - FlagWebPort string = "web-port" - DefaultWebPort int = 2312 - DescriptionWebPort string = "Web client port" - - ViperWebAPIHost string = "web.api.host" - FlagWebAPIHost string = "web-api-host" - DefaultWebAPIHost string = "api" - DescriptionWebAPIHost string = "Target API server host" - - ViperWebAPIKey string = "web.api.key" - FlagWebAPIKey string = "web-api-key" - DefaultWebAPIKey string = "bottin" - DescriptionWebAPIKey string = "Target API server key" - - ViperWebAPIPort string = "web.api.port" - FlagWebAPIPort string = "web-api-port" - DefaultWebAPIPort int = 1312 - DescriptionWebAPIPort string = "Target API server port" - - ViperWebAPIProtocol string = "web.api.protocol" - FlagWebAPIProtocol string = "web-api-protocol" - DefaultWebAPIProtocol string = "http" - DescriptionWebAPIProtocol string = "Target API server protocol (http/https)" -) - type Config struct { - API struct { - TLS struct { - Enabled bool `yaml:"enabled"` - - // Path to file containing TLS certificate - Certfile string `yaml:"certfile"` - - // Path to file containing TLS private key - Keyfile string `yaml:"keyfile"` - } `yaml:"tls"` - Port int `yaml:"port"` - Key string `yaml:"key"` - } `yaml:"api"` - DB struct { - Database string `yaml:"database"` - Host string `yaml:"host"` - SSLMode string `yaml:"sslmode"` - Password string `yaml:"password"` - Port int `yaml:"port"` - User string `yaml:"user"` - } `yaml:"db"` - Web struct { - User string `yaml:"user"` - Password string `yaml:"password"` - Port int `yaml:"port"` - API struct { + Client struct { + API struct { Host string `yaml:"host"` Key string `yaml:"key"` Port int `yaml:"port"` Protocol string `yaml:"protocol"` } `yaml:"api"` - } `yaml:"web"` -} + } `yaml:"client"` -// DefaultConfig returns a Config filled with the default values from the -// `Default*` constants defined in this file. -func DefaultConfig() (cfg Config) { - cfg.API.TLS.Enabled = DefaultAPITLSEnabled - cfg.API.TLS.Certfile = DefaultAPITLSCertfile - cfg.API.TLS.Keyfile = DefaultAPITLSKeyfile - cfg.API.Port = DefaultAPIPort - cfg.API.Key = DefaultAPIKey - cfg.DB.Database = DefaultDBDatabase - cfg.DB.Host = DefaultDBHost - cfg.DB.SSLMode = DefaultDBSSLMode - cfg.DB.Password = DefaultDBPassword - cfg.DB.Port = DefaultDBPort - cfg.DB.User = DefaultDBUser - cfg.Web.User = DefaultWebUser - cfg.Web.Password = DefaultWebPassword - cfg.Web.Port = DefaultWebPort - cfg.Web.API.Host = DefaultWebAPIHost - cfg.Web.API.Key = DefaultWebAPIKey - cfg.Web.API.Port = DefaultWebAPIPort - cfg.Web.API.Protocol = DefaultWebAPIProtocol - return -} - -func init() { - // rootCmd - - cobra.OnInitialize(initConfig) - - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") - - // apiCmd - - rootCmd.AddCommand(apiCmd) - - // api.tls.enabled - apiCmd.Flags().Bool(FlagAPITLSEnabled, DefaultAPITLSEnabled, DescriptionAPITLSEnabled) - if err := viper.BindPFlag(ViperAPITLSEnabled, apiCmd.Flags().Lookup(FlagAPITLSEnabled)); err != nil { - log.Fatal(err) - } - - // api.tls.certfile - apiCmd.Flags().String(FlagAPITLSCertfile, DefaultAPITLSCertfile, DescriptionAPITLSCertfile) - if err := viper.BindPFlag(ViperAPITLSCertfile, apiCmd.Flags().Lookup(FlagAPITLSCertfile)); err != nil { - log.Fatal(err) - } - - // api.tls.keyfile - apiCmd.Flags().String(FlagAPITLSKeyfile, DefaultAPITLSKeyfile, DescriptionAPITLSKeyFile) - if err := viper.BindPFlag(ViperAPITLSKeyfile, apiCmd.Flags().Lookup(FlagAPITLSKeyfile)); err != nil { - log.Fatal(err) - } - - // api.key - apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) - if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { - log.Fatal(err) - } - - // api.port - apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort) - if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil { - log.Fatal(err) - } - - // db.database - apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase) - if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil { - log.Fatal(err) - } - - // db.sslmode - apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode) - if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil { - log.Fatal(err) - } - - // db.host - apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) - if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { - log.Fatal(err) - } - - // db.password - apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword) - if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil { - log.Fatal(err) - } - - // db.port - apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort) - if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil { - log.Fatal(err) - } - - // db.user - apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser) - if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil { - log.Fatal(err) - } - - // WebCmd - rootCmd.AddCommand(webCmd) - - // web.api.host - webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost) - if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil { - log.Fatal(err) - } - - // web.api.key - webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey) - if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil { - log.Fatal(err) - } - - // web.api.protocol - webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol) - if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil { - log.Fatal(err) - } - - // web.api.port - webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort) - if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil { - log.Fatal(err) - } - - // web.password - webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword) - if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil { - log.Fatal(err) - } - - // web.port - webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort) - if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil { - log.Fatal(err) - } - - // web.user - webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser) - if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil { - log.Fatal(err) - } + Server struct { + API struct { + DB struct { + Database string `yaml:"database"` + Host string `yaml:"host"` + Password string `yaml:"password"` + Port int `yaml:"port"` + SSLMode string `yaml:"sslmode"` + User string `yaml:"user"` + } `yaml:"db"` + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + TLS struct { + Enabled bool `yaml:"enabled"` + Certfile string `yaml:"certfile"` + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` + } `yaml:"api"` + UI struct { + API struct { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + } `yaml:"api"` + Password string `yaml:"password"` + Port int `yaml:"port"` + TLS struct { + Enabled bool `yaml:"enabled"` + Certfile string `yaml:"certfile"` + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` + User string `yaml:"user"` + } `yaml:"ui"` + } `yaml:"server"` } var cfgFile string @@ -311,3 +85,363 @@ func initConfig() { fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) } } + +func init() { + // rootCmd + + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") + + // client.api.host + rootCmd.PersistentFlags().String( + "client-api-host", + "api", + "API server host", + ) + if err := viper.BindPFlag( + "client.api.host", + rootCmd.PersistentFlags().Lookup("client-api-host"), + ); err != nil { + log.Fatal(err) + } + + // client.api.key + rootCmd.PersistentFlags().String( + "client-api-key", + "bottin", + "API server key", + ) + if err := viper.BindPFlag( + "client.api.key", + rootCmd.PersistentFlags().Lookup("client-api-key"), + ); err != nil { + log.Fatal(err) + } + + // client.api.port + rootCmd.PersistentFlags().Int( + "client-api-port", + 1312, + "API server port", + ) + if err := viper.BindPFlag( + "client.api.port", + rootCmd.PersistentFlags().Lookup("client-api-port"), + ); err != nil { + log.Fatal(err) + } + + // client.api.protocol + rootCmd.PersistentFlags().String( + "client-api-protocol", + "https", + "API server protocol", + ) + if err := viper.BindPFlag( + "client.api.protocol", + rootCmd.PersistentFlags().Lookup("client-api-protocol"), + ); err != nil { + log.Fatal(err) + } + + // server + rootCmd.AddCommand(serverCmd) + + // server api + serverCmd.AddCommand(apiCmd) + + // server api db + // server.api.db.database + apiCmd.PersistentFlags().String( + "server-api-db-database", + "bottin", + "Postgres database name", + ) + if err := viper.BindPFlag( + "server.api.db.database", + apiCmd.PersistentFlags().Lookup("server-api-db-database"), + ); err != nil { + log.Fatal(err) + } + + // server.api.db.host + apiCmd.PersistentFlags().String( + "server-api-db-host", + "db", + "Postgres host name", + ) + if err := viper.BindPFlag( + "server.api.db.host", + apiCmd.PersistentFlags().Lookup("server-api-db-host"), + ); err != nil { + log.Fatal(err) + } + + // server.api.db.password + apiCmd.PersistentFlags().String( + "server-api-db-password", + "bottin", + "Postgres password", + ) + if err := viper.BindPFlag( + "server.api.db.password", + apiCmd.PersistentFlags().Lookup("server-api-db-password"), + ); err != nil { + log.Fatal(err) + } + + // server.api.db.port + apiCmd.PersistentFlags().Int( + "server-api-db-port", + 5432, + "Postgres port", + ) + if err := viper.BindPFlag( + "server.api.db.port", + apiCmd.PersistentFlags().Lookup("server-api-db-port"), + ); err != nil { + log.Fatal(err) + } + + // server.api.db.sslmode + apiCmd.PersistentFlags().String( + "server-api-db-sslmode", + "prefer", + "Postgres sslmode", + ) + if err := viper.BindPFlag( + "server.api.db.sslmode", + apiCmd.PersistentFlags().Lookup("server-api-db-sslmode"), + ); err != nil { + log.Fatal(err) + } + + // server.api.db.user + apiCmd.PersistentFlags().String( + "server-api-db-user", + "bottin", + "Postgres user name", + ) + if err := viper.BindPFlag( + "server.api.db.user", + apiCmd.PersistentFlags().Lookup("server-api-db-user"), + ); err != nil { + log.Fatal(err) + } + + // server.api.host + apiCmd.PersistentFlags().String( + "server-api-host", + "", + "API server hostname or IP to answer on (empty = any)", + ) + if err := viper.BindPFlag( + "server.api.host", + apiCmd.PersistentFlags().Lookup("server-api-host"), + ); err != nil { + log.Fatal(err) + } + + // server.api.key + apiCmd.PersistentFlags().String( + "server-api-key", + "bottin", + "API server key", + ) + if err := viper.BindPFlag( + "server.api.key", + apiCmd.PersistentFlags().Lookup("server-api-key"), + ); err != nil { + log.Fatal(err) + } + + // server.api.port + apiCmd.PersistentFlags().Int( + "server-api-port", + 1312, + "API server port", + ) + if err := viper.BindPFlag( + "server.api.port", + apiCmd.PersistentFlags().Lookup("server-api-port"), + ); err != nil { + log.Fatal(err) + } + + // server api tls + // server.api.tls.enabled + apiCmd.PersistentFlags().Bool( + "server-api-tls-enabled", + true, + "Use TLS for API server connections (requires certfile and keyfile)", + ) + if err := viper.BindPFlag( + "server.api.tls.enabled", + apiCmd.PersistentFlags().Lookup("server-api-tls-enabled"), + ); err != nil { + log.Fatal(err) + } + + // server.api.tls.certfile + apiCmd.PersistentFlags().String( + "server-api-tls-certfile", + "/etc/bottin/cert.pem", + "Path to certificate file", + ) + if err := viper.BindPFlag( + "server.api.tls.certfile", + apiCmd.PersistentFlags().Lookup("server-api-tls-certfile"), + ); err != nil { + log.Fatal(err) + } + + // server.api.tls.keyfile + apiCmd.PersistentFlags().String( + "server-api-tls-keyfile", + "/etc/bottin/key.pem", + "Path to private key file", + ) + if err := viper.BindPFlag( + "server.api.tls.keyfile", + apiCmd.PersistentFlags().Lookup("server-api-tls-keyfile"), + ); err != nil { + log.Fatal(err) + } + + // server ui + serverCmd.AddCommand(uiCmd) + + // server ui api + + // server.ui.api.host + uiCmd.PersistentFlags().String( + "server-ui-api-host", + "api", + "Web UI backend API server host name", + ) + if err := viper.BindPFlag( + "server.ui.api.host", + uiCmd.PersistentFlags().Lookup("server-ui-api-host"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.api.key + uiCmd.PersistentFlags().String( + "server-ui-api-key", + "bottin", + "Web UI backend API server key", + ) + if err := viper.BindPFlag( + "server.ui.api.key", + uiCmd.PersistentFlags().Lookup("server-ui-api-key"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.api.port + uiCmd.PersistentFlags().Int( + "server-ui-api-port", + 1312, + "Web UI backend API server port", + ) + if err := viper.BindPFlag( + "server.ui.api.port", + uiCmd.PersistentFlags().Lookup("server-ui-api-port"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.api.protocol + uiCmd.PersistentFlags().String( + "server-ui-api-protocol", + "https", + "Web UI backend API server protocol", + ) + if err := viper.BindPFlag( + "server.ui.api.protocol", + uiCmd.PersistentFlags().Lookup("server-ui-api-protocol"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.password + uiCmd.PersistentFlags().String( + "server-ui-password", + "bottin", + "Web UI password", + ) + if err := viper.BindPFlag( + "server.ui.password", + uiCmd.PersistentFlags().Lookup("server-ui-password"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.port + uiCmd.PersistentFlags().Int( + "server-ui-port", + 2312, + "Web UI port", + ) + if err := viper.BindPFlag( + "server.ui.port", + uiCmd.PersistentFlags().Lookup("server-ui-port"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.user + uiCmd.PersistentFlags().String( + "server-ui-user", + "bottin", + "Web UI user", + ) + if err := viper.BindPFlag( + "server.ui.user", + uiCmd.PersistentFlags().Lookup("server-ui-user"), + ); err != nil { + log.Fatal(err) + } + + // server ui tls + // server.ui.tls.enabled + uiCmd.PersistentFlags().Bool( + "server-ui-tls-enabled", + true, + "Web UI enable TLS (requires certfile and keyfile)", + ) + if err := viper.BindPFlag( + "server.ui.tls.enabled", + uiCmd.PersistentFlags().Lookup("server-ui-tls-enabled"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.tls.certfile + uiCmd.PersistentFlags().String( + "server-ui-tls-certfile", + "/etc/bottin/cert.pem", + "Path to Web UI TLS certificate file", + ) + if err := viper.BindPFlag( + "server.ui.tls.certfile", + uiCmd.PersistentFlags().Lookup("server-ui-tls-certfile"), + ); err != nil { + log.Fatal(err) + } + + // server.ui.tls.keyfile + uiCmd.PersistentFlags().String( + "server-ui-tls-keyfile", + "/etc/bottin/key.pem", + "Path to Web UI TLS private key file", + ) + if err := viper.BindPFlag( + "server.ui.tls.keyfile", + uiCmd.PersistentFlags().Lookup("server-ui-tls-keyfile"), + ); err != nil { + log.Fatal(err) + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 9f2e604..1156be7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,9 +3,9 @@ services: db: image: 'docker.io/library/postgres:16' environment: - POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}" - POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}" - POSTGRES_USER: "${BOTTIN_POSTGRES_USER}" + POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE}" + POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD}" + POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER}" volumes: - 'db-data:/var/lib/postgresql/data' restart: 'unless-stopped' @@ -15,33 +15,26 @@ services: - db build: . image: 'git.agecem.com/agecem/bottin:latest' - environment: - BOTTIN_DB_DATABASE: "${BOTTIN_POSTGRES_DATABASE}" - BOTTIN_DB_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}" - BOTTIN_DB_USER: "${BOTTIN_POSTGRES_USER}" - BOTTIN_API_KEY: "${BOTTIN_API_KEY}" + env_file: '.env' ports: - '1312:1312' volumes: - 'api-config:/etc/bottin' restart: 'unless-stopped' - command: ['bottin', '--config', '/etc/bottin/api.yaml', 'api'] + command: ['bottin', '--config', '/etc/bottin/api.yaml', 'server', 'api'] - web: + ui: depends_on: - api build: . image: 'git.agecem.com/agecem/bottin:latest' - environment: - BOTTIN_WEB_API_KEY: "${BOTTIN_API_KEY}" - BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD}" - BOTTIN_WEB_USER: "${BOTTIN_WEB_USER}" + env_file: '.env' ports: - '2312:2312' volumes: - - 'web-config:/etc/bottin' + - 'ui-config:/etc/bottin' restart: 'unless-stopped' - command: ['bottin', '--config', '/etc/bottin/web.yaml', 'web'] + command: ['bottin', '--config', '/etc/bottin/ui.yaml', 'server', 'ui'] # adminer: # image: adminer @@ -54,4 +47,4 @@ services: volumes: db-data: api-config: - web-config: + ui-config: diff --git a/go.mod b/go.mod index 2c52fee..489b37d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.agecem.com/agecem/bottin/v7 +module git.agecem.com/agecem/bottin/v8 go 1.22.0 diff --git a/request.go b/request.go index 8f47f10..f1a73b4 100644 --- a/request.go +++ b/request.go @@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - "/api/v7/health/", + "/api/v8/health/", nil, true, ) @@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v7/programme/", + "/api/v8/programme/", &buf, true, ) @@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v7/membre/", + "/api/v8/membre/", &buf, true, ) @@ -146,7 +146,7 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v7/membre/%s/", request.Param.MembreID), + fmt.Sprintf("/api/v8/membre/%s/", request.Param.MembreID), nil, true, ) @@ -180,7 +180,7 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v7/membre/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v8/membre/?limit=%d", request.Query.Limit), nil, true, ) @@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb statusCode, body, err := v.CallAndParse( http.MethodPut, - fmt.Sprintf("/api/v7/membre/%s/prefered_name/", request.Param.MembreID), + fmt.Sprintf("/api/v8/membre/%s/prefered_name/", request.Param.MembreID), &buf, true, ) @@ -258,7 +258,7 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v7/programme/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v8/programme/?limit=%d", request.Query.Limit), nil, true, ) @@ -292,7 +292,7 @@ func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresD statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v7/membre/display/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v8/membre/display/?limit=%d", request.Query.Limit), nil, true, ) @@ -333,7 +333,7 @@ func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDis statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v7/membre/%s/display/", request.Param.MembreID), + fmt.Sprintf("/api/v8/membre/%s/display/", request.Param.MembreID), nil, true, ) diff --git a/routes.go b/routes.go index ca7dcb2..a631aba 100644 --- a/routes.go +++ b/routes.go @@ -17,7 +17,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { _ = db _ = cfg - apiPath := "/api/v7" + apiPath := "/api/v8" apiGroup := e.Group(apiPath) p := pave.New() if err := pave.EchoRegister[HealthGETRequest]( @@ -248,7 +248,8 @@ func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { } } else { - //TODO cfg.API.DefaultLimit + //TODO cfg.Server.API.DefaultLimit + //TODO cfg.Client.API.Limit request.Query.Limit = 1000 } From 03c9ad5f3c52a98d65f830cfd51d401457587c2c Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 23 Jul 2024 11:40:40 -0400 Subject: [PATCH 06/17] =?UTF-8?q?fix:=20v=C3=A9rifier=20existence=20de=20c?= =?UTF-8?q?ertfile=20et=20keyfile=20pour=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Au lieu de print leur valeur à l'écran --- cmd.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd.go b/cmd.go index 8fbd66b..3fe7a06 100644 --- a/cmd.go +++ b/cmd.go @@ -129,8 +129,13 @@ var apiCmd = &cobra.Command{ ), ) case true: - //TODO - log.Printf("dbg: certfile='%s' keyfile='%s'", cfg.Server.API.TLS.Certfile, cfg.Server.API.TLS.Keyfile) + if cfg.Server.API.TLS.Certfile == "" { + log.Fatal("TLS enabled for API but no certificate file provided") + } + + if cfg.Server.API.TLS.Keyfile == "" { + log.Fatal("TLS enabled for UI but no private key file provided") + } e.Logger.Fatal( e.StartTLS( From 8a9decfe6cd705e22c4f0630056f7992800374d1 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 23 Jul 2024 11:44:41 -0400 Subject: [PATCH 07/17] =?UTF-8?q?fix:=20consid=C3=A9rer=20cfg.Server.API.H?= =?UTF-8?q?ost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Valeur n'avait aucun effet précédemment, permet maintenant de choisir sur quel hôte le serveur API est rejoignable --- cmd.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd.go b/cmd.go index 3fe7a06..a477a8a 100644 --- a/cmd.go +++ b/cmd.go @@ -125,7 +125,7 @@ var apiCmd = &cobra.Command{ case false: e.Logger.Fatal( e.Start( - fmt.Sprintf(":%d", cfg.Server.API.Port), + fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), ), ) case true: @@ -139,7 +139,7 @@ var apiCmd = &cobra.Command{ e.Logger.Fatal( e.StartTLS( - fmt.Sprintf(":%d", cfg.Server.API.Port), + fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), cfg.Server.API.TLS.Certfile, cfg.Server.API.TLS.Keyfile, ), From f5aa25a12a4ab7724736d8f4b3fe1b46ba823dfd Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 23 Jul 2024 11:46:37 -0400 Subject: [PATCH 08/17] =?UTF-8?q?feature:=20ajouter=20cfg.Server.UI.Host?= =?UTF-8?q?=20et=20impl=C3=A9menter=20UI=20TLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd.go | 24 ++++++++++++++++++++++-- config.go | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cmd.go b/cmd.go index a477a8a..a54cff6 100644 --- a/cmd.go +++ b/cmd.go @@ -251,7 +251,27 @@ Programme: [%s] %s }) // Execution - e.Logger.Fatal(e.Start( - fmt.Sprintf(":%d", cfg.Server.UI.Port))) + switch cfg.Server.UI.TLS.Enabled { + case false: + e.Logger.Fatal(e.Start( + fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port))) + case true: + if cfg.Server.UI.TLS.Certfile == "" { + log.Fatal("TLS enabled for UI but no certificate file provided") + } + + if cfg.Server.UI.TLS.Keyfile == "" { + log.Fatal("TLS enabled for UI but no private key file provided") + } + + e.Logger.Fatal( + e.StartTLS( + fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port), + cfg.Server.UI.TLS.Certfile, + cfg.Server.UI.TLS.Keyfile, + ), + ) + } + }, } diff --git a/config.go b/config.go index 01eecd5..c611e07 100644 --- a/config.go +++ b/config.go @@ -46,6 +46,7 @@ type Config struct { Port int `yaml:"port"` Protocol string `yaml:"protocol"` } `yaml:"api"` + Host string `yaml:"host"` Password string `yaml:"password"` Port int `yaml:"port"` TLS struct { @@ -366,6 +367,19 @@ func init() { log.Fatal(err) } + // server.ui.host + uiCmd.PersistentFlags().String( + "server-ui-host", + "", + "Web UI host", + ) + if err := viper.BindPFlag( + "server.ui.host", + uiCmd.PersistentFlags().Lookup("server-ui-host"), + ); err != nil { + log.Fatal(err) + } + // server.ui.password uiCmd.PersistentFlags().String( "server-ui-password", From 7ddf89a859a8ebd23defbe582eb635026cdba790 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 16:42:05 -0400 Subject: [PATCH 09/17] feature(config): add `server.ui.api.tls.skipverify` --- config.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/config.go b/config.go index c611e07..cf521a7 100644 --- a/config.go +++ b/config.go @@ -45,6 +45,9 @@ type Config struct { Key string `yaml:"key"` Port int `yaml:"port"` Protocol string `yaml:"protocol"` + TLS struct { + SkipVerify bool `yaml:"skipverify"` + } `yaml:"tls"` } `yaml:"api"` Host string `yaml:"host"` Password string `yaml:"password"` @@ -367,6 +370,19 @@ func init() { log.Fatal(err) } + // server.ui.api.tls.skipverify + uiCmd.PersistentFlags().Bool( + "server-ui-api-tls-skipverify", + false, + "Skip API server TLS certificate verification", + ) + if err := viper.BindPFlag( + "server.ui.api.tls.skipverify", + uiCmd.PersistentFlags().Lookup("server-ui-api-tls-skipverify"), + ); err != nil { + log.Fatal(err) + } + // server.ui.host uiCmd.PersistentFlags().String( "server-ui-host", From 2b6c631d64b07554f64ead5c6b57b9c46e876202 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 16:42:25 -0400 Subject: [PATCH 10/17] fix(compose): adjust `.env` inject --- docker-compose.yaml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 1156be7..4def68e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,9 +3,9 @@ services: db: image: 'docker.io/library/postgres:16' environment: - POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE}" - POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD}" - POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER}" + POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}" + POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}" + POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}" volumes: - 'db-data:/var/lib/postgresql/data' restart: 'unless-stopped' @@ -15,7 +15,13 @@ services: - db build: . image: 'git.agecem.com/agecem/bottin:latest' - env_file: '.env' + env: + BOTTIN_SERVER_API_DB_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}" + BOTTIN_SERVER_API_DB_HOST: "${BOTTIN_SERVER_API_DB_HOST:-db}" + BOTTIN_SERVER_API_DB_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}" + BOTTIN_SERVER_API_DB_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}" + #BOTTIN_SERVER_API_HOST: "${BOTTIN_SERVER_API_HOST:}" + #BOTTIN_SERVER_API_KEY: "${BOTTIN_SERVER_API_KEY ports: - '1312:1312' volumes: @@ -28,7 +34,9 @@ services: - api build: . image: 'git.agecem.com/agecem/bottin:latest' - env_file: '.env' + env: + BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD:-bottin}" + BOTTIN_WEB_USER: "${BOTTIN_WEB_USER:-bottin}" ports: - '2312:2312' volumes: From 9072f7114ac46551484ab74b79e2a82238b01605 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 16:43:22 -0400 Subject: [PATCH 11/17] =?UTF-8?q?feature(cmd):=20impl=C3=A9menter=20UI=20A?= =?UTF-8?q?PI=20TLS=20skip=20verify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd.go b/cmd.go index a54cff6..e801dd3 100644 --- a/cmd.go +++ b/cmd.go @@ -3,6 +3,7 @@ package main import ( "context" "crypto/subtle" + "crypto/tls" "fmt" "html/template" "log" @@ -181,8 +182,15 @@ var uiCmd = &cobra.Command{ } // API Client + var httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify, + }, + }, + } apiClient := APIClient{voki.New( - http.DefaultClient, + httpClient, cfg.Server.UI.API.Host, cfg.Server.UI.API.Key, cfg.Server.UI.API.Port, From bdff81c6b23bbe2eaa40d889fe2ec0b6915a1443 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 16:58:25 -0400 Subject: [PATCH 12/17] =?UTF-8?q?feature(compose):=20directly=20inject=20.?= =?UTF-8?q?env=20file=20=C3=A0=20containers=20api=20et=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 4def68e..5487eb1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,9 +3,9 @@ services: db: image: 'docker.io/library/postgres:16' environment: - POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}" - POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}" - POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}" + POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:?}" + POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:?}" + POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:?}" volumes: - 'db-data:/var/lib/postgresql/data' restart: 'unless-stopped' @@ -15,13 +15,7 @@ services: - db build: . image: 'git.agecem.com/agecem/bottin:latest' - env: - BOTTIN_SERVER_API_DB_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:-bottin}" - BOTTIN_SERVER_API_DB_HOST: "${BOTTIN_SERVER_API_DB_HOST:-db}" - BOTTIN_SERVER_API_DB_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:-bottin}" - BOTTIN_SERVER_API_DB_USER: "${BOTTIN_SERVER_API_DB_USER:-bottin}" - #BOTTIN_SERVER_API_HOST: "${BOTTIN_SERVER_API_HOST:}" - #BOTTIN_SERVER_API_KEY: "${BOTTIN_SERVER_API_KEY + env_file: '.env' ports: - '1312:1312' volumes: @@ -34,9 +28,7 @@ services: - api build: . image: 'git.agecem.com/agecem/bottin:latest' - env: - BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD:-bottin}" - BOTTIN_WEB_USER: "${BOTTIN_WEB_USER:-bottin}" + env_file: '.env' ports: - '2312:2312' volumes: From c9a45f8db868ecc429cda87a5eb7209c7bfeb32d Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 17:01:34 -0400 Subject: [PATCH 13/17] chores(Dockerfile): bump golang -> 1.23.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6479581..8d7516d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.3 as build +FROM golang:1.23.0 as build LABEL author="vlbeaudoin" From 882553521a8551a2d9c6f578375cc497a1506088 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 17:02:20 -0400 Subject: [PATCH 14/17] chores(Dockerfile): bump alpine -> 3.20.3 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6479581..462394a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 go build -a -o bottin . # Alpine -FROM alpine:3.20 +FROM alpine:3.20.3 WORKDIR /app From 0640395fd2ac905f61208ddf9fb7667aae8c59f6 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 3 Sep 2024 17:06:10 -0400 Subject: [PATCH 15/17] fix(Dockerfile): utiliser version de alpine qui existe actually --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e08a2f7..18bfd2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 go build -a -o bottin . # Alpine -FROM alpine:3.20.3 +FROM alpine:3.20.2 WORKDIR /app From b419a5b26045474e6b9bd4349e5134b247c47c22 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 18 Sep 2024 19:06:33 -0400 Subject: [PATCH 16/17] =?UTF-8?q?major:=20s=C3=A9parer=20commande=20de=20l?= =?UTF-8?q?ibrairie=20importable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump major version à 9 package main déplacé vers cmd/bottin/ pour garder `go install` qui nomme l'exécutable `bottin`, sans empêcher d'importer le code à l'extérieur du projet avec pkg/bottin/. Déplacer fichiers SQL vers queries/ Déplacer fichiers html vers templates/ Ajouter scripts/ avec génération et injection de certificats x509 (https) et les ajouter au Makefile Ajouter début d'exemple de manifests dans deployments/kubernetes/ --- .gitignore | 2 + Dockerfile | 8 +- Makefile | 12 + README.md | 58 +-- cmd.go | 285 --------------- config.go => cmd/bottin/main.go | 344 +++++++++++++++--- client_test.go => cmd/bottin/main_test.go | 13 +- docker-compose.yaml => compose.yaml | 5 +- deployments/kubernetes/bottin-pod.yaml | 60 +++ .../kubernetes/example-bottin-secret.yaml | 26 ++ go.mod | 2 +- main.go | 18 - client.go => pkg/bottin/client.go | 2 +- pkg/bottin/config.go | 53 +++ db.go => pkg/bottin/db.go | 13 +- entity.go => pkg/bottin/entity.go | 2 +- request.go => pkg/bottin/request.go | 20 +- response.go => pkg/bottin/response.go | 2 +- routes.go => pkg/bottin/routes.go | 6 +- queries/queries.go | 14 + {sql => queries}/schema.sql | 0 {sql => queries}/views.sql | 0 scripts/compose-inject-x509.sh | 6 + scripts/generate-self-signed-x509.sh | 2 + template.go => templates/templates.go | 11 +- 25 files changed, 513 insertions(+), 451 deletions(-) delete mode 100644 cmd.go rename config.go => cmd/bottin/main.go (56%) rename client_test.go => cmd/bottin/main_test.go (91%) rename docker-compose.yaml => compose.yaml (95%) create mode 100644 deployments/kubernetes/bottin-pod.yaml create mode 100644 deployments/kubernetes/example-bottin-secret.yaml delete mode 100644 main.go rename client.go => pkg/bottin/client.go (99%) create mode 100644 pkg/bottin/config.go rename db.go => pkg/bottin/db.go (97%) rename entity.go => pkg/bottin/entity.go (98%) rename request.go => pkg/bottin/request.go (94%) rename response.go => pkg/bottin/response.go (99%) rename routes.go => pkg/bottin/routes.go (99%) create mode 100644 queries/queries.go rename {sql => queries}/schema.sql (100%) rename {sql => queries}/views.sql (100%) create mode 100755 scripts/compose-inject-x509.sh create mode 100755 scripts/generate-self-signed-x509.sh rename template.go => templates/templates.go (55%) diff --git a/.gitignore b/.gitignore index e2f7237..a8c4332 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ # Dependency directories (remove the comment below to include it) # vendor/ +# cert files +*.pem # env .env diff --git a/Dockerfile b/Dockerfile index 18bfd2d..d24bb50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,14 @@ LABEL author="vlbeaudoin" WORKDIR /go/src/app -COPY go.mod go.sum client.go client_test.go cmd.go config.go db.go entity.go main.go request.go response.go routes.go template.go ./ +COPY go.mod go.sum LICENSE ./ -ADD sql/ sql/ +ADD cmd/ cmd/ +ADD pkg/ pkg/ +ADD queries/ queries/ ADD templates/ templates/ -RUN CGO_ENABLED=0 go build -a -o bottin . +RUN CGO_ENABLED=0 go build -a -o bottin ./cmd/bottin # Alpine diff --git a/Makefile b/Makefile index 31a6c23..827fc0c 100644 --- a/Makefile +++ b/Makefile @@ -14,3 +14,15 @@ help: ## Show this help .PHONY: test-integration test-integration: ## run integration tests through API client. Config is read from `~/.bottin.yaml`. WARNING: affects data in the database, do not run on production server docker-compose down && docker-compose up -d --build && sleep 2 && go test + +.PHONY: dev +dev: generate-self-signed-x509 compose-inject-x509 ## deploy development environment on docker-compose + docker-compose up -d + +.PHONY: generate-self-signed-x509 +generate-self-signed-x509: ## Générer une paire de clés x509 self-signed pour utilisation avec un serveur de développement + ./scripts/generate-self-signed-x509.sh + +.PHONY: compose-inject-x509 +compose-inject-x509: ## Copie la paire de clés x509 du current directory vers les containers orchestrés par docker-compose + ./scripts/compose-inject-x509.sh diff --git a/README.md b/README.md index 822c4f1..a024676 100644 --- a/README.md +++ b/README.md @@ -1,57 +1 @@ -# agecem/bottin - -Bottin de la masse étudiante, en Go - -https://git.agecem.com/agecem/bottin - -## fonctionalités - -### Serveur API - -- Insertion de membre et programme -- Lecture de membre -- Modification du nom d'usage de membre - -### Client web - -- Lecture de membre par requête au serveur API - -## usage - -Remplir .env avec les infos qui seront utilisées pour déployer le container - -Au minimum, il faut ces 3 entrées: - -*Remplacer `bottin` par quelque chose de plus sécuritaire* - -```sh -BOTTIN_SERVER_DB_DATABASE=bottin -BOTTIN_SERVER_DB_PASSWORD=bottin -BOTTIN_SERVER_DB_USER=bottin -``` - -*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options* - -Déployer avec docker-compose - -`$ docker-compose up -d` - -### Optionnel: configuration par fichiers YAML - -*seulement nécessaire si les fichiers `.env` et `docker-compose.yaml` ne contiennent pas toute l'information nécessaire* - -Pour modifier la configuration du serveur API - -`$ docker-compose exec -it api vi /etc/bottin/api.yaml` - -*Y remplir au minimum le champs `server.api.key` (string)* - -Pour modifier la configuration du client web - -`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml` - -*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)* - -Redémarrer les containers une fois la configuration modifiée - -`$ docker-compose down && docker-compose up -d` +Requiert un fichier .env ici pour un déploiement avec base de donnée diff --git a/cmd.go b/cmd.go deleted file mode 100644 index e801dd3..0000000 --- a/cmd.go +++ /dev/null @@ -1,285 +0,0 @@ -package main - -import ( - "context" - "crypto/subtle" - "crypto/tls" - "fmt" - "html/template" - "log" - "net/http" - "os" - - "codeberg.org/vlbeaudoin/voki/v3" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "bottin", - Short: "Bottin étudiant de l'AGECEM", -} - -// 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) - } -} - -var serverCmd = &cobra.Command{ - Use: "server", - Short: "Démarrer serveurs (API ou Web UI)", -} - -// apiCmd represents the api command -var apiCmd = &cobra.Command{ - Use: "api", - Short: "Démarrer le serveur API", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - var cfg Config - if err := viper.Unmarshal(&cfg); err != nil { - log.Fatal("parse config:", err) - } - - e := echo.New() - - // Middlewares - - e.Pre(middleware.AddTrailingSlash()) - - if cfg.Server.API.Key != "" { - e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { - return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil - })) - } else { - log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)") - } - - // DataClient - ctx := context.Background() - - //prep - pool, err := pgxpool.New( - ctx, - fmt.Sprintf( - "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", - cfg.Server.API.DB.User, - cfg.Server.API.DB.Password, - cfg.Server.API.DB.Database, - cfg.Server.API.DB.Host, - cfg.Server.API.DB.Port, - cfg.Server.API.DB.SSLMode, - )) - if err != nil { - log.Fatal("init pgx pool:", err) - } - defer pool.Close() - - db := &PostgresClient{ - Ctx: ctx, - Pool: pool, - } - if err := db.Pool.Ping(ctx); err != nil { - log.Fatal("ping db:", err) - } - - if err := db.CreateOrReplaceSchema(); err != nil { - log.Fatal("create or replace schema:", err) - } - - if err := db.CreateOrReplaceViews(); err != nil { - log.Fatal("create or replace views:", err) - } - - // Routes - if err := addRoutes(e, db, cfg); err != nil { - log.Fatal("add routes:", err) - } - /* - h := handlers.New(client) - - e.GET("/v8/health/", h.GetHealth) - - e.POST("/v8/membres/", h.PostMembres) - - e.GET("/v8/membres/", h.ListMembres) - - e.GET("/v8/membres/:membre_id/", h.ReadMembre) - - e.PUT("/v8/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) - - e.POST("/v8/programmes/", h.PostProgrammes) - - e.POST("/v8/seed/", h.PostSeed) - */ - - // Execution - switch cfg.Server.API.TLS.Enabled { - case false: - e.Logger.Fatal( - e.Start( - fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), - ), - ) - case true: - if cfg.Server.API.TLS.Certfile == "" { - log.Fatal("TLS enabled for API but no certificate file provided") - } - - if cfg.Server.API.TLS.Keyfile == "" { - log.Fatal("TLS enabled for UI but no private key file provided") - } - - e.Logger.Fatal( - e.StartTLS( - fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), - cfg.Server.API.TLS.Certfile, - cfg.Server.API.TLS.Keyfile, - ), - ) - } - }, -} - -// uiCmd represents the ui command -var uiCmd = &cobra.Command{ - Use: "ui", - Aliases: []string{"web", "interface"}, - Short: "Démarrer l'interface Web UI", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - // Parse config - var cfg Config - if err := viper.Unmarshal(&cfg); err != nil { - log.Fatal("init config:", err) - } - - e := echo.New() - - // Middlewares - - // Trailing slash - e.Pre(middleware.AddTrailingSlash()) - - // Auth - e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { - usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1 - passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1 - return usersMatch && passwordsMatch, nil - })) - - // Templating - e.Renderer = &Template{ - templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), - } - - // API Client - var httpClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify, - }, - }, - } - apiClient := APIClient{voki.New( - httpClient, - cfg.Server.UI.API.Host, - cfg.Server.UI.API.Key, - cfg.Server.UI.API.Port, - cfg.Server.UI.API.Protocol, - )} - defer apiClient.Voki.CloseIdleConnections() - - // Routes - e.GET("/", func(c echo.Context) error { - pingResult, err := apiClient.GetHealth() - if err != nil { - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)}, - ) - } - - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: pingResult}, - ) - }) - - e.GET("/membre/", func(c echo.Context) error { - membreID := c.QueryParam("membre_id") - switch { - case membreID == "": - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"}, - ) - case !IsMembreID(membreID): - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)}, - ) - } - - membre, err := apiClient.GetMembreForDisplay(membreID) - if err != nil { - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)}, - ) - } - - return c.Render( - http.StatusOK, - "index-html", - voki.MessageResponse{Message: fmt.Sprintf(` -Numéro étudiant: %s -Nom d'usage: %s -Programme: [%s] %s -`, - membre.ID, - membre.Name, - membre.ProgrammeID, - membre.ProgrammeName, - )}, - ) - }) - - // Execution - switch cfg.Server.UI.TLS.Enabled { - case false: - e.Logger.Fatal(e.Start( - fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port))) - case true: - if cfg.Server.UI.TLS.Certfile == "" { - log.Fatal("TLS enabled for UI but no certificate file provided") - } - - if cfg.Server.UI.TLS.Keyfile == "" { - log.Fatal("TLS enabled for UI but no private key file provided") - } - - e.Logger.Fatal( - e.StartTLS( - fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port), - cfg.Server.UI.TLS.Certfile, - cfg.Server.UI.TLS.Keyfile, - ), - ) - } - - }, -} diff --git a/config.go b/cmd/bottin/main.go similarity index 56% rename from config.go rename to cmd/bottin/main.go index cf521a7..526a344 100644 --- a/config.go +++ b/cmd/bottin/main.go @@ -1,67 +1,25 @@ package main import ( + "context" + "crypto/subtle" + "crypto/tls" "fmt" "log" + "net/http" "os" "strings" + "codeberg.org/vlbeaudoin/voki/v3" + "git.agecem.com/agecem/bottin/v9/pkg/bottin" + "git.agecem.com/agecem/bottin/v9/templates" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" "github.com/spf13/viper" ) -type Config struct { - Client struct { - API struct { - Host string `yaml:"host"` - Key string `yaml:"key"` - Port int `yaml:"port"` - Protocol string `yaml:"protocol"` - } `yaml:"api"` - } `yaml:"client"` - - Server struct { - API struct { - DB struct { - Database string `yaml:"database"` - Host string `yaml:"host"` - Password string `yaml:"password"` - Port int `yaml:"port"` - SSLMode string `yaml:"sslmode"` - User string `yaml:"user"` - } `yaml:"db"` - Host string `yaml:"host"` - Key string `yaml:"key"` - Port int `yaml:"port"` - TLS struct { - Enabled bool `yaml:"enabled"` - Certfile string `yaml:"certfile"` - Keyfile string `yaml:"keyfile"` - } `yaml:"tls"` - } `yaml:"api"` - UI struct { - API struct { - Host string `yaml:"host"` - Key string `yaml:"key"` - Port int `yaml:"port"` - Protocol string `yaml:"protocol"` - TLS struct { - SkipVerify bool `yaml:"skipverify"` - } `yaml:"tls"` - } `yaml:"api"` - Host string `yaml:"host"` - Password string `yaml:"password"` - Port int `yaml:"port"` - TLS struct { - Enabled bool `yaml:"enabled"` - Certfile string `yaml:"certfile"` - Keyfile string `yaml:"keyfile"` - } `yaml:"tls"` - User string `yaml:"user"` - } `yaml:"ui"` - } `yaml:"server"` -} - var cfgFile string // initConfig reads in config file and ENV variables if set. @@ -90,6 +48,17 @@ func initConfig() { } } +func main() { + /* TODO + if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { + log.Fatal(err) + } + */ + + // Handle the command-line via cobra and viper + execute() +} + func init() { // rootCmd @@ -475,3 +444,274 @@ func init() { log.Fatal(err) } } + +/* TODO +func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { + return fmt.Errorf("not implemented") +} +*/ + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "bottin", + Short: "Bottin étudiant de l'AGECEM", +} + +// 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) + } +} + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Démarrer serveurs (API ou Web UI)", +} + +// apiCmd represents the api command +var apiCmd = &cobra.Command{ + Use: "api", + Short: "Démarrer le serveur API", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + var cfg bottin.Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatal("parse config:", err) + } + + e := echo.New() + + // Middlewares + + e.Pre(middleware.AddTrailingSlash()) + + if cfg.Server.API.Key != "" { + e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { + return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil + })) + } else { + log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)") + } + + // DataClient + ctx := context.Background() + + //prep + pool, err := pgxpool.New( + ctx, + fmt.Sprintf( + "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", + cfg.Server.API.DB.User, + cfg.Server.API.DB.Password, + cfg.Server.API.DB.Database, + cfg.Server.API.DB.Host, + cfg.Server.API.DB.Port, + cfg.Server.API.DB.SSLMode, + )) + if err != nil { + log.Fatal("init pgx pool:", err) + } + defer pool.Close() + + db := &bottin.PostgresClient{ + Ctx: ctx, + Pool: pool, + } + if err := db.Pool.Ping(ctx); err != nil { + log.Fatal("ping db:", err) + } + + if err := db.CreateOrReplaceSchema(); err != nil { + log.Fatal("create or replace schema:", err) + } + + if err := db.CreateOrReplaceViews(); err != nil { + log.Fatal("create or replace views:", err) + } + + // Routes + if err := bottin.AddRoutes(e, db, cfg); err != nil { + log.Fatal("add routes:", err) + } + /* + h := handlers.New(client) + + e.GET("/v9/health/", h.GetHealth) + + e.POST("/v9/membres/", h.PostMembres) + + e.GET("/v9/membres/", h.ListMembres) + + e.GET("/v9/membres/:membre_id/", h.ReadMembre) + + e.PUT("/v9/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) + + e.POST("/v9/programmes/", h.PostProgrammes) + + e.POST("/v9/seed/", h.PostSeed) + */ + + // Execution + switch cfg.Server.API.TLS.Enabled { + case false: + e.Logger.Fatal( + e.Start( + fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), + ), + ) + case true: + if cfg.Server.API.TLS.Certfile == "" { + log.Fatal("TLS enabled for API but no certificate file provided") + } + + if cfg.Server.API.TLS.Keyfile == "" { + log.Fatal("TLS enabled for UI but no private key file provided") + } + + e.Logger.Fatal( + e.StartTLS( + fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port), + cfg.Server.API.TLS.Certfile, + cfg.Server.API.TLS.Keyfile, + ), + ) + } + }, +} + +// uiCmd represents the ui command +var uiCmd = &cobra.Command{ + Use: "ui", + Aliases: []string{"web", "interface"}, + Short: "Démarrer l'interface Web UI", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + // Parse config + var cfg bottin.Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatal("init config:", err) + } + + e := echo.New() + + // Middlewares + + // Trailing slash + e.Pre(middleware.AddTrailingSlash()) + + // Auth + e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { + usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1 + return usersMatch && passwordsMatch, nil + })) + + // Templating + e.Renderer = templates.NewTemplate() + + // API Client + var httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify, + }, + }, + } + apiClient := bottin.APIClient{ + Voki: voki.New( + httpClient, + cfg.Server.UI.API.Host, + cfg.Server.UI.API.Key, + cfg.Server.UI.API.Port, + cfg.Server.UI.API.Protocol, + )} + defer apiClient.Voki.CloseIdleConnections() + + // Routes + e.GET("/", func(c echo.Context) error { + pingResult, err := apiClient.GetHealth() + if err != nil { + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)}, + ) + } + + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: pingResult}, + ) + }) + + e.GET("/membre/", func(c echo.Context) error { + membreID := c.QueryParam("membre_id") + switch { + case membreID == "": + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"}, + ) + case !bottin.IsMembreID(membreID): + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)}, + ) + } + + membre, err := apiClient.GetMembreForDisplay(membreID) + if err != nil { + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)}, + ) + } + + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf(` +Numéro étudiant: %s +Nom d'usage: %s +Programme: [%s] %s +`, + membre.ID, + membre.Name, + membre.ProgrammeID, + membre.ProgrammeName, + )}, + ) + }) + + // Execution + switch cfg.Server.UI.TLS.Enabled { + case false: + e.Logger.Fatal(e.Start( + fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port))) + case true: + if cfg.Server.UI.TLS.Certfile == "" { + log.Fatal("TLS enabled for UI but no certificate file provided") + } + + if cfg.Server.UI.TLS.Keyfile == "" { + log.Fatal("TLS enabled for UI but no private key file provided") + } + + e.Logger.Fatal( + e.StartTLS( + fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port), + cfg.Server.UI.TLS.Certfile, + cfg.Server.UI.TLS.Keyfile, + ), + ) + } + + }, +} diff --git a/client_test.go b/cmd/bottin/main_test.go similarity index 91% rename from client_test.go rename to cmd/bottin/main_test.go index 261eb20..f8b6685 100644 --- a/client_test.go +++ b/cmd/bottin/main_test.go @@ -6,6 +6,7 @@ import ( "testing" "codeberg.org/vlbeaudoin/voki/v3" + "git.agecem.com/agecem/bottin/v9/pkg/bottin" "github.com/spf13/viper" ) @@ -14,7 +15,7 @@ func init() { } func TestAPI(t *testing.T) { - var cfg Config + var cfg bottin.Config if err := viper.Unmarshal(&cfg); err != nil { t.Error(err) return @@ -35,7 +36,7 @@ func TestAPI(t *testing.T) { defer httpClient.CloseIdleConnections() vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol) - apiClient := APIClient{vokiClient} + apiClient := bottin.APIClient{Voki: vokiClient} t.Run("get API health", func(t *testing.T) { health, err := apiClient.GetHealth() @@ -53,9 +54,9 @@ func TestAPI(t *testing.T) { t.Run("insert programmes", func(t *testing.T) { - programmes := []Programme{ - {"404.42", "Cool programme"}, - {"200.10", "Autre programme"}, + programmes := []bottin.Programme{ + {ID: "404.42", Name: "Cool programme"}, + {ID: "200.10", Name: "Autre programme"}, } t.Log("programmes:", programmes) _, err := apiClient.InsertProgrammes(programmes...) @@ -64,7 +65,7 @@ func TestAPI(t *testing.T) { } }) - testMembres := []Membre{ + testMembres := []bottin.Membre{ { ID: "0000000", FirstName: "Test", diff --git a/docker-compose.yaml b/compose.yaml similarity index 95% rename from docker-compose.yaml rename to compose.yaml index 5487eb1..686312c 100644 --- a/docker-compose.yaml +++ b/compose.yaml @@ -1,3 +1,4 @@ +name: 'bottin' services: db: @@ -13,7 +14,7 @@ services: api: depends_on: - db - build: . + build: ../.. image: 'git.agecem.com/agecem/bottin:latest' env_file: '.env' ports: @@ -26,7 +27,7 @@ services: ui: depends_on: - api - build: . + build: ../.. image: 'git.agecem.com/agecem/bottin:latest' env_file: '.env' ports: diff --git a/deployments/kubernetes/bottin-pod.yaml b/deployments/kubernetes/bottin-pod.yaml new file mode 100644 index 0000000..72569e4 --- /dev/null +++ b/deployments/kubernetes/bottin-pod.yaml @@ -0,0 +1,60 @@ +apiVersion: v1 +kind: Pod +metadata: + name: bottin-pod +spec: + initContainers: + - name: clone + image: alpine:3.20 + command: ['sh', '-c'] + args: + - apk add git && + git clone -- https://git.agecem.com/agecem/bottin /opt/bottin-src + volumeMounts: + - name: bottin-src + mountPath: /opt/bottin-src + - name: build + image: golang:1.23 + env: + - name: CGO_ENABLED + value: '0' + command: ['sh', '-c'] + args: + - cd /opt/bottin-src && + go build -a -o /opt/bottin-executable/bottin + volumeMounts: + - name: bottin-src + mountPath: /opt/bottin-src + - name: bottin-executable + mountPath: /opt/bottin-executable + containers: + - name: api + image: alpine:3.20 + command: ['sh', '-c'] + args: + - ln -s /opt/bottin-executable/bottin /usr/bin/bottin + volumeMounts: + - name: bottin-executable + mountPath: /opt/bottin-executable + - name: bottin-secret + readOnly: true + mountPath: '/etc/bottin' + - name: ui + image: alpine:3.20 + command: ['sh', '-c'] + args: + - bottin --config /etc/bottin/ui.yaml server ui + volumeMounts: + - name: bottin-executable + mountPath: /opt/bottin-executable + - name: bottin-secret + readOnly: true + mountPath: '/etc/bottin' + volumes: + - name: bottin-src + emptyDir: {} + - name: bottin-executable + emptyDir: {} + - name: bottin-secret + secret: + secretName: bottin-secret diff --git a/deployments/kubernetes/example-bottin-secret.yaml b/deployments/kubernetes/example-bottin-secret.yaml new file mode 100644 index 0000000..36bed97 --- /dev/null +++ b/deployments/kubernetes/example-bottin-secret.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Secret +metadata: + name: bottin-secret +stringData: + api.yaml: | + bottin: + server: + api: + db: + database: 'bottin' + host: 'db.example.com' + password: 'bottin' + sslmode: 'require' + user: 'bottin' + key: 'bottin' + ui.yaml: | + bottin: + server: + ui: + api: + tls: + skipverify: 'true' + key: 'bottin' + password: 'bottin' + user: 'bottin' diff --git a/go.mod b/go.mod index 489b37d..e3da402 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.agecem.com/agecem/bottin/v8 +module git.agecem.com/agecem/bottin/v9 go 1.22.0 diff --git a/main.go b/main.go deleted file mode 100644 index 6f81738..0000000 --- a/main.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -func main() { - /* TODO - if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { - log.Fatal(err) - } - */ - - // Handle the command-line via cobra and viper - execute() -} - -/* TODO -func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { - return fmt.Errorf("not implemented") -} -*/ diff --git a/client.go b/pkg/bottin/client.go similarity index 99% rename from client.go rename to pkg/bottin/client.go index e4958a6..529d9c8 100644 --- a/client.go +++ b/pkg/bottin/client.go @@ -1,4 +1,4 @@ -package main +package bottin import ( "fmt" diff --git a/pkg/bottin/config.go b/pkg/bottin/config.go new file mode 100644 index 0000000..b0c4650 --- /dev/null +++ b/pkg/bottin/config.go @@ -0,0 +1,53 @@ +package bottin + +type Config struct { + Client struct { + API struct { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + } `yaml:"api"` + } `yaml:"client"` + + Server struct { + API struct { + DB struct { + Database string `yaml:"database"` + Host string `yaml:"host"` + Password string `yaml:"password"` + Port int `yaml:"port"` + SSLMode string `yaml:"sslmode"` + User string `yaml:"user"` + } `yaml:"db"` + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + TLS struct { + Enabled bool `yaml:"enabled"` + Certfile string `yaml:"certfile"` + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` + } `yaml:"api"` + UI struct { + API struct { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + TLS struct { + SkipVerify bool `yaml:"skipverify"` + } `yaml:"tls"` + } `yaml:"api"` + Host string `yaml:"host"` + Password string `yaml:"password"` + Port int `yaml:"port"` + TLS struct { + Enabled bool `yaml:"enabled"` + Certfile string `yaml:"certfile"` + Keyfile string `yaml:"keyfile"` + } `yaml:"tls"` + User string `yaml:"user"` + } `yaml:"ui"` + } `yaml:"server"` +} diff --git a/db.go b/pkg/bottin/db.go similarity index 97% rename from db.go rename to pkg/bottin/db.go index f0b2f4e..0769fbc 100644 --- a/db.go +++ b/pkg/bottin/db.go @@ -1,20 +1,15 @@ -package main +package bottin import ( "context" _ "embed" "fmt" + "git.agecem.com/agecem/bottin/v9/queries" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) -//go:embed sql/schema.sql -var sqlSchema string - -//go:embed sql/views.sql -var sqlViews string - type PostgresClient struct { //TODO move context out of client Ctx context.Context @@ -22,12 +17,12 @@ type PostgresClient struct { } func (db *PostgresClient) CreateOrReplaceSchema() error { - _, err := db.Pool.Exec(db.Ctx, sqlSchema) + _, err := db.Pool.Exec(db.Ctx, queries.SQLSchema()) return err } func (db *PostgresClient) CreateOrReplaceViews() error { - _, err := db.Pool.Exec(db.Ctx, sqlViews) + _, err := db.Pool.Exec(db.Ctx, queries.SQLViews()) return err } diff --git a/entity.go b/pkg/bottin/entity.go similarity index 98% rename from entity.go rename to pkg/bottin/entity.go index 672f54c..8279b9e 100644 --- a/entity.go +++ b/pkg/bottin/entity.go @@ -1,4 +1,4 @@ -package main +package bottin import "unicode" diff --git a/request.go b/pkg/bottin/request.go similarity index 94% rename from request.go rename to pkg/bottin/request.go index f1a73b4..555ddab 100644 --- a/request.go +++ b/pkg/bottin/request.go @@ -1,4 +1,4 @@ -package main +package bottin import ( "bytes" @@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - "/api/v8/health/", + "/api/v9/health/", nil, true, ) @@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v8/programme/", + "/api/v9/programme/", &buf, true, ) @@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v8/membre/", + "/api/v9/membre/", &buf, true, ) @@ -146,7 +146,7 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v8/membre/%s/", request.Param.MembreID), + fmt.Sprintf("/api/v9/membre/%s/", request.Param.MembreID), nil, true, ) @@ -180,7 +180,7 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v8/membre/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v9/membre/?limit=%d", request.Query.Limit), nil, true, ) @@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb statusCode, body, err := v.CallAndParse( http.MethodPut, - fmt.Sprintf("/api/v8/membre/%s/prefered_name/", request.Param.MembreID), + fmt.Sprintf("/api/v9/membre/%s/prefered_name/", request.Param.MembreID), &buf, true, ) @@ -258,7 +258,7 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v8/programme/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v9/programme/?limit=%d", request.Query.Limit), nil, true, ) @@ -292,7 +292,7 @@ func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresD statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v8/membre/display/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v9/membre/display/?limit=%d", request.Query.Limit), nil, true, ) @@ -333,7 +333,7 @@ func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDis statusCode, body, err := v.CallAndParse( http.MethodGet, - fmt.Sprintf("/api/v8/membre/%s/display/", request.Param.MembreID), + fmt.Sprintf("/api/v9/membre/%s/display/", request.Param.MembreID), nil, true, ) diff --git a/response.go b/pkg/bottin/response.go similarity index 99% rename from response.go rename to pkg/bottin/response.go index 19818df..cd034f1 100644 --- a/response.go +++ b/pkg/bottin/response.go @@ -1,4 +1,4 @@ -package main +package bottin import ( "fmt" diff --git a/routes.go b/pkg/bottin/routes.go similarity index 99% rename from routes.go rename to pkg/bottin/routes.go index a631aba..dc97041 100644 --- a/routes.go +++ b/pkg/bottin/routes.go @@ -1,4 +1,4 @@ -package main +package bottin import ( "encoding/csv" @@ -13,11 +13,11 @@ import ( "github.com/labstack/echo/v4" ) -func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { +func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { _ = db _ = cfg - apiPath := "/api/v8" + apiPath := "/api/v9" apiGroup := e.Group(apiPath) p := pave.New() if err := pave.EchoRegister[HealthGETRequest]( diff --git a/queries/queries.go b/queries/queries.go new file mode 100644 index 0000000..0fa44d8 --- /dev/null +++ b/queries/queries.go @@ -0,0 +1,14 @@ +package queries + +import ( + _ "embed" +) + +//go:embed schema.sql +var sqlSchema string + +//go:embed views.sql +var sqlViews string + +func SQLSchema() string { return sqlSchema } +func SQLViews() string { return sqlViews } diff --git a/sql/schema.sql b/queries/schema.sql similarity index 100% rename from sql/schema.sql rename to queries/schema.sql diff --git a/sql/views.sql b/queries/views.sql similarity index 100% rename from sql/views.sql rename to queries/views.sql diff --git a/scripts/compose-inject-x509.sh b/scripts/compose-inject-x509.sh new file mode 100755 index 0000000..42ed90c --- /dev/null +++ b/scripts/compose-inject-x509.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +docker-compose cp cert.pem api:/etc/bottin/cert.pem +docker-compose cp key.pem api:/etc/bottin/key.pem +docker-compose cp cert.pem ui:/etc/bottin/cert.pem +docker-compose cp key.pem ui:/etc/bottin/key.pem diff --git a/scripts/generate-self-signed-x509.sh b/scripts/generate-self-signed-x509.sh new file mode 100755 index 0000000..2e1a5bc --- /dev/null +++ b/scripts/generate-self-signed-x509.sh @@ -0,0 +1,2 @@ +#!/bin/sh +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes diff --git a/template.go b/templates/templates.go similarity index 55% rename from template.go rename to templates/templates.go index a5b4980..2539cde 100644 --- a/template.go +++ b/templates/templates.go @@ -1,4 +1,4 @@ -package main +package templates import ( "embed" @@ -8,7 +8,7 @@ import ( "github.com/labstack/echo/v4" ) -//go:embed templates/* +//go:embed *.html var templatesFS embed.FS type Template struct { @@ -18,3 +18,10 @@ type Template struct { func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } + +// NewTemplate returns a new Template instance with templates embedded from *.html +func NewTemplate() *Template { + return &Template{ + templates: template.Must(template.ParseFS(templatesFS, "*.html")), + } +} From 1ad0d614776e110d73b1466843a5b313b2897512 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 19 Sep 2024 14:41:24 -0400 Subject: [PATCH 17/17] docs: rapporter ancien README.md --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a024676..bf96431 100644 --- a/README.md +++ b/README.md @@ -1 +1,58 @@ -Requiert un fichier .env ici pour un déploiement avec base de donnée +# agecem/bottin + +Bottin de la masse étudiante, en Go + +https://git.agecem.com/agecem/bottin + +## fonctionalités + +### Serveur API + +- Insertion de membre et programme +- Lecture de membre +- Modification du nom d'usage de membre + +### Client web + +- Lecture de membre par requête au serveur API + +## usage + +Remplir .env avec les infos qui seront utilisées pour déployer le container + +Au minimum, il faut ces 3 entrées: + +*Remplacer `bottin` par quelque chose de plus sécuritaire* + +```sh +BOTTIN_SERVER_DB_DATABASE=bottin +BOTTIN_SERVER_DB_PASSWORD=bottin +BOTTIN_SERVER_DB_USER=bottin +``` + +*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options* + +Déployer avec docker-compose + +`$ docker-compose up -d` + +### Optionnel: configuration par fichiers YAML + +*seulement nécessaire si les fichiers `.env` et `docker-compose.yaml` ne contiennent pas toute l'information nécessaire* + +Pour modifier la configuration du serveur API + +`$ docker-compose exec -it api vi /etc/bottin/api.yaml` + +*Y remplir au minimum le champs `server.api.key` (string)* + +Pour modifier la configuration du client web + +`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml` + +*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)* + +Redémarrer les containers une fois la configuration modifiée + +`$ docker-compose down && docker-compose up -d` +v