Massive refactor!

This commit is contained in:
2025-04-22 23:26:11 +01:00
parent 05bb05d45c
commit 5c3a847900
42 changed files with 597 additions and 301 deletions

32
bootstrap/license.go Normal file
View File

@@ -0,0 +1,32 @@
package bootstrap
import (
"log"
"time"
internal "synlotto-website/internal/licensecheck"
"synlotto-website/models"
)
var globalChecker *internal.LicenseChecker
func InitLicenseChecker(config *models.Config) error {
checker := &internal.LicenseChecker{
LicenseAPIURL: config.License.APIURL,
APIKey: config.License.APIKey,
PollInterval: 10 * time.Minute,
}
if err := checker.Validate(); err != nil {
return err
}
checker.StartBackgroundCheck()
globalChecker = checker
log.Println("✅ License validation started.")
return nil
}
func GetLicenseChecker() *internal.LicenseChecker {
return globalChecker
}

View File

@@ -3,23 +3,27 @@ package handlers
import ( import (
"log" "log"
"net/http" "net/http"
"synlotto-website/helpers"
"synlotto-website/models"
"time" "time"
securityHelpers "license-server/helpers/security"
httpHelpers "synlotto-website/helpers/http"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models"
"synlotto-website/storage"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
func Login(w http.ResponseWriter, r *http.Request) { func Login(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
session, _ := helpers.GetSession(w, r) session, _ := httpHelpers.GetSession(w, r)
if _, ok := session.Values["user_id"].(int); ok { if _, ok := session.Values["user_id"].(int); ok {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
tmpl := helpers.LoadTemplateFiles("login.html", "templates/account/login.html") tmpl := templateHelpers.LoadTemplateFiles("login.html", "templates/account/login.html")
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
context["csrfField"] = csrf.TemplateField(r) context["csrfField"] = csrf.TemplateField(r)
err := tmpl.ExecuteTemplate(w, "layout", context) err := tmpl.ExecuteTemplate(w, "layout", context)
@@ -34,12 +38,12 @@ func Login(w http.ResponseWriter, r *http.Request) {
password := r.FormValue("password") password := r.FormValue("password")
user := models.GetUserByUsername(username) user := models.GetUserByUsername(username)
if user == nil || !helpers.CheckPasswordHash(user.PasswordHash, password) { if user == nil || !securityHelpers.CheckPasswordHash(user.PasswordHash, password) {
http.Error(w, "Invalid credentials", http.StatusUnauthorized) http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return return
} }
session, _ := helpers.GetSession(w, r) session, _ := httpHelpers.GetSession(w, r)
for k := range session.Values { for k := range session.Values {
delete(session.Values, k) delete(session.Values, k)
@@ -65,18 +69,18 @@ func Login(w http.ResponseWriter, r *http.Request) {
} }
} }
if user == nil || !helpers.CheckPasswordHash(user.PasswordHash, password) { if user == nil || !securityHelpers.CheckPasswordHash(user.PasswordHash, password) {
models.LogLoginAttempt(username, false) storage.LogLoginAttempt(username, false)
http.Error(w, "Invalid credentials", http.StatusUnauthorized) http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return return
} }
models.LogLoginAttempt(username, true) storage.LogLoginAttempt(username, true)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
} }
func Logout(w http.ResponseWriter, r *http.Request) { func Logout(w http.ResponseWriter, r *http.Request) {
session, _ := helpers.GetSession(w, r) session, _ := httpHelpers.GetSession(w, r)
for k := range session.Values { for k := range session.Values {
delete(session.Values, k) delete(session.Values, k)
@@ -95,7 +99,7 @@ func Logout(w http.ResponseWriter, r *http.Request) {
func Signup(w http.ResponseWriter, r *http.Request) { func Signup(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
tmpl := helpers.LoadTemplateFiles("signup.html", "templates/account/signup.html") tmpl := templateHelpers.LoadTemplateFiles("signup.html", "templates/account/signup.html")
tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
"csrfField": csrf.TemplateField(r), "csrfField": csrf.TemplateField(r),
@@ -106,7 +110,7 @@ func Signup(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username") username := r.FormValue("username")
password := r.FormValue("password") password := r.FormValue("password")
hashed, err := helpers.HashPassword(password) hashed, err := securityHelpers.HashPassword(password)
if err != nil { if err != nil {
http.Error(w, "Server error", http.StatusInternalServerError) http.Error(w, "Server error", http.StatusInternalServerError)
return return

View File

@@ -4,7 +4,9 @@ import (
"database/sql" "database/sql"
"log" "log"
"net/http" "net/http"
"synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/middleware" "synlotto-website/middleware"
"synlotto-website/models" "synlotto-website/models"
) )
@@ -19,7 +21,7 @@ type AdminLogEntry struct {
func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc { func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
rows, err := db.Query(` rows, err := db.Query(`
SELECT accessed_at, user_id, path, ip, user_agent SELECT accessed_at, user_id, path, ip, user_agent
@@ -45,7 +47,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
} }
context["AuditLogs"] = logs context["AuditLogs"] = logs
tmpl := helpers.LoadTemplateFiles("access_log.html", "templates/admin/logs/access_log.html") tmpl := templateHelpers.LoadTemplateFiles("access_log.html", "templates/admin/logs/access_log.html")
_ = tmpl.ExecuteTemplate(w, "layout", context) _ = tmpl.ExecuteTemplate(w, "layout", context)
}) })
@@ -53,7 +55,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
func AuditLogHandler(db *sql.DB) http.HandlerFunc { func AuditLogHandler(db *sql.DB) http.HandlerFunc {
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
rows, err := db.Query(` rows, err := db.Query(`
SELECT timestamp, user_id, action, ip, user_agent SELECT timestamp, user_id, action, ip, user_agent
@@ -81,7 +83,7 @@ func AuditLogHandler(db *sql.DB) http.HandlerFunc {
context["AuditLogs"] = logs context["AuditLogs"] = logs
tmpl := helpers.LoadTemplateFiles("audit.html", "templates/admin/logs/audit.html") tmpl := templateHelpers.LoadTemplateFiles("audit.html", "templates/admin/logs/audit.html")
err = tmpl.ExecuteTemplate(w, "layout", context) err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -6,12 +6,13 @@ import (
"net/http" "net/http"
helpers "synlotto-website/helpers" helpers "synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models" "synlotto-website/models"
) )
func AdminDashboardHandler(db *sql.DB) http.HandlerFunc { func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
// userID, ok := helpers.GetCurrentUserID(r) // userID, ok := securityHelpers.GetCurrentUserID(r)
// if !ok { // if !ok {
// http.Redirect(w, r, "/login", http.StatusSeeOther) // http.Redirect(w, r, "/login", http.StatusSeeOther)
// return // return
@@ -19,7 +20,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
// TODO: check is_admin from users table here // TODO: check is_admin from users table here
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
// Total ticket stats // Total ticket stats
var total, winners int var total, winners int
@@ -54,7 +55,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
} }
context["MatchLogs"] = logs context["MatchLogs"] = logs
tmpl := helpers.LoadTemplateFiles("dashboard.html", "templates/admin/dashboard.html") tmpl := templateHelpers.LoadTemplateFiles("dashboard.html", "templates/admin/dashboard.html")
err = tmpl.ExecuteTemplate(w, "layout", context) err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -6,12 +6,14 @@ import (
"net/http" "net/http"
helpers "synlotto-website/helpers" helpers "synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models" "synlotto-website/models"
) )
func NewDrawHandler(db *sql.DB) http.HandlerFunc { func NewDrawHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
game := r.FormValue("game_type") game := r.FormValue("game_type")
@@ -30,7 +32,7 @@ func NewDrawHandler(db *sql.DB) http.HandlerFunc {
return return
} }
tmpl := helpers.LoadTemplateFiles("new_draw", "templates/admin/draws/new_draw.html") tmpl := templateHelpers.LoadTemplateFiles("new_draw", "templates/admin/draws/new_draw.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
}) })
@@ -72,7 +74,7 @@ func DeleteDrawHandler(db *sql.DB) http.HandlerFunc {
func ListDrawsHandler(db *sql.DB) http.HandlerFunc { func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
draws := []models.DrawSummary{} draws := []models.DrawSummary{}
rows, err := db.Query(` rows, err := db.Query(`
@@ -100,7 +102,7 @@ func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
context["Draws"] = draws context["Draws"] = draws
tmpl := helpers.LoadTemplateFiles("list.html", "templates/admin/draws/list.html") tmpl := templateHelpers.LoadTemplateFiles("list.html", "templates/admin/draws/list.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
}) })

View File

@@ -8,14 +8,15 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"synlotto-website/helpers" templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models"
services "synlotto-website/services/tickets" services "synlotto-website/services/tickets"
"synlotto-website/models"
) )
func AdminTriggersHandler(db *sql.DB) http.HandlerFunc { func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
if flash := r.URL.Query().Get("flash"); flash != "" { if flash := r.URL.Query().Get("flash"); flash != "" {
context["Flash"] = flash context["Flash"] = flash
@@ -71,7 +72,7 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
return return
} }
tmpl := helpers.LoadTemplateFiles("triggers.html", "templates/admin/triggers.html") tmpl := templateHelpers.LoadTemplateFiles("triggers.html", "templates/admin/triggers.html")
err := tmpl.ExecuteTemplate(w, "layout", context) err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"synlotto-website/helpers" helpers "synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models" "synlotto-website/models"
) )
func AddPrizesHandler(db *sql.DB) http.HandlerFunc { func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
tmpl := helpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html") tmpl := templateHelpers.LoadTemplateFiles("add_prizes.html", "templates/admin/draws/prizes/add_prizes.html")
tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r, models.TemplateData{})) tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, models.TemplateData{}))
return return
} }
@@ -44,9 +45,9 @@ func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc { func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
tmpl := helpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html") tmpl := templateHelpers.LoadTemplateFiles("modify_prizes.html", "templates/admin/draws/prizes/modify_prizes.html")
tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r, models.TemplateData{})) tmpl.ExecuteTemplate(w, "layout", templateHelpers.TemplateContext(w, r, models.TemplateData{}))
return return
} }

