diff --git a/go.mod b/go.mod index ea4b159..b21fe98 100644 --- a/go.mod +++ b/go.mod @@ -2,16 +2,23 @@ module synlotto-website go 1.24.1 +require ( + github.com/gorilla/csrf v1.7.2 + github.com/gorilla/sessions v1.4.0 + golang.org/x/crypto v0.36.0 + modernc.org/sqlite v1.36.1 +) + require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect modernc.org/libc v1.61.13 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect - modernc.org/sqlite v1.36.1 // indirect ) diff --git a/go.sum b/go.sum index b838c33..39a5de3 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,57 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= +github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ= modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/handlers/account.go b/handlers/account.go new file mode 100644 index 0000000..5d1504d --- /dev/null +++ b/handlers/account.go @@ -0,0 +1,69 @@ +package handlers + +import ( + "html/template" + "net/http" + "synlotto-website/models" + + "github.com/gorilla/csrf" +) + +func Login(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/account/login.html", + )) + + tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "csrfField": csrf.TemplateField(r), + }) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + user := models.GetUserByUsername(username) + if user == nil || !CheckPasswordHash(user.PasswordHash, password) { + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + session, _ := GetSession(w, r) + session.Values["user_id"] = user.Id + session.Save(r, w) + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func Signup(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/account/signup.html", + )) + + tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "csrfField": csrf.TemplateField(r), + }) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + hashed, err := HashPassword(password) + if err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + + err = models.CreateUser(username, hashed) + if err != nil { + http.Error(w, "Could not create user", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/login", http.StatusSeeOther) +} diff --git a/handlers/auth.go b/handlers/auth.go new file mode 100644 index 0000000..160b364 --- /dev/null +++ b/handlers/auth.go @@ -0,0 +1,13 @@ +package handlers + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +func CheckPasswordHash(hash, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/handlers/common.go b/handlers/common.go index ba82039..eca3fbf 100644 --- a/handlers/common.go +++ b/handlers/common.go @@ -1,17 +1,8 @@ package handlers import ( - "html/template" "synlotto-website/models" ) -var Tmpl = template.Must(template.ParseFiles( - "templates/layout.html", - "templates/index.html", - "templates/new_draw.html", - "templates/new_ticket.html", - "templates/tickets.html", -)) - var Draws []models.ThunderballResult var MyTickets []models.MyTicket diff --git a/handlers/draw_handler.go b/handlers/draw_handler.go index 9348e43..156bcb7 100644 --- a/handlers/draw_handler.go +++ b/handlers/draw_handler.go @@ -1,17 +1,24 @@ package handlers import ( + "html/template" "log" "net/http" "synlotto-website/helpers" "synlotto-website/models" + + "github.com/gorilla/csrf" ) func Home(w http.ResponseWriter, r *http.Request) { log.Println("✅ Home hit") - err := Tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ - "Page": "index", + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/index.html", + )) + + err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ "Data": Draws, }) if err != nil { @@ -23,9 +30,15 @@ func Home(w http.ResponseWriter, r *http.Request) { func NewDraw(w http.ResponseWriter, r *http.Request) { log.Println("➡️ New draw form opened") - err := Tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ - "Page": "new_draw", - "Data": nil, + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/new_draw.html", + )) + + err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "csrfField": csrf.TemplateField(r), + "Page": "new_draw", + "Data": nil, }) if err != nil { log.Println("❌ Template error:", err) diff --git a/handlers/session.go b/handlers/session.go new file mode 100644 index 0000000..2f3e740 --- /dev/null +++ b/handlers/session.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "net/http" + + "github.com/gorilla/sessions" +) + +var store = sessions.NewCookieStore([]byte("super-secret-key")) // ToDo: Make global + +func init() { + store.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400 * 1, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + } +} + +func GetSession(w http.ResponseWriter, r *http.Request) (*sessions.Session, error) { + return store.Get(r, "session-name") +} + +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 +} + +func RequireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, ok := GetCurrentUserID(r) + if !ok { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + next(w, r) + } +} diff --git a/handlers/ticket_handler.go b/handlers/ticket_handler.go index 410dd89..6970e13 100644 --- a/handlers/ticket_handler.go +++ b/handlers/ticket_handler.go @@ -2,21 +2,29 @@ package handlers import ( "database/sql" + "html/template" "log" "net/http" - "synlotto-website/helpers" "synlotto-website/models" "synlotto-website/storage" + + "github.com/gorilla/csrf" ) func NewTicket(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Println("➡️ New ticket form opened") - err := Tmpl.ExecuteTemplate(w, "new_ticket", map[string]interface{}{ - "Page": "new_ticket", - "Data": nil, + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/new_ticket.html", + )) + + err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ + "csrfField": csrf.TemplateField(r), + "Page": "new_ticket", + "Data": nil, }) if err != nil { log.Println("❌ Template error:", err) @@ -27,6 +35,10 @@ func NewTicket(db *sql.DB) http.HandlerFunc { func SubmitTicket(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + if _, ok := GetCurrentUserID(r); !ok { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } ticket := models.MyTicket{ GameType: r.FormValue("game_type"), DrawDate: r.FormValue("draw_date"), @@ -53,6 +65,11 @@ func ListTickets(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Println("📋 Tickets page hit") + tmpl := template.Must(template.ParseFiles( + "templates/layout.html", + "templates/tickets.html", + )) + rows, err := db.Query(` SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, bonus1, bonus2, duplicate FROM my_tickets @@ -80,7 +97,7 @@ func ListTickets(db *sql.DB) http.HandlerFunc { tickets = append(tickets, t) } - err = Tmpl.ExecuteTemplate(w, "tickets", map[string]any{ + err = tmpl.ExecuteTemplate(w, "layout", map[string]any{ "Page": "tickets", "Data": tickets, }) diff --git a/main.go b/main.go index 059898b..9eb69ba 100644 --- a/main.go +++ b/main.go @@ -4,31 +4,32 @@ import ( "log" "net/http" "synlotto-website/handlers" + "synlotto-website/models" "synlotto-website/storage" + + "github.com/gorilla/csrf" ) func main() { db := storage.InitDB("synlotto.db") + models.SetDB(db) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/": - handlers.Home(w, r) - case "/new": - handlers.NewDraw(w, r) - case "/submit": - handlers.Submit(w, r) - case "/ticket": - handlers.NewTicket(db) - case "/tickets": - handlers.ListTickets(db) - case "/submit-ticket": - handlers.SubmitTicket(db) - default: - http.NotFound(w, r) - } - }) + csrfMiddleware := csrf.Protect( + []byte("32-byte-long-auth-key-here"), // TodO: Make Global + csrf.Secure(false), + ) + + mux := http.NewServeMux() + + mux.HandleFunc("/", handlers.Home) + mux.HandleFunc("/new", handlers.NewDraw) + mux.HandleFunc("/submit", handlers.Submit) + mux.HandleFunc("/ticket", handlers.NewTicket(db)) + mux.HandleFunc("/tickets", handlers.ListTickets(db)) + mux.HandleFunc("/submit-ticket", handlers.RequireAuth(handlers.SubmitTicket(db))) + mux.HandleFunc("/login", handlers.Login) + mux.HandleFunc("/signup", handlers.Signup) log.Println("🌐 Running on http://localhost:8080") - log.Fatal(http.ListenAndServe(":8080", nil)) + http.ListenAndServe(":8080", csrfMiddleware(mux)) } diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..b399a9c --- /dev/null +++ b/models/user.go @@ -0,0 +1,37 @@ +package models + +import ( + "database/sql" + "log" +) + +type User struct { + Id int + Username string + PasswordHash string +} + +var db *sql.DB + +func SetDB(database *sql.DB) { + db = database +} + +func CreateUser(username, passwordHash string) error { + _, err := db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", username, passwordHash) + return err +} + +func GetUserByUsername(username string) *User { + row := db.QueryRow("SELECT id, username, password_hash FROM users WHERE username = ?", username) + + var user User + err := row.Scan(&user.Id, &user.Username, &user.PasswordHash) + if err != nil { + if err != sql.ErrNoRows { + log.Println("DB error:", err) + } + return nil + } + return &user +} diff --git a/storage/db.go b/storage/db.go index 273d976..35e262c 100644 --- a/storage/db.go +++ b/storage/db.go @@ -27,7 +27,11 @@ func InitDB(filepath string) *sql.DB { thunderball INTEGER );` - createMyTickets := ` + if _, err := db.Exec(createThunderballResultsTable); err != nil { + log.Fatal("❌ Failed to create Thunderball table:", err) + } + + createMyTicketsTable := ` CREATE TABLE IF NOT EXISTS my_tickets ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_type TEXT NOT NULL, @@ -43,12 +47,20 @@ func InitDB(filepath string) *sql.DB { created_at DATETIME DEFAULT CURRENT_TIMESTAMP );` - if _, err := db.Exec(createThunderballResultsTable); err != nil { - log.Fatal("❌ Failed to create Thunderball table:", err) - } - if _, err := db.Exec(createMyTickets); err != nil { + if _, err := db.Exec(createMyTicketsTable); err != nil { log.Fatal("❌ Failed to create MyTickets table:", err) } + createUsersTable := ` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL + );` + + if _, err := db.Exec(createUsersTable); err != nil { + log.Fatal("❌ Failed to create Users table:", err) + } + return db } diff --git a/synlotto.db b/synlotto.db index a51eaf6..5c08a8e 100644 Binary files a/synlotto.db and b/synlotto.db differ diff --git a/templates/account/login.html b/templates/account/login.html new file mode 100644 index 0000000..720dc1b --- /dev/null +++ b/templates/account/login.html @@ -0,0 +1,9 @@ +{{ define "content" }} +