diff --git a/handlers/account.go b/handlers/account.go index 7f33341..8a092fb 100644 --- a/handlers/account.go +++ b/handlers/account.go @@ -25,7 +25,7 @@ func Login(w http.ResponseWriter, r *http.Request) { err := tmpl.ExecuteTemplate(w, "layout", context) if err != nil { log.Println("❌ Template render error:", err) - http.Error(w, "Error rendering login page", http.StatusInternalServerError) + http.Error(w, "Error rendering login page", http.StatusInternalServerError) // Take hte flash message from licnse server this just does a black page also should be using db ahain see licvense server } return } diff --git a/handlers/admin/audit.go b/handlers/admin/audit.go index 03c2273..49e443f 100644 --- a/handlers/admin/audit.go +++ b/handlers/admin/audit.go @@ -34,7 +34,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc { } defer rows.Close() - var logs []AdminLogEntry + var logs []AdminLogEntry // ToDo should be in models for rows.Next() { var entry AdminLogEntry if err := rows.Scan(&entry.AccessedAt, &entry.UserID, &entry.Path, &entry.IP, &entry.UserAgent); err != nil { diff --git a/handlers/syndicate_invites.go b/handlers/syndicate_invites.go index d948995..375150c 100644 --- a/handlers/syndicate_invites.go +++ b/handlers/syndicate_invites.go @@ -193,3 +193,30 @@ func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc { http.Redirect(w, r, "/syndicate", http.StatusSeeOther) } } + +func ManageInviteTokensHandler(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 + } + + syndicateID := helpers.Atoi(r.URL.Query().Get("id")) + + if !storage.IsSyndicateManager(db, syndicateID, userID) { + helpers.RenderError(w, r, 403) + return + } + + tokens := storage.GetInviteTokensForSyndicate(db, syndicateID) + + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["Tokens"] = tokens + context["SyndicateID"] = syndicateID + + tmpl := helpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html") + tmpl.ExecuteTemplate(w, "layout", context) + } +} diff --git a/handlers/template_context.go b/handlers/template_context.go index 13a20f7..4c55913 100644 --- a/handlers/template_context.go +++ b/handlers/template_context.go @@ -20,7 +20,7 @@ func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) model switch v := session.Values["user_id"].(type) { case int: - user = models.GetUserByID(v) + user = models.GetUserByID(v) // ToDo should be storage not models case int64: user = models.GetUserByID(int(v)) } diff --git a/helpers/pages.go b/helpers/pages.go index 4e6ba84..0c6e3af 100644 --- a/helpers/pages.go +++ b/helpers/pages.go @@ -34,7 +34,7 @@ func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) { return } - log.Println("✅ Successfully rendered 500 page") + log.Println("✅ Successfully rendered error page") // ToDo: log these to database } //ToDo Pages.go /template.go to be merged? diff --git a/helpers/ratelimit.go b/helpers/ratelimit.go index 08f219c..d4220ca 100644 --- a/helpers/ratelimit.go +++ b/helpers/ratelimit.go @@ -17,7 +17,7 @@ func GetVisitorLimiter(ip string) *rate.Limiter { limiter, exists := visitors[ip] if !exists { - limiter = rate.NewLimiter(1, 5) + limiter = rate.NewLimiter(3, 5) visitors[ip] = limiter } return limiter diff --git a/main.go b/main.go index 268b186..6794f29 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,13 @@ package main import ( - "database/sql" "log" "net/http" "synlotto-website/handlers" - admin "synlotto-website/handlers/admin" "synlotto-website/helpers" "synlotto-website/middleware" "synlotto-website/models" + "synlotto-website/routes" "synlotto-website/storage" "github.com/gorilla/csrf" @@ -16,7 +15,7 @@ import ( func main() { db := storage.InitDB("synlotto.db") - models.SetDB(db) + models.SetDB(db) // Should be in storage not models. var isProduction = false @@ -27,13 +26,12 @@ func main() { ) mux := http.NewServeMux() - setupAdminRoutes(mux, db) - setupAccountRoutes(mux, db) - setupResultRoutes(mux, db) - setupSyndicateRoutes(mux, db) + routes.SetupAdminRoutes(mux, db) + routes.SetupAccountRoutes(mux, db) + routes.SetupResultRoutes(mux, db) + routes.SetupSyndicateRoutes(mux, db) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - mux.HandleFunc("/", handlers.Home(db)) wrapped := helpers.RateLimit(csrfMiddleware(mux)) @@ -44,55 +42,3 @@ func main() { log.Println("🌐 Running on http://localhost:8080") http.ListenAndServe(":8080", wrapped) } - -func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) { - mux.HandleFunc("/admin/access", middleware.AdminOnly(db, admin.AdminAccessLogHandler(db))) - mux.HandleFunc("/admin/audit", middleware.AdminOnly(db, admin.AuditLogHandler(db))) - mux.HandleFunc("/admin/dashboard", middleware.AdminOnly(db, admin.AdminDashboardHandler(db))) - mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db))) - - // Draw management - mux.HandleFunc("/admin/draws", middleware.AdminOnly(db, admin.ListDrawsHandler(db))) - // mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.RenderNewDrawForm(db))) - // mux.HandleFunc("/admin/draws/submit", middleware.AdminOnly(db, admin.CreateDrawHandler(db))) - mux.HandleFunc("/admin/draws/modify", middleware.AdminOnly(db, admin.ModifyDrawHandler(db))) - mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db))) - - // Prize management - mux.HandleFunc("/admin/draws/prizes/add", middleware.AdminOnly(db, admin.AddPrizesHandler(db))) - mux.HandleFunc("/admin/draws/prizes/modify", middleware.AdminOnly(db, admin.ModifyPrizesHandler(db))) -} - -func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) { - mux.HandleFunc("/login", middleware.Auth(false)(handlers.Login)) - mux.HandleFunc("/logout", handlers.Logout) - mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup)) - mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) - mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) - mux.HandleFunc("/account/messages", middleware.Auth(true)(handlers.MessagesInboxHandler(db))) - mux.HandleFunc("/account/messages/read", middleware.Auth(true)(handlers.ReadMessageHandler(db))) - mux.HandleFunc("/account/messages/archive", middleware.Auth(true)(handlers.ArchiveMessageHandler(db))) - mux.HandleFunc("/account/messages/archived", middleware.Auth(true)(handlers.ArchivedMessagesHandler(db))) - mux.HandleFunc("/account/messages/restore", middleware.Auth(true)(handlers.RestoreMessageHandler(db))) - mux.HandleFunc("/account/messages/send", middleware.Auth(true)(handlers.SendMessageHandler(db))) - mux.HandleFunc("/account/notifications", middleware.Auth(true)(handlers.NotificationsHandler(db))) - mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db))) -} - -func setupResultRoutes(mux *http.ServeMux, db *sql.DB) { - mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) -} - -func setupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) { - mux.HandleFunc("/syndicate", middleware.Auth(true)(handlers.ListSyndicatesHandler(db))) - mux.HandleFunc("/syndicate/create", middleware.Auth(true)(handlers.CreateSyndicateHandler(db))) - mux.HandleFunc("/syndicate/view", middleware.Auth(true)(handlers.ViewSyndicateHandler(db))) - mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(handlers.SyndicateTicketsHandler(db))) - mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(handlers.SyndicateLogTicketHandler(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/middleware/recover.go b/middleware/recover.go index 55f2354..0a3a9d9 100644 --- a/middleware/recover.go +++ b/middleware/recover.go @@ -13,7 +13,6 @@ func Recover(next http.Handler) http.Handler { if rec := recover(); rec != nil { log.Printf("🔥 Recovered from panic: %v\n%s", rec, debug.Stack()) - // ✅ Call your custom template-based fallback helpers.RenderError(w, r, http.StatusInternalServerError) } }() diff --git a/middleware/security.go b/middleware/security.go index 20e8055..0d2093d 100644 --- a/middleware/security.go +++ b/middleware/security.go @@ -2,7 +2,6 @@ package middleware import "net/http" -// Redirects all HTTP to HTTPS (only in production) func EnforceHTTPS(next http.Handler, enabled bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if enabled && r.Header.Get("X-Forwarded-Proto") != "https" && r.TLS == nil { diff --git a/models/syndicate.go b/models/syndicate.go index 8ed274d..8d33304 100644 --- a/models/syndicate.go +++ b/models/syndicate.go @@ -1,6 +1,9 @@ package models -import "time" +import ( + "database/sql" + "time" +) type Syndicate struct { ID int @@ -27,3 +30,12 @@ type SyndicateInvite struct { Status string CreatedAt time.Time } + +type SyndicateInviteToken struct { + Token string + InvitedByUserID int + AcceptedByUserID sql.NullInt64 + CreatedAt time.Time + ExpiresAt time.Time + AcceptedAt sql.NullTime +} diff --git a/models/user.go b/models/user.go index b05faab..a79c730 100644 --- a/models/user.go +++ b/models/user.go @@ -41,7 +41,7 @@ func SetDB(database *sql.DB) { func CreateUser(username, passwordHash string) error { _, err := db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, passwordHash) - + //ToDo: Why is SQL in here? return err } @@ -77,11 +77,11 @@ func GetUserByID(id int) *User { func LogLoginAttempt(username string, success bool) { _, err := db.Exec("INSERT INTO auditlog (username, success, timestamp) VALUES (?, ?, ?)", - username, boolToInt(success), time.Now().Format(time.RFC3339)) + username, boolToInt(success), time.Now().Format(time.RFC3339)) // tOdO: SHOULD BE USING UTC if err != nil { log.Println("❌ Failed to log login:", err) } -} +} // ToDo this shouldn't be in models. Also why did i build a bool to int? just use the bool func boolToInt(b bool) int { if b { diff --git a/routes/accountroutes.go b/routes/accountroutes.go new file mode 100644 index 0000000..e80a0c1 --- /dev/null +++ b/routes/accountroutes.go @@ -0,0 +1,25 @@ +package routes + +import ( + "database/sql" + "net/http" + + "synlotto-website/handlers" + "synlotto-website/middleware" +) + +func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/login", middleware.Auth(false)(handlers.Login)) + mux.HandleFunc("/logout", handlers.Logout) + mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup)) + mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) + mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) + mux.HandleFunc("/account/messages", middleware.Auth(true)(handlers.MessagesInboxHandler(db))) + mux.HandleFunc("/account/messages/read", middleware.Auth(true)(handlers.ReadMessageHandler(db))) + mux.HandleFunc("/account/messages/archive", middleware.Auth(true)(handlers.ArchiveMessageHandler(db))) + mux.HandleFunc("/account/messages/archived", middleware.Auth(true)(handlers.ArchivedMessagesHandler(db))) + mux.HandleFunc("/account/messages/restore", middleware.Auth(true)(handlers.RestoreMessageHandler(db))) + mux.HandleFunc("/account/messages/send", middleware.Auth(true)(handlers.SendMessageHandler(db))) + mux.HandleFunc("/account/notifications", middleware.Auth(true)(handlers.NotificationsHandler(db))) + mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db))) +} diff --git a/routes/adminroutes.go b/routes/adminroutes.go new file mode 100644 index 0000000..d66c540 --- /dev/null +++ b/routes/adminroutes.go @@ -0,0 +1,27 @@ +package routes + +import ( + "database/sql" + "net/http" + + admin "synlotto-website/handlers/admin" + "synlotto-website/middleware" +) + +func SetupAdminRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/admin/access", middleware.AdminOnly(db, admin.AdminAccessLogHandler(db))) + mux.HandleFunc("/admin/audit", middleware.AdminOnly(db, admin.AuditLogHandler(db))) + mux.HandleFunc("/admin/dashboard", middleware.AdminOnly(db, admin.AdminDashboardHandler(db))) + mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db))) + + // Draw management + mux.HandleFunc("/admin/draws", middleware.AdminOnly(db, admin.ListDrawsHandler(db))) + // mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.RenderNewDrawForm(db))) + // mux.HandleFunc("/admin/draws/submit", middleware.AdminOnly(db, admin.CreateDrawHandler(db))) + mux.HandleFunc("/admin/draws/modify", middleware.AdminOnly(db, admin.ModifyDrawHandler(db))) + mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db))) + + // Prize management + mux.HandleFunc("/admin/draws/prizes/add", middleware.AdminOnly(db, admin.AddPrizesHandler(db))) + mux.HandleFunc("/admin/draws/prizes/modify", middleware.AdminOnly(db, admin.ModifyPrizesHandler(db))) +} diff --git a/routes/resultroutes.go b/routes/resultroutes.go new file mode 100644 index 0000000..8b2c160 --- /dev/null +++ b/routes/resultroutes.go @@ -0,0 +1,12 @@ +package routes + +import ( + "database/sql" + "net/http" + + "synlotto-website/handlers" +) + +func SetupResultRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) +} diff --git a/routes/syndicateroutes.go b/routes/syndicateroutes.go new file mode 100644 index 0000000..9f1ad3a --- /dev/null +++ b/routes/syndicateroutes.go @@ -0,0 +1,24 @@ +package routes + +import ( + "database/sql" + "net/http" + + "synlotto-website/handlers" + "synlotto-website/middleware" +) + +func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/syndicate", middleware.Auth(true)(handlers.ListSyndicatesHandler(db))) + mux.HandleFunc("/syndicate/create", middleware.Auth(true)(handlers.CreateSyndicateHandler(db))) + mux.HandleFunc("/syndicate/view", middleware.Auth(true)(handlers.ViewSyndicateHandler(db))) + mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(handlers.SyndicateTicketsHandler(db))) + mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(handlers.SyndicateLogTicketHandler(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/invite/tokens", middleware.Auth(true)(handlers.ManageInviteTokensHandler(db))) + mux.HandleFunc("/syndicate/join", middleware.Auth(true)(handlers.JoinSyndicateWithTokenHandler(db))) + +} diff --git a/storage/db.go b/storage/db.go index 9541c43..ffe8334 100644 --- a/storage/db.go +++ b/storage/db.go @@ -28,6 +28,7 @@ func InitDB(filepath string) *sql.DB { SchemaSyndicates, SchemaSyndicateMembers, SchemaSyndicateInvites, + SchemaSyndicateInviteTokens, } for _, stmt := range schemas { diff --git a/storage/syndicate.go b/storage/syndicate.go index 24355c8..da5ddbf 100644 --- a/storage/syndicate.go +++ b/storage/syndicate.go @@ -60,6 +60,15 @@ func GetSyndicateByID(db *sql.DB, id int) (*models.Syndicate, error) { return &s, nil } +func IsSyndicateManager(db *sql.DB, syndicateID, userID int) bool { + var count int + err := db.QueryRow(` + SELECT COUNT(*) FROM syndicates + WHERE id = ? AND owner_id = ? + `, syndicateID, userID).Scan(&count) + return err == nil && count > 0 +} + func GetSyndicateMembers(db *sql.DB, syndicateID int) []models.SyndicateMember { rows, err := db.Query(` SELECT m.user_id, u.username, m.joined_at @@ -90,7 +99,7 @@ func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool { } func GetUserByUsername(db *sql.DB, username string) *models.User { - row := db.QueryRow(`SELECT id, username, is_admin FROM users WHERE username = ?`, username) + row := db.QueryRow(`SELECT id, username, is_admin FROM users WHERE username = ?`, username) // ToDo: needs hash var u models.User err := row.Scan(&u.Id, &u.Username, &u.IsAdmin) if err != nil { @@ -258,3 +267,31 @@ func InviteToSyndicate(db *sql.DB, inviterID, syndicateID int, username string) `, syndicateID, inviteeID) return err } + +func GetInviteTokensForSyndicate(db *sql.DB, syndicateID int) []models.SyndicateInviteToken { + rows, err := db.Query(` + SELECT token, invited_by_user_id, accepted_by_user_id, created_at, expires_at, accepted_at + FROM syndicate_invite_tokens + WHERE syndicate_id = ? + ORDER BY created_at DESC + `, syndicateID) + if err != nil { + return nil + } + defer rows.Close() + + var tokens []models.SyndicateInviteToken + for rows.Next() { + var t models.SyndicateInviteToken + _ = rows.Scan( + &t.Token, + &t.InvitedByUserID, + &t.AcceptedByUserID, + &t.CreatedAt, + &t.ExpiresAt, + &t.AcceptedAt, + ) + tokens = append(tokens, t) + } + return tokens +} diff --git a/templates/syndicate/manager/invite_links.html b/templates/syndicate/manager/invite_links.html new file mode 100644 index 0000000..67ded4a --- /dev/null +++ b/templates/syndicate/manager/invite_links.html @@ -0,0 +1,57 @@ +{{ define "content" }} +
| Invite Link | +Invited By | +Status | +Expires | +Actions | +
|---|---|---|---|---|
+ /syndicate/join?token={{ .Token }}
+ |
+ User #{{ .InvitedByUserID }} | ++ {{ if .AcceptedByUserID.Valid }} + + Accepted by User #{{ .AcceptedByUserID.Int64 }} + + {{ else if .ExpiresAt.Before (now) }} + Expired + {{ else }} + Pending + {{ end }} + | +{{ .ExpiresAt.Format "02 Jan 2006 15:04" }} | ++ {{ if not .AcceptedByUserID.Valid }} + + {{ end }} + + | +