View File

@@ -4,15 +4,17 @@ import (
"database/sql" "database/sql"
"log" "log"
"net/http" "net/http"
"synlotto-website/helpers"
templateHandlers "synlotto-website/handlers/template"
templateHelpers "synlotto-website/helpers/template"
) )
func Home(db *sql.DB) http.HandlerFunc { func Home(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
tmpl := helpers.LoadTemplateFiles("index.html", "templates/index.html") tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/index.html")
err := tmpl.ExecuteTemplate(w, "layout", context) err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -5,6 +5,8 @@ import (
"log" "log"
"net/http" "net/http"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
) )
@@ -13,11 +15,11 @@ func NewDraw(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Println("➡️ New draw form opened") log.Println("➡️ New draw form opened")
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
context["Page"] = "new_draw" context["Page"] = "new_draw"
context["Data"] = nil context["Data"] = nil
tmpl := helpers.LoadTemplateFiles("new_draw.html", "templates/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future. tmpl := templateHelpers.LoadTemplateFiles("new_draw.html", "templates/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future.
err := tmpl.ExecuteTemplate(w, "layout", context) err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -5,6 +5,11 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
templateHandlers "synlotto-website/handlers/template"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
"synlotto-website/storage" "synlotto-website/storage"
@@ -14,18 +19,18 @@ func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
tmpl := helpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html") tmpl := templateHelpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
case http.MethodPost: case http.MethodPost:
name := r.FormValue("name") name := r.FormValue("name")
description := r.FormValue("description") description := r.FormValue("description")
userId, ok := helpers.GetCurrentUserID(r) userId, ok := securityHelpers.GetCurrentUserID(r)
if !ok || name == "" { if !ok || name == "" {
helpers.SetFlash(w, r, "Invalid data submitted") templateHelpers.SetFlash(w, r, "Invalid data submitted")
http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther) http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther)
return return
} }
@@ -33,23 +38,23 @@ func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
_, err := storage.CreateSyndicate(db, userId, name, description) _, err := storage.CreateSyndicate(db, userId, name, description)
if err != nil { if err != nil {
log.Printf("❌ CreateSyndicate failed: %v", err) log.Printf("❌ CreateSyndicate failed: %v", err)
helpers.SetFlash(w, r, "Failed to create syndicate") templateHelpers.SetFlash(w, r, "Failed to create syndicate")
} else { } else {
helpers.SetFlash(w, r, "Syndicate created successfully") templateHelpers.SetFlash(w, r, "Syndicate created successfully")
} }
http.Redirect(w, r, "/syndicate", http.StatusSeeOther) http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
default: default:
helpers.RenderError(w, r, http.StatusMethodNotAllowed) templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
} }
} }
} }
func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc { func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors. templateHelpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors.
return return
} }
@@ -68,28 +73,28 @@ func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
} }
} }
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["ManagedSyndicates"] = managed context["ManagedSyndicates"] = managed
context["JoinedSyndicates"] = filteredJoined context["JoinedSyndicates"] = filteredJoined
tmpl := helpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html") tmpl := templateHelpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
} }
func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc { func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
syndicateID := helpers.Atoi(r.URL.Query().Get("id")) syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
syndicate, err := storage.GetSyndicateByID(db, syndicateID) syndicate, err := storage.GetSyndicateByID(db, syndicateID)
if err != nil || syndicate == nil { if err != nil || syndicate == nil {
helpers.RenderError(w, r, 404) templateHelpers.RenderError(w, r, 404)
return return
} }
@@ -97,45 +102,45 @@ func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc {
isMember := storage.IsSyndicateMember(db, syndicateID, userID) isMember := storage.IsSyndicateMember(db, syndicateID, userID)
if !isManager && !isMember { if !isManager && !isMember {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
members := storage.GetSyndicateMembers(db, syndicateID) members := storage.GetSyndicateMembers(db, syndicateID)
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Syndicate"] = syndicate context["Syndicate"] = syndicate
context["Members"] = members context["Members"] = members
context["IsManager"] = isManager context["IsManager"] = isManager
tmpl := helpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html") tmpl := templateHelpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
} }
func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc { func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
syndicateId := helpers.Atoi(r.URL.Query().Get("id")) syndicateId := helpers.Atoi(r.URL.Query().Get("id"))
syndicate, err := storage.GetSyndicateByID(db, syndicateId) syndicate, err := storage.GetSyndicateByID(db, syndicateId)
if err != nil || syndicate.OwnerID != userID { if err != nil || syndicate.OwnerID != userID {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Syndicate"] = syndicate context["Syndicate"] = syndicate
tmpl := helpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html") tmpl := templateHelpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
case http.MethodPost: case http.MethodPost:
@@ -153,46 +158,46 @@ func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc {
}) })
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to add ticket.") templateHelpers.SetFlash(w, r, "Failed to add ticket.")
} else { } else {
helpers.SetFlash(w, r, "Ticket added for syndicate.") templateHelpers.SetFlash(w, r, "Ticket added for syndicate.")
} }
http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther) http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther)
default: default:
helpers.RenderError(w, r, 405) templateHelpers.RenderError(w, r, 405)
} }
} }
} }
func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc { func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
syndicateID := helpers.Atoi(r.URL.Query().Get("id")) syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
if syndicateID == 0 { if syndicateID == 0 {
helpers.RenderError(w, r, 400) templateHelpers.RenderError(w, r, 400)
return return
} }
if !storage.IsSyndicateMember(db, syndicateID, userID) { if !storage.IsSyndicateMember(db, syndicateID, userID) {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
tickets := storage.GetSyndicateTickets(db, syndicateID) tickets := storage.GetSyndicateTickets(db, syndicateID)
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["SyndicateID"] = syndicateID context["SyndicateID"] = syndicateID
context["Tickets"] = tickets context["Tickets"] = tickets
tmpl := helpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html") tmpl := templateHelpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
} }

View File

@@ -7,29 +7,33 @@ import (
"strconv" "strconv"
"time" "time"
templateHandlers "synlotto-website/handlers/template"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/storage" "synlotto-website/storage"
) )
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, http.StatusForbidden) templateHelpers.RenderError(w, r, http.StatusForbidden)
return return
} }
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
syndicateID := helpers.Atoi(r.URL.Query().Get("id")) syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["SyndicateID"] = syndicateID context["SyndicateID"] = syndicateID
tmpl := helpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html") tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html")
err := tmpl.ExecuteTemplate(w, "layout", context) err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {
helpers.RenderError(w, r, 500) templateHelpers.RenderError(w, r, 500)
} }
case http.MethodPost: case http.MethodPost:
syndicateID := helpers.Atoi(r.FormValue("syndicate_id")) syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
@@ -37,32 +41,32 @@ func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
err := storage.InviteToSyndicate(db, userID, syndicateID, username) err := storage.InviteToSyndicate(db, userID, syndicateID, username)
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to send invite: "+err.Error()) templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
} else { } else {
helpers.SetFlash(w, r, "Invite sent successfully.") templateHelpers.SetFlash(w, r, "Invite sent successfully.")
} }
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
default: default:
helpers.RenderError(w, r, http.StatusMethodNotAllowed) templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
} }
} }
} }
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc { func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
invites := storage.GetPendingInvites(db, userID) invites := storage.GetPendingInvites(db, userID)
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Invites"] = invites context["Invites"] = invites
tmpl := helpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html") tmpl := templateHelpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
} }
@@ -70,16 +74,16 @@ func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc { func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
inviteID := helpers.Atoi(r.URL.Query().Get("id")) inviteID := helpers.Atoi(r.URL.Query().Get("id"))
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
err := storage.AcceptInvite(db, inviteID, userID) err := storage.AcceptInvite(db, inviteID, userID)
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to accept invite") templateHelpers.SetFlash(w, r, "Failed to accept invite")
} else { } else {
helpers.SetFlash(w, r, "You have joined the syndicate") templateHelpers.SetFlash(w, r, "You have joined the syndicate")
} }
http.Redirect(w, r, "/syndicate", http.StatusSeeOther) http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
} }
@@ -94,7 +98,7 @@ func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
} }
func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) { func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) {
token, err := helpers.GenerateSecureToken() token, err := securityHelpers.GenerateSecureToken()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -142,16 +146,16 @@ func AcceptInviteToken(db *sql.DB, token string, userID int) error {
func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc { func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, http.StatusForbidden) templateHelpers.RenderError(w, r, http.StatusForbidden)
return return
} }
syndicateID := helpers.Atoi(r.URL.Query().Get("id")) syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
token, err := CreateInviteToken(db, syndicateID, userID, 48) // token valid for 48 hours token, err := CreateInviteToken(db, syndicateID, userID, 48)
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to generate invite link.") templateHelpers.SetFlash(w, r, "Failed to generate invite link.")
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
return return
} }
@@ -164,31 +168,31 @@ func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
} }
inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token) inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token)
helpers.SetFlash(w, r, "Invite link created: "+inviteLink) templateHelpers.SetFlash(w, r, "Invite link created: "+inviteLink)
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther) http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
} }
} }
func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc { func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, http.StatusForbidden) templateHelpers.RenderError(w, r, http.StatusForbidden)
return return
} }
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
if token == "" { if token == "" {
helpers.SetFlash(w, r, "Invalid or missing invite token.") templateHelpers.SetFlash(w, r, "Invalid or missing invite token.")
http.Redirect(w, r, "/syndicate", http.StatusSeeOther) http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
return return
} }
err := AcceptInviteToken(db, token, userID) err := AcceptInviteToken(db, token, userID)
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error()) templateHelpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
} else { } else {
helpers.SetFlash(w, r, "You have joined the syndicate!") templateHelpers.SetFlash(w, r, "You have joined the syndicate!")
} }
http.Redirect(w, r, "/syndicate", http.StatusSeeOther) http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
} }
@@ -196,27 +200,27 @@ func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc { func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
syndicateID := helpers.Atoi(r.URL.Query().Get("id")) syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
if !storage.IsSyndicateManager(db, syndicateID, userID) { if !storage.IsSyndicateManager(db, syndicateID, userID) {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
tokens := storage.GetInviteTokensForSyndicate(db, syndicateID) tokens := storage.GetInviteTokensForSyndicate(db, syndicateID)
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Tokens"] = tokens context["Tokens"] = tokens
context["SyndicateID"] = syndicateID context["SyndicateID"] = syndicateID
tmpl := helpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html") tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
} }

View File

@@ -8,10 +8,14 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
draws "synlotto-website/services/draws"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
draws "synlotto-website/services/draws"
"time"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
@@ -39,11 +43,11 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
} }
} }
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
context["csrfField"] = csrf.TemplateField(r) context["csrfField"] = csrf.TemplateField(r)
context["DrawDates"] = drawDates context["DrawDates"] = drawDates
tmpl := helpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html") tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html")
err = tmpl.ExecuteTemplate(w, "layout", context) err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {
@@ -60,7 +64,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
return return
} }
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
return return
@@ -183,7 +187,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
return return
} }
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
return return
@@ -265,7 +269,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
func GetMyTickets(db *sql.DB) http.HandlerFunc { func GetMyTickets(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
return return
@@ -355,10 +359,10 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
tickets = append(tickets, t) tickets = append(tickets, t)
} }
context := helpers.TemplateContext(w, r, models.TemplateData{}) context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
context["Tickets"] = tickets context["Tickets"] = tickets
tmpl := helpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html") tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html")
err = tmpl.ExecuteTemplate(w, "layout", context) err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -4,20 +4,26 @@ import (
"database/sql" "database/sql"
"log" "log"
"net/http" "net/http"
"strconv"
templateHandlers "synlotto-website/handlers/template"
"synlotto-website/helpers" "synlotto-website/helpers"
httpHelpers "synlotto-website/helpers/http"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/storage" "synlotto-website/storage"
) )
func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
page := helpers.Atoi(r.URL.Query().Get("page")) page := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 { if page < 1 {
page = 1 page = 1
} }
@@ -31,18 +37,18 @@ func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {
messages := storage.GetInboxMessages(db, userID, page, perPage) messages := storage.GetInboxMessages(db, userID, page, perPage)
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Messages"] = messages context["Messages"] = messages
context["CurrentPage"] = page context["CurrentPage"] = page
context["TotalPages"] = totalPages context["TotalPages"] = totalPages
context["PageRange"] = helpers.PageRange(page, totalPages) context["PageRange"] = templateHelpers.PageRange(page, totalPages)
tmpl := helpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html") tmpl := templateHelpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html")
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
helpers.RenderError(w, r, 500) templateHelpers.RenderError(w, r, 500)
} }
} }
} }
@@ -52,10 +58,10 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc {
idStr := r.URL.Query().Get("id") idStr := r.URL.Query().Get("id")
messageID := helpers.Atoi(idStr) messageID := helpers.Atoi(idStr)
session, _ := helpers.GetSession(w, r) session, _ := httpHelpers.GetSession(w, r)
userID, ok := session.Values["user_id"].(int) userID, ok := session.Values["user_id"].(int)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
@@ -67,11 +73,11 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc {
_ = storage.MarkMessageAsRead(db, messageID, userID) _ = storage.MarkMessageAsRead(db, messageID, userID)
} }
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Message"] = message context["Message"] = message
tmpl := helpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html") tmpl := templateHelpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
@@ -80,17 +86,17 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc {
func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc { func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
id := helpers.Atoi(r.URL.Query().Get("id")) id := helpers.Atoi(r.URL.Query().Get("id"))
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
err := storage.ArchiveMessage(db, userID, id) err := storage.ArchiveMessage(db, userID, id)
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to archive message.") templateHelpers.SetFlash(w, r, "Failed to archive message.")
} else { } else {
helpers.SetFlash(w, r, "Message archived.") templateHelpers.SetFlash(w, r, "Message archived.")
} }
http.Redirect(w, r, "/account/messages", http.StatusSeeOther) http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
@@ -99,9 +105,9 @@ func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc { func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
@@ -114,13 +120,13 @@ func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
messages := storage.GetArchivedMessages(db, userID, page, perPage) messages := storage.GetArchivedMessages(db, userID, page, perPage)
hasMore := len(messages) == perPage hasMore := len(messages) == perPage
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Messages"] = messages context["Messages"] = messages
context["Page"] = page context["Page"] = page
context["HasMore"] = hasMore context["HasMore"] = hasMore
tmpl := helpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html") tmpl := templateHelpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html")
tmpl.ExecuteTemplate(w, "layout", context) tmpl.ExecuteTemplate(w, "layout", context)
} }
} }
@@ -129,19 +135,17 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
// Display the form data := templateHandlers.BuildTemplateData(db, w, r)
data := BuildTemplateData(db, w, r) context := templateHelpers.TemplateContext(w, r, data)
context := helpers.TemplateContext(w, r, data) tmpl := templateHelpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html")
tmpl := helpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html")
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
helpers.RenderError(w, r, 500) templateHelpers.RenderError(w, r, 500)
} }
case http.MethodPost: case http.MethodPost:
// Handle form submission senderID, ok := securityHelpers.GetCurrentUserID(r)
senderID, ok := helpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
@@ -150,13 +154,13 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc {
body := r.FormValue("message") body := r.FormValue("message")
if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil { if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil {
helpers.SetFlash(w, r, "Failed to send message.") templateHelpers.SetFlash(w, r, "Failed to send message.")
} else { } else {
helpers.SetFlash(w, r, "Message sent.") templateHelpers.SetFlash(w, r, "Message sent.")
} }
http.Redirect(w, r, "/account/messages", http.StatusSeeOther) http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
default: default:
helpers.RenderError(w, r, 405) templateHelpers.RenderError(w, r, 405)
} }
} }
} }
@@ -164,17 +168,17 @@ func SendMessageHandler(db *sql.DB) http.HandlerFunc {
func RestoreMessageHandler(db *sql.DB) http.HandlerFunc { func RestoreMessageHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
id := helpers.Atoi(r.URL.Query().Get("id")) id := helpers.Atoi(r.URL.Query().Get("id"))
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok { if !ok {
helpers.RenderError(w, r, 403) templateHelpers.RenderError(w, r, 403)
return return
} }
err := storage.RestoreMessage(db, userID, id) err := storage.RestoreMessage(db, userID, id)
if err != nil { if err != nil {
helpers.SetFlash(w, r, "Failed to restore message.") templateHelpers.SetFlash(w, r, "Failed to restore message.")
} else { } else {
helpers.SetFlash(w, r, "Message restored.") templateHelpers.SetFlash(w, r, "Message restored.")
} }
http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther) http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther)

