bottin-agenda/cmd/server.go

259 lines
6.4 KiB
Go

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)
}