package web import ( "crypto/rand" "crypto/subtle" "encoding/hex" "fmt" "net/http" "strings" "time" "golang.org/x/crypto/bcrypt" ) type session struct { token string expiresAt time.Time } const ( sessionCookieName = "felhom_session" sessionMaxAge = 24 * time.Hour ) // RequireAuth returns middleware that checks for valid session or shows login. func (s *Server) RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip auth if no password is configured if s.cfg.Web.PasswordHash == "" { next.ServeHTTP(w, r) return } if r.URL.Path == "/api/health" { next.ServeHTTP(w, r) return } if r.URL.Path == "/login" && r.Method == http.MethodPost { s.handleLogin(w, r) return } if r.URL.Path == "/login" { s.renderLogin(w, "") return } if r.URL.Path == "/logout" { s.handleLogout(w, r) return } cookie, err := r.Cookie(sessionCookieName) if err != nil || !s.isValidSession(cookie.Value) { if strings.HasPrefix(r.URL.Path, "/api/") { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, `{"ok":false,"error":"authentication required"}`) return } http.Redirect(w, r, "/login", http.StatusFound) return } next.ServeHTTP(w, r) }) } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() password := r.FormValue("password") if password == "" { s.renderLogin(w, "Kérjük adja meg a jelszót") return } if err := bcrypt.CompareHashAndPassword([]byte(s.cfg.Web.PasswordHash), []byte(password)); err != nil { s.logger.Printf("[WARN] Failed login from %s", r.RemoteAddr) s.renderLogin(w, "Hibás jelszó") return } token := s.createSession() http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: token, Path: "/", MaxAge: int(sessionMaxAge.Seconds()), HttpOnly: true, SameSite: http.SameSiteStrictMode, Secure: true, }) s.logger.Printf("[INFO] Login from %s", r.RemoteAddr) http.Redirect(w, r, "/", http.StatusFound) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(sessionCookieName); err == nil { s.sessionsMu.Lock() delete(s.sessions, cookie.Value) s.sessionsMu.Unlock() } http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1}) http.Redirect(w, r, "/login", http.StatusFound) } func (s *Server) createSession() string { b := make([]byte, 32) _, _ = rand.Read(b) token := hex.EncodeToString(b) s.sessionsMu.Lock() s.sessions[token] = &session{token: token, expiresAt: time.Now().Add(sessionMaxAge)} s.sessionsMu.Unlock() return token } func (s *Server) isValidSession(token string) bool { s.sessionsMu.RLock() defer s.sessionsMu.RUnlock() sess, ok := s.sessions[token] if !ok || time.Now().After(sess.expiresAt) { return false } return subtle.ConstantTimeCompare([]byte(sess.token), []byte(token)) == 1 } func (s *Server) cleanupSessions() { for range time.Tick(15 * time.Minute) { s.sessionsMu.Lock() now := time.Now() for t, sess := range s.sessions { if now.After(sess.expiresAt) { delete(s.sessions, t) } } s.sessionsMu.Unlock() } } func (s *Server) renderLogin(w http.ResponseWriter, errorMsg string) { data := map[string]interface{}{ "Title": "Bejelentkezés", "CustomerName": s.cfg.Customer.Name, "Error": errorMsg, } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := s.tmpl.ExecuteTemplate(w, "login", data); err != nil { s.logger.Printf("[ERROR] Template error (login): %v", err) http.Error(w, "Internal error", http.StatusInternalServerError) } }