View File

@@ -6,16 +6,19 @@ import (
"net/http" "net/http"
"strconv" "strconv"
templateHandlers "synlotto-website/handlers/template"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/storage" "synlotto-website/storage"
) )
func NotificationsHandler(db *sql.DB) http.HandlerFunc { func NotificationsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
tmpl := helpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html") tmpl := templateHelpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html")
err := tmpl.ExecuteTemplate(w, "layout", context) err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {
@@ -52,11 +55,11 @@ func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc {
} }
} }
data := BuildTemplateData(db, w, r) data := templateHandlers.BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data) context := templateHelpers.TemplateContext(w, r, data)
context["Notification"] = notification context["Notification"] = notification
tmpl := helpers.LoadTemplateFiles("read.html", "templates/account/notifications/read.html") tmpl := templateHelpers.LoadTemplateFiles("read.html", "templates/account/notifications/read.html")
err = tmpl.ExecuteTemplate(w, "layout", context) err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil { if err != nil {

View File

@@ -9,6 +9,8 @@ import (
"sort" "sort"
"strconv" "strconv"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/middleware" "synlotto-website/middleware"
"synlotto-website/models" "synlotto-website/models"
@@ -111,7 +113,7 @@ func ResultsThunderball(db *sql.DB) http.HandlerFunc {
noResultsMsg = "No results found for \"" + query + "\"" noResultsMsg = "No results found for \"" + query + "\""
} }
tmpl := helpers.LoadTemplateFiles("thunderball.html", "templates/results/thunderball.html") tmpl := templateHelpers.LoadTemplateFiles("thunderball.html", "templates/results/thunderball.html")
err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ err = tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
"Results": results, "Results": results,

View File

@@ -2,14 +2,20 @@ package handlers
import ( import (
"database/sql" "database/sql"
"log"
"net/http" "net/http"
"synlotto-website/helpers"
httpHelper "synlotto-website/helpers/http"
"synlotto-website/models" "synlotto-website/models"
"synlotto-website/storage" "synlotto-website/storage"
) )
func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData { func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) models.TemplateData {
session, _ := helpers.GetSession(w, r) session, err := httpHelper.GetSession(w, r)
if err != nil {
log.Printf("Session error: %v", err)
}
var user *models.User var user *models.User
var isAdmin bool var isAdmin bool
@@ -18,19 +24,15 @@ func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) model
var messageCount int var messageCount int
var messages []models.Message var messages []models.Message
switch v := session.Values["user_id"].(type) { if userId, ok := session.Values["user_id"].(int); ok {
case int: user = storage.GetUserByID(db, userId)
user = models.GetUserByID(v) // ToDo should be storage not models if user != nil {
case int64: isAdmin = user.IsAdmin
user = models.GetUserByID(int(v)) notificationCount = storage.GetNotificationCount(db, user.Id)
} notifications = storage.GetRecentNotifications(db, user.Id, 15)
messageCount, _ = storage.GetMessageCount(db, user.Id)
if user != nil { messages = storage.GetRecentMessages(db, user.Id, 15)
isAdmin = user.IsAdmin }
notificationCount = storage.GetNotificationCount(db, user.Id)
notifications = storage.GetRecentNotifications(db, user.Id, 15)
messageCount, _ = storage.GetMessageCount(db, user.Id)
messages = storage.GetRecentMessages(db, user.Id, 15)
} }
return models.TemplateData{ return models.TemplateData{

51
helpers/http/session.go Normal file
View File

@@ -0,0 +1,51 @@
package helpers
import (
"net/http"
"time"
session "synlotto-website/handlers/session"
"synlotto-website/constants"
"github.com/gorilla/sessions"
)
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
return session.GetSession(w, r)
}
func IsSessionExpired(session *sessions.Session) bool {
last, ok := session.Values["last_activity"].(time.Time)
if !ok {
return false
}
return time.Since(last) > constants.SessionDuration
}
func UpdateSessionActivity(session *sessions.Session, r *http.Request, w http.ResponseWriter) {
session.Values["last_activity"] = time.Now().UTC()
session.Save(r, w)
}
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, _ := GetSession(w, r)
if IsSessionExpired(session) {
session.Options.MaxAge = -1
session.Save(r, w)
newSession, _ := GetSession(w, r)
newSession.Values["flash"] = "Your session has timed out."
newSession.Save(r, w)
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
return
}
UpdateSessionActivity(session, r, w)
next(w, r)
}
}

