Add Syndicate Invite Token System (Secure Links)

- Added route + handler: GenerateInviteLinkHandler to create signed tokens
- Added handler: JoinSyndicateWithTokenHandler to join using invite token
- Integrated secure token generation via helpers.GenerateSecureToken()
- Created DB model: syndicate_invite_tokens (assumed pre-existing)
- Updated syndicate view template to allow managers to generate links
- Flash messaging for invite success/failure
- Invite links are scoped to manager role and valid for 48 hours
This commit is contained in:
2025-04-04 22:33:40 +01:00
parent 8b02a3137d
commit 152c5cb18c
6 changed files with 146 additions and 6 deletions

View File

@@ -56,7 +56,6 @@ func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
managed := storage.GetSyndicatesByOwner(db, userID)
member := storage.GetSyndicatesByMember(db, userID)
// Filter out syndicates where the user is both the owner and a member
managedMap := make(map[int]bool)
for _, s := range managed {
managedMap[s.ID] = true
@@ -181,7 +180,6 @@ func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc {
return
}
// Check membership
if !storage.IsSyndicateMember(db, syndicateID, userID) {
helpers.RenderError(w, r, 403)
return

View File

@@ -2,8 +2,11 @@ package handlers
import (
"database/sql"
"fmt"
"net/http"
"strconv"
"time"
"synlotto-website/helpers"
"synlotto-website/storage"
)
@@ -89,3 +92,104 @@ func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther)
}
}
func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) {
token, err := helpers.GenerateSecureToken()
if err != nil {
return "", err
}
expires := time.Now().Add(time.Duration(ttlHours) * time.Hour)
_, err = db.Exec(`
INSERT INTO syndicate_invite_tokens (syndicate_id, token, invited_by_user_id, expires_at)
VALUES (?, ?, ?, ?)
`, syndicateID, token, invitedByID, expires)
return token, err
}
func AcceptInviteToken(db *sql.DB, token string, userID int) error {
var syndicateID int
var expiresAt, acceptedAt sql.NullTime
err := db.QueryRow(`
SELECT syndicate_id, expires_at, accepted_at
FROM syndicate_invite_tokens
WHERE token = ?
`, token).Scan(&syndicateID, &expiresAt, &acceptedAt)
if err != nil {
return fmt.Errorf("invalid or expired token")
}
if acceptedAt.Valid || expiresAt.Time.Before(time.Now()) {
return fmt.Errorf("token already used or expired")
}
_, err = db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, role, status, joined_at)
VALUES (?, ?, 'member', 'active', CURRENT_TIMESTAMP)
`, syndicateID, userID)
if err != nil {
return err
}
_, err = db.Exec(`
UPDATE syndicate_invite_tokens
SET accepted_by_user_id = ?, accepted_at = CURRENT_TIMESTAMP
WHERE token = ?
`, userID, token)
return err
}
func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, http.StatusForbidden)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
token, err := CreateInviteToken(db, syndicateID, userID, 48) // token valid for 48 hours
if err != nil {
helpers.SetFlash(w, r, "Failed to generate invite link.")
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
return
}
origin := r.Host
if r.TLS != nil {
origin = "https://" + origin
} else {
origin = "http://" + origin
}
inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token)
helpers.SetFlash(w, r, "Invite link created: "+inviteLink)
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
}
}
func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, http.StatusForbidden)
return
}
token := r.URL.Query().Get("token")
if token == "" {
helpers.SetFlash(w, r, "Invalid or missing invite token.")
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
return
}
err := AcceptInviteToken(db, token, userID)
if err != nil {
helpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
} else {
helpers.SetFlash(w, r, "You have joined the syndicate!")
}
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
}
}