Victor Lacasse-Beaudoin
7bf489315e
Exposes the API spec in pave format, which intends to show information about all API routes. Also pave V1SeedPOST and V1SpecGET
292 lines
8.1 KiB
Go
292 lines
8.1 KiB
Go
/*
|
|
Copyright © 2023 AGECEM
|
|
*/
|
|
package cmd
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"fmt"
|
|
"log"
|
|
|
|
"embed"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
|
|
"codeberg.org/vlbeaudoin/pave"
|
|
"codeberg.org/vlbeaudoin/serpents"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
|
|
"git.agecem.com/agecem/agecem-org/api"
|
|
"git.agecem.com/agecem/agecem-org/apihandler"
|
|
"git.agecem.com/agecem/agecem-org/apirequest"
|
|
"git.agecem.com/agecem/agecem-org/apiresponse"
|
|
"git.agecem.com/agecem/agecem-org/config"
|
|
"git.agecem.com/agecem/agecem-org/media"
|
|
"git.agecem.com/agecem/agecem-org/public"
|
|
"git.agecem.com/agecem/agecem-org/templates"
|
|
"git.agecem.com/agecem/agecem-org/webhandler"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
)
|
|
|
|
type Template struct {
|
|
templates *template.Template
|
|
}
|
|
|
|
var cfg config.Config
|
|
|
|
var (
|
|
publicFS embed.FS
|
|
templatesFS embed.FS
|
|
)
|
|
|
|
// serverCmd represents the server command
|
|
var serverCmd = &cobra.Command{
|
|
Use: "server",
|
|
Short: "Démarrer le serveur web",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := viper.Unmarshal(&cfg); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
mediaClient, err := media.NewMediaClientFromViper()
|
|
switch err != nil {
|
|
case true:
|
|
log.Printf("media.NewMediaClientFromViper error: %s", err)
|
|
case false:
|
|
new_buckets, err := mediaClient.Seed()
|
|
if err != nil {
|
|
log.Printf("(*media.MediaClient).Seed error: %s", err)
|
|
} else {
|
|
log.Printf("Seeded %d buckets.\n", len(new_buckets))
|
|
}
|
|
}
|
|
|
|
RunServer()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(serverCmd)
|
|
publicFS = public.GetPublicFS()
|
|
templatesFS = templates.GetTemplatesFS()
|
|
|
|
serpents.Int(serverCmd.Flags(),
|
|
"server.port", "server-port", 8080,
|
|
"Port to run the webserver on")
|
|
|
|
// Not currently used
|
|
/*
|
|
// server.documents.location - --server-documents-location
|
|
serverCmd.Flags().String("server-documents-location", "us-east", "Storage bucket location (config: server.documents.location)")
|
|
viper.BindPFlag("server.documents.location", serverCmd.Flags().Lookup("server-documents-location"))
|
|
*/
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.documents.endpoint", "server-documents-endpoint", "minio:9000",
|
|
"Storage server endpoint")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.documents.access_key_id", "server-documents-access-key-id", "agecem-org",
|
|
"Storage server access key id")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.documents.secret_access_key", "server-documents-secret-access-key", "agecem-org",
|
|
"Storage server secret access key")
|
|
|
|
serpents.Bool(serverCmd.Flags(),
|
|
"server.documents.use_ssl", "server-documents-use-ssl", false,
|
|
"Storage server SSL status")
|
|
|
|
serpents.StringToString(serverCmd.Flags(),
|
|
"server.documents.buckets", "server-documents-buckets", map[string]string{
|
|
"proces-verbaux": "Procès-verbaux",
|
|
"politiques": "Politiques",
|
|
"reglements": "Règlements",
|
|
"formulaires": "Formulaires",
|
|
},
|
|
"Buckets that are allowed to be accessed by the API")
|
|
|
|
serpents.Bool(serverCmd.Flags(),
|
|
"server.api.auth", "server-api-auth", true,
|
|
"Enable to allow key authentication for /v1 routes")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.api.key", "server-api-key", "agecem-org",
|
|
"Key to use for authenticating to /v1 routes")
|
|
|
|
serpents.Int(serverCmd.Flags(),
|
|
"server.api.port", "server-api-port", 8080,
|
|
"API server port")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.api.protocol", "server-api-protocol", "http",
|
|
"API server protocol (http/https)")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.api.host", "server-api-host", "localhost",
|
|
"API server host")
|
|
|
|
serpents.Bool(serverCmd.Flags(),
|
|
"server.admin.auth", "server-admin-auth", true,
|
|
"Enable to allow basic authentication for /admin routes")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.admin.username", "server-admin-username", "agecem-org",
|
|
"Username for basic authentication for /admin routes")
|
|
|
|
serpents.String(serverCmd.Flags(),
|
|
"server.admin.password", "server-admin-password", "agecem-org",
|
|
"Password for basic authentication for /admin routes")
|
|
}
|
|
|
|
func RunServer() {
|
|
e := echo.New()
|
|
|
|
t := &Template{
|
|
templates: template.Must(template.ParseFS(templatesFS, "html/*.gohtml")),
|
|
}
|
|
|
|
e.Renderer = t
|
|
|
|
e.Pre(middleware.RemoveTrailingSlash())
|
|
|
|
groupStatic := e.Group("/public/*")
|
|
groupStatic.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
|
Root: "/",
|
|
Filesystem: http.FS(publicFS),
|
|
//TODO
|
|
//Browse: true,
|
|
}))
|
|
|
|
groupV1 := e.Group("/v1")
|
|
|
|
groupV1.Use(middleware.AddTrailingSlash())
|
|
|
|
if cfg.Server.Api.Auth {
|
|
if len(cfg.Server.Api.Key) < 10 {
|
|
log.Fatal("server.api.auth is enabled, but server.api.key is too small (needs at least 10 characters)")
|
|
}
|
|
|
|
groupV1.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
|
|
return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.Api.Key)) == 1, nil
|
|
}))
|
|
|
|
log.Println("Key auth for /v1 activated")
|
|
}
|
|
|
|
groupAdmin := e.Group("/admin")
|
|
|
|
groupAdmin.Use(middleware.AddTrailingSlash())
|
|
|
|
if cfg.Server.Admin.Auth {
|
|
if len(cfg.Server.Admin.Username) < 5 {
|
|
log.Fatal("server.admin.auth is enabled, but server.admin.username is too small (needs at least 5 characters)")
|
|
}
|
|
|
|
if len(cfg.Server.Admin.Password) < 10 {
|
|
log.Fatal("server.admin.auth is enabled, but server.admin.password is too small (needs at least 10 characters)")
|
|
}
|
|
|
|
groupAdmin.Use(middleware.BasicAuth(func(username_entered, password_entered string, c echo.Context) (bool, error) {
|
|
// Be careful to use constant time comparison to prevent timing attacks
|
|
if subtle.ConstantTimeCompare([]byte(username_entered), []byte(cfg.Server.Admin.Username)) == 1 &&
|
|
subtle.ConstantTimeCompare([]byte(password_entered), []byte(cfg.Server.Admin.Password)) == 1 {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}))
|
|
|
|
log.Println("Basic auth for /admin activated")
|
|
}
|
|
|
|
// API Routes
|
|
mediaClient, err := media.NewMediaClientFromViper()
|
|
if err != nil {
|
|
log.Fatal("Error during NewMediaClientFromViper for API handlers")
|
|
}
|
|
|
|
p := pave.New()
|
|
|
|
v1Handler := apihandler.V1Handler{
|
|
Config: cfg,
|
|
MediaClient: mediaClient,
|
|
Pave: &p,
|
|
}
|
|
|
|
groupV1.GET("", v1Handler.HandleV1)
|
|
|
|
if err := pave.EchoRegister[
|
|
apirequest.V1SeedPOST,
|
|
apiresponse.V1SeedPOST](groupV1, &p, "/v1", http.MethodPost, "/seed", "Créer buckets manquants définis dans `server.documents.buckets`", "V1SeedPOST", v1Handler.HandleV1Seed); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if err := pave.EchoRegister[
|
|
apirequest.V1SpecGET,
|
|
apiresponse.V1SpecGET](groupV1, &p, "/v1", http.MethodGet, "/spec", apihandler.DescriptionV1SpecGET, "V1SpecGET", v1Handler.HandleV1Spec); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
groupV1.GET("/bucket", v1Handler.HandleV1BucketList)
|
|
|
|
groupV1.GET("/bucket/:bucket", v1Handler.HandleV1BucketRead)
|
|
|
|
groupV1.POST("/bucket/:bucket", v1Handler.HandleV1DocumentCreate)
|
|
|
|
groupV1.GET("/bucket/:bucket/:document", v1Handler.HandleV1DocumentRead)
|
|
|
|
groupV1.PUT("/bucket/:bucket/:document", v1Handler.HandleV1DocumentUpdate)
|
|
|
|
groupV1.DELETE("/bucket/:bucket/:document", v1Handler.HandleV1DocumentDelete)
|
|
|
|
// HTML Routes
|
|
client := http.DefaultClient
|
|
defer client.CloseIdleConnections()
|
|
|
|
apiClient, err := api.NewFromViper(client)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
webHandler := webhandler.WebHandler{
|
|
ApiClient: apiClient,
|
|
}
|
|
|
|
e.GET("/", webhandler.HandleIndex)
|
|
|
|
//e.GET("/a-propos", webhandler.HandleAPropos)
|
|
|
|
//e.GET("/actualite", webhandler.HandleActualite)
|
|
|
|
//e.GET("/actualite/:article", webhandler.HandleActualiteArticle)
|
|
|
|
e.GET("/vie-etudiante", webhandler.HandleVieEtudiante)
|
|
|
|
e.GET("/vie-etudiante/:organisme", webhandler.HandleVieEtudianteOrganisme)
|
|
|
|
e.GET("/documentation", webHandler.HandleDocumentation)
|
|
|
|
e.GET("/formulaires", webhandler.HandleFormulaires)
|
|
|
|
// Public Routes
|
|
|
|
e.GET("/public/documentation/:bucket/:document", webHandler.HandlePublicDocumentation)
|
|
|
|
// Admin Routes
|
|
|
|
groupAdmin.GET("", webhandler.HandleAdmin)
|
|
|
|
groupAdmin.GET("/documents/upload", webHandler.HandleAdminDocumentsUpload)
|
|
|
|
groupAdmin.POST("/documents/upload", webHandler.HandleAdminDocumentsUploadPOST)
|
|
|
|
e.Logger.Fatal(e.Start(
|
|
fmt.Sprintf(":%d", cfg.Server.Port)))
|
|
}
|
|
|
|
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
|
return t.templates.ExecuteTemplate(w, name, data)
|
|
}
|