View File

@@ -1,4 +1,4 @@
package helpers package security
import ( import (
"database/sql" "database/sql"

View File

@@ -1,4 +1,4 @@
package helpers package security
import "golang.org/x/crypto/bcrypt" import "golang.org/x/crypto/bcrypt"

View File

@@ -1,4 +1,4 @@
package helpers package security
import ( import (
"crypto/rand" "crypto/rand"

17
helpers/security/users.go Normal file
View File

@@ -0,0 +1,17 @@
package security
import (
"net/http"
httpHelpers "synlotto-website/helpers/http"
)
func GetCurrentUserID(r *http.Request) (int, bool) {
session, err := httpHelpers.GetSession(nil, r)
if err != nil {
return 0, false
}
id, ok := session.Values["user_id"].(int)
return id, ok
}

View File

@@ -65,13 +65,3 @@ func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
next(w, r) next(w, r)
} }
} }
func GetCurrentUserID(r *http.Request) (int, bool) {
session, err := GetSession(nil, r)
if err != nil {
return 0, false
}
id, ok := session.Values["user_id"].(int)
return id, ok
}

View File

@@ -5,14 +5,21 @@ import (
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time"
"synlotto-website/config"
helpers "synlotto-website/helpers/http"
"synlotto-website/models" "synlotto-website/models"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
func TemplateContext(w http.ResponseWriter, r *http.Request, data models.TemplateData) map[string]interface{} { func TemplateContext(w http.ResponseWriter, r *http.Request, data models.TemplateData) map[string]interface{} {
session, _ := GetSession(w, r) cfg := config.Get()
if cfg == nil {
log.Println("⚠️ Config not initialized!")
}
session, _ := helpers.GetSession(w, r)
var flash string var flash string
if f, ok := session.Values["flash"].(string); ok { if f, ok := session.Values["flash"].(string); ok {
@@ -30,6 +37,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request, data models.Templat
"Notifications": data.Notifications, "Notifications": data.Notifications,
"MessageCount": data.MessageCount, "MessageCount": data.MessageCount,
"Messages": data.Messages, "Messages": data.Messages,
"SiteName": cfg.Site.SiteName,
"YearStart": cfg.Site.CopyrightStart,
} }
} }
@@ -58,9 +67,8 @@ func TemplateFuncs() template.FuncMap {
} }
return *p return *p
}, },
"inSlice": InSlice, "inSlice": InSlice,
"lower": lower, "lower": lower,
"rangeClass": rangeClass,
"truncate": func(s string, max int) string { "truncate": func(s string, max int) string {
if len(s) <= max { if len(s) <= max {
return s return s
@@ -68,22 +76,37 @@ func TemplateFuncs() template.FuncMap {
return s[:max] + "..." return s[:max] + "..."
}, },
"PageRange": PageRange, "PageRange": PageRange,
"now": time.Now,
"humanTime": func(v interface{}) string {
switch t := v.(type) {
case time.Time:
return t.Local().Format("02 Jan 2006 15:04")
case string:
parsed, err := time.Parse(time.RFC3339, t)
if err == nil {
return parsed.Local().Format("02 Jan 2006 15:04")
}
return t
default:
return ""
}
},
} }
} }
func LoadTemplateFiles(name string, files ...string) *template.Template { func LoadTemplateFiles(name string, files ...string) *template.Template {
shared := []string{ shared := []string{
"templates/layout.html", "templates/main/layout.html",
"templates/topbar.html", "templates/main/topbar.html",
"templates/main/footer.html",
} }
all := append(shared, files...) all := append(shared, files...)
log.Printf("📄 Loading templates: %v", all)
return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...)) return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...))
} }
func SetFlash(w http.ResponseWriter, r *http.Request, message string) { func SetFlash(w http.ResponseWriter, r *http.Request, message string) {
session, _ := GetSession(w, r) session, _ := helpers.GetSession(w, r)
session.Values["flash"] = message session.Values["flash"] = message
session.Save(r, w) session.Save(r, w)
} }
@@ -101,6 +124,15 @@ func lower(input string) string {
return strings.ToLower(input) return strings.ToLower(input)
} }
func PageRange(current, total int) []int {
var pages []int
for i := 1; i <= total; i++ {
pages = append(pages, i)
}
return pages
}
// ToDo: Should be ball range class, and should it even be here?
func rangeClass(n int) string { func rangeClass(n int) string {
switch { switch {
case n >= 1 && n <= 9: case n >= 1 && n <= 9:
@@ -117,11 +149,3 @@ func rangeClass(n int) string {
return "50-plus" return "50-plus"
} }
} }
func PageRange(current, total int) []int {
var pages []int
for i := 1; i <= total; i++ {
pages = append(pages, i)
}
return pages
}

View File

@@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"synlotto-website/models" "synlotto-website/models"
) )
@@ -36,5 +37,3 @@ func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
log.Println("✅ Successfully rendered error page") // ToDo: log these to database log.Println("✅ Successfully rendered error page") // ToDo: log these to database
} }
//ToDo Pages.go /template.go to be merged?

