Migrate to version 2

This commit is contained in:
Victor Lacasse-Beaudoin 2023-05-29 17:58:23 -04:00
parent 4ac3625f45
commit 96f8dfa35e
21 changed files with 777 additions and 746 deletions

142
cmd/api.go Normal file
View file

@ -0,0 +1,142 @@
package cmd
import (
"crypto/subtle"
"fmt"
"log"
"git.agecem.com/agecem/bottin-agenda/data"
"git.agecem.com/agecem/bottin-agenda/handlers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
apiPort int
apiKey string
)
// 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) {
apiKey = viper.GetString("api.key")
apiPort = viper.GetInt("api.port")
/*
dbConnection := data.PostgresConnection{
User: viper.GetString("db.user"),
Password: viper.GetString("db.password"),
Host: viper.GetString("db.host"),
Database: viper.GetString("db.database"),
Port: viper.GetInt("db.port"),
}
*/
bottinApiKey := viper.GetString("bottin.api.key")
bottinApiHost := viper.GetString("bottin.api.host")
bottinApiProtocol := viper.GetString("bottin.api.protocol")
bottinApiPort := viper.GetInt("bottin.api.port")
bottinConnection := data.NewApiClient(
bottinApiKey,
bottinApiHost,
bottinApiProtocol,
bottinApiPort,
)
e := echo.New()
// Middlewares
e.Pre(middleware.AddTrailingSlash())
if apiKey != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil
}))
}
// Routes
e.GET("/v2/", handlers.GetV2)
// Check bottin is ready
message, err := bottinConnection.GetV4()
if err != nil {
log.Fatal(err)
}
//TODO
log.Println(message)
// Execution
e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
},
}
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"))
}

View file

@ -1,7 +1,7 @@
package cmd
import (
"log"
"fmt"
"os"
"github.com/spf13/cobra"
@ -13,10 +13,7 @@ var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "bottin-agenda",
Short: "Application de distribution d'agendas",
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
Short: "Application de gestion de distribution d'agendas",
}
// Execute adds all child commands to the root command and sets flags appropriately.
@ -31,56 +28,7 @@ func Execute() {
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin-agenda.yaml)")
// bottin.host
rootCmd.PersistentFlags().String(
"bottin-host", "",
"The host of the bottin instance (config: 'bottin.host')")
viper.BindPFlag(
"bottin.host",
rootCmd.PersistentFlags().Lookup("bottin-host"))
rootCmd.MarkPersistentFlagRequired("bottin.host")
// bottin.protocol
rootCmd.PersistentFlags().String(
"bottin-protocol", "http",
"The protocol of the bottin instance (config: 'bottin.protocol')")
viper.BindPFlag(
"bottin.protocol",
rootCmd.PersistentFlags().Lookup("bottin-protocol"))
rootCmd.MarkPersistentFlagRequired("bottin.protocol")
// bottin.port
rootCmd.PersistentFlags().Int(
"bottin-port", 1312,
"The port to the bottin instance (config: 'bottin.port')")
viper.BindPFlag(
"bottin.port",
rootCmd.PersistentFlags().Lookup("bottin-port"))
rootCmd.MarkPersistentFlagRequired("bottin.port")
// bottin.username
rootCmd.PersistentFlags().String(
"bottin-username", "",
"The username to the bottin instance (config: 'bottin.username')")
viper.BindPFlag(
"bottin.username",
rootCmd.PersistentFlags().Lookup("bottin-username"))
rootCmd.MarkPersistentFlagRequired("bottin.username")
// bottin.password
rootCmd.PersistentFlags().String(
"bottin-password", "",
"The password to the bottin instance (config: 'bottin.password')")
viper.BindPFlag(
"bottin.password",
rootCmd.PersistentFlags().Lookup("bottin-password"))
rootCmd.MarkPersistentFlagRequired("bottin.password")
}
// initConfig reads in config file and ENV variables if set.
@ -103,6 +51,6 @@ func initConfig() {
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
log.Printf("Using config file: %s\n", viper.ConfigFileUsed())
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}

View file

@ -1,131 +0,0 @@
package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"git.agecem.com/agecem/bottin-agenda/data"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
protocol, host, username, password string
port int
check bool
)
// scanCmd represents the scan command
var scanCmd = &cobra.Command{
Use: "scan",
Short: "Check a bottin instance for existing membre",
Run: func(cmd *cobra.Command, args []string) {
args_length := len(args)
if args_length != 1 {
log.Fatalf("[e] Wrong number of arguments, got '%d' needs 1", args_length)
} else {
// Search locally for corresponding Membre
membre, err := readMembre(args[0])
if err != nil {
log.Printf("[e] %s", err)
}
membre_exists := (membre.ID != 0)
if membre_exists {
log.Fatal("[e] Membre is already in the bottin-agenda database")
} else {
log.Println("[i] Membre not already in bottin-agenda, confirming existence with bottin")
// Search in bottin for corresponding Membre
membre_bottin, err := readMembreBottin(args[0])
if err != nil {
log.Fatalf("[e] %s", err)
}
membre_bottin_exists := (membre_bottin.ID != 0)
if membre_bottin_exists {
log.Println("[ok] Membre found in bottin")
if !check {
log.Println("[i] Adding to bottin-agenda")
membre_bottin.CreatedAt = time.Now()
membre_bottin.UpdatedAt = time.Now()
// Add to bottin-agenda database
data.InsertMembre(&membre_bottin)
} else {
log.Fatal("[i] Skipping insert because of --check flag")
}
} else {
log.Fatal("[e] Membre does not exist in bottin database")
}
}
}
},
}
func init() {
rootCmd.AddCommand(scanCmd)
// check
scanCmd.PersistentFlags().BoolVar(
&check, "check", false,
"Do not write anything to database.")
}
func updateFlagsBottin() {
protocol = viper.GetString("bottin.protocol")
host = viper.GetString("bottin.host")
port = viper.GetInt("bottin.port")
username = viper.GetString("bottin.username")
password = viper.GetString("bottin.password")
}
func readMembre(num_etud string) (data.Membre, error) {
data.OpenDatabase()
data.MigrateDatabase()
membre, err := data.ReadMembre(num_etud)
if err != nil {
return membre, err
}
return membre, nil
}
func readMembreBottin(num_etud string) (data.Membre, error) {
updateFlagsBottin()
membre := data.Membre{}
request := fmt.Sprintf("%s://%s:%s@%s:%d/v1/membre/%s", protocol, username, password, host, port, num_etud)
response, err := http.Get(request)
if err != nil {
return membre, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return membre, err
}
err = json.Unmarshal([]byte(body), &membre)
if err != nil {
return membre, err
}
return membre, nil
}

View file

@ -1,259 +0,0 @@
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)
}