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 switch cfg.API.TLS.Enabled { case false: e.Logger.Fatal( e.Start( fmt.Sprintf(":%d", cfg.API.Port), ), ) 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.Certfile, cfg.API.TLS.Keyfile, ), ) } }, } // 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))) }, }