View File

@@ -0,0 +1,25 @@
package internal
import (
"sync"
"time"
)
type LicenseChecker struct {
LicenseAPIURL string
APIKey string
PollInterval time.Duration
mu sync.RWMutex
lastGood time.Time
valid bool
}
func (lc *LicenseChecker) setValid(ok bool) {
lc.mu.Lock()
defer lc.mu.Unlock()
lc.valid = ok
if ok {
lc.lastGood = time.Now()
}
}

View File

@@ -0,0 +1,76 @@
package internal
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)
func (lc *LicenseChecker) Validate() error {
url := fmt.Sprintf("%s/license/lookup?key=%s&format=json", lc.LicenseAPIURL, lc.APIKey)
resp, err := http.Get(url)
if err != nil {
lc.setValid(false)
return fmt.Errorf("license lookup failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
lc.setValid(false)
return fmt.Errorf("license lookup error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
lc.setValid(false)
return fmt.Errorf("reading response failed: %w", err)
}
var data struct {
Revoked bool `json:"revoked"`
ExpiresAt time.Time `json:"expires_at"`
}
if err := json.Unmarshal(body, &data); err != nil {
lc.setValid(false)
return fmt.Errorf("unmarshal error: %w", err)
}
if data.Revoked || time.Now().After(data.ExpiresAt) {
lc.setValid(false)
return fmt.Errorf("license expired or revoked")
}
lc.mu.Lock()
lc.valid = true
lc.lastGood = time.Now()
lc.mu.Unlock()
log.Printf("✅ License validated. Expires: %s", data.ExpiresAt)
return nil
}
func (lc *LicenseChecker) StartBackgroundCheck() {
go func() {
for {
time.Sleep(lc.PollInterval)
err := lc.Validate()
if err != nil {
log.Printf("⚠️ License check failed: %v", err)
}
}
}()
}
func (lc *LicenseChecker) IsValid() bool {
lc.mu.RLock()
defer lc.mu.RUnlock()
return lc.valid
}

View File

@@ -34,6 +34,10 @@ func main() {
logging.Error("❌ Failed to init session: %v", err) logging.Error("❌ Failed to init session: %v", err)
} }
// if err := bootstrap.InitLicenseChecker(appState.Config); err != nil {
// logging.Error("❌ Invalid license: %v", err)
// }
err = bootstrap.InitCSRFProtection([]byte(appState.Config.CSRF.CSRFKey), appState.Config.HttpServer.ProductionMode) err = bootstrap.InitCSRFProtection([]byte(appState.Config.CSRF.CSRFKey), appState.Config.HttpServer.ProductionMode)
if err != nil { if err != nil {
logging.Error("Failed to init CSRF: %v", err) logging.Error("Failed to init CSRF: %v", err)

View File

@@ -4,7 +4,8 @@ import (
"log" "log"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
) )
func Recover(next http.Handler) http.Handler { func Recover(next http.Handler) http.Handler {
@@ -13,7 +14,7 @@ func Recover(next http.Handler) http.Handler {
if rec := recover(); rec != nil { if rec := recover(); rec != nil {
log.Printf("🔥 Recovered from panic: %v\n%s", rec, debug.Stack()) log.Printf("🔥 Recovered from panic: %v\n%s", rec, debug.Stack())
helpers.RenderError(w, r, http.StatusInternalServerError) templateHelpers.RenderError(w, r, http.StatusInternalServerError)
} }
}() }()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)

View File

@@ -11,6 +11,11 @@ type Config struct {
CSRFKey string `json:"csrfKey"` CSRFKey string `json:"csrfKey"`
} `json:"csrf"` } `json:"csrf"`
License struct {
APIURL string `json:"apiUrl"`
APIKey string `json:"apiKey"`
} `json:"license"`
Session struct { Session struct {
AuthKeyPath string `json:"authKeyPath"` AuthKeyPath string `json:"authKeyPath"`
EncryptionKeyPath string `json:"encryptionKeyPath"` EncryptionKeyPath string `json:"encryptionKeyPath"`

View File

@@ -59,35 +59,3 @@ func GetUserByUsername(username string) *User {
return &user return &user
} }
func GetUserByID(id int) *User {
row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id)
var user User
err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin)
if err != nil {
if err != sql.ErrNoRows {
log.Println("DB error:", err)
}
return nil
}
return &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)) // 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 {
return 1
}
return 0
}

