diff --git a/Dockerfile b/Dockerfile index 7c94ff6..361fe34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,8 @@ WORKDIR /go/src/app COPY go.mod go.sum db.go entity.go main.go responses.go ./ ADD cmd/ cmd/ -ADD templates/ templates/ ADD sql/ sql/ +ADD templates/ templates/ RUN CGO_ENABLED=0 go build -a -o bottin . diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..413e1a2 --- /dev/null +++ b/cmd.go @@ -0,0 +1,314 @@ +package main + +import ( + "crypto/subtle" + "embed" + "fmt" + "html/template" + "io" + "log" + "os" + "strings" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + apiPort int + apiKey string +) + +// 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) { + apiKey = viper.GetString(ViperAPIKey) + apiPort = viper.GetInt(ViperAPIPort) + + e := echo.New() + + // Middlewares + + e.Pre(middleware.AddTrailingSlash()) + + if apiKey != "" { + e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { + return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil + })) + } + + // DataClient + /* + client, err := data.NewDataClientFromViper() + if err != nil { + log.Fatalf("Could not establish database connection.\n Error: %s\n", err) + } + defer client.DB.Close() + + err = client.DB.Ping() + if err != nil { + log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) + } + + _, err = client.Seed() + if err != nil { + log.Fatalf("Error during client.Seed(): %s", err) + } + */ + + // Routes + /* + 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", apiPort))) + }, +} + +func init() { + 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.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) + } +} + +var cfgFile string + +// 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) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") +} + +// 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()) + } +} + +var ( + webUser string + webPassword string + webPort int + webApiHost string + webApiKey string + webApiPort int + webApiProtocol string +) + +//go:embed templates/* +var templatesFS embed.FS + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +// webCmd represents the web command +var webCmd = &cobra.Command{ + Use: "web", + Short: "Démarrer le client web", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + webApiHost = viper.GetString(viperWebAPIHost) + webApiKey = viper.GetString(viperWebAPIKey) + webApiPort = viper.GetInt(viperWebAPIPort) + webApiProtocol = viper.GetString(viperWebAPIProtocol) + webPassword = viper.GetString(viperWebPassword) + webPort = viper.GetInt(viperWebPort) + webUser = viper.GetString(viperWebUser) + + // Ping API server + /* + client := http.DefaultClient + defer client.CloseIdleConnections() + + apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) + + pingResult, err := apiClient.GetHealth() + if err != nil { + log.Fatal(err) + } + + log.Println(pingResult) + */ + + e := echo.New() + + // Middlewares + + e.Pre(middleware.AddTrailingSlash()) + + e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { + usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1 + return usersMatch && passwordsMatch, nil + })) + + // Template + + t := &Template{ + templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), + } + + e.Renderer = t + + // Routes + /* + handler := webhandlers.Handler{APIClient: apiClient} + + e.GET("/", handler.GetIndex) + e.GET("/membre/", handler.GetMembre) + */ + + // Execution + + e.Logger.Fatal(e.Start( + fmt.Sprintf(":%d", webPort))) + }, +} + +func init() { + rootCmd.AddCommand(webCmd) + //templatesFS = web.GetTemplates() + + // 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) + } +} diff --git a/cmd/api.go b/cmd/api.go deleted file mode 100644 index 1e9f6d8..0000000 --- a/cmd/api.go +++ /dev/null @@ -1,165 +0,0 @@ -package cmd - -import ( - "crypto/subtle" - "fmt" - "log" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - apiPort int - apiKey string -) - -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" - - 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" -) - -// 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) { - apiKey = viper.GetString(ViperAPIKey) - apiPort = viper.GetInt(ViperAPIPort) - - e := echo.New() - - // Middlewares - - e.Pre(middleware.AddTrailingSlash()) - - if apiKey != "" { - e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { - return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil - })) - } - - // DataClient - /* - client, err := data.NewDataClientFromViper() - if err != nil { - log.Fatalf("Could not establish database connection.\n Error: %s\n", err) - } - defer client.DB.Close() - - err = client.DB.Ping() - if err != nil { - log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) - } - - _, err = client.Seed() - if err != nil { - log.Fatalf("Error during client.Seed(): %s", err) - } - */ - - // Routes - /* - 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", apiPort))) - }, -} - -func init() { - 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.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) - } -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 26e0636..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,59 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var cfgFile string - -// 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) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") -} - -// 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/cmd/web.go b/cmd/web.go deleted file mode 100644 index 2ce2fad..0000000 --- a/cmd/web.go +++ /dev/null @@ -1,183 +0,0 @@ -package cmd - -import ( - "crypto/subtle" - "embed" - "fmt" - "html/template" - "io" - "log" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - webUser string - webPassword string - webPort int - webApiHost string - webApiKey string - webApiPort int - webApiProtocol string -) - -const ( - 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 = "api.host" - flagWebAPIHost string = "api-host" - defaultWebAPIHost string = "api" - descriptionWebAPIHost string = "Target API server host" - - viperWebAPIKey string = "api.key" - flagWebAPIKey string = "api-key" - defaultWebAPIKey string = "bottin" - descriptionWebAPIKey string = "Target API server key" - - viperWebAPIPort string = "api.port" - flagWebAPIPort string = "api-port" - defaultWebAPIPort int = 1312 - descriptionWebAPIPort string = "Target API server port" - - viperWebAPIProtocol string = "api.protocol" - flagWebAPIProtocol string = "api-protocol" - defaultWebAPIProtocol string = "http" - descriptionWebAPIProtocol string = "Target API server protocol (http/https)" -) - -var templatesFS embed.FS - -type Template struct { - templates *template.Template -} - -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -// webCmd represents the web command -var webCmd = &cobra.Command{ - Use: "web", - Short: "Démarrer le client web", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - webApiHost = viper.GetString(viperWebAPIHost) - webApiKey = viper.GetString(viperWebAPIKey) - webApiPort = viper.GetInt(viperWebAPIPort) - webApiProtocol = viper.GetString(viperWebAPIProtocol) - webPassword = viper.GetString(viperWebPassword) - webPort = viper.GetInt(viperWebPort) - webUser = viper.GetString(viperWebUser) - - // Ping API server - /* - client := http.DefaultClient - defer client.CloseIdleConnections() - - apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) - - pingResult, err := apiClient.GetHealth() - if err != nil { - log.Fatal(err) - } - - log.Println(pingResult) - */ - - e := echo.New() - - // Middlewares - - e.Pre(middleware.AddTrailingSlash()) - - e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { - usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1 - passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1 - return usersMatch && passwordsMatch, nil - })) - - // Template - - t := &Template{ - templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), - } - - e.Renderer = t - - // Routes - /* - handler := webhandlers.Handler{APIClient: apiClient} - - e.GET("/", handler.GetIndex) - e.GET("/membre/", handler.GetMembre) - */ - - // Execution - - e.Logger.Fatal(e.Start( - fmt.Sprintf(":%d", webPort))) - }, -} - -func init() { - rootCmd.AddCommand(webCmd) - //templatesFS = web.GetTemplates() - - // 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) - } -} diff --git a/db_test.go b/db_test.go index d9d235a..0f508a5 100644 --- a/db_test.go +++ b/db_test.go @@ -8,6 +8,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) +// path to a file containing the db password +var passfile string + func TestDB(t *testing.T) { ctx := context.Background() @@ -17,11 +20,11 @@ func TestDB(t *testing.T) { fmt.Sprintf( "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", "bottin", + dbPassword, "bottin", - "bottin", - "localhost", + "postgres.agecem.com", 5432, - "prefer", //TODO change to "require" + "require", //TODO change to "require" )) if err != nil { t.Error(err) diff --git a/main.go b/main.go index cb9123b..58be335 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,18 @@ import ( "context" "fmt" "io" - "log" - "os" ) func main() { - if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { - log.Fatal(err) - } + //TODO + /* + if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { + log.Fatal(err) + } + */ + + // Handle the command-line + Execute() } func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {