package web
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/salvacybersec/keyhunter/pkg/dorks"
"github.com/salvacybersec/keyhunter/pkg/providers"
"github.com/salvacybersec/keyhunter/pkg/recon"
"github.com/salvacybersec/keyhunter/pkg/storage"
)
// Config holds all dependencies and settings needed by the web server.
type Config struct {
DB *storage.DB
EncKey []byte
Providers *providers.Registry
Dorks *dorks.Registry
ReconEngine *recon.Engine
Port int
AuthUser string
AuthPass string
AuthToken string
}
// Server is the KeyHunter web dashboard backed by chi.
type Server struct {
router chi.Router
cfg Config
tmpl *template.Template
}
// NewServer creates a Server, parsing embedded templates and building routes.
func NewServer(cfg Config) (*Server, error) {
// Parse all templates from the embedded filesystem.
tmpl, err := template.ParseFS(templateFiles, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("parsing templates: %w", err)
}
s := &Server{
cfg: cfg,
tmpl: tmpl,
}
r := chi.NewRouter()
// Middleware stack.
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Auth middleware (no-op when auth fields are empty).
r.Use(AuthMiddleware(cfg.AuthUser, cfg.AuthPass, cfg.AuthToken))
// Static file serving from embedded FS.
staticSub, err := fs.Sub(staticFiles, "static")
if err != nil {
return nil, fmt.Errorf("creating static sub-filesystem: %w", err)
}
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
// Page routes.
r.Get("/", s.handleOverview)
s.router = r
return s, nil
}
// Router returns the chi router for testing.
func (s *Server) Router() chi.Router {
return s.router
}
// ListenAndServe starts the HTTP server on the configured port.
func (s *Server) ListenAndServe() error {
addr := fmt.Sprintf(":%d", s.cfg.Port)
return http.ListenAndServe(addr, s.router)
}