View File

@@ -4,14 +4,16 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
account "synlotto-website/handlers/account"
"synlotto-website/handlers" "synlotto-website/handlers"
"synlotto-website/middleware" "synlotto-website/middleware"
) )
func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) { func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/login", middleware.Protected(handlers.Login)) mux.HandleFunc("/login", middleware.Protected(account.Login))
mux.HandleFunc("/logout", handlers.Logout) mux.HandleFunc("/logout", account.Logout)
mux.HandleFunc("/signup", middleware.Protected(handlers.Signup)) mux.HandleFunc("/signup", middleware.Protected(account.Signup))
mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db))
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db))
mux.HandleFunc("/account/messages", middleware.Protected(handlers.MessagesInboxHandler(db))) mux.HandleFunc("/account/messages", middleware.Protected(handlers.MessagesInboxHandler(db)))

View File

@@ -4,21 +4,22 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
"synlotto-website/handlers" lotterySyndicateHandlers "synlotto-website/handlers/lottery/syndicate"
"synlotto-website/middleware" "synlotto-website/middleware"
) )
func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) { func SetupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/syndicate", middleware.Auth(true)(handlers.ListSyndicatesHandler(db))) mux.HandleFunc("/syndicate", middleware.Auth(true)(lotterySyndicateHandlers.ListSyndicatesHandler(db)))
mux.HandleFunc("/syndicate/create", middleware.Auth(true)(handlers.CreateSyndicateHandler(db))) mux.HandleFunc("/syndicate/create", middleware.Auth(true)(lotterySyndicateHandlers.CreateSyndicateHandler(db)))
mux.HandleFunc("/syndicate/view", middleware.Auth(true)(handlers.ViewSyndicateHandler(db))) mux.HandleFunc("/syndicate/view", middleware.Auth(true)(lotterySyndicateHandlers.ViewSyndicateHandler(db)))
mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(handlers.SyndicateTicketsHandler(db))) mux.HandleFunc("/syndicate/tickets", middleware.Auth(true)(lotterySyndicateHandlers.SyndicateTicketsHandler(db)))
mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(handlers.SyndicateLogTicketHandler(db))) mux.HandleFunc("/syndicate/tickets/new", middleware.Auth(true)(lotterySyndicateHandlers.SyndicateLogTicketHandler(db)))
mux.HandleFunc("/syndicate/invites", middleware.Auth(true)(handlers.ViewInvitesHandler(db))) mux.HandleFunc("/syndicate/invites", middleware.Auth(true)(lotterySyndicateHandlers.ViewInvitesHandler(db)))
mux.HandleFunc("/syndicate/invites/accept", middleware.Auth(true)(handlers.AcceptInviteHandler(db))) mux.HandleFunc("/syndicate/invites/accept", middleware.Auth(true)(lotterySyndicateHandlers.AcceptInviteHandler(db)))
mux.HandleFunc("/syndicate/invites/decline", middleware.Auth(true)(handlers.DeclineInviteHandler(db))) mux.HandleFunc("/syndicate/invites/decline", middleware.Auth(true)(lotterySyndicateHandlers.DeclineInviteHandler(db)))
mux.HandleFunc("/syndicate/invite/token", middleware.Auth(true)(handlers.GenerateInviteLinkHandler(db))) mux.HandleFunc("/syndicate/invite/token", middleware.Auth(true)(lotterySyndicateHandlers.GenerateInviteLinkHandler(db)))
mux.HandleFunc("/syndicate/invite/tokens", middleware.Auth(true)(handlers.ManageInviteTokensHandler(db))) mux.HandleFunc("/syndicate/invite/tokens", middleware.Auth(true)(lotterySyndicateHandlers.ManageInviteTokensHandler(db)))
mux.HandleFunc("/syndicate/join", middleware.Auth(true)(handlers.JoinSyndicateWithTokenHandler(db))) mux.HandleFunc("/syndicate/join", middleware.Auth(true)(lotterySyndicateHandlers.JoinSyndicateWithTokenHandler(db)))
} }

