diff --git a/cmd.go b/cmd.go index dba7205..f00a722 100644 --- a/cmd.go +++ b/cmd.go @@ -4,24 +4,20 @@ Copyright © 2023-2024 AGECEM package main import ( - "crypto/subtle" "embed" "encoding/json" "fmt" "io" "log" - "net/http" "os" "strings" "text/template" - "codeberg.org/vlbeaudoin/pave/v2" "codeberg.org/vlbeaudoin/serpents" "git.agecem.com/agecem/agecem-org/public" "git.agecem.com/agecem/agecem-org/templates" "git.agecem.com/agecem/agecem-org/version" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -217,179 +213,6 @@ func init() { "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 := NewMediaClientFromViper() - if err != nil { - log.Fatal("Error during NewMediaClientFromViper for API handlers") - } - - p := pave.New() - - v1Handler := V1Handler{ - Config: cfg, - MediaClient: mediaClient, - Pave: &p, - } - - groupV1.GET("", v1Handler.ListRoutes) - - if err := pave.EchoRegister[ - ExecuteSeedRequest, - ExecuteSeedResponse](groupV1, &p, "/v1", http.MethodPost, "/seed", "Créer buckets manquants définis dans `server.documents.buckets`", "ExecuteSeed", v1Handler.ExecuteSeed); err != nil { - log.Fatal(err) - } - - if err := pave.EchoRegister[ - UpdateDocumentKeyRequest, - UpdateDocumentKeyResponse](groupV1, &p, "/v1", http.MethodPut, "/bucket/:bucket/:document/key", "Renommer un document", "UpdateDocumentKey", v1Handler.UpdateDocumentKey); err != nil { - - log.Fatal(err) - } - - if err := pave.EchoRegister[ - ReadSpecRequest, - ReadSpecResponse](groupV1, &p, "/v1", http.MethodGet, "/spec", DescriptionV1SpecGET, "SpecRead", v1Handler.ReadSpec); err != nil { - log.Fatal(err) - } - - if err := pave.EchoRegister[ - ListBucketsRequest, - ListBucketsResponse](groupV1, &p, "/v1", http.MethodGet, "/bucket", "List buckets", "ListBuckets", v1Handler.ListBuckets); err != nil { - log.Fatal(err) - } - - if err := pave.EchoRegister[ - ReadBucketRequest, - ReadBucketResponse](groupV1, &p, "/v1", http.MethodGet, "/bucket/:bucket", "Read bucket content", "ReadBucket", v1Handler.ReadBucket); err != nil { - log.Fatal(err) - } - - if err := pave.EchoRegister[ - CreateDocumentsRequest, - CreateDocumentsResponse](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket/many", "Upload documents to specified bucket", "CreateDocuments", v1Handler.CreateDocuments); err != nil { - log.Fatal(err) - } - - if err := pave.EchoRegister[ - CreateDocumentRequest, - CreateDocumentResponse](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket", "Upload document to specified bucket", "CreateDocument", v1Handler.CreateDocument); err != nil { - log.Fatal(err) - } - - // Do not move to pave, uses echo.Stream instead of echo.JSON - groupV1.GET("/bucket/:bucket/:document", v1Handler.ReadDocument) - - if err := pave.EchoRegister[ - DeleteDocumentRequest, - DeleteDocumentResponse](groupV1, &p, "/v1", http.MethodDelete, "/bucket/:bucket/:document", "Delete document in specified bucket", "DeleteDocument", v1Handler.DeleteDocument); err != nil { - log.Fatal(err) - } - - // HTML Routes - client := http.DefaultClient - defer client.CloseIdleConnections() - - apiClient, err := NewAPIClientFromViper(client) - if err != nil { - log.Fatal(err) - } - - webHandler := WebHandler{ - ApiClient: apiClient, - } - - e.GET("/", HandleIndex) - - //e.GET("/a-propos", HandleAPropos) - - //e.GET("/actualite", HandleActualite) - - //e.GET("/actualite/:article", HandleActualiteArticle) - - e.GET("/vie-etudiante", HandleVieEtudiante) - - e.GET("/vie-etudiante/:organisme", HandleVieEtudianteOrganisme) - - e.GET("/documentation", webHandler.HandleDocumentation) - - e.GET("/formulaires", HandleFormulaires) - - // Public Routes - - e.GET("/public/documentation/:bucket/:document", webHandler.HandlePublicDocumentation) - - // Admin Routes - - groupAdmin.GET("", 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) } diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..af5d808 --- /dev/null +++ b/routes.go @@ -0,0 +1,186 @@ +package main + +import ( + "crypto/subtle" + "fmt" + "log" + "net/http" + "text/template" + + "codeberg.org/vlbeaudoin/pave/v2" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +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 := NewMediaClientFromViper() + if err != nil { + log.Fatal("Error during NewMediaClientFromViper for API handlers") + } + + p := pave.New() + + v1Handler := V1Handler{ + Config: cfg, + MediaClient: mediaClient, + Pave: &p, + } + + groupV1.GET("", v1Handler.ListRoutes) + + if err := pave.EchoRegister[ + ExecuteSeedRequest, + ExecuteSeedResponse](groupV1, &p, "/v1", http.MethodPost, "/seed", "Créer buckets manquants définis dans `server.documents.buckets`", "ExecuteSeed", v1Handler.ExecuteSeed); err != nil { + log.Fatal(err) + } + + if err := pave.EchoRegister[ + UpdateDocumentKeyRequest, + UpdateDocumentKeyResponse](groupV1, &p, "/v1", http.MethodPut, "/bucket/:bucket/:document/key", "Renommer un document", "UpdateDocumentKey", v1Handler.UpdateDocumentKey); err != nil { + + log.Fatal(err) + } + + if err := pave.EchoRegister[ + ReadSpecRequest, + ReadSpecResponse](groupV1, &p, "/v1", http.MethodGet, "/spec", DescriptionV1SpecGET, "SpecRead", v1Handler.ReadSpec); err != nil { + log.Fatal(err) + } + + if err := pave.EchoRegister[ + ListBucketsRequest, + ListBucketsResponse](groupV1, &p, "/v1", http.MethodGet, "/bucket", "List buckets", "ListBuckets", v1Handler.ListBuckets); err != nil { + log.Fatal(err) + } + + if err := pave.EchoRegister[ + ReadBucketRequest, + ReadBucketResponse](groupV1, &p, "/v1", http.MethodGet, "/bucket/:bucket", "Read bucket content", "ReadBucket", v1Handler.ReadBucket); err != nil { + log.Fatal(err) + } + + if err := pave.EchoRegister[ + CreateDocumentsRequest, + CreateDocumentsResponse](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket/many", "Upload documents to specified bucket", "CreateDocuments", v1Handler.CreateDocuments); err != nil { + log.Fatal(err) + } + + if err := pave.EchoRegister[ + CreateDocumentRequest, + CreateDocumentResponse](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket", "Upload document to specified bucket", "CreateDocument", v1Handler.CreateDocument); err != nil { + log.Fatal(err) + } + + // Do not move to pave, uses echo.Stream instead of echo.JSON + groupV1.GET("/bucket/:bucket/:document", v1Handler.ReadDocument) + + if err := pave.EchoRegister[ + DeleteDocumentRequest, + DeleteDocumentResponse](groupV1, &p, "/v1", http.MethodDelete, "/bucket/:bucket/:document", "Delete document in specified bucket", "DeleteDocument", v1Handler.DeleteDocument); err != nil { + log.Fatal(err) + } + + // HTML Routes + client := http.DefaultClient + defer client.CloseIdleConnections() + + apiClient, err := NewAPIClientFromViper(client) + if err != nil { + log.Fatal(err) + } + + webHandler := WebHandler{ + ApiClient: apiClient, + } + + e.GET("/", HandleIndex) + + //e.GET("/a-propos", HandleAPropos) + + //e.GET("/actualite", HandleActualite) + + //e.GET("/actualite/:article", HandleActualiteArticle) + + e.GET("/vie-etudiante", HandleVieEtudiante) + + e.GET("/vie-etudiante/:organisme", HandleVieEtudianteOrganisme) + + e.GET("/documentation", webHandler.HandleDocumentation) + + e.GET("/formulaires", HandleFormulaires) + + // Public Routes + + e.GET("/public/documentation/:bucket/:document", webHandler.HandlePublicDocumentation) + + // Admin Routes + + groupAdmin.GET("", HandleAdmin) + + groupAdmin.GET("/documents/upload", webHandler.HandleAdminDocumentsUpload) + + groupAdmin.POST("/documents/upload", webHandler.HandleAdminDocumentsUploadPOST) + + e.Logger.Fatal(e.Start( + fmt.Sprintf(":%d", cfg.Server.Port))) +}