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 (
"log"
"net/http"
"synlotto-website/helpers"
"synlotto-website/models"
"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"
)
func Login(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
session, _ := helpers.GetSession(w, r)
session, _ := httpHelpers.GetSession(w, r)
if _, ok := session.Values["user_id"].(int); ok {
http.Redirect(w, r, "/", http.StatusSeeOther)
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)
err := tmpl.ExecuteTemplate(w, "layout", context)
@@ -34,12 +38,12 @@ func Login(w http.ResponseWriter, r *http.Request) {
password := r.FormValue("password")
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)
return
}
session, _ := helpers.GetSession(w, r)
session, _ := httpHelpers.GetSession(w, r)
for k := range session.Values {
delete(session.Values, k)
@@ -65,18 +69,18 @@ func Login(w http.ResponseWriter, r *http.Request) {
}
}
if user == nil || !helpers.CheckPasswordHash(user.PasswordHash, password) {
models.LogLoginAttempt(username, false)
if user == nil || !securityHelpers.CheckPasswordHash(user.PasswordHash, password) {
storage.LogLoginAttempt(username, false)
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
models.LogLoginAttempt(username, true)
storage.LogLoginAttempt(username, true)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func Logout(w http.ResponseWriter, r *http.Request) {
session, _ := helpers.GetSession(w, r)
session, _ := httpHelpers.GetSession(w, r)
for k := range session.Values {
delete(session.Values, k)
@@ -95,7 +99,7 @@ func Logout(w http.ResponseWriter, r *http.Request) {
func Signup(w http.ResponseWriter, r *http.Request) {
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{}{
"csrfField": csrf.TemplateField(r),
@@ -106,7 +110,7 @@ func Signup(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
hashed, err := helpers.HashPassword(password)
hashed, err := securityHelpers.HashPassword(password)
if err != nil {
http.Error(w, "Server error", http.StatusInternalServerError)
return

View File

@@ -4,7 +4,9 @@ import (
"database/sql"
"log"
"net/http"
"synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/middleware"
"synlotto-website/models"
)
@@ -19,7 +21,7 @@ type AdminLogEntry struct {
func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
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(`
SELECT accessed_at, user_id, path, ip, user_agent
@@ -45,7 +47,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
}
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)
})
@@ -53,7 +55,7 @@ func AdminAccessLogHandler(db *sql.DB) http.HandlerFunc {
func AuditLogHandler(db *sql.DB) http.HandlerFunc {
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(`
SELECT timestamp, user_id, action, ip, user_agent
@@ -81,7 +83,7 @@ func AuditLogHandler(db *sql.DB) http.HandlerFunc {
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)
if err != nil {

View File

@@ -6,12 +6,13 @@ import (
"net/http"
helpers "synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models"
)
func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
// userID, ok := helpers.GetCurrentUserID(r)
// userID, ok := securityHelpers.GetCurrentUserID(r)
// if !ok {
// http.Redirect(w, r, "/login", http.StatusSeeOther)
// return
@@ -19,7 +20,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
// 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
var total, winners int
@@ -54,7 +55,7 @@ func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
}
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)
if err != nil {

View File

@@ -6,12 +6,14 @@ import (
"net/http"
helpers "synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models"
)
func NewDrawHandler(db *sql.DB) http.HandlerFunc {
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 {
game := r.FormValue("game_type")
@@ -30,7 +32,7 @@ func NewDrawHandler(db *sql.DB) http.HandlerFunc {
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)
})
@@ -72,7 +74,7 @@ func DeleteDrawHandler(db *sql.DB) http.HandlerFunc {
func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
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{}
rows, err := db.Query(`
@@ -100,7 +102,7 @@ func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
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)
})

View File

@@ -8,14 +8,15 @@ import (
"net/url"
"strconv"
"synlotto-website/helpers"
"synlotto-website/models"
templateHelpers "synlotto-website/helpers/template"
services "synlotto-website/services/tickets"
"synlotto-website/models"
)
func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
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 != "" {
context["Flash"] = flash
@@ -71,7 +72,7 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
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)
if err != nil {

View File

@@ -5,16 +5,17 @@ import (
"fmt"
"net/http"
"strconv"
"synlotto-website/helpers"
helpers "synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/models"
)
func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
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
}
@@ -44,9 +45,9 @@ func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
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
}

View File

@@ -4,15 +4,17 @@ import (
"database/sql"
"log"
"net/http"
"synlotto-website/helpers"
templateHandlers "synlotto-website/handlers/template"
templateHelpers "synlotto-website/helpers/template"
)
func Home(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
data := templateHandlers.BuildTemplateData(db, w, r)
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)
if err != nil {

View File

@@ -5,6 +5,8 @@ import (
"log"
"net/http"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/models"
)
@@ -13,11 +15,11 @@ func NewDraw(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
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["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)
if err != nil {

View File

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

View File

@@ -7,29 +7,33 @@ import (
"strconv"
"time"
templateHandlers "synlotto-website/handlers/template"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/storage"
)
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, http.StatusForbidden)
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
switch r.Method {
case http.MethodGet:
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
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)
if err != nil {
helpers.RenderError(w, r, 500)
templateHelpers.RenderError(w, r, 500)
}
case http.MethodPost:
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)
if err != nil {
helpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
} 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)
default:
helpers.RenderError(w, r, http.StatusMethodNotAllowed)
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
}
}
}
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, 403)
templateHelpers.RenderError(w, r, 403)
return
}
invites := storage.GetPendingInvites(db, userID)
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
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)
}
}
@@ -70,16 +74,16 @@ func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, 403)
templateHelpers.RenderError(w, r, 403)
return
}
err := storage.AcceptInvite(db, inviteID, userID)
if err != nil {
helpers.SetFlash(w, r, "Failed to accept invite")
templateHelpers.SetFlash(w, r, "Failed to accept invite")
} 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)
}
@@ -94,7 +98,7 @@ func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
}
func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) {
token, err := helpers.GenerateSecureToken()
token, err := securityHelpers.GenerateSecureToken()
if err != nil {
return "", err
}
@@ -142,16 +146,16 @@ func AcceptInviteToken(db *sql.DB, token string, userID int) error {
func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, http.StatusForbidden)
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
token, err := CreateInviteToken(db, syndicateID, userID, 48) // token valid for 48 hours
token, err := CreateInviteToken(db, syndicateID, userID, 48)
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)
return
}
@@ -164,31 +168,31 @@ func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
}
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)
}
}
func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, http.StatusForbidden)
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
token := r.URL.Query().Get("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)
return
}
err := AcceptInviteToken(db, token, userID)
if err != nil {
helpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
templateHelpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
} 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)
}
@@ -196,27 +200,27 @@ func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
helpers.RenderError(w, r, 403)
templateHelpers.RenderError(w, r, 403)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
if !storage.IsSyndicateManager(db, syndicateID, userID) {
helpers.RenderError(w, r, 403)
templateHelpers.RenderError(w, r, 403)
return
}
tokens := storage.GetInviteTokensForSyndicate(db, syndicateID)
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["Tokens"] = tokens
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)
}
}

View File

@@ -8,10 +8,14 @@ import (
"net/http"
"os"
"strconv"
"time"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
draws "synlotto-website/services/draws"
"synlotto-website/helpers"
"synlotto-website/models"
draws "synlotto-website/services/draws"
"time"
"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["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)
if err != nil {
@@ -60,7 +64,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
return
}
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
@@ -183,7 +187,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
return
}
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
@@ -265,7 +269,7 @@ func SubmitTicket(db *sql.DB) http.HandlerFunc {
func GetMyTickets(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
@@ -355,10 +359,10 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
tickets = append(tickets, t)
}
context := helpers.TemplateContext(w, r, models.TemplateData{})
context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
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)
if err != nil {

View File

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

View File

@@ -6,16 +6,19 @@ import (
"net/http"
"strconv"
templateHandlers "synlotto-website/handlers/template"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/storage"
)
func NotificationsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
data := templateHandlers.BuildTemplateData(db, w, r)
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)
if err != nil {
@@ -52,11 +55,11 @@ func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc {
}
}
data := BuildTemplateData(db, w, r)
context := helpers.TemplateContext(w, r, data)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
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)
if err != nil {

View File

@@ -9,6 +9,8 @@ import (
"sort"
"strconv"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/middleware"
"synlotto-website/models"
@@ -111,7 +113,7 @@ func ResultsThunderball(db *sql.DB) http.HandlerFunc {
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{}{
"Results": results,

View File

@@ -2,14 +2,20 @@ package handlers
import (
"database/sql"
"log"
"net/http"
"synlotto-website/helpers"
httpHelper "synlotto-website/helpers/http"
"synlotto-website/models"
"synlotto-website/storage"
)
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 isAdmin bool
@@ -18,13 +24,8 @@ func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) model
var messageCount int
var messages []models.Message
switch v := session.Values["user_id"].(type) {
case int:
user = models.GetUserByID(v) // ToDo should be storage not models
case int64:
user = models.GetUserByID(int(v))
}
if userId, ok := session.Values["user_id"].(int); ok {
user = storage.GetUserByID(db, userId)
if user != nil {
isAdmin = user.IsAdmin
notificationCount = storage.GetNotificationCount(db, user.Id)
@@ -32,6 +33,7 @@ func BuildTemplateData(db *sql.DB, w http.ResponseWriter, r *http.Request) model
messageCount, _ = storage.GetMessageCount(db, user.Id)
messages = storage.GetRecentMessages(db, user.Id, 15)
}
}
return models.TemplateData{
User: user,

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 (
"database/sql"

View File

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

View File

@@ -1,4 +1,4 @@
package helpers
package security
import (
"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)
}
}
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"
"net/http"
"strings"
"time"
"synlotto-website/config"
helpers "synlotto-website/helpers/http"
"synlotto-website/models"
"github.com/gorilla/csrf"
)
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
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,
"MessageCount": data.MessageCount,
"Messages": data.Messages,
"SiteName": cfg.Site.SiteName,
"YearStart": cfg.Site.CopyrightStart,
}
}
@@ -60,7 +69,6 @@ func TemplateFuncs() template.FuncMap {
},
"inSlice": InSlice,
"lower": lower,
"rangeClass": rangeClass,
"truncate": func(s string, max int) string {
if len(s) <= max {
return s
@@ -68,22 +76,37 @@ func TemplateFuncs() template.FuncMap {
return s[:max] + "..."
},
"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 {
shared := []string{
"templates/layout.html",
"templates/topbar.html",
"templates/main/layout.html",
"templates/main/topbar.html",
"templates/main/footer.html",
}
all := append(shared, files...)
log.Printf("📄 Loading templates: %v", all)
return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...))
}
func SetFlash(w http.ResponseWriter, r *http.Request, message string) {
session, _ := GetSession(w, r)
session, _ := helpers.GetSession(w, r)
session.Values["flash"] = message
session.Save(r, w)
}
@@ -101,6 +124,15 @@ func lower(input string) string {
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 {
switch {
case n >= 1 && n <= 9:
@@ -117,11 +149,3 @@ func rangeClass(n int) string {
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"
"net/http"
"os"
"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
}
//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)
}
// 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)
if err != nil {
logging.Error("Failed to init CSRF: %v", err)

View File

@@ -4,7 +4,8 @@ import (
"log"
"net/http"
"runtime/debug"
"synlotto-website/helpers"
templateHelpers "synlotto-website/helpers/template"
)
func Recover(next http.Handler) http.Handler {
@@ -13,7 +14,7 @@ func Recover(next http.Handler) http.Handler {
if rec := recover(); rec != nil {
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)

View File

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

View File

@@ -59,35 +59,3 @@ func GetUserByUsername(username string) *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"
"net/http"
account "synlotto-website/handlers/account"
"synlotto-website/handlers"
"synlotto-website/middleware"
)
func SetupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/login", middleware.Protected(handlers.Login))
mux.HandleFunc("/logout", handlers.Logout)
mux.HandleFunc("/signup", middleware.Protected(handlers.Signup))
mux.HandleFunc("/login", middleware.Protected(account.Login))
mux.HandleFunc("/logout", account.Logout)
mux.HandleFunc("/signup", middleware.Protected(account.Signup))
mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db))
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db))
mux.HandleFunc("/account/messages", middleware.Protected(handlers.MessagesInboxHandler(db)))

View File

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

View File

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

View File

@@ -5,16 +5,18 @@ import (
"log"
"net/http"
"synlotto-website/helpers"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/middleware"
)
func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
return middleware.Auth(true)(func(w http.ResponseWriter, r *http.Request) {
userID, ok := helpers.GetCurrentUserID(r)
if !ok || !helpers.IsAdmin(db, userID) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok || !securityHelpers.IsAdmin(db, userID) {
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
}
@@ -36,5 +38,3 @@ func AdminOnly(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
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
}
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 {
_, err := db.Exec(`
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" }}
<!DOCTYPE html>
<html lang="en">
<head>
<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-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/site.css">
@@ -12,9 +13,9 @@
<body class="d-flex flex-column min-vh-100">
<!-- Topbar -->
{{ template "topbar" . }}
<!-- Main Layout -->
<div class="container-fluid flex-grow-1">
<div class="row">
<!-- Main layout using Flexbox -->
<div class="d-flex flex-grow-1">
<!-- Sidebar -->
<nav class="col-md-2 d-none d-md-block bg-light sidebar pt-3">
<div class="position-sticky">
@@ -62,8 +63,9 @@
</div>
</nav>
<!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 pt-4">
<main class="col px-md-4 pt-4">
{{ if .Flash }}
<div class="alert alert-info" role="alert">
{{ .Flash }}
@@ -72,15 +74,13 @@
{{ template "content" . }}
</main>
</div>
</div>
<!-- Footer -->
<footer class="bg-light text-center text-muted py-3 mt-auto border-top">
<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>
{{ template "footer" . }}
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
{{ end }}