@@ -39,13 +39,14 @@ import (
3939 " fmt"
4040 " log"
4141 " net/http"
42+ " net/url"
4243 " os"
4344 " time"
4445
4546 " {{.ModuleName}}/database/models"
46- " {{.ModuleName}}/shared /email"
47+ " github.com/livetemplate/lvt/pkg /email"
4748 {{- if .EnablePassword }}
48- " {{.ModuleName}}/shared /password"
49+ " github.com/livetemplate/lvt/pkg /password"
4950 {{- end }}
5051 " github.com/go-playground/validator/v10"
5152 " github.com/google/uuid"
@@ -69,6 +70,8 @@ type {{.StructName}}State struct {
6970 Token string ` json:"token"`
7071 ShowMagicLink bool ` json:"show_magic_link"`
7172 ShowPassword bool ` json:"show_password"`
73+ FlashError string ` json:"flash_error"`
74+ FlashSuccess string ` json:"flash_success"`
7275}
7376
7477func New{{.StructName }}Controller(queries *models.Queries , emailSender email.EmailSender , baseURL string) *{{.StructName }}Controller {
@@ -105,24 +108,8 @@ var successMessages = map[string]string{
105108}
106109
107110// Mount is called once per session to initialize state.
108- // It reads query params from redirects (e .g. , ?error = invalid_credentials) and sets flash messages .
111+ // Flash messages are handled via cookies in the HTTP handler for proper one-time display .
109112func (c *{{.StructName }}Controller) Mount(state {{.StructName }}State, ctx *livetemplate.Context ) ({{.StructName }}State, error) {
110- // Check for error code from redirect
111- if errCode := ctx.GetString (" error" ); errCode != " " {
112- if msg, ok := errorMessages[errCode]; ok {
113- ctx.SetFlash (" error" , msg)
114- } else {
115- ctx.SetFlash (" error" , " An error occurred" )
116- }
117- }
118-
119- // Check for success code from redirect
120- if successCode := ctx.GetString (" success" ); successCode != " " {
121- if msg, ok := successMessages[successCode]; ok {
122- ctx.SetFlash (" success" , msg)
123- }
124- }
125-
126113 return state, nil
127114}
128115
@@ -766,16 +753,123 @@ func Handler(queries *models.Queries) http.Handler {
766753 }
767754
768755 // Return handler that clones template per-request
769- // Query params (?error= xxx, ?success= xxx) are handled in Mount via ctx.GetString ()
770756 return http.HandlerFunc (func(w http.ResponseWriter , r *http.Request ) {
757+ // Handle error/success query params by setting flash cookies and redirecting to clean URL
758+ errCode := r.URL.Query ().Get (" error" )
759+ successCode := r.URL.Query ().Get (" success" )
760+
761+ if errCode != " " || successCode != " " {
762+ // Clear livetemplate-id to force new session on next request
763+ // This ensures the flash message will be properly displayed
764+ http.SetCookie (w, &http.Cookie {
765+ Name: " livetemplate-id" ,
766+ Value: " " ,
767+ Path: " /" ,
768+ MaxAge: -1,
769+ })
770+
771+ // Set flash message in cookie (URL-encoded to avoid quoting issues)
772+ if errCode != " " {
773+ if msg, ok := errorMessages[errCode]; ok {
774+ http.SetCookie (w, &http.Cookie {
775+ Name: " flash_error" ,
776+ Value: url.QueryEscape (msg),
777+ Path: " /" ,
778+ MaxAge: 10, // Short-lived
779+ HttpOnly: true,
780+ })
781+ }
782+ }
783+ if successCode != " " {
784+ if msg, ok := successMessages[successCode]; ok {
785+ http.SetCookie (w, &http.Cookie {
786+ Name: " flash_success" ,
787+ Value: url.QueryEscape (msg),
788+ Path: " /" ,
789+ MaxAge: 10,
790+ HttpOnly: true,
791+ })
792+ }
793+ }
794+
795+ // Set marker so we know flash was triggered (for one-time display)
796+ http.SetCookie (w, &http.Cookie {
797+ Name: " flash_pending" ,
798+ Value: " 1" ,
799+ Path: " /" ,
800+ MaxAge: 10,
801+ HttpOnly: true,
802+ })
803+
804+ // Redirect to clean URL
805+ http.Redirect (w, r, " /auth" , http.StatusSeeOther )
806+ return
807+ }
808+
771809 tmpl, err := baseTmpl.Clone ()
772810 if err != nil {
773811 log.Printf (" Failed to clone auth template: %v " , err)
774812 http.Error (w, " Internal server error" , http.StatusInternalServerError )
775813 return
776814 }
777815
816+ // Only process flash logic for GET requests (not HEAD/WebSocket preflight)
817+ if r.Method == " GET" {
818+ // Check if this is a fresh flash display or a reload
819+ _, errFlashPending := r.Cookie (" flash_pending" )
820+ _, errSession := r.Cookie (" livetemplate-id" )
821+
822+ hasFlashPending := errFlashPending == nil // true if cookie exists
823+ hasSession := errSession == nil // true if cookie exists
824+
825+ // If we have a session but no flash_pending marker, this is a reload
826+ // Force new session to clear any stale flash from previous session
827+ if hasSession && !hasFlashPending {
828+ http.SetCookie (w, &http.Cookie {
829+ Name: " livetemplate-id" ,
830+ Value: " " ,
831+ Path: " /" ,
832+ MaxAge: -1,
833+ })
834+ }
835+
836+ // Clear the flash_pending marker after first use
837+ if hasFlashPending {
838+ http.SetCookie (w, &http.Cookie {
839+ Name: " flash_pending" ,
840+ Value: " " ,
841+ Path: " /" ,
842+ MaxAge: -1,
843+ })
844+ }
845+ }
846+
847+ // Read and clear flash cookies, set in initial state
778848 state := New{{.StructName }}State()
849+
850+ if cookie, err := r.Cookie (" flash_error" ); err == nil && cookie.Value != " " {
851+ if msg, err := url.QueryUnescape (cookie.Value ); err == nil {
852+ state.FlashError = msg
853+ }
854+ http.SetCookie (w, &http.Cookie {
855+ Name: " flash_error" ,
856+ Value: " " ,
857+ Path: " /" ,
858+ MaxAge: -1,
859+ })
860+ }
861+ if cookie, err := r.Cookie (" flash_success" ); err == nil && cookie.Value != " " {
862+ if msg, err := url.QueryUnescape (cookie.Value ); err == nil {
863+ state.FlashSuccess = msg
864+ }
865+ http.SetCookie (w, &http.Cookie {
866+ Name: " flash_success" ,
867+ Value: " " ,
868+ Path: " /" ,
869+ MaxAge: -1,
870+ })
871+ }
872+
779873 tmpl.Handle (controller, livetemplate.AsState (state)).ServeHTTP (w, r)
780874 })
781875}
0 commit comments