diff --git a/internal/handlers/admin/users.go b/internal/handlers/admin/users.go index d7328cf..3ccc7a5 100644 --- a/internal/handlers/admin/users.go +++ b/internal/handlers/admin/users.go @@ -4,12 +4,12 @@ import ( "crypto/rand" "encoding/hex" "net/http" - "net/url" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/mattnite/forgejo-tickets/internal/forgejo" + "github.com/mattnite/forgejo-tickets/internal/middleware" "github.com/mattnite/forgejo-tickets/internal/models" "github.com/rs/zerolog/log" ) @@ -202,11 +202,8 @@ func (h *UserHandler) Approve(c *gin.Context) { log.Error().Err(err).Msg("send approval email error") } - redirectURL := "/users/pending?" + url.Values{ - "flash": {"User " + user.Email + " has been approved"}, - "flash_type": {"success"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "success", "User "+user.Email+" has been approved") + c.Redirect(http.StatusSeeOther, "/users/pending") } func (h *UserHandler) Reject(c *gin.Context) { @@ -222,11 +219,8 @@ func (h *UserHandler) Reject(c *gin.Context) { return } - redirectURL := "/users/pending?" + url.Values{ - "flash": {"User request has been rejected"}, - "flash_type": {"success"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "success", "User request has been rejected") + c.Redirect(http.StatusSeeOther, "/users/pending") } func (h *UserHandler) UpdateRepos(c *gin.Context) { @@ -257,9 +251,6 @@ func (h *UserHandler) UpdateRepos(c *gin.Context) { h.deps.DB.Create(&models.UserRepo{UserID: userID, RepoID: repoID}) } - redirectURL := "/users/" + userID.String() + "?" + url.Values{ - "flash": {"Project assignments updated"}, - "flash_type": {"success"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "success", "Project assignments updated") + c.Redirect(http.StatusSeeOther, "/users/"+userID.String()) } diff --git a/internal/handlers/public/auth.go b/internal/handlers/public/auth.go index d61a06b..1e029f2 100644 --- a/internal/handlers/public/auth.go +++ b/internal/handlers/public/auth.go @@ -2,11 +2,11 @@ package public import ( "net/http" - "net/url" "strings" "github.com/gin-gonic/gin" "github.com/mattnite/forgejo-tickets/internal/auth" + "github.com/mattnite/forgejo-tickets/internal/middleware" "github.com/mattnite/forgejo-tickets/internal/models" "github.com/rs/zerolog/log" ) @@ -111,11 +111,8 @@ func (h *AuthHandler) Register(c *gin.Context) { } } - redirectURL := "/login?" + url.Values{ - "flash": {"Account requested! Please check your email to verify your address. After verification, an admin will review your request."}, - "flash_type": {"success"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "success", "Account requested! Please check your email to verify your address. After verification, an admin will review your request.") + c.Redirect(http.StatusSeeOther, "/login") } func (h *AuthHandler) Logout(c *gin.Context) { @@ -138,11 +135,8 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) { return } - redirectURL := "/login?" + url.Values{ - "flash": {"Email verified successfully. Your account is pending admin approval."}, - "flash_type": {"success"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "success", "Email verified successfully. Your account is pending admin approval.") + c.Redirect(http.StatusSeeOther, "/login") } func (h *AuthHandler) ForgotPasswordForm(c *gin.Context) { @@ -206,9 +200,6 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) { return } - redirectURL := "/login?" + url.Values{ - "flash": {"Password reset successfully. You can now log in."}, - "flash_type": {"success"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "success", "Password reset successfully. You can now log in.") + c.Redirect(http.StatusSeeOther, "/login") } diff --git a/internal/handlers/public/oauth.go b/internal/handlers/public/oauth.go index 839f4b0..af83a01 100644 --- a/internal/handlers/public/oauth.go +++ b/internal/handlers/public/oauth.go @@ -4,11 +4,11 @@ import ( "crypto/rand" "encoding/hex" "net/http" - "net/url" "strings" "github.com/gin-gonic/gin" "github.com/mattnite/forgejo-tickets/internal/auth" + "github.com/mattnite/forgejo-tickets/internal/middleware" "github.com/rs/zerolog/log" "golang.org/x/oauth2" ) @@ -98,11 +98,8 @@ func (h *OAuthHandler) Callback(c *gin.Context) { user, err := h.deps.Auth.FindOrCreateOAuthUser(c.Request.Context(), provider.Name, info) if err != nil { if strings.Contains(err.Error(), "pending admin approval") { - redirectURL := "/login?" + url.Values{ - "flash": {err.Error()}, - "flash_type": {"info"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "info", err.Error()) + c.Redirect(http.StatusSeeOther, "/login") return } log.Error().Err(err).Msg("find or create oauth user error") @@ -196,11 +193,8 @@ func (h *OAuthHandler) AppleCallback(c *gin.Context) { user, err := h.deps.Auth.FindOrCreateOAuthUser(c.Request.Context(), "apple", info) if err != nil { if strings.Contains(err.Error(), "pending admin approval") { - redirectURL := "/login?" + url.Values{ - "flash": {err.Error()}, - "flash_type": {"info"}, - }.Encode() - c.Redirect(http.StatusSeeOther, redirectURL) + middleware.SetFlash(c, "info", err.Error()) + c.Redirect(http.StatusSeeOther, "/login") return } log.Error().Err(err).Msg("find or create apple user error") diff --git a/internal/middleware/flash.go b/internal/middleware/flash.go new file mode 100644 index 0000000..fd3167c --- /dev/null +++ b/internal/middleware/flash.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "encoding/base64" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// SetFlash sets a flash message cookie that will be consumed on the next render. +func SetFlash(c *gin.Context, flashType, message string) { + // Encode as "type:message" in base64 to avoid cookie value issues + value := base64.StdEncoding.EncodeToString([]byte(flashType + ":" + message)) + http.SetCookie(c.Writer, &http.Cookie{ + Name: "flash", + Value: value, + Path: "/", + MaxAge: 60, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) +} + +// GetFlash reads and clears the flash message cookie. +func GetFlash(r *http.Request, w http.ResponseWriter) (flashType, message string) { + cookie, err := r.Cookie("flash") + if err != nil || cookie.Value == "" { + return "", "" + } + // Clear the cookie + http.SetCookie(w, &http.Cookie{ + Name: "flash", + Path: "/", + MaxAge: -1, + }) + data, err := base64.StdEncoding.DecodeString(cookie.Value) + if err != nil { + return "", "" + } + parts := strings.SplitN(string(data), ":", 2) + if len(parts) != 2 { + return "", "" + } + return parts[0], parts[1] +} diff --git a/internal/templates/render.go b/internal/templates/render.go index f9872c7..3e1d1e7 100644 --- a/internal/templates/render.go +++ b/internal/templates/render.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/csrf" "github.com/mattnite/forgejo-tickets/internal/auth" + "github.com/mattnite/forgejo-tickets/internal/middleware" "github.com/mattnite/forgejo-tickets/internal/models" ) @@ -103,12 +104,8 @@ func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, name string, Data: data, } - if msg := req.URL.Query().Get("flash"); msg != "" { - flashType := req.URL.Query().Get("flash_type") - if flashType == "" { - flashType = "info" - } - pd.Flash = &Flash{Type: flashType, Message: msg} + if flashType, flashMsg := middleware.GetFlash(req, w); flashMsg != "" { + pd.Flash = &Flash{Type: flashType, Message: flashMsg} } var buf bytes.Buffer