241 lines
8.0 KiB
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)
|
|
}
|
|
}
|