feat: add minimal web dashboard with stats, top credentials, and sessions
Implements Phase 1.5 — an embedded web UI using Go templates, Pico CSS (dark theme), and htmx for auto-refreshing stats and active sessions. Adds read query methods to the Store interface (GetDashboardStats, GetTopUsernames, GetTopPasswords, GetTopIPs, GetRecentSessions) with implementations for both SQLite and MemoryStore. Introduces the internal/web package with server, handlers, templates, and tests. Web server is opt-in via [web] config section and runs alongside SSH with graceful shutdown. Bumps version to 0.2.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
104
internal/web/handlers.go
Normal file
104
internal/web/handlers.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.t-juice.club/torjus/oubliette/internal/storage"
|
||||
)
|
||||
|
||||
type dashboardData struct {
|
||||
Stats *storage.DashboardStats
|
||||
TopUsernames []storage.TopEntry
|
||||
TopPasswords []storage.TopEntry
|
||||
TopIPs []storage.TopEntry
|
||||
ActiveSessions []storage.Session
|
||||
RecentSessions []storage.Session
|
||||
}
|
||||
|
||||
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
stats, err := s.store.GetDashboardStats(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get dashboard stats", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
topUsernames, err := s.store.GetTopUsernames(ctx, 10)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get top usernames", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
topPasswords, err := s.store.GetTopPasswords(ctx, 10)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get top passwords", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
topIPs, err := s.store.GetTopIPs(ctx, 10)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get top IPs", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
activeSessions, err := s.store.GetRecentSessions(ctx, 50, true)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get active sessions", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
recentSessions, err := s.store.GetRecentSessions(ctx, 50, false)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get recent sessions", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := dashboardData{
|
||||
Stats: stats,
|
||||
TopUsernames: topUsernames,
|
||||
TopPasswords: topPasswords,
|
||||
TopIPs: topIPs,
|
||||
ActiveSessions: activeSessions,
|
||||
RecentSessions: recentSessions,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := s.tmpl.ExecuteTemplate(w, "layout.html", data); err != nil {
|
||||
s.logger.Error("failed to render dashboard", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleFragmentStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := s.store.GetDashboardStats(r.Context())
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get dashboard stats", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := s.tmpl.ExecuteTemplate(w, "stats", stats); err != nil {
|
||||
s.logger.Error("failed to render stats fragment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleFragmentActiveSessions(w http.ResponseWriter, r *http.Request) {
|
||||
sessions, err := s.store.GetRecentSessions(r.Context(), 50, true)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get active sessions", "err", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := s.tmpl.ExecuteTemplate(w, "active_sessions", sessions); err != nil {
|
||||
s.logger.Error("failed to render active sessions fragment", "err", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user