Files
website/internal/handlers/lottery/syndicate/syndicate_invites.go

241 lines
8.0 KiB
Go

// 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=<syndicate_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=<invite_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=<invite_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=<syndicate_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=<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=<syndicate_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)
}
}