agecem-org/cmd/server.go
Victor Lacasse-Beaudoin 0738a851e6 rename!: templates gohtml -> html
L'extension `gohtml` était pratique pour dénoter que le fichier était un
template et n'allait pas être exposé directement avant manipulations,
par contre ça rendait le formattage par défaut plus complexe.

Les fichier sont maintenant simplement `*.html`, et il est clair que ce
sont des templates car ils sont de toute façon dans un dossier appelé
`templates/html/`, ce qui devrait être assez clair.

BREAKING: fichiers dans `templates/html/` doivent avoir l'extension `.html`
2023-12-12 17:34:43 -05:00

306 lines
8.8 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/*.html")),
}
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.V1GET)
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.V1SeedPOST); err != nil {
log.Fatal(err)
}
if err := pave.EchoRegister[
apirequest.V1SpecGET,
apiresponse.V1SpecGET](groupV1, &p, "/v1", http.MethodGet, "/spec", apihandler.DescriptionV1SpecGET, "V1SpecGET", v1Handler.V1SpecGET); err != nil {
log.Fatal(err)
}
if err := pave.EchoRegister[
apirequest.V1BucketsGET,
apiresponse.V1BucketsGET](groupV1, &p, "/v1", http.MethodGet, "/bucket", "List buckets", "V1BucketsGET", v1Handler.V1BucketsGET); err != nil {
log.Fatal(err)
}
if err := pave.EchoRegister[
apirequest.V1BucketGET,
apiresponse.V1BucketGET](groupV1, &p, "/v1", http.MethodGet, "/bucket/:bucket", "Read bucket content", "V1BucketGET", v1Handler.V1BucketGET); err != nil {
log.Fatal(err)
}
if err := pave.EchoRegister[
apirequest.V1DocumentPOST,
apiresponse.V1DocumentPOST](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket", "Upload document to specified bucket", "V1DocumentPOST", v1Handler.V1DocumentPOST); err != nil {
log.Fatal(err)
}
groupV1.GET("/bucket/:bucket/:document", v1Handler.V1DocumentGET)
if err := pave.EchoRegister[
apirequest.V1DocumentDELETE,
apiresponse.V1DocumentDELETE](groupV1, &p, "/v1", http.MethodDelete, "/bucket/:bucket/:document", "Delete document in specified bucket", "V1DocumentDELETE", v1Handler.V1DocumentDELETE); err != nil {
log.Fatal(err)
}
// 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)
}