diff --git a/handlers/syndicate.go b/handlers/syndicate.go
index 8b21278..9f0a574 100644
--- a/handlers/syndicate.go
+++ b/handlers/syndicate.go
@@ -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
diff --git a/handlers/syndicate_invites.go b/handlers/syndicate_invites.go
index 364b392..d948995 100644
--- a/handlers/syndicate_invites.go
+++ b/handlers/syndicate_invites.go
@@ -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)
+ }
+}
diff --git a/helpers/token.go b/helpers/token.go
new file mode 100644
index 0000000..5391a84
--- /dev/null
+++ b/helpers/token.go
@@ -0,0 +1,15 @@
+package helpers
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+)
+
+func GenerateSecureToken() (string, error) {
+ b := make([]byte, 32)
+ _, err := rand.Read(b)
+ if err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(b), nil
+}
diff --git a/main.go b/main.go
index a1857bd..268b186 100644
--- a/main.go
+++ b/main.go
@@ -92,5 +92,7 @@ func setupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/syndicate/invites", middleware.Auth(true)(handlers.ViewInvitesHandler(db)))
mux.HandleFunc("/syndicate/invites/accept", middleware.Auth(true)(handlers.AcceptInviteHandler(db)))
mux.HandleFunc("/syndicate/invites/decline", middleware.Auth(true)(handlers.DeclineInviteHandler(db)))
+ mux.HandleFunc("/syndicate/invite/token", middleware.Auth(true)(handlers.GenerateInviteLinkHandler(db)))
+ mux.HandleFunc("/syndicate/join", middleware.Auth(true)(handlers.JoinSyndicateWithTokenHandler(db)))
}
diff --git a/storage/schema.go b/storage/schema.go
index ec01fea..4f841f7 100644
--- a/storage/schema.go
+++ b/storage/schema.go
@@ -209,3 +209,18 @@ CREATE TABLE IF NOT EXISTS syndicate_invites (
FOREIGN KEY(syndicate_id) REFERENCES syndicates(id),
FOREIGN KEY(invited_user_id) REFERENCES users(id)
);`
+
+const SchemaSyndicateInviteTokens = `
+CREATE TABLE IF NOT EXISTS syndicate_invite_tokens (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ syndicate_id INTEGER NOT NULL,
+ token TEXT NOT NULL UNIQUE,
+ invited_by_user_id INTEGER NOT NULL,
+ accepted_by_user_id INTEGER,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ accepted_at TIMESTAMP,
+ expires_at TIMESTAMP,
+ FOREIGN KEY (syndicate_id) REFERENCES syndicates(id),
+ FOREIGN KEY (invited_by_user_id) REFERENCES users(id),
+ FOREIGN KEY (accepted_by_user_id) REFERENCES users(id)
+);`
diff --git a/templates/syndicate/view.html b/templates/syndicate/view.html
index 6519351..7785391 100644
--- a/templates/syndicate/view.html
+++ b/templates/syndicate/view.html
@@ -20,9 +20,15 @@
Manager Controls
You can add or remove members, and manage tickets.
+
Invite Members
+
+