- 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
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"synlotto-website/helpers"
|
|
"synlotto-website/storage"
|
|
)
|
|
|
|
func SyndicateInviteHandler(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
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
|
data := BuildTemplateData(db, w, r)
|
|
context := helpers.TemplateContext(w, r, data)
|
|
context["SyndicateID"] = syndicateID
|
|
|
|
tmpl := helpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html")
|
|
err := tmpl.ExecuteTemplate(w, "layout", context)
|
|
if err != nil {
|
|
helpers.RenderError(w, r, 500)
|
|
}
|
|
case http.MethodPost:
|
|
syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
|
|
username := r.FormValue("username")
|
|
|
|
err := storage.InviteToSyndicate(db, userID, syndicateID, username)
|
|
if err != nil {
|
|
helpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
|
|
} else {
|
|
helpers.SetFlash(w, r, "Invite sent successfully.")
|
|
}
|
|
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
|
|
|
|
default:
|
|
helpers.RenderError(w, r, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := helpers.GetCurrentUserID(r)
|
|
if !ok {
|
|
helpers.RenderError(w, r, 403)
|
|
return
|
|
}
|
|
|
|
invites := storage.GetPendingInvites(db, userID)
|
|
data := BuildTemplateData(db, w, r)
|
|
context := helpers.TemplateContext(w, r, data)
|
|
context["Invites"] = invites
|
|
|
|
tmpl := helpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html")
|
|
tmpl.ExecuteTemplate(w, "layout", context)
|
|
}
|
|
}
|
|
|
|
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
|
userID, ok := helpers.GetCurrentUserID(r)
|
|
if !ok {
|
|
helpers.RenderError(w, r, 403)
|
|
return
|
|
}
|
|
err := storage.AcceptInvite(db, inviteID, userID)
|
|
if err != nil {
|
|
helpers.SetFlash(w, r, "Failed to accept invite")
|
|
} else {
|
|
helpers.SetFlash(w, r, "You have joined the syndicate")
|
|
}
|
|
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
|
_ = storage.UpdateInviteStatus(db, inviteID, "declined")
|
|
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)
|
|
}
|
|
}
|