package api import ( "encoding/json" "fmt" "log" "net/http" "regexp" "strconv" "strings" "time" "github.com/intodns/backend/internal/checker" "github.com/intodns/backend/internal/store" ) var validHostname = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) func cleanDomain(raw string) string { domain := raw domain = strings.TrimPrefix(domain, "http://") domain = strings.TrimPrefix(domain, "https://") domain = strings.TrimRight(domain, "/") if idx := strings.Index(domain, "/"); idx != -1 { domain = domain[:idx] } if idx := strings.LastIndex(domain, ":"); idx != -1 { domain = domain[:idx] } return strings.ToLower(strings.TrimSpace(domain)) } // CheckHandler returns the full report as JSON (non-streaming). func CheckHandler(ch *checker.Checker, st *store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { domain := cleanDomain(r.URL.Query().Get("domain")) if domain == "" { writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "missing 'domain' query parameter"}) return } if !validHostname.MatchString(domain) { writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "invalid domain name"}) return } report := ch.Check(domain) if st != nil { if err := st.SaveReport(report, extractIP(r), r.UserAgent()); err != nil { log.Printf("failed to save report: %v", err) } } writeJSON(w, http.StatusOK, report) } } // CheckStreamHandler streams results via Server-Sent Events as each category completes. func CheckStreamHandler(ch *checker.Checker, st *store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { domain := cleanDomain(r.URL.Query().Get("domain")) if domain == "" { writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "missing 'domain' query parameter"}) return } if !validHostname.MatchString(domain) { writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "invalid domain name"}) return } flusher, ok := w.(http.Flusher) if !ok { // Fallback to non-streaming. report := ch.Check(domain) if st != nil { if err := st.SaveReport(report, extractIP(r), r.UserAgent()); err != nil { log.Printf("failed to save report: %v", err) } } writeJSON(w, http.StatusOK, report) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") w.WriteHeader(http.StatusOK) flusher.Flush() start := time.Now() catCh := ch.CheckStream(domain) var categories []checker.Category for event := range catCh { categories = append(categories, event.Category) data, err := json.Marshal(event) if err != nil { continue } fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } // Build and save the final report. if st != nil { var summary checker.Summary for _, cat := range categories { for _, check := range cat.Checks { switch check.Status { case checker.StatusPass: summary.Pass++ case checker.StatusWarn: summary.Warn++ case checker.StatusFail: summary.Fail++ case checker.StatusInfo: summary.Info++ } } } report := &checker.Report{ Domain: domain, Timestamp: time.Now().UTC().Format(time.RFC3339), DurationMs: time.Since(start).Milliseconds(), Summary: summary, Categories: categories, } if err := st.SaveReport(report, extractIP(r), r.UserAgent()); err != nil { log.Printf("failed to save streamed report: %v", err) } } fmt.Fprintf(w, "data: {\"done\":true}\n\n") flusher.Flush() } } // HistoryHandler returns recent check history as JSON. // Query params: domain (optional), limit (default 20, max 100). func HistoryHandler(st *store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { domain := r.URL.Query().Get("domain") limitStr := r.URL.Query().Get("limit") limit := 20 if limitStr != "" { if n, err := strconv.Atoi(limitStr); err == nil && n > 0 { limit = n } } if limit > 100 { limit = 100 } var entries []store.HistoryEntry var err error if domain != "" { entries, err = st.GetHistory(domain, limit) } else { entries, err = st.GetRecent(limit) } if err != nil { log.Printf("history query error: %v", err) writeJSON(w, http.StatusInternalServerError, ErrorResponse{Error: "failed to query history"}) return } if entries == nil { entries = []store.HistoryEntry{} } writeJSON(w, http.StatusOK, entries) } } // ReportHandler returns the full saved report JSON for a specific history entry. // Query param: id (required). func ReportHandler(st *store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { idStr := r.URL.Query().Get("id") if idStr == "" { writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "missing 'id' query parameter"}) return } id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, ErrorResponse{Error: "invalid 'id' parameter"}) return } entry, err := st.GetReport(id) if err != nil { log.Printf("report query error: %v", err) writeJSON(w, http.StatusInternalServerError, ErrorResponse{Error: "failed to query report"}) return } if entry == nil { writeJSON(w, http.StatusNotFound, ErrorResponse{Error: "report not found"}) return } writeJSON(w, http.StatusOK, entry) } } // HealthHandler returns an http.HandlerFunc for the health endpoint. func HealthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, HealthResponse{Status: "ok"}) } } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) }