package handler import ( "context" "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/anotherhadi/iknowyou/internal/respond" "github.com/anotherhadi/iknowyou/internal/search" "github.com/anotherhadi/iknowyou/internal/tools" ) type SearchHandler struct { manager *search.Manager demo bool } func NewSearchHandler(manager *search.Manager, demo bool) *SearchHandler { return &SearchHandler{manager: manager, demo: demo} } type postSearchRequest struct { Target string `json:"target"` InputType tools.InputType `json:"input_type"` Profile string `json:"profile,omitempty"` } type searchSummary struct { ID string `json:"id"` Target string `json:"target"` InputType tools.InputType `json:"input_type"` Profile string `json:"profile,omitempty"` Status search.Status `json:"status"` StartedAt string `json:"started_at"` PlannedTools []search.ToolStatus `json:"planned_tools"` } type searchDetail struct { searchSummary Events []tools.Event `json:"events"` } func toSummary(s *search.Search) searchSummary { planned := s.PlannedTools if planned == nil { planned = []search.ToolStatus{} } return searchSummary{ ID: s.ID, Target: s.Target, InputType: s.InputType, Profile: s.Profile, Status: s.Status(), StartedAt: s.StartedAt.UTC().Format("2006-01-02T15:04:05Z"), PlannedTools: planned, } } var validInputTypes = map[tools.InputType]struct{}{ tools.InputTypeEmail: {}, tools.InputTypeUsername: {}, tools.InputTypePhone: {}, tools.InputTypeIP: {}, tools.InputTypeDomain: {}, tools.InputTypePassword: {}, tools.InputTypeName: {}, } // POST /searches func (h *SearchHandler) Create(w http.ResponseWriter, r *http.Request) { if h.demo { respond.Error(w, http.StatusForbidden, "demo mode: searches are disabled") return } var req postSearchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respond.Error(w, http.StatusBadRequest, "invalid JSON body") return } if req.Target == "" { respond.Error(w, http.StatusBadRequest, "target is required") return } if len(req.Target) > 500 { respond.Error(w, http.StatusBadRequest, "target is too long (max 500 characters)") return } if req.Target[0] == '-' || req.Target[0] == '@' { respond.Error(w, http.StatusBadRequest, "invalid target") return } if req.InputType == "" { respond.Error(w, http.StatusBadRequest, "input_type is required") return } if _, ok := validInputTypes[req.InputType]; !ok { respond.Error(w, http.StatusBadRequest, "invalid input_type") return } s, err := h.manager.Start(context.WithoutCancel(r.Context()), req.Target, req.InputType, req.Profile) if err != nil { respond.Error(w, http.StatusInternalServerError, err.Error()) return } respond.JSON(w, http.StatusCreated, toSummary(s)) } // GET /searches func (h *SearchHandler) List(w http.ResponseWriter, r *http.Request) { all := h.manager.All() summaries := make([]searchSummary, len(all)) for i, s := range all { summaries[i] = toSummary(s) } respond.JSON(w, http.StatusOK, summaries) } // GET /searches/{id} func (h *SearchHandler) Get(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") s, err := h.manager.Get(id) if err != nil { respond.Error(w, http.StatusNotFound, err.Error()) return } detail := searchDetail{ searchSummary: toSummary(s), Events: s.Events(), } respond.JSON(w, http.StatusOK, detail) } // DELETE /searches/{id} func (h *SearchHandler) Delete(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.manager.Delete(id); err != nil { respond.Error(w, http.StatusNotFound, err.Error()) return } w.WriteHeader(http.StatusNoContent) }