Refactor HTTP Handlers and Improve Session Error Handling
- Enable SecureRoutes middleware to provide security headers for routes - Changed the behavior of session retrieval failure to redirect the user to the /admin/login page instead of throwing a 500 error. - Created an `htmxGroup` Echo group to bundle all `/htmx` related routes. - Moved all the htmx related routes under this new group and applied the `RequireLogin` middleware to this group. - Updated route paths, removing the '/htmx' prefix for readability and consistency.
- commit
- 5bd60c886eb4a15697007746dc66bb622340ea71
- author
- bnema · · 2023-10-02 16:29 UTC
- parent
- 438f23b
- signature
- unsigned
8 files · +119 -63
M internal/httpserve/handler/oauth.go +6 -3
| @@ -47,12 +47,15 @@ func RenderLoginPage(c echo.Context, a *app.App) error { | |||
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | 49 | func StartOAuthGithub(c echo.Context, a *app.App) error { | |
| 50 | + | // Clear the session in case it is already set or corrupted | |
| 51 | + | err := ResetSession(c) | |
| 52 | + | if err != nil { | |
| 53 | + | return err | |
| 54 | + | } | |
| 55 | + | //Initiate the Github OAuth flow | |
| 50 | 56 | clientID := os.Getenv("GITHUB_APP_ID") | |
| 51 | 57 | redirectDomain := app.GenerateOauthCallbackURL(a.Config) | |
| 52 | - | fmt.Print(redirectDomain) | |
| 53 | 58 | encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain)) | |
| 54 | - | fmt.Print("redirectDomain:", redirectDomain) | |
| 55 | - | fmt.Print("encoded state :", encodedState) | |
| 56 | 59 | // Redirect to Gordon's Proxy to grab the oauth access | |
| 57 | 60 | oauthURL := fmt.Sprintf( | |
| 58 | 61 | "https://gordon.bnema.dev/github-proxy/authorize?client_id=%s&redirect_uri=%s&state=%s", | |
| @@ -47,12 +47,15 @@ func RenderLoginPage(c echo.Context, a *app.App) error { | |||||
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | func StartOAuthGithub(c echo.Context, a *app.App) error { | 49 | func StartOAuthGithub(c echo.Context, a *app.App) error { | ||
| 50 | + | // Clear the session in case it is already set or corrupted | |||
| 51 | + | err := ResetSession(c) | |||
| 52 | + | if err != nil { | |||
| 53 | + | return err | |||
| 54 | + | } | |||
| 55 | + | //Initiate the Github OAuth flow | |||
| 50 | clientID := os.Getenv("GITHUB_APP_ID") | 56 | clientID := os.Getenv("GITHUB_APP_ID") | ||
| 51 | redirectDomain := app.GenerateOauthCallbackURL(a.Config) | 57 | redirectDomain := app.GenerateOauthCallbackURL(a.Config) | ||
| 52 | - | fmt.Print(redirectDomain) | |||
| 53 | encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain)) | 58 | encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain)) | ||
| 54 | - | fmt.Print("redirectDomain:", redirectDomain) | |||
| 55 | - | fmt.Print("encoded state :", encodedState) | |||
| 56 | // Redirect to Gordon's Proxy to grab the oauth access | 59 | // Redirect to Gordon's Proxy to grab the oauth access | ||
| 57 | oauthURL := fmt.Sprintf( | 60 | oauthURL := fmt.Sprintf( | ||
| 58 | "https://gordon.bnema.dev/github-proxy/authorize?client_id=%s&redirect_uri=%s&state=%s", | 61 | "https://gordon.bnema.dev/github-proxy/authorize?client_id=%s&redirect_uri=%s&state=%s", | ||
A internal/httpserve/handler/session.go +36 -0
| @@ -0,0 +1,36 @@ | |||
| 1 | + | package handler | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "log" | |
| 5 | + | "net/http" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | _ "github.com/joho/godotenv/autoload" | |
| 9 | + | "github.com/labstack/echo-contrib/session" | |
| 10 | + | "github.com/labstack/echo/v4" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // ResetSession resets the session cookie and session data | |
| 14 | + | func ResetSession(c echo.Context) error { | |
| 15 | + | // Manually expire the cookie | |
| 16 | + | cookie := new(http.Cookie) | |
| 17 | + | cookie.Name = "session" | |
| 18 | + | cookie.Value = "" | |
| 19 | + | cookie.Path = "/" | |
| 20 | + | cookie.Expires = time.Unix(0, 0) | |
| 21 | + | cookie.MaxAge = -1 // Clear immediately | |
| 22 | + | c.SetCookie(cookie) | |
| 23 | + | // Invalidate the existing session | |
| 24 | + | sess, err := session.Get("session", c) | |
| 25 | + | if err != nil { | |
| 26 | + | log.Println(err) | |
| 27 | + | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | |
| 28 | + | } | |
| 29 | + | sess.Options.MaxAge = -1 | |
| 30 | + | if err := sess.Save(c.Request(), c.Response()); err != nil { | |
| 31 | + | log.Println("Could not delete session:", err) | |
| 32 | + | return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete session") | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | return nil | |
| 36 | + | } | |
| @@ -0,0 +1,36 @@ | |||||
| 1 | + | package handler | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "log" | |||
| 5 | + | "net/http" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | _ "github.com/joho/godotenv/autoload" | |||
| 9 | + | "github.com/labstack/echo-contrib/session" | |||
| 10 | + | "github.com/labstack/echo/v4" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // ResetSession resets the session cookie and session data | |||
| 14 | + | func ResetSession(c echo.Context) error { | |||
| 15 | + | // Manually expire the cookie | |||
| 16 | + | cookie := new(http.Cookie) | |||
| 17 | + | cookie.Name = "session" | |||
| 18 | + | cookie.Value = "" | |||
| 19 | + | cookie.Path = "/" | |||
| 20 | + | cookie.Expires = time.Unix(0, 0) | |||
| 21 | + | cookie.MaxAge = -1 // Clear immediately | |||
| 22 | + | c.SetCookie(cookie) | |||
| 23 | + | // Invalidate the existing session | |||
| 24 | + | sess, err := session.Get("session", c) | |||
| 25 | + | if err != nil { | |||
| 26 | + | log.Println(err) | |||
| 27 | + | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | |||
| 28 | + | } | |||
| 29 | + | sess.Options.MaxAge = -1 | |||
| 30 | + | if err := sess.Save(c.Request(), c.Response()); err != nil { | |||
| 31 | + | log.Println("Could not delete session:", err) | |||
| 32 | + | return echo.NewHTTPError(http.StatusInternalServerError, "Could not delete session") | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | return nil | |||
| 36 | + | } | |||
M internal/httpserve/middleware/colorschemedetect.go +1 -1
| @@ -5,7 +5,7 @@ import ( | |||
| 5 | 5 | "github.com/labstack/echo/v4" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | - | // ColorSchemeDetection detects the current color scheme and sets it in the context | |
| 8 | + | // ColorSchemeDetection detects the current color scheme and sets it in the context based on the "Sec-CH-Prefers-Color-Scheme" header | |
| 9 | 9 | func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc { | |
| 10 | 10 | ColorSchemeKey := "CurrentColorScheme" | |
| 11 | 11 | return func(c echo.Context) error { | |
| @@ -5,7 +5,7 @@ import ( | |||||
| 5 | "github.com/labstack/echo/v4" | 5 | "github.com/labstack/echo/v4" | ||
| 6 | ) | 6 | ) | ||
| 7 | 7 | ||||
| 8 | - | // ColorSchemeDetection detects the current color scheme and sets it in the context | 8 | + | // ColorSchemeDetection detects the current color scheme and sets it in the context based on the "Sec-CH-Prefers-Color-Scheme" header |
| 9 | func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc { | 9 | func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc { | ||
| 10 | ColorSchemeKey := "CurrentColorScheme" | 10 | ColorSchemeKey := "CurrentColorScheme" | ||
| 11 | return func(c echo.Context) error { | 11 | return func(c echo.Context) error { | ||
M internal/httpserve/middleware/commondata.go +1 -1
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | func SetCommonDataMiddleware(a *app.App) echo.MiddlewareFunc { | |
| 9 | 9 | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 10 | 10 | return func(c echo.Context) error { | |
| 11 | - | // Add more common data if needed | |
| 11 | + | // Will pass data to the renderer | |
| 12 | 12 | return next(c) | |
| 13 | 13 | } | |
| 14 | 14 | } | |
| @@ -8,7 +8,7 @@ import ( | |||||
| 8 | func SetCommonDataMiddleware(a *app.App) echo.MiddlewareFunc { | 8 | func SetCommonDataMiddleware(a *app.App) echo.MiddlewareFunc { | ||
| 9 | return func(next echo.HandlerFunc) echo.HandlerFunc { | 9 | return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
| 10 | return func(c echo.Context) error { | 10 | return func(c echo.Context) error { | ||
| 11 | - | // Add more common data if needed | 11 | + | // Will pass data to the renderer |
| 12 | return next(c) | 12 | return next(c) | ||
| 13 | } | 13 | } | ||
| 14 | } | 14 | } | ||
M internal/httpserve/middleware/requirelogin.go +5 -1
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "log" | |
| 4 | 5 | "net/http" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/labstack/echo-contrib/session" | |
| @@ -11,7 +12,10 @@ func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 11 | 12 | return func(c echo.Context) error { | |
| 12 | 13 | sess, err := session.Get("session", c) | |
| 13 | 14 | if err != nil { | |
| 14 | - | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | |
| 15 | + | ||
| 16 | + | log.Println(err) | |
| 17 | + | ||
| 18 | + | return c.Redirect(http.StatusSeeOther, "/admin/login") | |
| 15 | 19 | } | |
| 16 | 20 | ||
| 17 | 21 | // Check if the user is authenticated | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package middleware | 1 | package middleware | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "log" | |||
| 4 | "net/http" | 5 | "net/http" | ||
| 5 | 6 | ||||
| 6 | "github.com/labstack/echo-contrib/session" | 7 | "github.com/labstack/echo-contrib/session" | ||
| @@ -11,7 +12,10 @@ func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc { | |||||
| 11 | return func(c echo.Context) error { | 12 | return func(c echo.Context) error { | ||
| 12 | sess, err := session.Get("session", c) | 13 | sess, err := session.Get("session", c) | ||
| 13 | if err != nil { | 14 | if err != nil { | ||
| 14 | - | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | 15 | + | |
| 16 | + | log.Println(err) | |||
| 17 | + | ||||
| 18 | + | return c.Redirect(http.StatusSeeOther, "/admin/login") | |||
| 15 | } | 19 | } | ||
| 16 | 20 | ||||
| 17 | // Check if the user is authenticated | 21 | // Check if the user is authenticated | ||
M internal/httpserve/middleware/secure.go +20 -1
| @@ -1,6 +1,12 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "net/http" | |
| 5 | + | "os" | |
| 6 | + | ||
| 7 | + | "github.com/gorilla/sessions" | |
| 8 | + | _ "github.com/joho/godotenv/autoload" | |
| 9 | + | "github.com/labstack/echo-contrib/session" | |
| 4 | 10 | "github.com/labstack/echo/v4" | |
| 5 | 11 | "github.com/labstack/echo/v4/middleware" | |
| 6 | 12 | ) | |
| @@ -12,6 +18,19 @@ func SecureRoutes() echo.MiddlewareFunc { | |||
| 12 | 18 | XFrameOptions: "SAMEORIGIN", | |
| 13 | 19 | HSTSMaxAge: 3600, | |
| 14 | 20 | HSTSExcludeSubdomains: false, | |
| 15 | - | ContentSecurityPolicy: "default-src 'self'", | |
| 21 | + | ContentSecurityPolicy: "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-inline'", | |
| 16 | 22 | }) | |
| 17 | 23 | } | |
| 24 | + | ||
| 25 | + | // InitSessionMiddleware initializes the session middleware with secure options | |
| 26 | + | func InitSessionMiddleware() echo.MiddlewareFunc { | |
| 27 | + | store := sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET"))) | |
| 28 | + | store.Options = &sessions.Options{ | |
| 29 | + | Path: "/", | |
| 30 | + | HttpOnly: true, | |
| 31 | + | Secure: true, | |
| 32 | + | SameSite: http.SameSiteStrictMode, | |
| 33 | + | MaxAge: 86400 * 7, | |
| 34 | + | } | |
| 35 | + | return session.Middleware(store) | |
| 36 | + | } | |
| @@ -1,6 +1,12 @@ | |||||
| 1 | package middleware | 1 | package middleware | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "net/http" | |||
| 5 | + | "os" | |||
| 6 | + | ||||
| 7 | + | "github.com/gorilla/sessions" | |||
| 8 | + | _ "github.com/joho/godotenv/autoload" | |||
| 9 | + | "github.com/labstack/echo-contrib/session" | |||
| 4 | "github.com/labstack/echo/v4" | 10 | "github.com/labstack/echo/v4" | ||
| 5 | "github.com/labstack/echo/v4/middleware" | 11 | "github.com/labstack/echo/v4/middleware" | ||
| 6 | ) | 12 | ) | ||
| @@ -12,6 +18,19 @@ func SecureRoutes() echo.MiddlewareFunc { | |||||
| 12 | XFrameOptions: "SAMEORIGIN", | 18 | XFrameOptions: "SAMEORIGIN", | ||
| 13 | HSTSMaxAge: 3600, | 19 | HSTSMaxAge: 3600, | ||
| 14 | HSTSExcludeSubdomains: false, | 20 | HSTSExcludeSubdomains: false, | ||
| 15 | - | ContentSecurityPolicy: "default-src 'self'", | 21 | + | ContentSecurityPolicy: "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-inline'", |
| 16 | }) | 22 | }) | ||
| 17 | } | 23 | } | ||
| 24 | + | ||||
| 25 | + | // InitSessionMiddleware initializes the session middleware with secure options | |||
| 26 | + | func InitSessionMiddleware() echo.MiddlewareFunc { | |||
| 27 | + | store := sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET"))) | |||
| 28 | + | store.Options = &sessions.Options{ | |||
| 29 | + | Path: "/", | |||
| 30 | + | HttpOnly: true, | |||
| 31 | + | Secure: true, | |||
| 32 | + | SameSite: http.SameSiteStrictMode, | |||
| 33 | + | MaxAge: 86400 * 7, | |||
| 34 | + | } | |||
| 35 | + | return session.Middleware(store) | |||
| 36 | + | } | |||
M internal/httpserve/router.go +28 -19
| @@ -1,28 +1,31 @@ | |||
| 1 | 1 | package httpserve | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "os" | |
| 5 | - | ||
| 6 | 4 | "github.com/bnema/gordon/internal/app" | |
| 7 | 5 | "github.com/bnema/gordon/internal/httpserve/handler" | |
| 8 | 6 | "github.com/bnema/gordon/internal/httpserve/middleware" | |
| 9 | - | "github.com/gorilla/sessions" | |
| 10 | 7 | _ "github.com/joho/godotenv/autoload" | |
| 11 | - | "github.com/labstack/echo-contrib/session" | |
| 12 | 8 | "github.com/labstack/echo/v4" | |
| 13 | 9 | ) | |
| 14 | 10 | ||
| 15 | 11 | // RegisterRoutes registers all routes for the application | |
| 16 | 12 | func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo { | |
| 17 | 13 | AdminPath := a.Config.Admin.Path | |
| 14 | + | // Use middlewares | |
| 15 | + | e.Use(middleware.SecureRoutes()) | |
| 16 | + | // SetCommonDataMiddleware will pass data to the renderer | |
| 18 | 17 | e.Use(middleware.SetCommonDataMiddleware(a)) | |
| 18 | + | // Error handler middleware | |
| 19 | 19 | e.Use(middleware.ErrorHandler) | |
| 20 | - | // Add session middleware | |
| 21 | - | e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET"))))) | |
| 20 | + | // Initiate the session middleware | |
| 21 | + | e.Use(middleware.InitSessionMiddleware()) | |
| 22 | + | ||
| 23 | + | // Color scheme detection for dark/light mode | |
| 22 | 24 | e.Use(middleware.ColorSchemeDetection) | |
| 25 | + | // Language detection | |
| 23 | 26 | e.Use(middleware.LanguageDetection) | |
| 24 | 27 | ||
| 25 | - | // Register routes | |
| 28 | + | // --- Register routes --- // | |
| 26 | 29 | bindAdminRoute(e, a, AdminPath) | |
| 27 | 30 | bindStaticRoute(e, a, "/*") | |
| 28 | 31 | bindLoginRoute(e, a, AdminPath) | |
| @@ -67,54 +70,60 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) { | |||
| 67 | 70 | } | |
| 68 | 71 | ||
| 69 | 72 | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |
| 73 | + | // Create a group for /htmx endpoints | |
| 74 | + | htmxGroup := e.Group("/htmx") | |
| 75 | + | ||
| 76 | + | // Apply middleware to the group, so all endpoints are protected | |
| 77 | + | htmxGroup.Use(middleware.RequireLogin) | |
| 78 | + | ||
| 70 | 79 | // List all images component | |
| 71 | - | e.GET("/htmx/image-manager", func(c echo.Context) error { | |
| 80 | + | htmxGroup.GET("/image-manager", func(c echo.Context) error { | |
| 72 | 81 | return handler.ImageManagerComponent(c, a) | |
| 73 | 82 | }) | |
| 74 | 83 | // Delete an image | |
| 75 | - | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { | |
| 84 | + | htmxGroup.DELETE("/image-manager/delete/:ID", func(c echo.Context) error { | |
| 76 | 85 | return handler.ImageManagerDelete(c, a) | |
| 77 | 86 | }) | |
| 78 | 87 | ||
| 79 | 88 | // List all containers | |
| 80 | - | e.GET("/htmx/container-manager", func(c echo.Context) error { | |
| 89 | + | htmxGroup.GET("/container-manager", func(c echo.Context) error { | |
| 81 | 90 | return handler.ContainerManagerComponent(c, a) | |
| 82 | 91 | }) | |
| 83 | 92 | // Stop a container | |
| 84 | - | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { | |
| 93 | + | htmxGroup.POST("/container-manager/stop/:ID", func(c echo.Context) error { | |
| 85 | 94 | return handler.ContainerManagerStop(c, a) | |
| 86 | 95 | }) | |
| 87 | 96 | // Delete a container | |
| 88 | - | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | |
| 97 | + | htmxGroup.DELETE("/container-manager/delete/:ID", func(c echo.Context) error { | |
| 89 | 98 | return handler.ContainerManagerDelete(c, a) | |
| 90 | 99 | }) | |
| 91 | 100 | // Start a container | |
| 92 | - | e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error { | |
| 101 | + | htmxGroup.POST("/container-manager/start/:ID", func(c echo.Context) error { | |
| 93 | 102 | return handler.ContainerManagerStart(c, a) | |
| 94 | 103 | }) | |
| 95 | 104 | // Edit a container view | |
| 96 | - | e.GET("/htmx/container-manager/edit/:ID", func(c echo.Context) error { | |
| 105 | + | htmxGroup.GET("/container-manager/edit/:ID", func(c echo.Context) error { | |
| 97 | 106 | return handler.ContainerManagerEditGET(c, a) | |
| 98 | 107 | }) | |
| 99 | 108 | // Edit a container action | |
| 100 | - | e.POST("/htmx/container-manager/edit/:ID", func(c echo.Context) error { | |
| 109 | + | htmxGroup.POST("/container-manager/edit/:ID", func(c echo.Context) error { | |
| 101 | 110 | return handler.ContainerManagerEditPOST(c, a) | |
| 102 | 111 | }) | |
| 103 | 112 | ||
| 104 | 113 | // Display upload-image component | |
| 105 | - | e.GET("/htmx/upload-image", func(c echo.Context) error { | |
| 114 | + | htmxGroup.GET("/upload-image", func(c echo.Context) error { | |
| 106 | 115 | return handler.UploadImageGETHandler(c, a) | |
| 107 | 116 | }) | |
| 108 | 117 | // Upload image | |
| 109 | - | e.POST("/htmx/upload-image", func(c echo.Context) error { | |
| 118 | + | htmxGroup.POST("/upload-image", func(c echo.Context) error { | |
| 110 | 119 | return handler.UploadImagePOSTHandler(c, a) | |
| 111 | 120 | }) | |
| 112 | 121 | // Display create-container component | |
| 113 | - | e.GET("htmx/create-container/:ID", func(c echo.Context) error { | |
| 122 | + | htmxGroup.GET("/create-container/:ID", func(c echo.Context) error { | |
| 114 | 123 | return handler.CreateContainerGET(c, a) | |
| 115 | 124 | }) | |
| 116 | 125 | // Create container | |
| 117 | - | e.POST("htmx/create-container/:ID", func(c echo.Context) error { | |
| 126 | + | htmxGroup.POST("/create-container/:ID", func(c echo.Context) error { | |
| 118 | 127 | return handler.CreateContainerPOST(c, a) | |
| 119 | 128 | }) | |
| 120 | 129 | } | |
| @@ -1,28 +1,31 @@ | |||||
| 1 | package httpserve | 1 | package httpserve | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "os" | |||
| 5 | - | ||||
| 6 | "github.com/bnema/gordon/internal/app" | 4 | "github.com/bnema/gordon/internal/app" | ||
| 7 | "github.com/bnema/gordon/internal/httpserve/handler" | 5 | "github.com/bnema/gordon/internal/httpserve/handler" | ||
| 8 | "github.com/bnema/gordon/internal/httpserve/middleware" | 6 | "github.com/bnema/gordon/internal/httpserve/middleware" | ||
| 9 | - | "github.com/gorilla/sessions" | |||
| 10 | _ "github.com/joho/godotenv/autoload" | 7 | _ "github.com/joho/godotenv/autoload" | ||
| 11 | - | "github.com/labstack/echo-contrib/session" | |||
| 12 | "github.com/labstack/echo/v4" | 8 | "github.com/labstack/echo/v4" | ||
| 13 | ) | 9 | ) | ||
| 14 | 10 | ||||
| 15 | // RegisterRoutes registers all routes for the application | 11 | // RegisterRoutes registers all routes for the application | ||
| 16 | func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo { | 12 | func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo { | ||
| 17 | AdminPath := a.Config.Admin.Path | 13 | AdminPath := a.Config.Admin.Path | ||
| 14 | + | // Use middlewares | |||
| 15 | + | e.Use(middleware.SecureRoutes()) | |||
| 16 | + | // SetCommonDataMiddleware will pass data to the renderer | |||
| 18 | e.Use(middleware.SetCommonDataMiddleware(a)) | 17 | e.Use(middleware.SetCommonDataMiddleware(a)) | ||
| 18 | + | // Error handler middleware | |||
| 19 | e.Use(middleware.ErrorHandler) | 19 | e.Use(middleware.ErrorHandler) | ||
| 20 | - | // Add session middleware | 20 | + | // Initiate the session middleware |
| 21 | - | e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET"))))) | 21 | + | e.Use(middleware.InitSessionMiddleware()) |
| 22 | + | ||||
| 23 | + | // Color scheme detection for dark/light mode | |||
| 22 | e.Use(middleware.ColorSchemeDetection) | 24 | e.Use(middleware.ColorSchemeDetection) | ||
| 25 | + | // Language detection | |||
| 23 | e.Use(middleware.LanguageDetection) | 26 | e.Use(middleware.LanguageDetection) | ||
| 24 | 27 | ||||
| 25 | - | // Register routes | 28 | + | // --- Register routes --- // |
| 26 | bindAdminRoute(e, a, AdminPath) | 29 | bindAdminRoute(e, a, AdminPath) | ||
| 27 | bindStaticRoute(e, a, "/*") | 30 | bindStaticRoute(e, a, "/*") | ||
| 28 | bindLoginRoute(e, a, AdminPath) | 31 | bindLoginRoute(e, a, AdminPath) | ||
| @@ -67,54 +70,60 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) { | |||||
| 67 | } | 70 | } | ||
| 68 | 71 | ||||
| 69 | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | 72 | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | ||
| 73 | + | // Create a group for /htmx endpoints | |||
| 74 | + | htmxGroup := e.Group("/htmx") | |||
| 75 | + | ||||
| 76 | + | // Apply middleware to the group, so all endpoints are protected | |||
| 77 | + | htmxGroup.Use(middleware.RequireLogin) | |||
| 78 | + | ||||
| 70 | // List all images component | 79 | // List all images component | ||
| 71 | - | e.GET("/htmx/image-manager", func(c echo.Context) error { | 80 | + | htmxGroup.GET("/image-manager", func(c echo.Context) error { |
| 72 | return handler.ImageManagerComponent(c, a) | 81 | return handler.ImageManagerComponent(c, a) | ||
| 73 | }) | 82 | }) | ||
| 74 | // Delete an image | 83 | // Delete an image | ||
| 75 | - | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { | 84 | + | htmxGroup.DELETE("/image-manager/delete/:ID", func(c echo.Context) error { |
| 76 | return handler.ImageManagerDelete(c, a) | 85 | return handler.ImageManagerDelete(c, a) | ||
| 77 | }) | 86 | }) | ||
| 78 | 87 | ||||
| 79 | // List all containers | 88 | // List all containers | ||
| 80 | - | e.GET("/htmx/container-manager", func(c echo.Context) error { | 89 | + | htmxGroup.GET("/container-manager", func(c echo.Context) error { |
| 81 | return handler.ContainerManagerComponent(c, a) | 90 | return handler.ContainerManagerComponent(c, a) | ||
| 82 | }) | 91 | }) | ||
| 83 | // Stop a container | 92 | // Stop a container | ||
| 84 | - | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { | 93 | + | htmxGroup.POST("/container-manager/stop/:ID", func(c echo.Context) error { |
| 85 | return handler.ContainerManagerStop(c, a) | 94 | return handler.ContainerManagerStop(c, a) | ||
| 86 | }) | 95 | }) | ||
| 87 | // Delete a container | 96 | // Delete a container | ||
| 88 | - | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | 97 | + | htmxGroup.DELETE("/container-manager/delete/:ID", func(c echo.Context) error { |
| 89 | return handler.ContainerManagerDelete(c, a) | 98 | return handler.ContainerManagerDelete(c, a) | ||
| 90 | }) | 99 | }) | ||
| 91 | // Start a container | 100 | // Start a container | ||
| 92 | - | e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error { | 101 | + | htmxGroup.POST("/container-manager/start/:ID", func(c echo.Context) error { |
| 93 | return handler.ContainerManagerStart(c, a) | 102 | return handler.ContainerManagerStart(c, a) | ||
| 94 | }) | 103 | }) | ||
| 95 | // Edit a container view | 104 | // Edit a container view | ||
| 96 | - | e.GET("/htmx/container-manager/edit/:ID", func(c echo.Context) error { | 105 | + | htmxGroup.GET("/container-manager/edit/:ID", func(c echo.Context) error { |
| 97 | return handler.ContainerManagerEditGET(c, a) | 106 | return handler.ContainerManagerEditGET(c, a) | ||
| 98 | }) | 107 | }) | ||
| 99 | // Edit a container action | 108 | // Edit a container action | ||
| 100 | - | e.POST("/htmx/container-manager/edit/:ID", func(c echo.Context) error { | 109 | + | htmxGroup.POST("/container-manager/edit/:ID", func(c echo.Context) error { |
| 101 | return handler.ContainerManagerEditPOST(c, a) | 110 | return handler.ContainerManagerEditPOST(c, a) | ||
| 102 | }) | 111 | }) | ||
| 103 | 112 | ||||
| 104 | // Display upload-image component | 113 | // Display upload-image component | ||
| 105 | - | e.GET("/htmx/upload-image", func(c echo.Context) error { | 114 | + | htmxGroup.GET("/upload-image", func(c echo.Context) error { |
| 106 | return handler.UploadImageGETHandler(c, a) | 115 | return handler.UploadImageGETHandler(c, a) | ||
| 107 | }) | 116 | }) | ||
| 108 | // Upload image | 117 | // Upload image | ||
| 109 | - | e.POST("/htmx/upload-image", func(c echo.Context) error { | 118 | + | htmxGroup.POST("/upload-image", func(c echo.Context) error { |
| 110 | return handler.UploadImagePOSTHandler(c, a) | 119 | return handler.UploadImagePOSTHandler(c, a) | ||
| 111 | }) | 120 | }) | ||
| 112 | // Display create-container component | 121 | // Display create-container component | ||
| 113 | - | e.GET("htmx/create-container/:ID", func(c echo.Context) error { | 122 | + | htmxGroup.GET("/create-container/:ID", func(c echo.Context) error { |
| 114 | return handler.CreateContainerGET(c, a) | 123 | return handler.CreateContainerGET(c, a) | ||
| 115 | }) | 124 | }) | ||
| 116 | // Create container | 125 | // Create container | ||
| 117 | - | e.POST("htmx/create-container/:ID", func(c echo.Context) error { | 126 | + | htmxGroup.POST("/create-container/:ID", func(c echo.Context) error { |
| 118 | return handler.CreateContainerPOST(c, a) | 127 | return handler.CreateContainerPOST(c, a) | ||
| 119 | }) | 128 | }) | ||
| 120 | } | 129 | } | ||
M internal/webui/public/assets/css/tailwind.css +22 -37
| @@ -644,27 +644,10 @@ video { | |||
| 644 | 644 | height: 100%; | |
| 645 | 645 | } | |
| 646 | 646 | ||
| 647 | - | .h-fit { | |
| 648 | - | height: -moz-fit-content; | |
| 649 | - | height: fit-content; | |
| 650 | - | } | |
| 651 | - | ||
| 652 | 647 | .w-1\/2 { | |
| 653 | 648 | width: 50%; | |
| 654 | 649 | } | |
| 655 | 650 | ||
| 656 | - | .w-2\/3 { | |
| 657 | - | width: 66.666667%; | |
| 658 | - | } | |
| 659 | - | ||
| 660 | - | .w-72 { | |
| 661 | - | width: 18rem; | |
| 662 | - | } | |
| 663 | - | ||
| 664 | - | .w-full { | |
| 665 | - | width: 100%; | |
| 666 | - | } | |
| 667 | - | ||
| 668 | 651 | .w-1\/3 { | |
| 669 | 652 | width: 33.333333%; | |
| 670 | 653 | } | |
| @@ -673,10 +656,22 @@ video { | |||
| 673 | 656 | width: 25%; | |
| 674 | 657 | } | |
| 675 | 658 | ||
| 659 | + | .w-2\/3 { | |
| 660 | + | width: 66.666667%; | |
| 661 | + | } | |
| 662 | + | ||
| 676 | 663 | .w-5\/12 { | |
| 677 | 664 | width: 41.666667%; | |
| 678 | 665 | } | |
| 679 | 666 | ||
| 667 | + | .w-72 { | |
| 668 | + | width: 18rem; | |
| 669 | + | } | |
| 670 | + | ||
| 671 | + | .w-full { | |
| 672 | + | width: 100%; | |
| 673 | + | } | |
| 674 | + | ||
| 680 | 675 | .min-w-fit { | |
| 681 | 676 | min-width: -moz-fit-content; | |
| 682 | 677 | min-width: fit-content; | |
| @@ -793,6 +788,11 @@ video { | |||
| 793 | 788 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | |
| 794 | 789 | } | |
| 795 | 790 | ||
| 791 | + | .bg-gordonmascot-mblue { | |
| 792 | + | --tw-bg-opacity: 1; | |
| 793 | + | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |
| 794 | + | } | |
| 795 | + | ||
| 796 | 796 | .bg-gray-100 { | |
| 797 | 797 | --tw-bg-opacity: 1; | |
| 798 | 798 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | |
| @@ -827,11 +827,6 @@ video { | |||
| 827 | 827 | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | |
| 828 | 828 | } | |
| 829 | 829 | ||
| 830 | - | .bg-gordonmascot-mblue { | |
| 831 | - | --tw-bg-opacity: 1; | |
| 832 | - | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |
| 833 | - | } | |
| 834 | - | ||
| 835 | 830 | .p-0 { | |
| 836 | 831 | padding: 0px; | |
| 837 | 832 | } | |
| @@ -864,11 +859,6 @@ video { | |||
| 864 | 859 | padding-bottom: 0.5rem; | |
| 865 | 860 | } | |
| 866 | 861 | ||
| 867 | - | .py-4 { | |
| 868 | - | padding-top: 1rem; | |
| 869 | - | padding-bottom: 1rem; | |
| 870 | - | } | |
| 871 | - | ||
| 872 | 862 | .pb-4 { | |
| 873 | 863 | padding-bottom: 1rem; | |
| 874 | 864 | } | |
| @@ -919,6 +909,11 @@ video { | |||
| 919 | 909 | letter-spacing: -0.05em; | |
| 920 | 910 | } | |
| 921 | 911 | ||
| 912 | + | .text-blue-600 { | |
| 913 | + | --tw-text-opacity: 1; | |
| 914 | + | color: rgb(37 99 235 / var(--tw-text-opacity)); | |
| 915 | + | } | |
| 916 | + | ||
| 922 | 917 | .text-blue-800 { | |
| 923 | 918 | --tw-text-opacity: 1; | |
| 924 | 919 | color: rgb(30 64 175 / var(--tw-text-opacity)); | |
| @@ -974,16 +969,6 @@ video { | |||
| 974 | 969 | color: rgb(255 255 255 / var(--tw-text-opacity)); | |
| 975 | 970 | } | |
| 976 | 971 | ||
| 977 | - | .text-blue-600 { | |
| 978 | - | --tw-text-opacity: 1; | |
| 979 | - | color: rgb(37 99 235 / var(--tw-text-opacity)); | |
| 980 | - | } | |
| 981 | - | ||
| 982 | - | .text-black { | |
| 983 | - | --tw-text-opacity: 1; | |
| 984 | - | color: rgb(0 0 0 / var(--tw-text-opacity)); | |
| 985 | - | } | |
| 986 | - | ||
| 987 | 972 | .underline { | |
| 988 | 973 | text-decoration-line: underline; | |
| 989 | 974 | } | |
| @@ -644,27 +644,10 @@ video { | |||||
| 644 | height: 100%; | 644 | height: 100%; | ||
| 645 | } | 645 | } | ||
| 646 | 646 | ||||
| 647 | - | .h-fit { | |||
| 648 | - | height: -moz-fit-content; | |||
| 649 | - | height: fit-content; | |||
| 650 | - | } | |||
| 651 | - | ||||
| 652 | .w-1\/2 { | 647 | .w-1\/2 { | ||
| 653 | width: 50%; | 648 | width: 50%; | ||
| 654 | } | 649 | } | ||
| 655 | 650 | ||||
| 656 | - | .w-2\/3 { | |||
| 657 | - | width: 66.666667%; | |||
| 658 | - | } | |||
| 659 | - | ||||
| 660 | - | .w-72 { | |||
| 661 | - | width: 18rem; | |||
| 662 | - | } | |||
| 663 | - | ||||
| 664 | - | .w-full { | |||
| 665 | - | width: 100%; | |||
| 666 | - | } | |||
| 667 | - | ||||
| 668 | .w-1\/3 { | 651 | .w-1\/3 { | ||
| 669 | width: 33.333333%; | 652 | width: 33.333333%; | ||
| 670 | } | 653 | } | ||
| @@ -673,10 +656,22 @@ video { | |||||
| 673 | width: 25%; | 656 | width: 25%; | ||
| 674 | } | 657 | } | ||
| 675 | 658 | ||||
| 659 | + | .w-2\/3 { | |||
| 660 | + | width: 66.666667%; | |||
| 661 | + | } | |||
| 662 | + | ||||
| 676 | .w-5\/12 { | 663 | .w-5\/12 { | ||
| 677 | width: 41.666667%; | 664 | width: 41.666667%; | ||
| 678 | } | 665 | } | ||
| 679 | 666 | ||||
| 667 | + | .w-72 { | |||
| 668 | + | width: 18rem; | |||
| 669 | + | } | |||
| 670 | + | ||||
| 671 | + | .w-full { | |||
| 672 | + | width: 100%; | |||
| 673 | + | } | |||
| 674 | + | ||||
| 680 | .min-w-fit { | 675 | .min-w-fit { | ||
| 681 | min-width: -moz-fit-content; | 676 | min-width: -moz-fit-content; | ||
| 682 | min-width: fit-content; | 677 | min-width: fit-content; | ||
| @@ -793,6 +788,11 @@ video { | |||||
| 793 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | 788 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | ||
| 794 | } | 789 | } | ||
| 795 | 790 | ||||
| 791 | + | .bg-gordonmascot-mblue { | |||
| 792 | + | --tw-bg-opacity: 1; | |||
| 793 | + | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |||
| 794 | + | } | |||
| 795 | + | ||||
| 796 | .bg-gray-100 { | 796 | .bg-gray-100 { | ||
| 797 | --tw-bg-opacity: 1; | 797 | --tw-bg-opacity: 1; | ||
| 798 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | 798 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | ||
| @@ -827,11 +827,6 @@ video { | |||||
| 827 | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | 827 | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | ||
| 828 | } | 828 | } | ||
| 829 | 829 | ||||
| 830 | - | .bg-gordonmascot-mblue { | |||
| 831 | - | --tw-bg-opacity: 1; | |||
| 832 | - | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |||
| 833 | - | } | |||
| 834 | - | ||||
| 835 | .p-0 { | 830 | .p-0 { | ||
| 836 | padding: 0px; | 831 | padding: 0px; | ||
| 837 | } | 832 | } | ||
| @@ -864,11 +859,6 @@ video { | |||||
| 864 | padding-bottom: 0.5rem; | 859 | padding-bottom: 0.5rem; | ||
| 865 | } | 860 | } | ||
| 866 | 861 | ||||
| 867 | - | .py-4 { | |||
| 868 | - | padding-top: 1rem; | |||
| 869 | - | padding-bottom: 1rem; | |||
| 870 | - | } | |||
| 871 | - | ||||
| 872 | .pb-4 { | 862 | .pb-4 { | ||
| 873 | padding-bottom: 1rem; | 863 | padding-bottom: 1rem; | ||
| 874 | } | 864 | } | ||
| @@ -919,6 +909,11 @@ video { | |||||
| 919 | letter-spacing: -0.05em; | 909 | letter-spacing: -0.05em; | ||
| 920 | } | 910 | } | ||
| 921 | 911 | ||||
| 912 | + | .text-blue-600 { | |||
| 913 | + | --tw-text-opacity: 1; | |||
| 914 | + | color: rgb(37 99 235 / var(--tw-text-opacity)); | |||
| 915 | + | } | |||
| 916 | + | ||||
| 922 | .text-blue-800 { | 917 | .text-blue-800 { | ||
| 923 | --tw-text-opacity: 1; | 918 | --tw-text-opacity: 1; | ||
| 924 | color: rgb(30 64 175 / var(--tw-text-opacity)); | 919 | color: rgb(30 64 175 / var(--tw-text-opacity)); | ||
| @@ -974,16 +969,6 @@ video { | |||||
| 974 | color: rgb(255 255 255 / var(--tw-text-opacity)); | 969 | color: rgb(255 255 255 / var(--tw-text-opacity)); | ||
| 975 | } | 970 | } | ||
| 976 | 971 | ||||
| 977 | - | .text-blue-600 { | |||
| 978 | - | --tw-text-opacity: 1; | |||
| 979 | - | color: rgb(37 99 235 / var(--tw-text-opacity)); | |||
| 980 | - | } | |||
| 981 | - | ||||
| 982 | - | .text-black { | |||
| 983 | - | --tw-text-opacity: 1; | |||
| 984 | - | color: rgb(0 0 0 / var(--tw-text-opacity)); | |||
| 985 | - | } | |||
| 986 | - | ||||
| 987 | .underline { | 972 | .underline { | ||
| 988 | text-decoration-line: underline; | 973 | text-decoration-line: underline; | ||
| 989 | } | 974 | } | ||