View File

@@ -4,12 +4,14 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "log"
"synlotto-website/handlers"
lotteryTicketHandlers "synlotto-website/handlers/lottery/tickets"
thunderballrules "synlotto-website/rules"
services "synlotto-website/services/draws"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/matcher" "synlotto-website/matcher"
"synlotto-website/models" "synlotto-website/models"
thunderballrules "synlotto-website/rules"
services "synlotto-website/services/draws"
) )
func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) {
@@ -27,7 +29,6 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
} }
defer rows.Close() defer rows.Close()
// Buffer results to avoid writing while iterating
var pending []models.Ticket var pending []models.Ticket
for rows.Next() { for rows.Next() {
@@ -64,7 +65,7 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
} }
draw := services.GetDrawResultForTicket(db, t.GameType, t.DrawDate) draw := services.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
result := handlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules) result := lotteryTicketHandlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules)
if result.MatchedDrawID == 0 { if result.MatchedDrawID == 0 {
continue continue
@@ -105,7 +106,6 @@ func UpdateMissingPrizes(db *sql.DB) error {
var tickets []TicketInfo var tickets []TicketInfo
// Step 1: Load all relevant tickets
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, game_type, draw_date, matched_main, matched_bonus SELECT id, game_type, draw_date, matched_main, matched_bonus
FROM my_tickets FROM my_tickets
@@ -125,7 +125,6 @@ func UpdateMissingPrizes(db *sql.DB) error {
tickets = append(tickets, t) tickets = append(tickets, t)
} }
// Step 2: Now that the reader is closed, perform updates
for _, t := range tickets { for _, t := range tickets {
if t.GameType != "Thunderball" { if t.GameType != "Thunderball" {
continue continue
@@ -196,7 +195,7 @@ func RefreshTicketPrizes(db *sql.DB) error {
} }
tickets = append(tickets, t) tickets = append(tickets, t)
} }
rows.Close() // ✅ Release read lock before updating rows.Close()
for _, row := range tickets { for _, row := range tickets {
matchTicket := models.MatchTicket{ matchTicket := models.MatchTicket{

View File

@@ -5,16 +5,18 @@ import (
"log" "log"
"net/http" "net/http"
"synlotto-website/helpers" securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/middleware" "synlotto-website/middleware"
) )
func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc { func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) { return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r) userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok || !helpers.IsAdmin(db, userID) { if !ok || !securityHelpers.IsAdmin(db, userID) {
log.Printf("⛔️ Unauthorized admin attempt: user_id=%v, IP=%s, Path=%s", userID, r.RemoteAddr, r.URL.Path) log.Printf("⛔️ Unauthorized admin attempt: user_id=%v, IP=%s, Path=%s", userID, r.RemoteAddr, r.URL.Path)
helpers.RenderError(w, r, http.StatusForbidden) templateHelpers.RenderError(w, r, http.StatusForbidden)
return return
} }
@@ -36,5 +38,3 @@ func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
next(w, r) next(w, r)
}) })
} }
// ToDo need to look into audit/access log tables and consolidate

