detached 2589b4c

Refactor session management improve security checks and error handling

- Increase requirements of a session before attempting to cut through the OAuth login flow
- Fix langkey setting for language detection in context
- Refactor login requirement middleware to use redirect instead of JSON error
- Utilize Echo's built-in Recover middleware for better error recovery
- Enable Gzip middleware for static routes for optimized delivery
- Create a new Logout route handler to invalidate session and redirect
- Implement DB session existence check and invalidation logic
commit
2589b4cd1b412676e6b3c87c08953dc095939b37
author
bnema · · 2023-11-04 11:29 UTC
parent
7797a53
signature
unsigned
13 files · +160 -69
M .gitignore +2 -1
@@ -12,4 +12,5 @@ gordon
1212 dist/
1313 data/
1414 setup.sh
15-.iscontainer⏎̸
15+.iscontainer
16+sqlite.db⏎̸
@@ -12,4 +12,5 @@ gordon
12 dist/12 dist/
13 data/13 data/
14 setup.sh14 setup.sh
15-.iscontainer⏎̸15+.iscontainer
16+sqlite.db⏎̸
M go.mod +4 -2
@@ -10,7 +10,7 @@ require (
1010 github.com/gorilla/sessions v1.2.1
1111 github.com/joho/godotenv v1.5.1
1212 github.com/labstack/echo-contrib v0.15.0
13- github.com/labstack/echo/v4 v4.11.1
13+ github.com/labstack/echo/v4 v4.11.2
1414 github.com/microcosm-cc/bluemonday v1.0.25
1515 github.com/spf13/cobra v1.7.0
1616 gopkg.in/yaml.v3 v3.0.1
@@ -18,6 +18,7 @@ require (
1818 )
1919
2020 require (
21+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
2122 github.com/dustin/go-humanize v1.0.1 // indirect
2223 github.com/inconshreveable/mousetrap v1.1.0 // indirect
2324 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@@ -27,7 +28,7 @@ require (
2728 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
2829 github.com/rivo/uniseg v0.4.4 // indirect
2930 github.com/spf13/pflag v1.0.5 // indirect
30- github.com/stretchr/testify v1.8.2 // indirect
31+ github.com/stretchr/testify v1.8.4 // indirect
3132 lukechampine.com/uint128 v1.3.0 // indirect
3233 modernc.org/cc/v3 v3.41.0 // indirect
3334 modernc.org/ccgo/v3 v3.16.15 // indirect
@@ -51,6 +52,7 @@ require (
5152 github.com/gorilla/context v1.1.1 // indirect
5253 github.com/gorilla/css v1.0.0 // indirect
5354 github.com/gorilla/securecookie v1.1.1 // indirect
55+ github.com/labstack/echo v3.3.10+incompatible
5456 github.com/labstack/gommon v0.4.0 // indirect
5557 github.com/mattn/go-colorable v0.1.13 // indirect
5658 github.com/mattn/go-isatty v0.0.20 // indirect
@@ -10,7 +10,7 @@ require (
10 github.com/gorilla/sessions v1.2.110 github.com/gorilla/sessions v1.2.1
11 github.com/joho/godotenv v1.5.111 github.com/joho/godotenv v1.5.1
12 github.com/labstack/echo-contrib v0.15.012 github.com/labstack/echo-contrib v0.15.0
13- github.com/labstack/echo/v4 v4.11.113+ github.com/labstack/echo/v4 v4.11.2
14 github.com/microcosm-cc/bluemonday v1.0.2514 github.com/microcosm-cc/bluemonday v1.0.25
15 github.com/spf13/cobra v1.7.015 github.com/spf13/cobra v1.7.0
16 gopkg.in/yaml.v3 v3.0.116 gopkg.in/yaml.v3 v3.0.1
@@ -18,6 +18,7 @@ require (
18 )18 )
19 19
20 require (20 require (
21+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
21 github.com/dustin/go-humanize v1.0.1 // indirect22 github.com/dustin/go-humanize v1.0.1 // indirect
22 github.com/inconshreveable/mousetrap v1.1.0 // indirect23 github.com/inconshreveable/mousetrap v1.1.0 // indirect
23 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect24 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@@ -27,7 +28,7 @@ require (
27 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect28 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
28 github.com/rivo/uniseg v0.4.4 // indirect29 github.com/rivo/uniseg v0.4.4 // indirect
29 github.com/spf13/pflag v1.0.5 // indirect30 github.com/spf13/pflag v1.0.5 // indirect
30- github.com/stretchr/testify v1.8.2 // indirect31+ github.com/stretchr/testify v1.8.4 // indirect
31 lukechampine.com/uint128 v1.3.0 // indirect32 lukechampine.com/uint128 v1.3.0 // indirect
32 modernc.org/cc/v3 v3.41.0 // indirect33 modernc.org/cc/v3 v3.41.0 // indirect
33 modernc.org/ccgo/v3 v3.16.15 // indirect34 modernc.org/ccgo/v3 v3.16.15 // indirect
@@ -51,6 +52,7 @@ require (
51 github.com/gorilla/context v1.1.1 // indirect52 github.com/gorilla/context v1.1.1 // indirect
52 github.com/gorilla/css v1.0.0 // indirect53 github.com/gorilla/css v1.0.0 // indirect
53 github.com/gorilla/securecookie v1.1.1 // indirect54 github.com/gorilla/securecookie v1.1.1 // indirect
55+ github.com/labstack/echo v3.3.10+incompatible
54 github.com/labstack/gommon v0.4.0 // indirect56 github.com/labstack/gommon v0.4.0 // indirect
55 github.com/mattn/go-colorable v0.1.13 // indirect57 github.com/mattn/go-colorable v0.1.13 // indirect
56 github.com/mattn/go-isatty v0.0.20 // indirect58 github.com/mattn/go-isatty v0.0.20 // indirect
M go.sum +7 -0 generated file: collapsed
M internal/db/queries/sessions.go +21 -0
@@ -120,3 +120,24 @@ func ExtendSessionExpiration(a *server.App, accountID string, sessionID string,
120120
121121 return nil
122122 }
123+
124+func CheckDBSessionExists(a *server.App, sessionID string) (bool, error) {
125+ var sessionExists bool
126+ query := "SELECT EXISTS(SELECT 1 FROM sessions WHERE id = ?)"
127+ err := a.DB.QueryRow(query, sessionID).Scan(&sessionExists)
128+ if err != nil {
129+ return false, err
130+ }
131+
132+ return sessionExists, nil
133+}
134+
135+func InvalidateDBSession(a *server.App, sessionID string) error {
136+ query := "UPDATE sessions SET is_online = ? WHERE id = ?"
137+ _, err := a.DB.Exec(query, false, sessionID)
138+ if err != nil {
139+ return err
140+ }
141+
142+ return nil
143+}
@@ -120,3 +120,24 @@ func ExtendSessionExpiration(a *server.App, accountID string, sessionID string,
120 120
121 return nil121 return nil
122 }122 }
123+
124+func CheckDBSessionExists(a *server.App, sessionID string) (bool, error) {
125+ var sessionExists bool
126+ query := "SELECT EXISTS(SELECT 1 FROM sessions WHERE id = ?)"
127+ err := a.DB.QueryRow(query, sessionID).Scan(&sessionExists)
128+ if err != nil {
129+ return false, err
130+ }
131+
132+ return sessionExists, nil
133+}
134+
135+func InvalidateDBSession(a *server.App, sessionID string) error {
136+ query := "UPDATE sessions SET is_online = ? WHERE id = ?"
137+ _, err := a.DB.Exec(query, false, sessionID)
138+ if err != nil {
139+ return err
140+ }
141+
142+ return nil
143+}
M internal/httpserve/handler/adminroutes.go +2 -2
@@ -18,7 +18,6 @@ var (
1818 // GetLocalizedData returns the data for the localization
1919 func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) {
2020 lang := c.Get("LangKey")
21-
2221 if lang == nil {
2322 return nil, fmt.Errorf("LangKey not found in context")
2423 }
@@ -28,7 +27,8 @@ func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, er
2827 return nil, fmt.Errorf("failed to get localization: %w", err)
2928 }
3029 return map[string]interface{}{
31- "Lang": yamlData,
30+ "Lang": yamlData,
31+ "LogoutURL": a.Config.Admin.Path + "/logout",
3232 }, nil
3333 }
3434
@@ -18,7 +18,6 @@ var (
18 // GetLocalizedData returns the data for the localization18 // GetLocalizedData returns the data for the localization
19 func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) {19 func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) {
20 lang := c.Get("LangKey")20 lang := c.Get("LangKey")
21-
22 if lang == nil {21 if lang == nil {
23 return nil, fmt.Errorf("LangKey not found in context")22 return nil, fmt.Errorf("LangKey not found in context")
24 }23 }
@@ -28,7 +27,8 @@ func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, er
28 return nil, fmt.Errorf("failed to get localization: %w", err)27 return nil, fmt.Errorf("failed to get localization: %w", err)
29 }28 }
30 return map[string]interface{}{29 return map[string]interface{}{
31- "Lang": yamlData,30+ "Lang": yamlData,
31+ "LogoutURL": a.Config.Admin.Path + "/logout",
32 }, nil32 }, nil
33 }33 }
34 34
A internal/httpserve/handler/logout.go +51 -0
@@ -0,0 +1,51 @@
1+package handler
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "time"
7+
8+ "github.com/bnema/gordon/internal/db/queries"
9+ "github.com/bnema/gordon/internal/server"
10+ "github.com/gorilla/sessions"
11+ "github.com/labstack/echo/v4"
12+)
13+
14+func Logout(c echo.Context, a *server.App) error {
15+ // Retrieve the session
16+ sess, err := getSession(c)
17+ if err != nil || sess == nil {
18+ // if there is no session, redirect to the login page
19+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
20+
21+ }
22+
23+ // Invalidate the session in the database
24+ if sess != nil {
25+ err := queries.InvalidateDBSession(a, sess.Values["sessionID"].(string))
26+ if err != nil {
27+ return fmt.Errorf("failed to invalidate session: %w", err)
28+ }
29+ }
30+
31+ // Delete the session
32+ sess.Options = &sessions.Options{
33+ Path: "/",
34+ MaxAge: -1,
35+ HttpOnly: true,
36+ }
37+
38+ err = sess.Save(c.Request(), c.Response())
39+ if err != nil {
40+ return fmt.Errorf("failed to save session: %w", err)
41+ }
42+
43+ // Drop the cookie
44+ cookie := new(http.Cookie)
45+ cookie.Name = "session"
46+ cookie.Value = ""
47+ cookie.Expires = time.Now().Add(-1 * time.Hour)
48+ c.SetCookie(cookie)
49+
50+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
51+}
@@ -0,0 +1,51 @@
1+package handler
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "time"
7+
8+ "github.com/bnema/gordon/internal/db/queries"
9+ "github.com/bnema/gordon/internal/server"
10+ "github.com/gorilla/sessions"
11+ "github.com/labstack/echo/v4"
12+)
13+
14+func Logout(c echo.Context, a *server.App) error {
15+ // Retrieve the session
16+ sess, err := getSession(c)
17+ if err != nil || sess == nil {
18+ // if there is no session, redirect to the login page
19+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
20+
21+ }
22+
23+ // Invalidate the session in the database
24+ if sess != nil {
25+ err := queries.InvalidateDBSession(a, sess.Values["sessionID"].(string))
26+ if err != nil {
27+ return fmt.Errorf("failed to invalidate session: %w", err)
28+ }
29+ }
30+
31+ // Delete the session
32+ sess.Options = &sessions.Options{
33+ Path: "/",
34+ MaxAge: -1,
35+ HttpOnly: true,
36+ }
37+
38+ err = sess.Save(c.Request(), c.Response())
39+ if err != nil {
40+ return fmt.Errorf("failed to save session: %w", err)
41+ }
42+
43+ // Drop the cookie
44+ cookie := new(http.Cookie)
45+ cookie.Name = "session"
46+ cookie.Value = ""
47+ cookie.Expires = time.Now().Add(-1 * time.Hour)
48+ c.SetCookie(cookie)
49+
50+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
51+}
M internal/httpserve/handler/oauth.go +53 -22
@@ -57,38 +57,69 @@ func RenderLoginPage(c echo.Context, a *server.App) error {
5757 return c.HTML(200, renderedHTML)
5858 }
5959
60-// StartOAuthGithub starts the Github OAuth flow
60+// StartOAuthGithub starts the Github OAuth flow.
6161 func StartOAuthGithub(c echo.Context, a *server.App) error {
62- // Check if the session is already here and valid before going for the oauth flow
62+ fmt.Println("Attempting to get an existing session.")
6363 sess, err := getSession(c)
64+ if err != nil || sess == nil || sess.Values == nil {
65+ fmt.Println("Session retrieval failed, initiating new GitHub OAuth flow.")
66+ return initiateGithubOAuthFlow(c, a)
67+ }
68+
69+ fmt.Println("Attempting to assert session values.")
70+ sessionID, ok1 := sess.Values["sessionID"].(string)
71+ accountID, ok2 := sess.Values["accountID"].(string)
72+ authenticated, ok3 := sess.Values["authenticated"].(bool)
73+
74+ if !ok1 || !ok2 || !ok3 || !authenticated {
75+ fmt.Println("Session assertion failed, or user not authenticated. Initiating new OAuth flow.")
76+ return initiateGithubOAuthFlow(c, a)
77+ }
78+
79+ fmt.Println("Checking if session exists in DB.")
80+ sessionExists, err := queries.CheckDBSessionExists(a, sessionID)
6481 if err != nil {
65- return err
82+ fmt.Printf("Error checking if session exists: %v\n", err)
83+ return fmt.Errorf("could not check if session exists: %w", err)
6684 }
6785
68- // Check if the session values are valid
69- sessionID, accountID, ok := sess.Values["accountID"].(string), sess.Values["sessionID"].(string), sess.Values["authenticated"].(bool)
70- if ok && accountID != "" && sessionID != "" {
71- // Check if the session is expired
72- sessionExpired, err := IsSessionExpiredInDB(a, accountID, sessionID)
73- if err != nil {
74- return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
75- }
76- if !sessionExpired {
77- sessionExpiryTime := 30 * time.Minute
78- // Extend session expiration conditionally, e.g., if it's close to expiring
79- err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
80- if err != nil {
81- return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
82- }
83- return c.Redirect(http.StatusFound, a.Config.Admin.Path)
84- }
86+ if !sessionExists {
87+ fmt.Println("Session does not exist in DB, initiating new OAuth flow.")
88+ return initiateGithubOAuthFlow(c, a)
8589 }
8690
87- //Initiate the Github OAuth flow
91+ fmt.Println("Getting the expiration time of the session.")
92+ currentTime := time.Now()
93+ sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
94+ if err != nil {
95+ fmt.Printf("Error getting session expiration: %v\n", err)
96+ return fmt.Errorf("could not get session expiration: %w", err)
97+ }
98+
99+ if currentTime.After(sessionExpiration) {
100+ fmt.Println("Current time is after session expiration, initiating new OAuth flow.")
101+ return initiateGithubOAuthFlow(c, a)
102+ }
103+
104+ fmt.Println("Extending session expiration time by 30 minutes.")
105+ newExpirationTime := currentTime.Add(30 * time.Minute)
106+ err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime)
107+ if err != nil {
108+ fmt.Printf("Error extending session expiration: %v\n", err)
109+ return fmt.Errorf("could not extend session expiration: %w", err)
110+ }
111+
112+ fmt.Println("Session is valid and extended. Redirecting to admin panel.")
113+ return c.Redirect(http.StatusFound, a.Config.Admin.Path)
114+}
115+
116+// initiateGithubOAuthFlow starts a new GitHub OAuth flow
117+func initiateGithubOAuthFlow(c echo.Context, a *server.App) error {
88118 clientID := os.Getenv("GITHUB_APP_ID")
89119 redirectDomain := a.GenerateOauthCallbackURL()
90120 encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
91- // Redirect to Gordon's Proxy to grab the oauth access
121+
122+ // Redirect to GitHub's OAuth endpoint to grab the OAuth access
92123 proxyURL := a.Config.Build.ProxyURL
93124 oauthURL := fmt.Sprintf(
94125 "%s/github/authorize?client_id=%s&redirect_uri=%s&state=%s",
@@ -57,38 +57,69 @@ func RenderLoginPage(c echo.Context, a *server.App) error {
57 return c.HTML(200, renderedHTML)57 return c.HTML(200, renderedHTML)
58 }58 }
59 59
60-// StartOAuthGithub starts the Github OAuth flow60+// StartOAuthGithub starts the Github OAuth flow.
61 func StartOAuthGithub(c echo.Context, a *server.App) error {61 func StartOAuthGithub(c echo.Context, a *server.App) error {
62- // Check if the session is already here and valid before going for the oauth flow62+ fmt.Println("Attempting to get an existing session.")
63 sess, err := getSession(c)63 sess, err := getSession(c)
64+ if err != nil || sess == nil || sess.Values == nil {
65+ fmt.Println("Session retrieval failed, initiating new GitHub OAuth flow.")
66+ return initiateGithubOAuthFlow(c, a)
67+ }
68+
69+ fmt.Println("Attempting to assert session values.")
70+ sessionID, ok1 := sess.Values["sessionID"].(string)
71+ accountID, ok2 := sess.Values["accountID"].(string)
72+ authenticated, ok3 := sess.Values["authenticated"].(bool)
73+
74+ if !ok1 || !ok2 || !ok3 || !authenticated {
75+ fmt.Println("Session assertion failed, or user not authenticated. Initiating new OAuth flow.")
76+ return initiateGithubOAuthFlow(c, a)
77+ }
78+
79+ fmt.Println("Checking if session exists in DB.")
80+ sessionExists, err := queries.CheckDBSessionExists(a, sessionID)
64 if err != nil {81 if err != nil {
65- return err82+ fmt.Printf("Error checking if session exists: %v\n", err)
83+ return fmt.Errorf("could not check if session exists: %w", err)
66 }84 }
67 85
68- // Check if the session values are valid86+ if !sessionExists {
69- sessionID, accountID, ok := sess.Values["accountID"].(string), sess.Values["sessionID"].(string), sess.Values["authenticated"].(bool)87+ fmt.Println("Session does not exist in DB, initiating new OAuth flow.")
70- if ok && accountID != "" && sessionID != "" {88+ return initiateGithubOAuthFlow(c, a)
71- // Check if the session is expired
72- sessionExpired, err := IsSessionExpiredInDB(a, accountID, sessionID)
73- if err != nil {
74- return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
75- }
76- if !sessionExpired {
77- sessionExpiryTime := 30 * time.Minute
78- // Extend session expiration conditionally, e.g., if it's close to expiring
79- err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime))
80- if err != nil {
81- return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
82- }
83- return c.Redirect(http.StatusFound, a.Config.Admin.Path)
84- }
85 }89 }
86 90
87- //Initiate the Github OAuth flow91+ fmt.Println("Getting the expiration time of the session.")
92+ currentTime := time.Now()
93+ sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
94+ if err != nil {
95+ fmt.Printf("Error getting session expiration: %v\n", err)
96+ return fmt.Errorf("could not get session expiration: %w", err)
97+ }
98+
99+ if currentTime.After(sessionExpiration) {
100+ fmt.Println("Current time is after session expiration, initiating new OAuth flow.")
101+ return initiateGithubOAuthFlow(c, a)
102+ }
103+
104+ fmt.Println("Extending session expiration time by 30 minutes.")
105+ newExpirationTime := currentTime.Add(30 * time.Minute)
106+ err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime)
107+ if err != nil {
108+ fmt.Printf("Error extending session expiration: %v\n", err)
109+ return fmt.Errorf("could not extend session expiration: %w", err)
110+ }
111+
112+ fmt.Println("Session is valid and extended. Redirecting to admin panel.")
113+ return c.Redirect(http.StatusFound, a.Config.Admin.Path)
114+}
115+
116+// initiateGithubOAuthFlow starts a new GitHub OAuth flow
117+func initiateGithubOAuthFlow(c echo.Context, a *server.App) error {
88 clientID := os.Getenv("GITHUB_APP_ID")118 clientID := os.Getenv("GITHUB_APP_ID")
89 redirectDomain := a.GenerateOauthCallbackURL()119 redirectDomain := a.GenerateOauthCallbackURL()
90 encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))120 encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
91- // Redirect to Gordon's Proxy to grab the oauth access121+
122+ // Redirect to GitHub's OAuth endpoint to grab the OAuth access
92 proxyURL := a.Config.Build.ProxyURL123 proxyURL := a.Config.Build.ProxyURL
93 oauthURL := fmt.Sprintf(124 oauthURL := fmt.Sprintf(
94 "%s/github/authorize?client_id=%s&redirect_uri=%s&state=%s",125 "%s/github/authorize?client_id=%s&redirect_uri=%s&state=%s",
M internal/httpserve/handler/sessiondb.go +2 -2
@@ -47,9 +47,9 @@ func ValidateSessionAndUser(c echo.Context, a *server.App) error {
4747 return nil
4848 }
4949
50-func IsSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {
50+func IsSessionExpiredInDB(a *server.App, accountID string, sessionID string) (bool, error) {
5151 currentTime := time.Now()
52- expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
52+ expirationTime, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
5353 if err != nil {
5454 return false, err
5555 }
@@ -47,9 +47,9 @@ func ValidateSessionAndUser(c echo.Context, a *server.App) error {
47 return nil47 return nil
48 }48 }
49 49
50-func IsSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {50+func IsSessionExpiredInDB(a *server.App, accountID string, sessionID string) (bool, error) {
51 currentTime := time.Now()51 currentTime := time.Now()
52- expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)52+ expirationTime, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
53 if err != nil {53 if err != nil {
54 return false, err54 return false, err
55 }55 }
M internal/httpserve/middleware/langdetect.go +1 -16
@@ -3,34 +3,19 @@ package middleware
33 import (
44 "strings"
55
6- "github.com/labstack/echo-contrib/session"
76 "github.com/labstack/echo/v4"
87 )
98
10-// LangKey is a key for setting and getting the language from the context
11-const LangKey = "CurrentLang"
12-
139 // LanguageDetectiondetects the current language and sets it in the context
1410 func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc {
1511 return func(c echo.Context) error {
1612 lang := detectCurrentLanguage(c)
17- c.Set(LangKey, lang)
13+ c.Set("LangKey", lang)
1814 return next(c)
1915 }
2016 }
2117
2218 func detectCurrentLanguage(c echo.Context) string {
23- // Check if the lang key is set in the session storage
24- sess, err := session.Get("session", c)
25- if err != nil {
26- // If session storage is not available, proceed with "Accept-Language" header detection
27- return "en"
28- }
29-
30- if langValue, ok := sess.Values["lang"]; ok && langValue != nil {
31- return langValue.(string) // Return the lang value from the session storage
32- }
33-
3419 // If lang key is not set, proceed with "Accept-Language" header detection
3520 header := c.Request().Header.Get("Accept-Language")
3621 if strings.Contains(header, "fr") {
@@ -3,34 +3,19 @@ package middleware
3 import (3 import (
4 "strings"4 "strings"
5 5
6- "github.com/labstack/echo-contrib/session"
7 "github.com/labstack/echo/v4"6 "github.com/labstack/echo/v4"
8 )7 )
9 8
10-// LangKey is a key for setting and getting the language from the context
11-const LangKey = "CurrentLang"
12-
13 // LanguageDetectiondetects the current language and sets it in the context9 // LanguageDetectiondetects the current language and sets it in the context
14 func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc {10 func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc {
15 return func(c echo.Context) error {11 return func(c echo.Context) error {
16 lang := detectCurrentLanguage(c)12 lang := detectCurrentLanguage(c)
17- c.Set(LangKey, lang)13+ c.Set("LangKey", lang)
18 return next(c)14 return next(c)
19 }15 }
20 }16 }
21 17
22 func detectCurrentLanguage(c echo.Context) string {18 func detectCurrentLanguage(c echo.Context) string {
23- // Check if the lang key is set in the session storage
24- sess, err := session.Get("session", c)
25- if err != nil {
26- // If session storage is not available, proceed with "Accept-Language" header detection
27- return "en"
28- }
29-
30- if langValue, ok := sess.Values["lang"]; ok && langValue != nil {
31- return langValue.(string) // Return the lang value from the session storage
32- }
33-
34 // If lang key is not set, proceed with "Accept-Language" header detection19 // If lang key is not set, proceed with "Accept-Language" header detection
35 header := c.Request().Header.Get("Accept-Language")20 header := c.Request().Header.Get("Accept-Language")
36 if strings.Contains(header, "fr") {21 if strings.Contains(header, "fr") {
M internal/httpserve/middleware/requirelogin.go +8 -5
@@ -1,6 +1,7 @@
11 package middleware
22
33 import (
4+ "net/http"
45 "time"
56
67 "github.com/bnema/gordon/internal/httpserve/handler"
@@ -9,6 +10,10 @@ import (
910 "github.com/labstack/echo/v4"
1011 )
1112
13+func redirectToLogin(c echo.Context, a *server.App) error {
14+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
15+}
16+
1217 func RequireLogin(a *server.App) echo.MiddlewareFunc {
1318 return func(next echo.HandlerFunc) echo.HandlerFunc {
1419 return func(c echo.Context) error {
@@ -23,15 +28,13 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
2328 time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
2429 }
2530 }
26- return c.JSON(401, map[string]string{"error": err.Error()})
31+ // propose the redirect auth url as httpcode
32+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
2733 }
34+
2835 }
2936 }
3037
31-// func redirectToLogin(c echo.Context, a *server.App) error {
32-// return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
33-// }
34-
3538 // func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {
3639 // // Delete session cookie
3740 // cookie := new(http.Cookie)
@@ -1,6 +1,7 @@
1 package middleware1 package middleware
2 2
3 import (3 import (
4+ "net/http"
4 "time"5 "time"
5 6
6 "github.com/bnema/gordon/internal/httpserve/handler"7 "github.com/bnema/gordon/internal/httpserve/handler"
@@ -9,6 +10,10 @@ import (
9 "github.com/labstack/echo/v4"10 "github.com/labstack/echo/v4"
10 )11 )
11 12
13+func redirectToLogin(c echo.Context, a *server.App) error {
14+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
15+}
16+
12 func RequireLogin(a *server.App) echo.MiddlewareFunc {17 func RequireLogin(a *server.App) echo.MiddlewareFunc {
13 return func(next echo.HandlerFunc) echo.HandlerFunc {18 return func(next echo.HandlerFunc) echo.HandlerFunc {
14 return func(c echo.Context) error {19 return func(c echo.Context) error {
@@ -23,15 +28,13 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
23 time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)28 time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
24 }29 }
25 }30 }
26- return c.JSON(401, map[string]string{"error": err.Error()})31+ // propose the redirect auth url as httpcode
32+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
27 }33 }
34+
28 }35 }
29 }36 }
30 37
31-// func redirectToLogin(c echo.Context, a *server.App) error {
32-// return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
33-// }
34-
35 // func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {38 // func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error {
36 // // Delete session cookie39 // // Delete session cookie
37 // cookie := new(http.Cookie)40 // cookie := new(http.Cookie)
M internal/httpserve/router.go +5 -2
@@ -6,10 +6,13 @@ import (
66 "github.com/bnema/gordon/internal/server"
77 _ "github.com/joho/godotenv/autoload"
88 "github.com/labstack/echo/v4"
9+ echomid "github.com/labstack/echo/v4/middleware"
910 )
1011
1112 // RegisterRoutes registers all routes and middlewares
1213 func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
14+ // e.Use(echomid.Logger()) too verbose
15+ e.Use(echomid.Recover())
1316 // Initiate the session middleware
1417 e.Use(middleware.InitSessionMiddleware(a))
1518
@@ -63,7 +66,7 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) {
6366 func bindStaticRoute(e *echo.Echo, a *server.App, path string) {
6467 e.GET(path, func(c echo.Context) error {
6568 return handler.StaticRoute(c, a)
66- })
69+ }, echomid.Gzip())
6770 }
6871
6972 // bindLoginRoute binds all login routes
@@ -78,7 +81,7 @@ func bindLoginRoute(e *echo.Echo, a *server.App, adminPath string) {
7881 return handler.OAuthCallback(c, a)
7982 })
8083 e.GET(adminPath+"/logout", func(c echo.Context) error {
81- return c.HTML(200, "<h1>Logout</h1>")
84+ return handler.Logout(c, a)
8285 })
8386 }
8487
@@ -6,10 +6,13 @@ import (
6 "github.com/bnema/gordon/internal/server"6 "github.com/bnema/gordon/internal/server"
7 _ "github.com/joho/godotenv/autoload"7 _ "github.com/joho/godotenv/autoload"
8 "github.com/labstack/echo/v4"8 "github.com/labstack/echo/v4"
9+ echomid "github.com/labstack/echo/v4/middleware"
9 )10 )
10 11
11 // RegisterRoutes registers all routes and middlewares12 // RegisterRoutes registers all routes and middlewares
12 func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {13 func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
14+ // e.Use(echomid.Logger()) too verbose
15+ e.Use(echomid.Recover())
13 // Initiate the session middleware16 // Initiate the session middleware
14 e.Use(middleware.InitSessionMiddleware(a))17 e.Use(middleware.InitSessionMiddleware(a))
15 18
@@ -63,7 +66,7 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) {
63 func bindStaticRoute(e *echo.Echo, a *server.App, path string) {66 func bindStaticRoute(e *echo.Echo, a *server.App, path string) {
64 e.GET(path, func(c echo.Context) error {67 e.GET(path, func(c echo.Context) error {
65 return handler.StaticRoute(c, a)68 return handler.StaticRoute(c, a)
66- })69+ }, echomid.Gzip())
67 }70 }
68 71
69 // bindLoginRoute binds all login routes72 // bindLoginRoute binds all login routes
@@ -78,7 +81,7 @@ func bindLoginRoute(e *echo.Echo, a *server.App, adminPath string) {
78 return handler.OAuthCallback(c, a)81 return handler.OAuthCallback(c, a)
79 })82 })
80 e.GET(adminPath+"/logout", func(c echo.Context) error {83 e.GET(adminPath+"/logout", func(c echo.Context) error {
81- return c.HTML(200, "<h1>Logout</h1>")84+ return handler.Logout(c, a)
82 })85 })
83 }86 }
84 87
M internal/templating/models/html/admin/index.gohtml +2 -15
@@ -1,25 +1,12 @@
1-<!DOCTYPE html>
1+<!doctype html>
22 <html lang="en">
33 {{template "header.gohtml" .}}
44
55 <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-0">
66 {{template "menu.gohtml" .}}
77 <div class="flex-1 flex flex-col md:pt-10 space-y-4 p-4 md:ml-4">
8- <div class="hidden md:flex mb-4 justify-end">
9- <div class="w-72 h-10 bg-gray-300 rounded-lg">
10- <div class="flex justify-between items-center h-full px-2">
11- <input
12- type="text"
13- class="w-full h-full bg-transparent outline-none"
14- placeholder="Search"
15- />
16- <i class="iconoir-search"></i>
17- </div>
18- </div>
19- </div>
208 <div
21- class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"
22- >
9+ class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow">
2310 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
2411 <h2>{{index .Lang "dashboard" "content" "title"}}</h2>
2512 </div>
@@ -1,25 +1,12 @@
1-<!DOCTYPE html>1+<!doctype html>
2 <html lang="en">2 <html lang="en">
3 {{template "header.gohtml" .}}3 {{template "header.gohtml" .}}
4 4
5 <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-0">5 <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-0">
6 {{template "menu.gohtml" .}}6 {{template "menu.gohtml" .}}
7 <div class="flex-1 flex flex-col md:pt-10 space-y-4 p-4 md:ml-4">7 <div class="flex-1 flex flex-col md:pt-10 space-y-4 p-4 md:ml-4">
8- <div class="hidden md:flex mb-4 justify-end">
9- <div class="w-72 h-10 bg-gray-300 rounded-lg">
10- <div class="flex justify-between items-center h-full px-2">
11- <input
12- type="text"
13- class="w-full h-full bg-transparent outline-none"
14- placeholder="Search"
15- />
16- <i class="iconoir-search"></i>
17- </div>
18- </div>
19- </div>
20 <div8 <div
21- class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"9+ class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow">
22- >
23 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">10 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
24 <h2>{{index .Lang "dashboard" "content" "title"}}</h2>11 <h2>{{index .Lang "dashboard" "content" "title"}}</h2>
25 </div>12 </div>
M internal/templating/models/html/fragments/menu.gohtml +2 -2
@@ -22,13 +22,13 @@
2222 </div>
2323 <div class="w-full flex space-x-4 justify-end">
2424 <a
25- href="/admin/settings"
25+ href="{{ .UserSettingsURL }}"
2626 title="User settings"
2727 class="iconf text-gordonmascot-mblue text-lg md:text-xl"
2828 ></a
2929 >
3030 <a
31- href="/admin/logout"
31+ href="{{ .LogoutURL }}"
3232 title="Logout"
3333 class="iconf text-gordonmascot-mblue text-lg md:text-xl"
3434 >󰍃</a
@@ -22,13 +22,13 @@
22 </div>22 </div>
23 <div class="w-full flex space-x-4 justify-end">23 <div class="w-full flex space-x-4 justify-end">
24 <a24 <a
25- href="/admin/settings"25+ href="{{ .UserSettingsURL }}"
26 title="User settings"26 title="User settings"
27 class="iconf text-gordonmascot-mblue text-lg md:text-xl"27 class="iconf text-gordonmascot-mblue text-lg md:text-xl"
28 ></a28 ></a
29 >29 >
30 <a30 <a
31- href="/admin/logout"31+ href="{{ .LogoutURL }}"
32 title="Logout"32 title="Logout"
33 class="iconf text-gordonmascot-mblue text-lg md:text-xl"33 class="iconf text-gordonmascot-mblue text-lg md:text-xl"
34 >󰍃</a34 >󰍃</a

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