detached 5bd60c8

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 {
4747 }
4848
4949 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
5056 clientID := os.Getenv("GITHUB_APP_ID")
5157 redirectDomain := app.GenerateOauthCallbackURL(a.Config)
52- fmt.Print(redirectDomain)
5358 encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
54- fmt.Print("redirectDomain:", redirectDomain)
55- fmt.Print("encoded state :", encodedState)
5659 // Redirect to Gordon's Proxy to grab the oauth access
5760 oauthURL := fmt.Sprintf(
5861 "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 access59 // 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 (
55 "github.com/labstack/echo/v4"
66 )
77
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
99 func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc {
1010 ColorSchemeKey := "CurrentColorScheme"
1111 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 context8+// 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 (
88 func SetCommonDataMiddleware(a *app.App) echo.MiddlewareFunc {
99 return func(next echo.HandlerFunc) echo.HandlerFunc {
1010 return func(c echo.Context) error {
11- // Add more common data if needed
11+ // Will pass data to the renderer
1212 return next(c)
1313 }
1414 }
@@ -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 needed11+ // 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 @@
11 package middleware
22
33 import (
4+ "log"
45 "net/http"
56
67 "github.com/labstack/echo-contrib/session"
@@ -11,7 +12,10 @@ func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
1112 return func(c echo.Context) error {
1213 sess, err := session.Get("session", c)
1314 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")
1519 }
1620
1721 // Check if the user is authenticated
@@ -1,6 +1,7 @@
1 package middleware1 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 authenticated21 // Check if the user is authenticated
M internal/httpserve/middleware/secure.go +20 -1
@@ -1,6 +1,12 @@
11 package middleware
22
33 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"
410 "github.com/labstack/echo/v4"
511 "github.com/labstack/echo/v4/middleware"
612 )
@@ -12,6 +18,19 @@ func SecureRoutes() echo.MiddlewareFunc {
1218 XFrameOptions: "SAMEORIGIN",
1319 HSTSMaxAge: 3600,
1420 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'",
1622 })
1723 }
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 middleware1 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 @@
11 package httpserve
22
33 import (
4- "os"
5-
64 "github.com/bnema/gordon/internal/app"
75 "github.com/bnema/gordon/internal/httpserve/handler"
86 "github.com/bnema/gordon/internal/httpserve/middleware"
9- "github.com/gorilla/sessions"
107 _ "github.com/joho/godotenv/autoload"
11- "github.com/labstack/echo-contrib/session"
128 "github.com/labstack/echo/v4"
139 )
1410
1511 // RegisterRoutes registers all routes for the application
1612 func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo {
1713 AdminPath := a.Config.Admin.Path
14+ // Use middlewares
15+ e.Use(middleware.SecureRoutes())
16+ // SetCommonDataMiddleware will pass data to the renderer
1817 e.Use(middleware.SetCommonDataMiddleware(a))
18+ // Error handler middleware
1919 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
2224 e.Use(middleware.ColorSchemeDetection)
25+ // Language detection
2326 e.Use(middleware.LanguageDetection)
2427
25- // Register routes
28+ // --- Register routes --- //
2629 bindAdminRoute(e, a, AdminPath)
2730 bindStaticRoute(e, a, "/*")
2831 bindLoginRoute(e, a, AdminPath)
@@ -67,54 +70,60 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
6770 }
6871
6972 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+
7079 // 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 {
7281 return handler.ImageManagerComponent(c, a)
7382 })
7483 // 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 {
7685 return handler.ImageManagerDelete(c, a)
7786 })
7887
7988 // List all containers
80- e.GET("/htmx/container-manager", func(c echo.Context) error {
89+ htmxGroup.GET("/container-manager", func(c echo.Context) error {
8190 return handler.ContainerManagerComponent(c, a)
8291 })
8392 // 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 {
8594 return handler.ContainerManagerStop(c, a)
8695 })
8796 // 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 {
8998 return handler.ContainerManagerDelete(c, a)
9099 })
91100 // 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 {
93102 return handler.ContainerManagerStart(c, a)
94103 })
95104 // 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 {
97106 return handler.ContainerManagerEditGET(c, a)
98107 })
99108 // 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 {
101110 return handler.ContainerManagerEditPOST(c, a)
102111 })
103112
104113 // 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 {
106115 return handler.UploadImageGETHandler(c, a)
107116 })
108117 // Upload image
109- e.POST("/htmx/upload-image", func(c echo.Context) error {
118+ htmxGroup.POST("/upload-image", func(c echo.Context) error {
110119 return handler.UploadImagePOSTHandler(c, a)
111120 })
112121 // 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 {
114123 return handler.CreateContainerGET(c, a)
115124 })
116125 // 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 {
118127 return handler.CreateContainerPOST(c, a)
119128 })
120129 }
@@ -1,28 +1,31 @@
1 package httpserve1 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 application11 // 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.Path13 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 middleware20+ // 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 routes28+ // --- 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 component79 // 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 image83 // 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 containers88 // 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 container92 // 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 container96 // 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 container100 // 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 view104 // 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 action108 // 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 component113 // 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 image117 // 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 component121 // 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 container125 // 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 {
644644 height: 100%;
645645 }
646646
647-.h-fit {
648- height: -moz-fit-content;
649- height: fit-content;
650-}
651-
652647 .w-1\/2 {
653648 width: 50%;
654649 }
655650
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-
668651 .w-1\/3 {
669652 width: 33.333333%;
670653 }
@@ -673,10 +656,22 @@ video {
673656 width: 25%;
674657 }
675658
659+.w-2\/3 {
660+ width: 66.666667%;
661+}
662+
676663 .w-5\/12 {
677664 width: 41.666667%;
678665 }
679666
667+.w-72 {
668+ width: 18rem;
669+}
670+
671+.w-full {
672+ width: 100%;
673+}
674+
680675 .min-w-fit {
681676 min-width: -moz-fit-content;
682677 min-width: fit-content;
@@ -793,6 +788,11 @@ video {
793788 background-color: rgb(0 123 192 / var(--tw-bg-opacity));
794789 }
795790
791+.bg-gordonmascot-mblue {
792+ --tw-bg-opacity: 1;
793+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
794+}
795+
796796 .bg-gray-100 {
797797 --tw-bg-opacity: 1;
798798 background-color: rgb(243 244 246 / var(--tw-bg-opacity));
@@ -827,11 +827,6 @@ video {
827827 background-color: rgb(228 228 231 / var(--tw-bg-opacity));
828828 }
829829
830-.bg-gordonmascot-mblue {
831- --tw-bg-opacity: 1;
832- background-color: rgb(0 82 147 / var(--tw-bg-opacity));
833-}
834-
835830 .p-0 {
836831 padding: 0px;
837832 }
@@ -864,11 +859,6 @@ video {
864859 padding-bottom: 0.5rem;
865860 }
866861
867-.py-4 {
868- padding-top: 1rem;
869- padding-bottom: 1rem;
870-}
871-
872862 .pb-4 {
873863 padding-bottom: 1rem;
874864 }
@@ -919,6 +909,11 @@ video {
919909 letter-spacing: -0.05em;
920910 }
921911
912+.text-blue-600 {
913+ --tw-text-opacity: 1;
914+ color: rgb(37 99 235 / var(--tw-text-opacity));
915+}
916+
922917 .text-blue-800 {
923918 --tw-text-opacity: 1;
924919 color: rgb(30 64 175 / var(--tw-text-opacity));
@@ -974,16 +969,6 @@ video {
974969 color: rgb(255 255 255 / var(--tw-text-opacity));
975970 }
976971
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-
987972 .underline {
988973 text-decoration-line: underline;
989974 }
@@ -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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close