package cmd import ( "crypto/subtle" _ "embed" "errors" "fmt" "net/http" "time" "git.agecem.com/agecem/bottin-agenda/data" "git.agecem.com/agecem/bottin-agenda/embed" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( html string ) // serverCmd represents the server command var serverCmd = &cobra.Command{ Use: "server", Short: "Run the bottin-agenda server", Run: func(cmd *cobra.Command, args []string) { data.OpenDatabase() data.MigrateDatabase() runServer() }, } func init() { declareFlags() rootCmd.AddCommand(serverCmd) html = embed.ReadHtml() } func declareFlags() { // db.type serverCmd.PersistentFlags().String( "db-type", "", "Database type (config: 'db.type')") viper.BindPFlag( "db.type", serverCmd.PersistentFlags().Lookup("db-type")) serverCmd.MarkPersistentFlagRequired("db.type") // db.sqlite.path serverCmd.PersistentFlags().String( "db-sqlite-path", "", "Path to sqlite database (config: 'db.sqlite.path')") viper.BindPFlag( "db.sqlite.path", serverCmd.PersistentFlags().Lookup("db-sqlite-path")) // server.port serverCmd.PersistentFlags().Int( "server-port", 1313, "The port on which the web application will server content (config: 'server.port')") viper.BindPFlag( "server.port", serverCmd.PersistentFlags().Lookup("server-port")) // server.static_dir serverCmd.PersistentFlags().String( "static-dir", "/var/lib/bottin-agenda/static", "DEPRECATED The directory containing static assets (config: 'server.static_dir')") viper.BindPFlag( "server.static_dir", serverCmd.PersistentFlags().Lookup("static-dir")) // login.username serverCmd.PersistentFlags().String( "login-username", "agenda", "The username to login to the web ui. (config: 'login.username')") viper.BindPFlag( "login.username", serverCmd.PersistentFlags().Lookup("login-username")) // login.password serverCmd.PersistentFlags().String( "login-password", "agenda", "The password to login to the web ui. (config: 'login.password')") viper.BindPFlag( "login.password", serverCmd.PersistentFlags().Lookup("login-password")) // import.insert-batch-size serverCmd.PersistentFlags().Int( "insert-batch-size", 500, "The amount of inserts to do per batch (config: 'import.insert_batch_size')") viper.BindPFlag( "import.insert_batch_size", serverCmd.PersistentFlags().Lookup("insert-batch-size")) } func runServer() { port := fmt.Sprintf(":%d", viper.GetInt("server.port")) e := echo.New() g := e.Group("") e.Pre(middleware.RemoveTrailingSlash()) g.Use(middleware.BasicAuth(basicAuther)) g.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_rfc3339_nano} method=${method}, uri=${uri}, status=${status}" + "\n", })) // v1 routes g.GET("/v1", showAPISpec) g.GET("/v1/scan/:num_etud", showScan) g.POST("/v1/scan/:num_etud", postScan) // html routes registerHTMLRoutes(g) e.Logger.Fatal(e.Start(port)) } func basicAuther(username, password string, context echo.Context) (bool, error) { if subtle.ConstantTimeCompare([]byte(username), []byte(viper.GetString("login.username"))) == 1 && subtle.ConstantTimeCompare([]byte(password), []byte(viper.GetString("login.password"))) == 1 { return true, nil } return false, nil } func showAPISpec(c echo.Context) error { return c.String(http.StatusOK, `agecem/bottin-agenda API Spec ----- "/v1" | GET | Show API specifications "/v1/scan/:num_etud" | GET | Show JSON representing a membre with :num_etud "/v1/scan/:num_etud" | POST | Scan bottin for membre with :num_etud and add to local db if not already present ----- `) } // Handler for "-X GET /v1/scan/:num_etud" func showScan(c echo.Context) error { num_etud := c.Param("num_etud") membre, err := readMembre(num_etud) if err != nil { return c.JSON(http.StatusOK, map[string]error{ "message": err, }) } return c.JSON(200, membre) } // Handler for "-X POST /v1/scan/:num_etud" func postScan(c echo.Context) error { num_etud := c.Param("num_etud") membre, err := scanMembre(num_etud) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{ "message": fmt.Sprintf("Erreur lors de l'insertion de Membre '%s'", num_etud), }) } return c.JSON(http.StatusOK, map[string]string{ "message": fmt.Sprintf("Membre '%s' scannéE avec succès et peut reçevoir son agenda.", membre.NumEtud), }) } // Tries to insert a membre into the local db and return it with any error encountered func scanMembre(num_etud string) (data.Membre, error) { membre, err := readMembre(num_etud) if num_etud == "" { } if err != nil { return membre, err } membre_exists := (membre.ID != 0) membre_bottin, err := readMembreBottin(num_etud) if err != nil { return membre, err } membre_bottin_exists := (membre_bottin.ID != 0) if membre_exists { return membre, errors.New(fmt.Sprintf("Membre '%s' a déjà reçuE son agenda", num_etud)) } if !membre_bottin_exists { return membre, errors.New(fmt.Sprintf("Membre '%s' non-trouvéE dans le bottin. Est-ce bien entré?", num_etud)) } // This should happen if membre is not scanned and is present in bottin membre_bottin.CreatedAt = time.Now() membre_bottin.UpdatedAt = time.Now() err, _ = data.InsertMembre(&membre_bottin) if err != nil { return membre_bottin, err } return membre_bottin, nil } func registerHTMLRoutes(g *echo.Group) { g.GET("/", showScanHTML) g.POST("/", postScanHTML) } func showScanHTML(c echo.Context) error { num_etud := c.QueryParam("num_etud") if num_etud == "" { return c.HTML(http.StatusOK, html) } html_filled := html if num_etud != "" { html_filled = fmt.Sprintf(`%s Numéro étudiant: %s`, html, num_etud) } return c.HTML(http.StatusOK, html_filled) } func postScanHTML(c echo.Context) error { num_etud := c.FormValue("num_etud") if num_etud == "" { return c.HTML(http.StatusOK, html) } membre, err := scanMembre(num_etud) if err != nil { return c.HTML(http.StatusInternalServerError, fmt.Sprintf(`%s Erreur: %s`, html, err)) } html_filled := html if num_etud != "" { if membre.ID == 0 { html_filled = fmt.Sprintf(`%s Erreur lors de l'insertion de membre %s. Veuillez SVP signaler cette erreur.`, html, num_etud) } else { html_filled = fmt.Sprintf(`%s Membre '%s' scannéE avec succès et peut recevoir son agenda.`, html, num_etud) } } return c.HTML(http.StatusOK, html_filled) }