Victor Lacasse-Beaudoin
0738a851e6
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`
306 lines
8.8 KiB
Go
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)
|
|
}
|