260 lines
6.4 KiB
Go
260 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)
|
||
|
}
|