diff --git a/cmd/api.go b/cmd/api.go index 7297270..224df3c 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -93,60 +93,4 @@ var apiCmd = &cobra.Command{ func init() { rootCmd.AddCommand(apiCmd) - - // api.key - apiCmd.Flags().String( - "api-key", "bottin-agenda", - "API server key. Leave empty for no key auth. (config: 'api.key')") - viper.BindPFlag("api.key", apiCmd.Flags().Lookup("api-key")) - - // api.port - apiCmd.Flags().Int( - "api-port", 1313, - "API server port (config:'api.port')") - viper.BindPFlag("api.port", apiCmd.Flags().Lookup("api-port")) - - // bottin.api.host - apiCmd.Flags().String( - "bottin-api-host", "api", - "Remote bottin API server host (config:'bottin.api.host')") - viper.BindPFlag("bottin.api.host", apiCmd.Flags().Lookup("bottin-api-host")) - - // bottin.api.key - apiCmd.Flags().String( - "bottin-api-key", "bottin", - "Remote bottin API server key (config:'bottin.api.key')") - viper.BindPFlag("bottin.api.key", apiCmd.Flags().Lookup("bottin-api-key")) - - // bottin.api.protocol - apiCmd.Flags().String( - "bottin-api-protocol", "http", - "Remote bottin API server protocol (config:'bottin.api.protocol')") - viper.BindPFlag("bottin.api.protocol", apiCmd.Flags().Lookup("bottin-api-protocol")) - - // bottin.api.port - apiCmd.Flags().Int( - "bottin-api-port", 1312, - "Remote bottin API server port (config:'bottin.api.port')") - viper.BindPFlag("bottin.api.port", apiCmd.Flags().Lookup("bottin-api-port")) - - // db.database - apiCmd.Flags().String("db-database", "bottin-agenda", "Postgres database (config:'db.database')") - viper.BindPFlag("db.database", apiCmd.Flags().Lookup("db-database")) - - // db.host - apiCmd.Flags().String("db-host", "db", "Postgres host (config:'db.host')") - viper.BindPFlag("db.host", apiCmd.Flags().Lookup("db-host")) - - // db.password - apiCmd.Flags().String("db-password", "bottin-agenda", "Postgres password (config:'db.password')") - viper.BindPFlag("db.password", apiCmd.Flags().Lookup("db-password")) - - // db.port - apiCmd.Flags().Int("db-port", 5432, "Postgres port (config:'db.port')") - viper.BindPFlag("db.port", apiCmd.Flags().Lookup("db-port")) - - // db.user - apiCmd.Flags().String("db-user", "bottin-agenda", "Postgres user (config:'db.user')") - viper.BindPFlag("db.user", apiCmd.Flags().Lookup("db-user")) } diff --git a/cmd/root.go b/cmd/root.go index 7fa76ca..b75fdbc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,6 +29,104 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin-agenda.yaml)") + + // web.api.host + rootCmd.PersistentFlags().String( + "web-api-host", "api", + "Remote API server host (config:'web.api.host')") + viper.BindPFlag("web.api.host", rootCmd.PersistentFlags().Lookup("web-api-host")) + + // web.api.key + rootCmd.PersistentFlags().String( + "web-api-key", "bottin-agenda", + "Remote API server key (config:'web.api.key')") + viper.BindPFlag("web.api.key", rootCmd.PersistentFlags().Lookup("web-api-key")) + + // web.api.protocol + rootCmd.PersistentFlags().String( + "web-api-protocol", "http", + "Remote API server protocol (config:'web.api.protocol')") + viper.BindPFlag("web.api.protocol", rootCmd.PersistentFlags().Lookup("web-api-protocol")) + + // web.api.port + rootCmd.PersistentFlags().Int( + "web-api-port", 1313, + "Remote API server port (config:'web.api.port')") + viper.BindPFlag("web.api.port", rootCmd.PersistentFlags().Lookup("web-api-port")) + + // web.password + rootCmd.PersistentFlags().String( + "web-password", "bottin-agenda", + "Web client password (config:'web.password')") + viper.BindPFlag("web.password", rootCmd.PersistentFlags().Lookup("web-password")) + + // web.port + rootCmd.PersistentFlags().Int( + "web-port", 2313, + "Web client port (config:'web.port')") + viper.BindPFlag("web.port", rootCmd.PersistentFlags().Lookup("web-port")) + + // web.user + rootCmd.PersistentFlags().String( + "web-user", "bottin-agenda", + "Web client user (config:'web.user')") + viper.BindPFlag("web.user", rootCmd.PersistentFlags().Lookup("web-user")) + + // api.key + rootCmd.PersistentFlags().String( + "api-key", "bottin-agenda", + "API server key. Leave empty for no key auth. (config: 'api.key')") + viper.BindPFlag("api.key", rootCmd.PersistentFlags().Lookup("api-key")) + + // api.port + rootCmd.PersistentFlags().Int( + "api-port", 1313, + "API server port (config:'api.port')") + viper.BindPFlag("api.port", rootCmd.PersistentFlags().Lookup("api-port")) + + // bottin.api.host + rootCmd.PersistentFlags().String( + "bottin-api-host", "api", + "Remote bottin API server host (config:'bottin.api.host')") + viper.BindPFlag("bottin.api.host", rootCmd.PersistentFlags().Lookup("bottin-api-host")) + + // bottin.api.key + rootCmd.PersistentFlags().String( + "bottin-api-key", "bottin", + "Remote bottin API server key (config:'bottin.api.key')") + viper.BindPFlag("bottin.api.key", rootCmd.PersistentFlags().Lookup("bottin-api-key")) + + // bottin.api.protocol + rootCmd.PersistentFlags().String( + "bottin-api-protocol", "http", + "Remote bottin API server protocol (config:'bottin.api.protocol')") + viper.BindPFlag("bottin.api.protocol", rootCmd.PersistentFlags().Lookup("bottin-api-protocol")) + + // bottin.api.port + rootCmd.PersistentFlags().Int( + "bottin-api-port", 1312, + "Remote bottin API server port (config:'bottin.api.port')") + viper.BindPFlag("bottin.api.port", rootCmd.PersistentFlags().Lookup("bottin-api-port")) + + // db.database + rootCmd.PersistentFlags().String("db-database", "bottin-agenda", "Postgres database (config:'db.database')") + viper.BindPFlag("db.database", rootCmd.PersistentFlags().Lookup("db-database")) + + // db.host + rootCmd.PersistentFlags().String("db-host", "db", "Postgres host (config:'db.host')") + viper.BindPFlag("db.host", rootCmd.PersistentFlags().Lookup("db-host")) + + // db.password + rootCmd.PersistentFlags().String("db-password", "bottin-agenda", "Postgres password (config:'db.password')") + viper.BindPFlag("db.password", rootCmd.PersistentFlags().Lookup("db-password")) + + // db.port + rootCmd.PersistentFlags().Int("db-port", 5432, "Postgres port (config:'db.port')") + viper.BindPFlag("db.port", rootCmd.PersistentFlags().Lookup("db-port")) + + // db.user + rootCmd.PersistentFlags().String("db-user", "bottin-agenda", "Postgres user (config:'db.user')") + viper.BindPFlag("db.user", rootCmd.PersistentFlags().Lookup("db-user")) } // initConfig reads in config file and ENV variables if set. diff --git a/cmd/web.go b/cmd/web.go new file mode 100644 index 0000000..45274da --- /dev/null +++ b/cmd/web.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "crypto/subtle" + "embed" + "fmt" + "html/template" + "io" + "log" + + "git.agecem.com/agecem/bottin-agenda/data" + "git.agecem.com/agecem/bottin-agenda/models" + "git.agecem.com/agecem/bottin-agenda/web" + "git.agecem.com/agecem/bottin-agenda/web/webhandlers" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +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) { + var config models.Config + + err := viper.Unmarshal(&config) + if err != nil { + log.Fatalf("Error during webCmd#viper.Unmarshal(): %s", err) + } + + // Ping API server + + apiClient := data.NewApiClient(config.Web.Api.Key, config.Web.Api.Host, config.Web.Api.Protocol, config.Web.Api.Port) + + healthResult, err := apiClient.GetHealth() + if err != nil { + log.Fatalf("Error during webCmd#apiClient.GetHealth(): %s", err) + } + + log.Println(healthResult) + + 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(config.Web.User)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(config.Web.Password)) == 1 + return usersMatch && passwordsMatch, nil + })) + + // Template + + t := &Template{ + templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), + } + + e.Renderer = t + + // Routes + + e.GET("/", webhandlers.GetIndex) + //e.POST("/transaction", webhandlers.PostTransaction) + + // Execution + + e.Logger.Fatal(e.Start( + fmt.Sprintf(":%d", config.Web.Port))) + }, +} + +func init() { + rootCmd.AddCommand(webCmd) + templatesFS = web.GetTemplates() +} diff --git a/data/apiclient.go b/data/apiclient.go index 6132a12..f56979f 100644 --- a/data/apiclient.go +++ b/data/apiclient.go @@ -1,9 +1,14 @@ package data import ( + "encoding/json" + "errors" "fmt" "io" + "io/ioutil" "net/http" + + "git.agecem.com/agecem/bottin-agenda/responses" ) type ApiClient struct { @@ -58,3 +63,30 @@ func (a *ApiClient) Call(method, route string, requestBody io.Reader, useKey boo return response, nil } + +// GetHealth allows checking for API server health +func (a *ApiClient) GetHealth() (string, error) { + var response responses.GetHealthResponse + + getHealthResponse, err := a.Call(http.MethodGet, "/v3/health", nil, true) + if err != nil { + return response.Message, err + } + + defer getHealthResponse.Body.Close() + + body, err := ioutil.ReadAll(getHealthResponse.Body) + if err != nil { + return response.Message, err + } + + if err := json.Unmarshal(body, &response); err != nil { + return response.Message, err + } + + if response.Message == "" { + return response.Message, errors.New("Could not confirm that API server is up, no response message") + } + + return response.Message, nil +} diff --git a/models/models.go b/models/models.go index ac30776..691afc0 100644 --- a/models/models.go +++ b/models/models.go @@ -17,3 +17,44 @@ type Transaction struct { GivenAt *time.Time `db:"given_at" json:"given_at"` IsPerpetual bool `db:"is_perpetual" json:"is_perpetual"` } + +type Config struct { + Web WebConfig `json:"web"` + Api ApiConfig `json:"api"` + Bottin BottinConfig `json:"bottin"` + Db DbConfig `json:"db"` +} + +type ApiConfig struct { + Key string `json:"key"` + Port int `json:"port"` +} + +type BottinConfig struct { + Api struct { + Host string `json:"host"` + Key string `json:"key"` + Protocol string `json:"protocol"` + Port int `json:"port"` + } `json:"api"` +} + +type DbConfig struct { + Database string `json:"database"` + Host string `json:"host"` + Password string `json:"password"` + Port int `json:"port"` + User string `json:"user"` +} + +type WebConfig struct { + Api struct { + Host string `json:"host"` + Key string `json:"key"` + Port int `json:"port"` + Protocol string `json:"protocol"` + } `json:"api"` + Password string `json:"password"` + Port int `json:"port"` + User string `json:"user"` +} diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..465e5ec --- /dev/null +++ b/web/embed.go @@ -0,0 +1,10 @@ +package web + +import "embed" + +//go:embed templates/* +var templatesFS embed.FS + +func GetTemplates() embed.FS { + return templatesFS +} diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..af30688 --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,117 @@ +{{ define "index-html" }} + + + + + + AGECEM | Agenda + + + + + + +

+ Distribution d'agendas aux membres de l'AGECEM +

+ +

+ 1) Si l'agenda demandé est perpétuel, cochez 'Perpétuel?:'
+
+ 2) Sélectionnez le champs 'Numéro étudiant:'
+
+ 3) Scannez la carte étudiante d'unE membre
+ -ou-
+ Entrez manuellement le code à 7 chiffres
+
+ 4) Si aucune erreur ne survient, la personne est libre de partir avec son agenda +

