// internal/handlers/lottery/syndicate/syndicate_invites.go package handlers import ( "fmt" "net/http" "strconv" "time" templateHandlers "synlotto-website/internal/handlers/template" "synlotto-website/internal/helpers" securityHelpers "synlotto-website/internal/helpers/security" templateHelpers "synlotto-website/internal/helpers/template" "synlotto-website/internal/platform/bootstrap" syndicateStorage "synlotto-website/internal/storage/syndicate" ) // GET /syndicate/invite?id= // POST /syndicate/invite (syndicate_id, username) func SyndicateInviteHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { templateHelpers.RenderError(w, r, http.StatusForbidden) return } switch r.Method { case http.MethodGet: syndicateID := helpers.Atoi(r.URL.Query().Get("id")) data := templateHandlers.BuildTemplateData(app, w, r) ctx := templateHelpers.TemplateContext(w, r, data) ctx["SyndicateID"] = syndicateID tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "web/templates/syndicate/invite.html") if err := tmpl.ExecuteTemplate(w, "layout", ctx); err != nil { templateHelpers.RenderError(w, r, http.StatusInternalServerError) } case http.MethodPost: syndicateID := helpers.Atoi(r.FormValue("syndicate_id")) username := r.FormValue("username") if err := syndicateStorage.InviteToSyndicate(app.DB, userID, syndicateID, username); err != nil { templateHelpers.SetFlash(r, "Failed to send invite: "+err.Error()) } else { templateHelpers.SetFlash(r, "Invite sent successfully.") } http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) default: templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed) } } } // GET /syndicate/invites func ViewInvitesHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { templateHelpers.RenderError(w, r, http.StatusForbidden) return } invites := syndicateStorage.GetPendingSyndicateInvites(app.DB, userID) data := templateHandlers.BuildTemplateData(app, w, r) ctx := templateHelpers.TemplateContext(w, r, data) ctx["Invites"] = invites tmpl := templateHelpers.LoadTemplateFiles("invites.html", "web/templates/syndicate/invites.html") _ = tmpl.ExecuteTemplate(w, "layout", ctx) } } // POST /syndicate/invites/accept?id= func AcceptInviteHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { inviteID := helpers.Atoi(r.URL.Query().Get("id")) userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { templateHelpers.RenderError(w, r, http.StatusForbidden) return } if err := syndicateStorage.AcceptInvite(app.DB, inviteID, userID); err != nil { templateHelpers.SetFlash(r, "Failed to accept invite") } else { templateHelpers.SetFlash(r, "You have joined the syndicate") } http.Redirect(w, r, "/syndicate", http.StatusSeeOther) } } // POST /syndicate/invites/decline?id= func DeclineInviteHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { inviteID := helpers.Atoi(r.URL.Query().Get("id")) _ = syndicateStorage.UpdateInviteStatus(app.DB, inviteID, "declined") http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther) } } // ===== Invite Tokens ======================================================== // (Consider moving these two helpers to internal/storage/syndicate) // Create an invite token that expires after ttlHours. func CreateInviteToken(app *bootstrap.App, syndicateID, invitedByID int, ttlHours int) (string, error) { token, err := securityHelpers.GenerateSecureToken() if err != nil { return "", err } expires := time.Now().Add(time.Duration(ttlHours) * time.Hour) _, err = app.DB.Exec(` INSERT INTO syndicate_invite_tokens (syndicate_id, token, invited_by_user_id, expires_at) VALUES (?, ?, ?, ?) `, syndicateID, token, invitedByID, expires) return token, err } // Validate + consume a token to join a syndicate. func AcceptInviteToken(app *bootstrap.App, token string, userID int) error { var syndicateID int var expiresAt, acceptedAt struct { Valid bool Time time.Time } // Note: using separate variables to avoid importing database/sql here. row := app.DB.QueryRow(` SELECT syndicate_id, expires_at, accepted_at FROM syndicate_invite_tokens WHERE token = ? `, token) if err := row.Scan(&syndicateID, &expiresAt.Time, &acceptedAt.Time); err != nil { return fmt.Errorf("invalid or expired token") } // If driver returns zero time when NULL, treat missing as invalid.Valid=false expiresAt.Valid = !expiresAt.Time.IsZero() acceptedAt.Valid = !acceptedAt.Time.IsZero() if acceptedAt.Valid || (expiresAt.Valid && expiresAt.Time.Before(time.Now())) { return fmt.Errorf("token already used or expired") } if _, err := app.DB.Exec(` INSERT INTO syndicate_members (syndicate_id, user_id, role, status, joined_at) VALUES (?, ?, 'member', 'active', CURRENT_TIMESTAMP) `, syndicateID, userID); err != nil { return err } _, err := app.DB.Exec(` UPDATE syndicate_invite_tokens SET accepted_by_user_id = ?, accepted_at = CURRENT_TIMESTAMP WHERE token = ? `, userID, token) return err } // GET /syndicate/invite/token?id= func GenerateInviteLinkHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { templateHelpers.RenderError(w, r, http.StatusForbidden) return } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) token, err := CreateInviteToken(app, syndicateID, userID, 48) if err != nil { templateHelpers.SetFlash(r, "Failed to generate invite link.") http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) return } scheme := "http://" if r.TLS != nil { scheme = "https://" } inviteLink := fmt.Sprintf("%s%s/syndicate/join?token=%s", scheme, r.Host, token) templateHelpers.SetFlash(r, "Invite link created: "+inviteLink) http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) } } // GET /syndicate/join?token= func JoinSyndicateWithTokenHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { templateHelpers.RenderError(w, r, http.StatusForbidden) return } token := r.URL.Query().Get("token") if token == "" { templateHelpers.SetFlash(r, "Invalid or missing invite token.") http.Redirect(w, r, "/syndicate", http.StatusSeeOther) return } if err := AcceptInviteToken(app, token, userID); err != nil { templateHelpers.SetFlash(r, "Failed to join syndicate: "+err.Error()) } else { templateHelpers.SetFlash(r, "You have joined the syndicate!") } http.Redirect(w, r, "/syndicate", http.StatusSeeOther) } } // GET /syndicate/invite/tokens?id= func ManageInviteTokensHandler(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { templateHelpers.RenderError(w, r, http.StatusForbidden) return } syndicateID := helpers.Atoi(r.URL.Query().Get("id")) if !syndicateStorage.IsSyndicateManager(app.DB, syndicateID, userID) { templateHelpers.RenderError(w, r, http.StatusForbidden) return } tokens := syndicateStorage.GetInviteTokensForSyndicate(app.DB, syndicateID) data := templateHandlers.BuildTemplateData(app, w, r) ctx := templateHelpers.TemplateContext(w, r, data) ctx["Tokens"] = tokens ctx["SyndicateID"] = syndicateID tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "web/templates/syndicate/invite_links.html") _ = tmpl.ExecuteTemplate(w, "layout", ctx) } }