package ui import ( "bytes" "embed" "html/template" "io/fs" "log" "math" "net/http" "net/http/httptest" "path/filepath" "strconv" "strings" "epigas.gitea.cloud/RiskRancher/core/pkg/auth" "epigas.gitea.cloud/RiskRancher/core/pkg/domain" "epigas.gitea.cloud/RiskRancher/core/pkg/report" ) //go:embed templates/* templates/components/* static/* var CoreUIFS embed.FS var ( AppVersion = "dev" AppCommit = "none" ) var CoreTemplates *template.Template var Pages map[string]*template.Template // SetVersionInfo is called by main.go on startup to inject ldflags func SetVersionInfo(version, commit string) { AppVersion = version AppCommit = commit } func init() { funcMap := template.FuncMap{"lower": strings.ToLower} Pages = make(map[string]*template.Template) var err error CoreTemplates, err = template.New("").Funcs(funcMap).ParseFS(CoreUIFS, "templates/*.gohtml", "templates/components/*.gohtml") if err != nil && !strings.Contains(err.Error(), "pattern matches no files") { log.Printf("Warning: Failed to parse master core templates: %v", err) } dashTmpl := template.New("").Funcs(funcMap) dashTmpl, err = dashTmpl.ParseFS(CoreUIFS, "templates/base.gohtml", "templates/dashboard.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse dashboard shell. Err: %v", err) } Pages["dashboard"] = dashTmpl adminTmpl := template.New("").Funcs(funcMap) adminTmpl, err = adminTmpl.ParseFS(CoreUIFS, "templates/base.gohtml", "templates/admin.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse admin shell. Err: %v", err) } Pages["admin"] = adminTmpl Pages["login"], err = template.New("").Funcs(funcMap).ParseFS(CoreUIFS, "templates/login.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse login. Err: %v", err) } Pages["register"], err = template.New("").Funcs(funcMap).ParseFS(CoreUIFS, "templates/register.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse register. Err: %v", err) } Pages["assets"], err = template.New("").Funcs(funcMap).ParseFS(CoreUIFS, "templates/base.gohtml", "templates/assets.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse assets. Err: %v", err) } ingestTmpl := template.New("").Funcs(funcMap) ingestTmpl, err = ingestTmpl.ParseFS(CoreUIFS, "templates/base.gohtml", "templates/ingest.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse ingest shell. Err: %v", err) } Pages["ingest"] = ingestTmpl adapterTmpl := template.New("").Funcs(funcMap) adapterTmpl, err = adapterTmpl.ParseFS(CoreUIFS, "templates/base.gohtml", "templates/adapter_builder.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse adapter builder shell. Err: %v", err) } Pages["adapter_builder"] = adapterTmpl uploadTmpl := template.New("").Funcs(funcMap) uploadTmpl, err = uploadTmpl.ParseFS(CoreUIFS, "templates/base.gohtml", "templates/report_upload.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse report upload template. Err: %v", err) } Pages["report_upload"] = uploadTmpl parserTmpl := template.New("").Funcs(funcMap) parserTmpl, err = parserTmpl.ParseFS(CoreUIFS, "templates/base.gohtml", "templates/report_parser.gohtml", "templates/components/*.gohtml") if err != nil { log.Fatalf("FATAL: Failed to parse report parser template. Err: %v", err) } Pages["report_parser"] = parserTmpl } func StaticHandler() http.Handler { staticFS, err := fs.Sub(CoreUIFS, "static") if err != nil { log.Fatal("Failed to load embedded static files:", err) } return http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))) } type PageData struct { Tickets any CurrentTab string CurrentFilter string CurrentAsset string ReturnedCount int CountCritical int CountOverdue int CountMine int CurrentPage int TotalPages int NextPage int PrevPage int CountVerification int HasNext bool HasPrev bool Version string Commit string } func HandleDashboard(store domain.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userIDVal := r.Context().Value(auth.UserIDKey) if userIDVal == nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } userID := userIDVal.(int) user, err := store.GetUserByID(r.Context(), userID) if err != nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return } if user.GlobalRole == "Sheriff" { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } currentUserEmail := user.Email currentUserRole := user.GlobalRole tab := r.URL.Query().Get("tab") if tab == "" { tab = "holding_pen" } statusFilter := tab if tab == "holding_pen" { statusFilter = "Waiting to be Triaged" } else if tab == "chute" { statusFilter = "Assigned Out" } else if tab == "verification" { statusFilter = "Pending Verification" } filter := r.URL.Query().Get("filter") assetFilter := r.URL.Query().Get("asset") pageStr := r.URL.Query().Get("page") page, _ := strconv.Atoi(pageStr) if page < 1 { page = 1 } limit := 50 offset := (page - 1) * limit tickets, totalRecords, metrics, err := store.GetDashboardTickets( r.Context(), statusFilter, filter, assetFilter, currentUserEmail, currentUserRole, limit, offset, ) if err != nil { http.Error(w, "Database query error: "+err.Error(), http.StatusInternalServerError) return } totalPages := int(math.Ceil(float64(totalRecords) / float64(limit))) if totalPages == 0 { totalPages = 1 } data := PageData{ Tickets: tickets, CurrentTab: tab, CurrentFilter: filter, CurrentAsset: assetFilter, ReturnedCount: metrics["returned"], CountCritical: metrics["critical"], CountOverdue: metrics["overdue"], CountMine: metrics["mine"], CountVerification: metrics["verification"], CurrentPage: page, TotalPages: totalPages, NextPage: page + 1, PrevPage: page - 1, HasNext: page < totalPages, HasPrev: page > 1, Version: AppVersion, Commit: AppCommit, } var buf bytes.Buffer if err := Pages["dashboard"].ExecuteTemplate(&buf, "base", data); err != nil { http.Error(w, "Template rendering error: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") buf.WriteTo(w) } } func HandleLoginUI() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := Pages["login"].ExecuteTemplate(w, "login", nil); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) } } } func HandleRegisterUI() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := Pages["register"].ExecuteTemplate(w, "register", nil); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) } } } func HandleAdminDashboard(store domain.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { users, _ := store.GetAllUsers(r.Context()) config, _ := store.GetAppConfig(r.Context()) slas, _ := store.GetSLAPolicies(r.Context()) adapters, _ := store.GetAdapters(r.Context()) analytics, _ := store.GetSheriffAnalytics(r.Context()) activityFeed, _ := store.GetGlobalActivityFeed(r.Context(), 15) syncLogs, _ := store.GetRecentSyncLogs(r.Context(), 10) data := map[string]any{ "Users": users, "Config": config, "SLAs": slas, "Adapters": adapters, "Analytics": analytics, "Feed": activityFeed, "SyncLogs": syncLogs, "Version": AppVersion, "Commit": AppCommit, } var buf bytes.Buffer if err := Pages["admin"].ExecuteTemplate(&buf, "base", data); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") buf.WriteTo(w) } } func HandleIngestUI(store domain.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") adapters, _ := store.GetAdapters(r.Context()) data := map[string]any{ "Adapters": adapters, "Version": AppVersion, "Commit": AppCommit, } if err := Pages["ingest"].ExecuteTemplate(w, "base", data); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) } } } func HandleAdapterBuilderUI(store domain.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") data := map[string]any{ "Filename": r.URL.Query().Get("filename"), "Version": AppVersion, "Commit": AppCommit, } if err := Pages["adapter_builder"].ExecuteTemplate(w, "base", data); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) } } } func HandleParserUI(store domain.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") reportID := r.PathValue("id") filePath := filepath.Join(report.UploadDir, reportID) recorder := httptest.NewRecorder() report.ServeDOCXAsHTML(recorder, filePath) safeHTML := template.HTML(recorder.Body.String()) data := map[string]any{ "ReportID": reportID, "RenderedHTML": safeHTML, "Version": AppVersion, "Commit": AppCommit, } if err := Pages["report_parser"].ExecuteTemplate(w, "base", data); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) } } } func HandlePentestUploadUI(store domain.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") data := map[string]any{ "Version": AppVersion, "Commit": AppCommit, } if err := Pages["report_upload"].ExecuteTemplate(w, "base", data); err != nil { http.Error(w, "Template render error: "+err.Error(), http.StatusInternalServerError) } } }