+ +
+ +
+ +

{{ .Result }}

+ + + +{{ end }} diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go new file mode 100644 index 0000000..dce8399 --- /dev/null +++ b/web/webhandlers/handlers.go @@ -0,0 +1,43 @@ +package webhandlers + +import ( + "fmt" + "net/http" + + "git.agecem.com/agecem/bottin/v5/data" + "github.com/labstack/echo/v4" +) + +func GetIndex(c echo.Context) error { + return c.Render(http.StatusOK, "index-html", nil) +} + +func GetMembre(c echo.Context) error { + apiClient := data.NewApiClientFromViper() + + membreID := c.QueryParam("membre_id") + + membre, err := apiClient.GetMembre(membreID) + if err != nil { + return c.Render(http.StatusBadRequest, "index-html", struct { + Result string + }{ + Result: fmt.Sprintln("👎", err.Error()), + }) + } + + membreResult := fmt.Sprintf(`👍 + Membre trouvéE: [%s]`, membre.ID) + + if membre.PreferedName != "" { + membreResult = fmt.Sprintf("%s -> %s", membreResult, membre.PreferedName) + } else { + membreResult = fmt.Sprintf("%s -> %s, %s", membreResult, membre.LastName, membre.FirstName) + } + + return c.Render(http.StatusOK, "index-html", struct { + Result string + }{ + Result: membreResult, + }) +}