diff --git a/config/config.go b/config/config.go index 528efda..1e018b0 100644 --- a/config/config.go +++ b/config/config.go @@ -3,21 +3,24 @@ package config import "flag" type Config struct { - Port int `json:"port"` - LogPath string `json:"logPath"` - DatabasePath string `json:"databasePath"` + Port int `json:"port"` + LogPath string `json:"logPath"` + DatabasePath string `json:"databasePath"` + AdminPassword string `json:"adminPassword"` } func ParseFlags() Config { port := flag.Int("Port", 8080, "Listen port") logPath := flag.String("LogPath", "logs", "Path to log files") databasePath := flag.String("DatabasePath", "badgerdb", "Path to the database directory") + adminPassword := flag.String("AdminPassword", "", "Admin password for the web interface, disabled if empty") flag.Parse() return Config{ - Port: *port, - LogPath: *logPath, - DatabasePath: *databasePath, + Port: *port, + LogPath: *logPath, + DatabasePath: *databasePath, + AdminPassword: *adminPassword, } } diff --git a/go.mod b/go.mod index 2d7d2a4..2d949c0 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,16 @@ module pool-stats go 1.24.3 -require github.com/ostafen/clover/v2 v2.0.0-alpha.3.0.20250212110647-35f6fd38bde2 +require ( + github.com/gofrs/uuid/v5 v5.3.1 + github.com/ostafen/clover/v2 v2.0.0-alpha.3.0.20250212110647-35f6fd38bde2 +) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgraph-io/badger/v4 v4.5.1 // indirect github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gofrs/uuid/v5 v5.3.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/orderedcode v0.0.1 // indirect diff --git a/main.go b/main.go index 2fa0102..9733b76 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,7 @@ func main() { currentDayStatsRecalcJob := jobs.NewRecalculateCurrentDayStatsJob(db) go currentDayStatsRecalcJob.Run() - webServer := web.NewWebServer(db, config.Port) + webServer := web.NewWebServer(db, config.Port, config.AdminPassword) if err := webServer.Start(); err != nil { log.Fatalf("Failed to start web server: %v", err) } diff --git a/templates/layout.html b/templates/layout.html index f8f831c..c5b5b08 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -6,12 +6,50 @@ {{ template "title" . }} + +

{{ template "header" . }}

- {{ template "content" . }} {{ template "navigation" . }} + {{ template "content" . }} -{{ end }} {{ define "navigation" }} - {{ end }} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..1c12ec9 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,61 @@ +{{ define "title" }}Login{{ end }} {{ define "header" }}🔒 Login{{ end }} {{ +define "content" }} + + +
+
+

+ + {{ if .ErrorMessage }} +

{{ .ErrorMessage }}

