Merge pull request 'Use session-based flash messages instead of query params' (#46) from fix/session-flash into main

Reviewed-on: https://git.ts.mattnite.net/mattnite/forgejo-tickets/pulls/46
This commit is contained in:
Matthew Knight 2026-02-18 00:24:02 +00:00
commit dca569b278
5 changed files with 68 additions and 49 deletions

View File

@ -4,12 +4,12 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mattnite/forgejo-tickets/internal/forgejo" "github.com/mattnite/forgejo-tickets/internal/forgejo"
"github.com/mattnite/forgejo-tickets/internal/middleware"
"github.com/mattnite/forgejo-tickets/internal/models" "github.com/mattnite/forgejo-tickets/internal/models"
"github.com/rs/zerolog/log" "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") log.Error().Err(err).Msg("send approval email error")
} }
redirectURL := "/users/pending?" + url.Values{ middleware.SetFlash(c, "success", "User "+user.Email+" has been approved")
"flash": {"User " + user.Email + " has been approved"}, c.Redirect(http.StatusSeeOther, "/users/pending")
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
} }
func (h *UserHandler) Reject(c *gin.Context) { func (h *UserHandler) Reject(c *gin.Context) {
@ -222,11 +219,8 @@ func (h *UserHandler) Reject(c *gin.Context) {
return return
} }
redirectURL := "/users/pending?" + url.Values{ middleware.SetFlash(c, "success", "User request has been rejected")
"flash": {"User request has been rejected"}, c.Redirect(http.StatusSeeOther, "/users/pending")
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
} }
func (h *UserHandler) UpdateRepos(c *gin.Context) { 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}) h.deps.DB.Create(&models.UserRepo{UserID: userID, RepoID: repoID})
} }
redirectURL := "/users/" + userID.String() + "?" + url.Values{ middleware.SetFlash(c, "success", "Project assignments updated")
"flash": {"Project assignments updated"}, c.Redirect(http.StatusSeeOther, "/users/"+userID.String())
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
} }

View File

@ -2,11 +2,11 @@ package public
import ( import (
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mattnite/forgejo-tickets/internal/auth" "github.com/mattnite/forgejo-tickets/internal/auth"
"github.com/mattnite/forgejo-tickets/internal/middleware"
"github.com/mattnite/forgejo-tickets/internal/models" "github.com/mattnite/forgejo-tickets/internal/models"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -111,11 +111,8 @@ func (h *AuthHandler) Register(c *gin.Context) {
} }
} }
redirectURL := "/login?" + url.Values{ middleware.SetFlash(c, "success", "Account requested! Please check your email to verify your address. After verification, an admin will review your request.")
"flash": {"Account requested! Please check your email to verify your address. After verification, an admin will review your request."}, c.Redirect(http.StatusSeeOther, "/login")
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
} }
func (h *AuthHandler) Logout(c *gin.Context) { func (h *AuthHandler) Logout(c *gin.Context) {
@ -138,11 +135,8 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
return return
} }
redirectURL := "/login?" + url.Values{ middleware.SetFlash(c, "success", "Email verified successfully. Your account is pending admin approval.")
"flash": {"Email verified successfully. Your account is pending admin approval."}, c.Redirect(http.StatusSeeOther, "/login")
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
} }
func (h *AuthHandler) ForgotPasswordForm(c *gin.Context) { func (h *AuthHandler) ForgotPasswordForm(c *gin.Context) {
@ -206,9 +200,6 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
return return
} }
redirectURL := "/login?" + url.Values{ middleware.SetFlash(c, "success", "Password reset successfully. You can now log in.")
"flash": {"Password reset successfully. You can now log in."}, c.Redirect(http.StatusSeeOther, "/login")
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
} }

View File

@ -4,11 +4,11 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mattnite/forgejo-tickets/internal/auth" "github.com/mattnite/forgejo-tickets/internal/auth"
"github.com/mattnite/forgejo-tickets/internal/middleware"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/oauth2" "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) user, err := h.deps.Auth.FindOrCreateOAuthUser(c.Request.Context(), provider.Name, info)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "pending admin approval") { if strings.Contains(err.Error(), "pending admin approval") {
redirectURL := "/login?" + url.Values{ middleware.SetFlash(c, "info", err.Error())
"flash": {err.Error()}, c.Redirect(http.StatusSeeOther, "/login")
"flash_type": {"info"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
return return
} }
log.Error().Err(err).Msg("find or create oauth user error") 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) user, err := h.deps.Auth.FindOrCreateOAuthUser(c.Request.Context(), "apple", info)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "pending admin approval") { if strings.Contains(err.Error(), "pending admin approval") {
redirectURL := "/login?" + url.Values{ middleware.SetFlash(c, "info", err.Error())
"flash": {err.Error()}, c.Redirect(http.StatusSeeOther, "/login")
"flash_type": {"info"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
return return
} }
log.Error().Err(err).Msg("find or create apple user error") log.Error().Err(err).Msg("find or create apple user error")

View File

@ -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]
}

View File

@ -12,6 +12,7 @@ import (
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"github.com/mattnite/forgejo-tickets/internal/auth" "github.com/mattnite/forgejo-tickets/internal/auth"
"github.com/mattnite/forgejo-tickets/internal/middleware"
"github.com/mattnite/forgejo-tickets/internal/models" "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, Data: data,
} }
if msg := req.URL.Query().Get("flash"); msg != "" { if flashType, flashMsg := middleware.GetFlash(req, w); flashMsg != "" {
flashType := req.URL.Query().Get("flash_type") pd.Flash = &Flash{Type: flashType, Message: flashMsg}
if flashType == "" {
flashType = "info"
}
pd.Flash = &Flash{Type: flashType, Message: msg}
} }
var buf bytes.Buffer var buf bytes.Buffer