Embeds a DB-IP Lite country MMDB (~5MB) in the binary via go:embed, keeping the single-binary deployment story clean. Country codes are stored alongside login attempts and sessions, shown in the dashboard (Top IPs, Top Countries card, Recent/Active Sessions, session detail). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
204 lines
5.7 KiB
Go
204 lines
5.7 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"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
|
|
TopCountries []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
|
|
}
|
|
|
|
topCountries, err := s.store.GetTopCountries(ctx, 10)
|
|
if err != nil {
|
|
s.logger.Error("failed to get top countries", "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,
|
|
TopCountries: topCountries,
|
|
ActiveSessions: activeSessions,
|
|
RecentSessions: recentSessions,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := s.tmpl.dashboard.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.dashboard.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.dashboard.ExecuteTemplate(w, "active_sessions", sessions); err != nil {
|
|
s.logger.Error("failed to render active sessions fragment", "err", err)
|
|
}
|
|
}
|
|
|
|
type sessionDetailData struct {
|
|
Session *storage.Session
|
|
Logs []storage.SessionLog
|
|
EventCount int
|
|
}
|
|
|
|
func (s *Server) handleSessionDetail(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
sessionID := r.PathValue("id")
|
|
|
|
session, err := s.store.GetSession(ctx, sessionID)
|
|
if err != nil {
|
|
s.logger.Error("failed to get session", "err", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if session == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
logs, err := s.store.GetSessionLogs(ctx, sessionID)
|
|
if err != nil {
|
|
s.logger.Error("failed to get session logs", "err", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
events, err := s.store.GetSessionEvents(ctx, sessionID)
|
|
if err != nil {
|
|
s.logger.Error("failed to get session events", "err", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
data := sessionDetailData{
|
|
Session: session,
|
|
Logs: logs,
|
|
EventCount: len(events),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := s.tmpl.sessionDetail.ExecuteTemplate(w, "layout.html", data); err != nil {
|
|
s.logger.Error("failed to render session detail", "err", err)
|
|
}
|
|
}
|
|
|
|
type apiEvent struct {
|
|
T int64 `json:"t"`
|
|
D int `json:"d"`
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
type apiEventsResponse struct {
|
|
Events []apiEvent `json:"events"`
|
|
}
|
|
|
|
func (s *Server) handleAPISessionEvents(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
sessionID := r.PathValue("id")
|
|
|
|
events, err := s.store.GetSessionEvents(ctx, sessionID)
|
|
if err != nil {
|
|
s.logger.Error("failed to get session events", "err", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp := apiEventsResponse{Events: make([]apiEvent, len(events))}
|
|
var baseTime int64
|
|
for i, e := range events {
|
|
ms := e.Timestamp.UnixMilli()
|
|
if i == 0 {
|
|
baseTime = ms
|
|
}
|
|
resp.Events[i] = apiEvent{
|
|
T: ms - baseTime,
|
|
D: e.Direction,
|
|
Data: base64.StdEncoding.EncodeToString(e.Data),
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
s.logger.Error("failed to encode session events", "err", err)
|
|
}
|
|
}
|