+ {{ end }} +
+ +{{ end }} {{ template "layout" . }} diff --git a/web/dailyStatsHandler.go b/web/dailyStatsHandler.go index 2595022..273ef28 100644 --- a/web/dailyStatsHandler.go +++ b/web/dailyStatsHandler.go @@ -1,7 +1,6 @@ package web import ( - "html/template" "net/http" "pool-stats/constants" "pool-stats/database" @@ -10,6 +9,8 @@ import ( ) type DailyStatsPageData struct { + PageDataBase + DailyStats []models.DailyStats Start string @@ -25,16 +26,10 @@ type DailyStatsPageData struct { } func (ws *WebServer) DailyStatsHandler(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.Must(ws.templates.Clone()).ParseFiles("templates/daily_stats.html") - if err != nil { - http.Error(w, "Failed to parse template", http.StatusInternalServerError) - println("Error parsing template:", err.Error()) - return - } - startParam := r.URL.Query().Get("start") endParam := r.URL.Query().Get("end") var startTime, endTime time.Time + var err error if startParam == "" || endParam == "" { endTime = time.Now().Truncate(24 * time.Hour) @@ -91,9 +86,6 @@ func (ws *WebServer) DailyStatsHandler(w http.ResponseWriter, r *http.Request) { PrevPageStart: prevPageStart.Format(time.DateOnly), PrevPageEnd: prevPageEnd.Format(time.DateOnly), } - if err := tmpl.ExecuteTemplate(w, "daily_stats.html", data); err != nil { - http.Error(w, "Failed to render template", http.StatusInternalServerError) - println("Error rendering template:", err.Error()) - return - } + + ws.renderTemplate(w, r, "templates/daily_stats.html", &data) } diff --git a/web/indexHandler.go b/web/indexHandler.go index d3361a3..ff69ba0 100644 --- a/web/indexHandler.go +++ b/web/indexHandler.go @@ -1,7 +1,6 @@ package web import ( - "html/template" "net/http" "pool-stats/database" @@ -9,17 +8,12 @@ import ( ) type IndexPageData struct { + PageDataBase + Stats []models.TimeWindowHighShare } func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.Must(ws.templates.Clone()).ParseFiles("templates/index.html") - if err != nil { - http.Error(w, "Failed to parse template", 500) - println("Error parsing template:", err.Error()) - return - } - tws := database.GetTimeWindowHighShares(ws.db) if tws == nil { http.Error(w, "Failed to load time window high shares", 500) @@ -30,9 +24,5 @@ func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) { Stats: tws, } - if err := tmpl.ExecuteTemplate(w, "index.html", indexData); err != nil { - http.Error(w, "Failed to render template", 500) - println("Error rendering template:", err.Error()) - return - } + ws.renderTemplate(w, r, "templates/index.html", &indexData) } diff --git a/web/loginHandler.go b/web/loginHandler.go new file mode 100644 index 0000000..bc7beb5 --- /dev/null +++ b/web/loginHandler.go @@ -0,0 +1,42 @@ +package web + +import ( + "net/http" +) + +type LoginPageData struct { + PageDataBase + + ErrorMessage string +} + +func (ws *WebServer) LoginHandler(w http.ResponseWriter, r *http.Request) { + loginData := LoginPageData{} + + if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + loginData.ErrorMessage = "Failed to parse form data" + } else { + password := r.FormValue("password") + if password != ws.adminPassword || ws.adminPassword == "" { + loginData.ErrorMessage = "Invalid password" + } else { + sessionID := generateSessionID() + if sessionID == "" { + loginData.ErrorMessage = "Failed to generate session ID" + } + + ws.sessions[sessionID] = "admin" + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: sessionID, + Path: "/", + HttpOnly: true, + }) + http.Redirect(w, r, "/", http.StatusSeeOther) + } + } + } + + ws.renderTemplate(w, r, "templates/login.html", &loginData) +} diff --git a/web/logoutHandler.go b/web/logoutHandler.go new file mode 100644 index 0000000..618929b --- /dev/null +++ b/web/logoutHandler.go @@ -0,0 +1,25 @@ +package web + +import ( + "net/http" + "time" +) + +func (ws *WebServer) LogoutHandler(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err != nil { + http.Error(w, "No session found", http.StatusUnauthorized) + return + } + + delete(ws.sessions, cookie.Value) + + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: "", + Path: "/", + HttpOnly: true, + Expires: <-time.After(time.Second * 1), + }) + http.Redirect(w, r, "/", http.StatusSeeOther) +} diff --git a/web/models.go b/web/models.go new file mode 100644 index 0000000..d91754b --- /dev/null +++ b/web/models.go @@ -0,0 +1,18 @@ +package web + +type PageDataBase struct { + UserName string +} + +func (pageData PageDataBase) GetUserName() string { + return pageData.UserName +} + +func (pageData *PageDataBase) SetUserName(userName string) { + pageData.UserName = userName +} + +type IPageDataBase interface { + GetUserName() string + SetUserName(userName string) +} diff --git a/web/server.go b/web/server.go index 370979c..3c785de 100644 --- a/web/server.go +++ b/web/server.go @@ -3,20 +3,24 @@ package web import ( "html/template" "net/http" + "path/filepath" "pool-stats/helpers" "fmt" + "github.com/gofrs/uuid/v5" "github.com/ostafen/clover/v2" ) type WebServer struct { - db *clover.DB - port int - templates *template.Template + db *clover.DB + port int + templates *template.Template + sessions map[string]string + adminPassword string } -func NewWebServer(db *clover.DB, port int) *WebServer { +func NewWebServer(db *clover.DB, port int, adminPassword string) *WebServer { templates := template.New("base").Funcs(template.FuncMap{ "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, @@ -30,14 +34,18 @@ func NewWebServer(db *clover.DB, port int) *WebServer { )) return &WebServer{ - db: db, - port: port, - templates: templates, + db: db, + port: port, + templates: templates, + sessions: make(map[string]string), + adminPassword: adminPassword, } } func (ws *WebServer) Start() error { http.HandleFunc("/", ws.IndexHandler) + http.HandleFunc("/login", ws.LoginHandler) + http.HandleFunc("/logout", ws.LogoutHandler) http.HandleFunc("/shares", ws.SharesHandler) http.HandleFunc("/top-shares", ws.TopSharesHandler) http.HandleFunc("/daily-stats", ws.DailyStatsHandler) @@ -46,3 +54,48 @@ func (ws *WebServer) Start() error { println("Listening on", address) return http.ListenAndServe(address, nil) } + +func generateSessionID() string { + uuid, err := uuid.NewV4() + if err != nil { + fmt.Println("Error generating session ID:", err) + return "" + } + + return uuid.String() +} + +func (ws *WebServer) getUser(r *http.Request) string { + cookie, err := r.Cookie("session_id") + if err != nil { + return "" + } + if user, ok := ws.sessions[cookie.Value]; ok { + return user + } + return "" +} + +func (ws *WebServer) renderTemplate( + w http.ResponseWriter, + r *http.Request, + templateFile string, + data IPageDataBase) { + + data.SetUserName(ws.getUser(r)) + + tmpl, err := template.Must(ws.templates.Clone()).ParseFiles(templateFile) + if err != nil { + http.Error(w, "Failed to parse template", 500) + println("Error parsing template:", err.Error()) + return + } + + templateName := filepath.Base(templateFile) + + if err := tmpl.ExecuteTemplate(w, templateName, data); err != nil { + http.Error(w, "Failed to render template", 500) + println("Error rendering template:", err.Error()) + return + } +} diff --git a/web/sharesHandler.go b/web/sharesHandler.go index bc99579..2534c79 100644 --- a/web/sharesHandler.go +++ b/web/sharesHandler.go @@ -1,7 +1,6 @@ package web import ( - "html/template" "net/http" "pool-stats/database" "pool-stats/models" @@ -9,19 +8,14 @@ import ( ) type SharePageData struct { + PageDataBase + Shares []models.ShareLog Page int HasMore bool } func (ws *WebServer) SharesHandler(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.Must(ws.templates.Clone()).ParseFiles("templates/shares.html") - if err != nil { - http.Error(w, "Failed to parse template", 500) - println("Error parsing template:", err.Error()) - return - } - entriesPerPage := 10 page := r.URL.Query().Get("page") if page == "" { @@ -46,8 +40,6 @@ func (ws *WebServer) SharesHandler(w http.ResponseWriter, r *http.Request) { Page: offset/entriesPerPage + 1, HasMore: len(shareLogs) == entriesPerPage, } - if err := tmpl.ExecuteTemplate(w, "shares.html", data); err != nil { - http.Error(w, "Failed to render template", 500) - return - } + + ws.renderTemplate(w, r, "templates/shares.html", &data) } diff --git a/web/topSharesHandler.go b/web/topSharesHandler.go index 56e0ce0..3000773 100644 --- a/web/topSharesHandler.go +++ b/web/topSharesHandler.go @@ -1,24 +1,18 @@ package web import ( - "html/template" "net/http" "pool-stats/database" "pool-stats/models" ) type TopSharesPageData struct { + PageDataBase + Shares []models.ShareLog } func (ws *WebServer) TopSharesHandler(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.Must(ws.templates.Clone()).ParseFiles("templates/top_shares.html") - if err != nil { - http.Error(w, "Failed to parse template", 500) - println("Error parsing template:", err.Error()) - return - } - topShares := database.ListTopShares(ws.db) if topShares == nil { http.Error(w, "Failed to load top shares", 500) @@ -28,8 +22,6 @@ func (ws *WebServer) TopSharesHandler(w http.ResponseWriter, r *http.Request) { data := TopSharesPageData{ Shares: topShares, } - if err := tmpl.ExecuteTemplate(w, "top_shares.html", data); err != nil { - http.Error(w, "Failed to render template", 500) - return - } + + ws.renderTemplate(w, r, "templates/top_shares.html", &data) }