diff --git a/.gitignore b/.gitignore index a8c4332..e2f7237 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ -# cert files -*.pem # env .env diff --git a/Dockerfile b/Dockerfile index d24bb50..6479581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,19 @@ -FROM golang:1.23.0 as build +FROM golang:1.22.3 as build LABEL author="vlbeaudoin" WORKDIR /go/src/app -COPY go.mod go.sum LICENSE ./ +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 ./ -ADD cmd/ cmd/ -ADD pkg/ pkg/ -ADD queries/ queries/ +ADD sql/ sql/ ADD templates/ templates/ -RUN CGO_ENABLED=0 go build -a -o bottin ./cmd/bottin +RUN CGO_ENABLED=0 go build -a -o bottin . # Alpine -FROM alpine:3.20.2 +FROM alpine:3.20 WORKDIR /app diff --git a/Makefile b/Makefile index 827fc0c..31a6c23 100644 --- a/Makefile +++ b/Makefile @@ -14,15 +14,3 @@ 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 bf96431..acda47f 100644 --- a/README.md +++ b/README.md @@ -20,18 +20,17 @@ https://git.agecem.com/agecem/bottin 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* +(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 +BOTTIN_API_KEY=bottin +BOTTIN_POSTGRES_DATABASE=bottin +BOTTIN_POSTGRES_PASSWORD=bottin +BOTTIN_POSTGRES_USER=bottin +BOTTIN_WEB_PASSWORD=bottin +BOTTIN_WEB_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` @@ -44,15 +43,14 @@ 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)* +*Y remplir au minimum le champs `api.key` (string)* Pour modifier la configuration du client web -`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml` +`$ docker-compose exec -it web vi /etc/bottin/web.yaml` -*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)* +*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.password` (string)* Redémarrer les containers une fois la configuration modifiée `$ docker-compose down && docker-compose up -d` -v diff --git a/pkg/bottin/client.go b/client.go similarity index 99% rename from pkg/bottin/client.go rename to client.go index 529d9c8..e4958a6 100644 --- a/pkg/bottin/client.go +++ b/client.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "fmt" diff --git a/cmd/bottin/main_test.go b/client_test.go similarity index 79% rename from cmd/bottin/main_test.go rename to client_test.go index f8b6685..a2aea93 100644 --- a/cmd/bottin/main_test.go +++ b/client_test.go @@ -1,12 +1,10 @@ package main import ( - "crypto/tls" "net/http" "testing" "codeberg.org/vlbeaudoin/voki/v3" - "git.agecem.com/agecem/bottin/v9/pkg/bottin" "github.com/spf13/viper" ) @@ -15,28 +13,17 @@ func init() { } func TestAPI(t *testing.T) { - var cfg bottin.Config + var cfg Config if err := viper.Unmarshal(&cfg); err != nil { t.Error(err) return } - //httpClient := http.DefaultClient - //defer httpClient.CloseIdleConnections() - - transport := http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - - httpClient := http.Client{ - Transport: &transport, - } + httpClient := http.DefaultClient defer httpClient.CloseIdleConnections() - vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol) - apiClient := bottin.APIClient{Voki: vokiClient} + vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, "http") + apiClient := APIClient{vokiClient} t.Run("get API health", func(t *testing.T) { health, err := apiClient.GetHealth() @@ -54,9 +41,9 @@ func TestAPI(t *testing.T) { t.Run("insert programmes", func(t *testing.T) { - programmes := []bottin.Programme{ - {ID: "404.42", Name: "Cool programme"}, - {ID: "200.10", Name: "Autre programme"}, + programmes := []Programme{ + {"404.42", "Cool programme"}, + {"200.10", "Autre programme"}, } t.Log("programmes:", programmes) _, err := apiClient.InsertProgrammes(programmes...) @@ -65,7 +52,7 @@ func TestAPI(t *testing.T) { } }) - testMembres := []bottin.Membre{ + testMembres := []Membre{ { ID: "0000000", FirstName: "Test", diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..8c5571d --- /dev/null +++ b/cmd.go @@ -0,0 +1,226 @@ +package main + +import ( + "context" + "crypto/subtle" + "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) + } +} + +// 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.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 + })) + } + + // 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.DB.User, + cfg.DB.Password, + cfg.DB.Database, + cfg.DB.Host, + cfg.DB.Port, + cfg.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("/v7/health/", h.GetHealth) + + e.POST("/v7/membres/", h.PostMembres) + + e.GET("/v7/membres/", h.ListMembres) + + e.GET("/v7/membres/:membre_id/", h.ReadMembre) + + e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) + + e.POST("/v7/programmes/", h.PostProgrammes) + + e.POST("/v7/seed/", h.PostSeed) + */ + + // Execution + e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port))) + }, +} + +// webCmd represents the web command +var webCmd = &cobra.Command{ + Use: "web", + Short: "Démarrer le client web", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + // 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.Web.User)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1 + return usersMatch && passwordsMatch, nil + })) + + // Templating + e.Renderer = &Template{ + templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), + } + + // API Client + apiClient := APIClient{voki.New( + http.DefaultClient, + cfg.Web.API.Host, + cfg.Web.API.Key, + cfg.Web.API.Port, + cfg.Web.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 + e.Logger.Fatal(e.Start( + fmt.Sprintf(":%d", cfg.Web.Port))) + }, +} diff --git a/cmd/bottin/main.go b/cmd/bottin/main.go deleted file mode 100644 index 526a344..0000000 --- a/cmd/bottin/main.go +++ /dev/null @@ -1,717 +0,0 @@ -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" -) - -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" (without extension). - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".bottin") - } - - viper.SetEnvPrefix("BOTTIN") - 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 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 - - 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.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", - "", - "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", - "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) - } -} - -/* 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/compose.yaml b/compose.yaml deleted file mode 100644 index 686312c..0000000 --- a/compose.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: 'bottin' -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:?}" - volumes: - - 'db-data:/var/lib/postgresql/data' - restart: 'unless-stopped' - - api: - depends_on: - - db - build: ../.. - image: 'git.agecem.com/agecem/bottin:latest' - env_file: '.env' - ports: - - '1312:1312' - volumes: - - 'api-config:/etc/bottin' - restart: 'unless-stopped' - command: ['bottin', '--config', '/etc/bottin/api.yaml', 'server', 'api'] - - ui: - depends_on: - - api - build: ../.. - image: 'git.agecem.com/agecem/bottin:latest' - env_file: '.env' - ports: - - '2312:2312' - volumes: - - 'ui-config:/etc/bottin' - restart: 'unless-stopped' - command: ['bottin', '--config', '/etc/bottin/ui.yaml', 'server', 'ui'] - -# adminer: -# image: adminer -# restart: always -# ports: -# - 8088:8080 -# depends_on: -# - db - -volumes: - db-data: - api-config: - ui-config: diff --git a/config.go b/config.go new file mode 100644 index 0000000..3096960 --- /dev/null +++ b/config.go @@ -0,0 +1,268 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + 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 { + 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 { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + } `yaml:"api"` + } `yaml:"web"` +} + +// DefaultConfig returns a Config filled with the default values from the +// `Default*` constants defined in this file. +func DefaultConfig() (cfg Config) { + 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.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) + } +} + +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" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".bottin") + } + + viper.SetEnvPrefix("BOTTIN") + 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()) + } +} diff --git a/pkg/bottin/db.go b/db.go similarity index 97% rename from pkg/bottin/db.go rename to db.go index 0769fbc..f0b2f4e 100644 --- a/pkg/bottin/db.go +++ b/db.go @@ -1,15 +1,20 @@ -package bottin +package main 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 @@ -17,12 +22,12 @@ type PostgresClient struct { } func (db *PostgresClient) CreateOrReplaceSchema() error { - _, err := db.Pool.Exec(db.Ctx, queries.SQLSchema()) + _, err := db.Pool.Exec(db.Ctx, sqlSchema) return err } func (db *PostgresClient) CreateOrReplaceViews() error { - _, err := db.Pool.Exec(db.Ctx, queries.SQLViews()) + _, err := db.Pool.Exec(db.Ctx, sqlViews) return err } diff --git a/deployments/kubernetes/bottin-pod.yaml b/deployments/kubernetes/bottin-pod.yaml deleted file mode 100644 index 72569e4..0000000 --- a/deployments/kubernetes/bottin-pod.yaml +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 36bed97..0000000 --- a/deployments/kubernetes/example-bottin-secret.yaml +++ /dev/null @@ -1,26 +0,0 @@ -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/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..9f2e604 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,57 @@ +services: + + db: + image: 'docker.io/library/postgres:16' + environment: + POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}" + POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}" + POSTGRES_USER: "${BOTTIN_POSTGRES_USER}" + volumes: + - 'db-data:/var/lib/postgresql/data' + restart: 'unless-stopped' + + api: + depends_on: + - 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}" + ports: + - '1312:1312' + volumes: + - 'api-config:/etc/bottin' + restart: 'unless-stopped' + command: ['bottin', '--config', '/etc/bottin/api.yaml', 'api'] + + web: + 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}" + ports: + - '2312:2312' + volumes: + - 'web-config:/etc/bottin' + restart: 'unless-stopped' + command: ['bottin', '--config', '/etc/bottin/web.yaml', 'web'] + +# adminer: +# image: adminer +# restart: always +# ports: +# - 8088:8080 +# depends_on: +# - db + +volumes: + db-data: + api-config: + web-config: diff --git a/pkg/bottin/entity.go b/entity.go similarity index 98% rename from pkg/bottin/entity.go rename to entity.go index 8279b9e..672f54c 100644 --- a/pkg/bottin/entity.go +++ b/entity.go @@ -1,4 +1,4 @@ -package bottin +package main import "unicode" diff --git a/go.mod b/go.mod index e3da402..2c52fee 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.agecem.com/agecem/bottin/v9 +module git.agecem.com/agecem/bottin/v7 go 1.22.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..6f81738 --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +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/pkg/bottin/config.go b/pkg/bottin/config.go deleted file mode 100644 index b0c4650..0000000 --- a/pkg/bottin/config.go +++ /dev/null @@ -1,53 +0,0 @@ -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/queries/queries.go b/queries/queries.go deleted file mode 100644 index 0fa44d8..0000000 --- a/queries/queries.go +++ /dev/null @@ -1,14 +0,0 @@ -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/pkg/bottin/request.go b/request.go similarity index 94% rename from pkg/bottin/request.go rename to request.go index 555ddab..8f47f10 100644 --- a/pkg/bottin/request.go +++ b/request.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "bytes" @@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - "/api/v9/health/", + "/api/v7/health/", nil, true, ) @@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v9/programme/", + "/api/v7/programme/", &buf, true, ) @@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/v9/membre/", + "/api/v7/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/v9/membre/%s/", request.Param.MembreID), + fmt.Sprintf("/api/v7/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/v9/membre/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v7/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/v9/membre/%s/prefered_name/", request.Param.MembreID), + fmt.Sprintf("/api/v7/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/v9/programme/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v7/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/v9/membre/display/?limit=%d", request.Query.Limit), + fmt.Sprintf("/api/v7/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/v9/membre/%s/display/", request.Param.MembreID), + fmt.Sprintf("/api/v7/membre/%s/display/", request.Param.MembreID), nil, true, ) diff --git a/pkg/bottin/response.go b/response.go similarity index 99% rename from pkg/bottin/response.go rename to response.go index cd034f1..19818df 100644 --- a/pkg/bottin/response.go +++ b/response.go @@ -1,4 +1,4 @@ -package bottin +package main import ( "fmt" diff --git a/pkg/bottin/routes.go b/routes.go similarity index 98% rename from pkg/bottin/routes.go rename to routes.go index dc97041..ca7dcb2 100644 --- a/pkg/bottin/routes.go +++ b/routes.go @@ -1,4 +1,4 @@ -package bottin +package main 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/v9" + apiPath := "/api/v7" apiGroup := e.Group(apiPath) p := pave.New() if err := pave.EchoRegister[HealthGETRequest]( @@ -248,8 +248,7 @@ func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { } } else { - //TODO cfg.Server.API.DefaultLimit - //TODO cfg.Client.API.Limit + //TODO cfg.API.DefaultLimit request.Query.Limit = 1000 } diff --git a/scripts/compose-inject-x509.sh b/scripts/compose-inject-x509.sh deleted file mode 100755 index 42ed90c..0000000 --- a/scripts/compose-inject-x509.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/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 deleted file mode 100755 index 2e1a5bc..0000000 --- a/scripts/generate-self-signed-x509.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes diff --git a/queries/schema.sql b/sql/schema.sql similarity index 100% rename from queries/schema.sql rename to sql/schema.sql diff --git a/queries/views.sql b/sql/views.sql similarity index 100% rename from queries/views.sql rename to sql/views.sql diff --git a/templates/templates.go b/template.go similarity index 55% rename from templates/templates.go rename to template.go index 2539cde..a5b4980 100644 --- a/templates/templates.go +++ b/template.go @@ -1,4 +1,4 @@ -package templates +package main import ( "embed" @@ -8,7 +8,7 @@ import ( "github.com/labstack/echo/v4" ) -//go:embed *.html +//go:embed templates/* var templatesFS embed.FS type Template struct { @@ -18,10 +18,3 @@ 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")), - } -}