Files
iknowyou/back/internal/api/handler/search.go
2026-04-06 15:12:34 +02:00

147 lines
3.8 KiB
Go

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)
}