Compare commits

...

2 Commits

Author SHA1 Message Date
2ce810a4dd Refactoring cont. 2025-04-23 09:44:19 +01:00
5c3a847900 Massive refactor! 2025-04-22 23:26:11 +01:00
48 changed files with 703 additions and 440 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

@@ -4,11 +4,15 @@ import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/gob"
"fmt"
"net/http"
"os"
"time"
sessionHandlers "synlotto-website/handlers/session"
sessionHelpers "synlotto-website/helpers/session"
helpers "synlotto-website/helpers/session"
"synlotto-website/logging"
"synlotto-website/models"
@@ -17,12 +21,13 @@ import (
var (
sessionStore *sessions.CookieStore
sessionName string
Name string
authKey []byte
encryptKey []byte
)
func InitSession(cfg *models.Config) error {
gob.Register(time.Time{})
authPath := cfg.Session.AuthKeyPath
encPath := cfg.Session.EncryptionKeyPath
@@ -32,7 +37,7 @@ func InitSession(cfg *models.Config) error {
if err != nil {
return err
}
encoded := helpers.EncodeKey(key)
encoded := sessionHelpers.EncodeKey(key)
err = os.WriteFile(authPath, []byte(encoded), 0600)
if err != nil {
return err
@@ -45,7 +50,7 @@ func InitSession(cfg *models.Config) error {
if err != nil {
return err
}
encoded := helpers.EncodeKey(key)
encoded := sessionHelpers.EncodeKey(key)
err = os.WriteFile(encPath, []byte(encoded), 0600)
if err != nil {
return err
@@ -96,15 +101,15 @@ func loadSessionKeys(authPath, encryptionPath, name string, isProduction bool) e
return fmt.Errorf("auth and encryption keys must be 32 bytes each (got auth=%d, enc=%d)", len(authKey), len(encryptKey))
}
sessionStore = sessions.NewCookieStore(authKey, encryptKey)
sessionStore.Options = &sessions.Options{
sessionHandlers.SessionStore = sessions.NewCookieStore(authKey, encryptKey)
sessionHandlers.SessionStore.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 1,
MaxAge: 86400,
HttpOnly: true,
Secure: isProduction,
SameSite: http.SameSiteLaxMode,
}
sessionName = name
sessionHandlers.Name = name
return nil
}

View File

@@ -3,23 +3,27 @@ package handlers
import (
"log"
"net/http"
"synlotto-website/helpers"
"synlotto-website/models"
"time"
httpHelpers "synlotto-website/helpers/http"
securityHelpers "synlotto-website/helpers/security"
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(r, username, false)
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
models.LogLoginAttempt(username, true)
storage.LogLoginAttempt(r, 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

@@ -5,13 +5,15 @@ import (
"log"
"net/http"
helpers "synlotto-website/helpers"
httpHelpers "synlotto-website/helpers/http"
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)
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
// userID, ok := securityHelpers.GetCurrentUserID(r)
// if !ok {
// http.Redirect(w, r, "/login", http.StatusSeeOther)
// return
@@ -19,7 +21,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 +56,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

@@ -5,13 +5,15 @@ import (
"log"
"net/http"
helpers "synlotto-website/helpers"
httpHelpers "synlotto-website/helpers/http"
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{})
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
context := templateHelpers.TemplateContext(w, r, models.TemplateData{})
if r.Method == http.MethodPost {
game := r.FormValue("game_type")
@@ -30,14 +32,14 @@ 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)
})
}
func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
id := r.FormValue("id")
_, err := db.Exec(`UPDATE results_thunderball SET game_type=?, draw_date=?, ball_set=?, machine=? WHERE id=?`,
@@ -56,7 +58,7 @@ func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
}
func DeleteDrawHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
id := r.FormValue("id")
_, err := db.Exec(`DELETE FROM results_thunderball WHERE id = ?`, id)
@@ -71,8 +73,8 @@ 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{})
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
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,19 @@ import (
"fmt"
"net/http"
"strconv"
"synlotto-website/helpers"
httpHelpers "synlotto-website/helpers/http"
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) {
return httpHelpers.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
}
@@ -42,11 +45,11 @@ func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
}
func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
return httpHelpers.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,19 +5,22 @@ import (
"log"
"net/http"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/models"
"synlotto-website/storage"
)
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/admin/draws/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 {
@@ -27,7 +30,7 @@ func NewDraw(db *sql.DB) http.HandlerFunc {
}
}
func Submit(w http.ResponseWriter, r *http.Request) {
func Submit(db *sql.DB, w http.ResponseWriter, r *http.Request) {
log.Println("📝 Form submission received")
_ = r.ParseForm()
@@ -43,7 +46,12 @@ func Submit(w http.ResponseWriter, r *http.Request) {
Thunderball: helpers.Atoi(r.FormValue("thunderball")),
}
Draws = append(Draws, draw)
err := storage.InsertThunderballResult(db, draw)
if err != nil {
log.Println("❌ Failed to insert draw:", err)
http.Error(w, "Failed to save draw", http.StatusInternalServerError)
return
}
log.Printf("📅 %s | 🛠 %s | 🎱 %d | 🔢 %d,%d,%d,%d,%d | ⚡ %d\n",
draw.DrawDate, draw.Machine, draw.BallSet,

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,16 +8,21 @@ import (
"net/http"
"os"
"strconv"
"time"
httpHelpers "synlotto-website/helpers/http"
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"
)
func AddTicket(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
rows, err := db.Query(`
SELECT DISTINCT draw_date
@@ -39,11 +44,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 +65,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
@@ -176,14 +181,14 @@ func AddTicket(db *sql.DB) http.HandlerFunc {
}
func SubmitTicket(db *sql.DB) http.HandlerFunc {
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(10 << 20)
if err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
userID, ok := helpers.GetCurrentUserID(r)
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
@@ -264,8 +269,8 @@ 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)
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
@@ -355,10 +360,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

@@ -5,15 +5,20 @@ import (
"log"
"net/http"
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
}
@@ -31,18 +36,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 +57,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 +72,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 +85,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 +104,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 +119,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 +134,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 +153,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 +167,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"
"synlotto-website/helpers"
templateHandlers "synlotto-website/handlers/template"
httpHelpers "synlotto-website/helpers/http"
templateHelpers "synlotto-website/helpers/template"
"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 {
@@ -34,7 +37,7 @@ func MarkNotificationReadHandler(db *sql.DB) http.HandlerFunc {
return
}
session, _ := helpers.GetSession(w, r)
session, _ := httpHelpers.GetSession(w, r)
userID, ok := session.Values["user_id"].(int)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
@@ -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"
@@ -63,7 +65,7 @@ func ResultsThunderball(db *sql.DB) http.HandlerFunc {
args = append(args, ballSetFilter)
}
totalPages, totalResults := helpers.GetTotalPages(db, "results_thunderball", whereClause, args, pageSize)
totalPages, totalResults := templateHelpers.GetTotalPages(db, "results_thunderball", whereClause, args, pageSize)
if page < 1 || page > totalPages {
http.NotFound(w, r)
return
@@ -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

@@ -1,16 +1,23 @@
package handlers
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
var (
sessionStore *sessions.CookieStore
sessionName string
SessionStore *sessions.CookieStore
Name string
)
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
return sessionStore.Get(r, sessionName)
if SessionStore == nil {
return nil, fmt.Errorf("session store not initialized")
}
if Name == "" {
return nil, fmt.Errorf("session name not configured")
}
return SessionStore.Get(r, Name)
}

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,19 +24,15 @@ 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 user != nil {
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)
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)
notifications = storage.GetRecentNotifications(db, user.Id, 15)
messageCount, _ = storage.GetMessageCount(db, user.Id)
messages = storage.GetRecentMessages(db, user.Id, 15)
}
}
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 (
"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

@@ -1,77 +0,0 @@
package helpers
import (
"encoding/gob"
"net/http"
"time"
"github.com/gorilla/sessions"
)
var authKey = []byte("12345678901234567890123456789012") // ToDo: Make env var
var encryptKey = []byte("abcdefghijklmnopqrstuvwx12345678") // ToDo: Make env var
var sessionName = "synlotto-session"
var store = sessions.NewCookieStore(authKey, encryptKey)
const SessionTimeout = 30 * time.Minute
func init() {
gob.Register(time.Time{})
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 1,
HttpOnly: true,
Secure: false, // TODO: make env-configurable
SameSite: http.SameSiteLaxMode,
}
}
func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) {
return store.Get(r, sessionName)
}
func IsSessionExpired(session *sessions.Session) bool {
last, ok := session.Values["last_activity"].(time.Time)
if !ok {
return false
}
return time.Since(last) > SessionTimeout
}
func UpdateSessionActivity(session *sessions.Session, r *http.Request, w http.ResponseWriter) {
session.Values["last_activity"] = time.Now()
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, "/login", http.StatusSeeOther)
return
}
UpdateSessionActivity(session, r, w)
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 {
@@ -22,14 +29,16 @@ func TemplateContext(w http.ResponseWriter, r *http.Request, data models.Templat
}
return map[string]interface{}{
"CSRFField": csrf.TemplateField(r),
"Flash": flash,
"User": data.User,
"IsAdmin": data.IsAdmin,
"NotificationCount": data.NotificationCount,
"Notifications": data.Notifications,
"MessageCount": data.MessageCount,
"Messages": data.Messages,
"CSRFField": csrf.TemplateField(r),
"Flash": flash,
"User": data.User,
"IsAdmin": data.IsAdmin,
"NotificationCount": data.NotificationCount,
"Notifications": data.Notifications,
"MessageCount": data.MessageCount,
"Messages": data.Messages,
"SiteName": cfg.Site.SiteName,
"CopyrightYearStart": cfg.Site.CopyrightYearStart,
}
}
@@ -58,9 +67,8 @@ func TemplateFuncs() template.FuncMap {
}
return *p
},
"inSlice": InSlice,
"lower": lower,
"rangeClass": rangeClass,
"inSlice": InSlice,
"lower": lower,
"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,14 +4,15 @@ import (
"net/http"
"time"
httpHelpers "synlotto-website/helpers/http"
"synlotto-website/constants"
"synlotto-website/helpers"
)
func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, _ := helpers.GetSession(w, r)
session, _ := httpHelpers.GetSession(w, r)
_, ok := session.Values["user_id"].(int)
@@ -26,7 +27,7 @@ func Auth(required bool) func(http.HandlerFunc) http.HandlerFunc {
session.Options.MaxAge = -1
session.Save(r, w)
newSession, _ := helpers.GetSession(w, r)
newSession, _ := httpHelpers.GetSession(w, r)
newSession.Values["flash"] = "Your session has timed out."
newSession.Save(r, w)

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

@@ -1,19 +1,29 @@
package models
type Config struct {
CSRF struct {
CSRFKey string `json:"csrfKey"`
} `json:"csrf"`
HttpServer struct {
Port int `json:"port"`
Address string `json:"address"`
ProductionMode bool `json:"productionMode"`
} `json:"httpServer"`
CSRF 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"`
Name string `json:"sessionName"`
Name string `json:"name"`
} `json:"session"`
Site struct {
SiteName string `json:"siteName"`
CopyrightYearStart int `json:"copyrightYearStart"`
} `json:"site"`
}

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,16 +4,19 @@ import (
"database/sql"
"net/http"
account "synlotto-website/handlers/account"
lotteryDrawHandlers "synlotto-website/handlers/lottery/tickets"
"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("/account/tickets/add_ticket", handlers.AddTicket(db))
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db))
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", lotteryDrawHandlers.AddTicket(db))
mux.HandleFunc("/account/tickets/my_tickets", lotteryDrawHandlers.GetMyTickets(db))
mux.HandleFunc("/account/messages", middleware.Protected(handlers.MessagesInboxHandler(db)))
mux.HandleFunc("/account/messages/read", middleware.Protected(handlers.ReadMessageHandler(db)))
mux.HandleFunc("/account/messages/archive", middleware.Protected(handlers.ArchiveMessageHandler(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

@@ -10,6 +10,8 @@ import (
_ "modernc.org/sqlite"
)
var db *sql.DB
func InitDB(filepath string) *sql.DB {
var err error
cfg := config.Get()

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

@@ -8,4 +8,35 @@
<label class="block">Machine: <input name="machine" class="input"></label>
<button class="btn">Create Draw</button>
</form>
{{ end }}
{{ end }}
<!-- Old new draw
{{ define "content" }}
<a href="/">← Back</a>
<h2>Add New Thunderball Draw</h2>
<form method="POST" action="/submit">
{{ .csrfField }}
<div class="form-section">
<label>Date: <input type="date" name="date" required></label>
</div>
<div class="form-section">
<label>Machine: <input type="text" name="machine" required></label>
</div>
<div class="form-section">
<label>Ball Set: <input type="text" name="ballset" required></label>
</div>
<div class="form-section">
<label>Ball 1: <input type="text" name="ball1" required></label>
<label>Ball 2: <input type="text" name="ball2" required></label>
<label>Ball 3: <input type="text" name="ball3" required></label>
<label>Ball 4: <input type="text" name="ball4" required></label>
<label>Ball 5: <input type="text" name="ball5" required></label>
</div>
<div class="form-section">
<label>Thunderball: <input type="text" name="thunderball" required></label>
</div>
<button type="submit">Save Draw</button>
</form>
{{ end }} -->

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,25 +63,24 @@
</div>
</nav>
<!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 pt-4">
{{ if .Flash }}
<div class="alert alert-info" role="alert">
{{ .Flash }}
</div>
{{ end }}
{{ template "content" . }}
</main>
</div>
<!-- Main Content -->
<main class="col px-md-4 pt-4">
{{ if .Flash }}
<div class="alert alert-info" role="alert">
{{ .Flash }}
</div>
{{ end }}
{{ template "content" . }}
</main>
</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 }}
{{ end }}

View File

@@ -1,27 +0,0 @@
{{ define "content" }}
<a href="/">← Back</a>
<h2>Add New Thunderball Draw</h2>
<form method="POST" action="/submit">
{{ .csrfField }}
<div class="form-section">
<label>Date: <input type="date" name="date" required></label>
</div>
<div class="form-section">
<label>Machine: <input type="text" name="machine" required></label>
</div>
<div class="form-section">
<label>Ball Set: <input type="text" name="ballset" required></label>
</div>
<div class="form-section">
<label>Ball 1: <input type="text" name="ball1" required></label>
<label>Ball 2: <input type="text" name="ball2" required></label>
<label>Ball 3: <input type="text" name="ball3" required></label>
<label>Ball 4: <input type="text" name="ball4" required></label>
<label>Ball 5: <input type="text" name="ball5" required></label>
</div>
<div class="form-section">
<label>Thunderball: <input type="text" name="thunderball" required></label>
</div>
<button type="submit">Save Draw</button>
</form>
{{ end }}