added authentication among other things. considered working at this point.

This commit is contained in:
2025-03-25 11:27:21 +00:00
parent cf8b0041b2
commit f1ad9757ba
19 changed files with 310 additions and 58 deletions

11
go.mod
View File

@@ -2,16 +2,23 @@ module synlotto-website
go 1.24.1 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 ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // 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/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // 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/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.36.1 // indirect
) )

38
go.sum
View File

@@ -1,23 +1,57 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= 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 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= 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=

69
handlers/account.go Normal file
View File

@@ -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)
}

13
handlers/auth.go Normal file
View File

@@ -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
}

View File

@@ -1,17 +1,8 @@
package handlers package handlers
import ( import (
"html/template"
"synlotto-website/models" "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 Draws []models.ThunderballResult
var MyTickets []models.MyTicket var MyTickets []models.MyTicket

View File

@@ -1,17 +1,24 @@
package handlers package handlers
import ( import (
"html/template"
"log" "log"
"net/http" "net/http"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
"github.com/gorilla/csrf"
) )
func Home(w http.ResponseWriter, r *http.Request) { func Home(w http.ResponseWriter, r *http.Request) {
log.Println("✅ Home hit") log.Println("✅ Home hit")
err := Tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ tmpl := template.Must(template.ParseFiles(
"Page": "index", "templates/layout.html",
"templates/index.html",
))
err := tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{
"Data": Draws, "Data": Draws,
}) })
if err != nil { if err != nil {
@@ -23,7 +30,13 @@ func Home(w http.ResponseWriter, r *http.Request) {
func NewDraw(w http.ResponseWriter, r *http.Request) { func NewDraw(w http.ResponseWriter, r *http.Request) {
log.Println("➡️ New draw form opened") log.Println("➡️ New draw form opened")
err := Tmpl.ExecuteTemplate(w, "layout", map[string]interface{}{ 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", "Page": "new_draw",
"Data": nil, "Data": nil,
}) })

44
handlers/session.go Normal file
View File

@@ -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)
}
}

View File

@@ -2,19 +2,27 @@ package handlers
import ( import (
"database/sql" "database/sql"
"html/template"
"log" "log"
"net/http" "net/http"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
"synlotto-website/storage" "synlotto-website/storage"
"github.com/gorilla/csrf"
) )
func NewTicket(db *sql.DB) http.HandlerFunc { func NewTicket(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Println("➡️ New ticket form opened") log.Println("➡️ New ticket form opened")
err := Tmpl.ExecuteTemplate(w, "new_ticket", map[string]interface{}{ 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", "Page": "new_ticket",
"Data": nil, "Data": nil,
}) })
@@ -27,6 +35,10 @@ func NewTicket(db *sql.DB) http.HandlerFunc {
func SubmitTicket(db *sql.DB) http.HandlerFunc { func SubmitTicket(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, ok := GetCurrentUserID(r); !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
ticket := models.MyTicket{ ticket := models.MyTicket{
GameType: r.FormValue("game_type"), GameType: r.FormValue("game_type"),
DrawDate: r.FormValue("draw_date"), DrawDate: r.FormValue("draw_date"),
@@ -53,6 +65,11 @@ func ListTickets(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Println("📋 Tickets page hit") log.Println("📋 Tickets page hit")
tmpl := template.Must(template.ParseFiles(
"templates/layout.html",
"templates/tickets.html",
))
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, bonus1, bonus2, duplicate SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, bonus1, bonus2, duplicate
FROM my_tickets FROM my_tickets
@@ -80,7 +97,7 @@ func ListTickets(db *sql.DB) http.HandlerFunc {
tickets = append(tickets, t) tickets = append(tickets, t)
} }
err = Tmpl.ExecuteTemplate(w, "tickets", map[string]any{ err = tmpl.ExecuteTemplate(w, "layout", map[string]any{
"Page": "tickets", "Page": "tickets",
"Data": tickets, "Data": tickets,
}) })

39
main.go
View File

@@ -4,31 +4,32 @@ import (
"log" "log"
"net/http" "net/http"
"synlotto-website/handlers" "synlotto-website/handlers"
"synlotto-website/models"
"synlotto-website/storage" "synlotto-website/storage"
"github.com/gorilla/csrf"
) )
func main() { func main() {
db := storage.InitDB("synlotto.db") db := storage.InitDB("synlotto.db")
models.SetDB(db)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { csrfMiddleware := csrf.Protect(
switch r.URL.Path { []byte("32-byte-long-auth-key-here"), // TodO: Make Global
case "/": csrf.Secure(false),
handlers.Home(w, r) )
case "/new":
handlers.NewDraw(w, r) mux := http.NewServeMux()
case "/submit":
handlers.Submit(w, r) mux.HandleFunc("/", handlers.Home)
case "/ticket": mux.HandleFunc("/new", handlers.NewDraw)
handlers.NewTicket(db) mux.HandleFunc("/submit", handlers.Submit)
case "/tickets": mux.HandleFunc("/ticket", handlers.NewTicket(db))
handlers.ListTickets(db) mux.HandleFunc("/tickets", handlers.ListTickets(db))
case "/submit-ticket": mux.HandleFunc("/submit-ticket", handlers.RequireAuth(handlers.SubmitTicket(db)))
handlers.SubmitTicket(db) mux.HandleFunc("/login", handlers.Login)
default: mux.HandleFunc("/signup", handlers.Signup)
http.NotFound(w, r)
}
})
log.Println("🌐 Running on http://localhost:8080") log.Println("🌐 Running on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil)) http.ListenAndServe(":8080", csrfMiddleware(mux))
} }

37
models/user.go Normal file
View File

@@ -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
}

View File

@@ -27,7 +27,11 @@ func InitDB(filepath string) *sql.DB {
thunderball INTEGER 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 ( CREATE TABLE IF NOT EXISTS my_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
game_type TEXT NOT NULL, game_type TEXT NOT NULL,
@@ -43,12 +47,20 @@ func InitDB(filepath string) *sql.DB {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);` );`
if _, err := db.Exec(createThunderballResultsTable); err != nil { if _, err := db.Exec(createMyTicketsTable); err != nil {
log.Fatal("❌ Failed to create Thunderball table:", err)
}
if _, err := db.Exec(createMyTickets); err != nil {
log.Fatal("❌ Failed to create MyTickets table:", err) 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 return db
} }

Binary file not shown.

View File

@@ -0,0 +1,9 @@
{{ define "content" }}
<h2>Login</h2>
<form method="POST" action="/login">
{{ .csrfField }}
<label>Username: <input type="text" name="username" required></label><br>
<label>Password: <input type="password" name="password" required></label><br>
<button type="submit">Login</button>
</form>
{{ end }}

View File

@@ -0,0 +1,9 @@
{{ define "content" }}
<h2>Sign Up</h2>
<form method="POST" action="/signup">
{{ .csrfField }}
<label>Username: <input type="text" name="username" required></label><br>
<label>Password: <input type="password" name="password" required></label><br>
<button type="submit">Sign Up</button>
</form>
{{ end }}

View File

@@ -1,4 +1,4 @@
{{ define "index" }} {{ define "content" }}
<a href="/new">+ Add New Draw</a> <a href="/new">+ Add New Draw</a>
<table> <table>
<tr> <tr>

View File

@@ -15,11 +15,7 @@
<body> <body>
<h1>Lotto Tracker</h1> <h1>Lotto Tracker</h1>
{{ if eq .Page "index" }} {{ template "content" . }}
{{ template "index" .Data }}
{{ else if eq .Page "new_draw" }}
{{ template "new_draw" .Data }}
{{ end }}
</body> </body>
</html> </html>

View File

@@ -1,7 +1,8 @@
{{ define "new_draw" }} {{ define "content" }}
<a href="/">← Back</a> <a href="/">← Back</a>
<h2>Add New Thunderball Draw</h2> <h2>Add New Thunderball Draw</h2>
<form method="POST" action="/submit"> <form method="POST" action="/submit">
{{ .csrfField }}
<div class="form-section"> <div class="form-section">
<label>Date: <input type="date" name="date" required></label> <label>Date: <input type="date" name="date" required></label>
</div> </div>

View File

@@ -1,7 +1,8 @@
{{ define "new_ticket" }} {{ define "content" }}
<a href="/">← Back</a> <a href="/">← Back</a>
<h2>Log My Ticket</h2> <h2>Log My Ticket</h2>
<form method="POST" action="/submit-ticket"> <form method="POST" action="/submit-ticket">
{{ .csrfField }}
<div class="form-section"> <div class="form-section">
<label>Draw Date: <label>Draw Date:
<select name="draw_date" required> <select name="draw_date" required>

View File

@@ -1,4 +1,3 @@
{{ define "tickets" }}
{{ define "content" }} {{ define "content" }}
<a href="/">← Back to Home</a> <a href="/">← Back to Home</a>
<h2>My Tickets</h2> <h2>My Tickets</h2>
@@ -32,5 +31,4 @@
{{ end }} {{ end }}
</tbody> </tbody>
</table> </table>
{{ template "layout" . }}
{{ end }} {{ end }}