22
storage/audit.go Normal file
View File

@@ -0,0 +1,22 @@
package storage
import (
"net/http"
"time"
"synlotto-website/logging"
)
func LogLoginAttempt(r *http.Request, username string, success bool) {
ip := r.RemoteAddr
userAgent := r.UserAgent()
_, err := db.Exec(
`INSERT INTO audit_login (username, success, ip, user_agent, timestamp)
VALUES (?, ?, ?, ?, ?)`,
username, success, ip, userAgent, time.Now().UTC(),
)
if err != nil {
logging.Info("❌ Failed to log login:", err)
}
}

View File

@@ -98,16 +98,6 @@ func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool {
return err == nil && count > 0 return err == nil && count > 0
} }
func GetUserByUsername(db *sql.DB, username string) *models.User {
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 {
return nil
}
return &u
}
func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error { func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error {
_, err := db.Exec(` _, err := db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at) INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)

34
storage/users.go Normal file
View File

@@ -0,0 +1,34 @@
package storage
import (
"database/sql"
"synlotto-website/logging"
"synlotto-website/models"
)
func GetUserByID(db *sql.DB, id int) *models.User {
row := db.QueryRow("SELECT id, username, password_hash, is_admin FROM users WHERE id = ?", id)
var user models.User
err := row.Scan(&user.Id, &user.Username, &user.PasswordHash, &user.IsAdmin)
if err != nil {
if err != sql.ErrNoRows {
logging.Error("DB error:", err)
}
return nil
}
return &user
}
func GetUserByUsername(db *sql.DB, username string) *models.User {
row := db.QueryRow(`SELECT id, username, password_hash, is_admin FROM users WHERE username = ?`, username)
var u models.User
err := row.Scan(&u.Id, &u.Username, &u.PasswordHash, &u.IsAdmin)
if err != nil {
return nil
}
return &u
}

View File

@@ -0,0 +1,17 @@
{{ define "footer" }}
<footer class="bg-light text-center text-muted py-3 mt-auto border-top">
<small>
&copy; Copyright {{ .SiteName }}
{{ $currentYear := now.Year }}
{{ if eq .YearStart $currentYear }}
{{ $currentYear }}
{{ else }}
{{ .YearStart }} - {{ $currentYear }}
{{ end }}
All rights reserved.
| <a href="/legal/privacy">Privacy Policy</a> |
<a href="/legal/terms">Terms & Conditions</a> |
<a href="/contact">Contact Us</a>
</small>
</footer>
{{ end }}

View File

@@ -1,9 +1,10 @@
{{ define "layout" }} {{ define "layout" }}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SynLotto</title> <title>{{ .SiteName }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/site.css"> <link rel="stylesheet" href="/static/css/site.css">
@@ -12,9 +13,9 @@
<body class="d-flex flex-column min-vh-100"> <body class="d-flex flex-column min-vh-100">
<!-- Topbar --> <!-- Topbar -->
{{ template "topbar" . }} {{ template "topbar" . }}
<!-- Main Layout -->
<div class="container-fluid flex-grow-1"> <!-- Main layout using Flexbox -->
<div class="row"> <div class="d-flex flex-grow-1">
<!-- Sidebar --> <!-- Sidebar -->
<nav class="col-md-2 d-none d-md-block bg-light sidebar pt-3"> <nav class="col-md-2 d-none d-md-block bg-light sidebar pt-3">
<div class="position-sticky"> <div class="position-sticky">
@@ -62,25 +63,24 @@
</div> </div>
</nav> </nav>
<!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 pt-4"> <!-- Main Content -->
{{ if .Flash }} <main class="col px-md-4 pt-4">
<div class="alert alert-info" role="alert"> {{ if .Flash }}
{{ .Flash }} <div class="alert alert-info" role="alert">
</div> {{ .Flash }}
{{ end }} </div>
{{ template "content" . }} {{ end }}
</main> {{ template "content" . }}
</div> </main>
</div> </div>
<!-- Footer --> <!-- Footer -->
<footer class="bg-light text-center text-muted py-3 mt-auto border-top"> {{ template "footer" . }}
<small>&copy; xxx SynLotto. All rights reserved. | <a href="/privacy">Privacy Policy</a> | <a href="/privacy">Terms & Conditions</a> | <a href="/privacy">Contact Us </a></small>
</footer>
<!-- JS --> <!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>
{{ end }} {{ end }}