Files
xingrin/server/internal/handler/scan.go
yyhuni b859fc9062 refactor(modules): 更新模块路径为新的github用户命名空间
- 修改所有import路径,从github.com/orbit/server改为github.com/yyhuni/orbit/server
- 更新go.mod模块名为github.com/yyhuni/orbit/server
- 调整内部引用路径,确保包导入一致性
- 修改.gitignore,新增AGENTS.md和WARP.md忽略规则
- 更新Scan请求中engineNames字段的绑定规则,改为必须且仅能包含一个元素
2026-01-23 18:31:54 +08:00

215 lines
4.9 KiB
Go

package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yyhuni/orbit/server/internal/dto"
"github.com/yyhuni/orbit/server/internal/service"
)
// ScanHandler handles scan HTTP requests
type ScanHandler struct {
service *service.ScanService
}
// NewScanHandler creates a new scan handler
func NewScanHandler(service *service.ScanService) *ScanHandler {
return &ScanHandler{service: service}
}
// List returns paginated scans
// GET /api/scans
func (h *ScanHandler) List(c *gin.Context) {
var query dto.ScanListQuery
if !dto.BindQuery(c, &query) {
return
}
scans, total, err := h.service.List(&query)
if err != nil {
dto.InternalError(c, "Failed to list scans")
return
}
// Convert to response DTOs
items := make([]dto.ScanResponse, len(scans))
for i, scan := range scans {
items[i] = *h.service.ToScanResponse(&scan)
}
dto.Paginated(c, items, total, query.GetPage(), query.GetPageSize())
}
// GetByID returns a scan by ID
// GET /api/scans/:id
func (h *ScanHandler) GetByID(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
dto.BadRequest(c, "Invalid scan ID")
return
}
scan, err := h.service.GetByID(id)
if err != nil {
if errors.Is(err, service.ErrScanNotFound) {
dto.NotFound(c, "Scan not found")
return
}
dto.InternalError(c, "Failed to get scan")
return
}
dto.Success(c, h.service.ToScanDetailResponse(scan))
}
// Delete soft deletes a scan
// DELETE /api/scans/:id
func (h *ScanHandler) Delete(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
dto.BadRequest(c, "Invalid scan ID")
return
}
deletedCount, deletedNames, err := h.service.Delete(id)
if err != nil {
if errors.Is(err, service.ErrScanNotFound) {
dto.NotFound(c, "Scan not found")
return
}
dto.InternalError(c, "Failed to delete scan")
return
}
dto.Success(c, gin.H{
"scanId": id,
"deletedCount": deletedCount,
"deletedScans": deletedNames,
})
}
// BulkDelete soft deletes multiple scans
// POST /api/scans/bulk-delete
func (h *ScanHandler) BulkDelete(c *gin.Context) {
var req dto.BulkDeleteRequest
if !dto.BindJSON(c, &req) {
return
}
deletedCount, deletedNames, err := h.service.BulkDelete(req.IDs)
if err != nil {
dto.InternalError(c, "Failed to bulk delete scans")
return
}
dto.Success(c, gin.H{
"deletedCount": deletedCount,
"deletedScans": deletedNames,
})
}
// Statistics returns scan statistics
// GET /api/scans/statistics
func (h *ScanHandler) Statistics(c *gin.Context) {
stats, err := h.service.GetStatistics()
if err != nil {
dto.InternalError(c, "Failed to get scan statistics")
return
}
dto.Success(c, stats)
}
// Stop stops a running scan
// POST /api/scans/:id/stop
func (h *ScanHandler) Stop(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
dto.BadRequest(c, "Invalid scan ID")
return
}
revokedCount, err := h.service.Stop(id)
if err != nil {
if errors.Is(err, service.ErrScanNotFound) {
dto.NotFound(c, "Scan not found")
return
}
if errors.Is(err, service.ErrScanCannotStop) {
dto.BadRequest(c, "Cannot stop scan: scan is not running")
return
}
dto.InternalError(c, "Failed to stop scan")
return
}
dto.Success(c, dto.StopScanResponse{
RevokedTaskCount: revokedCount,
})
}
// Create starts a new scan
// POST /api/scans
//
// Request body:
//
// {
// "mode": "normal" | "quick", // scan mode (default: "normal")
// "targetId": 123, // required for mode=normal
// "targets": ["example.com"], // required for mode=quick (raw targets)
// "engineIds": [1, 2], // engine IDs to run
// "engineNames": ["subdomain_discovery"], // required, exactly one value
// "config": {} // optional scan configuration
// }
func (h *ScanHandler) Create(c *gin.Context) {
var req dto.CreateScanRequest
if !dto.BindJSON(c, &req) {
return
}
// Default mode is "normal"
if req.Mode == "" {
req.Mode = "normal"
}
switch req.Mode {
case "normal":
// Normal scan: requires targetId
if req.TargetID == 0 {
dto.BadRequest(c, "targetId is required for normal mode")
return
}
scan, err := h.service.CreateNormal(&req)
if err != nil {
if errors.Is(err, service.ErrTargetNotFound) {
dto.NotFound(c, "Target not found")
return
}
if errors.Is(err, service.ErrScanInvalidConfig) || errors.Is(err, service.ErrScanInvalidEngineNames) {
dto.BadRequest(c, err.Error())
return
}
dto.InternalError(c, "Failed to create scan")
return
}
dto.Created(c, h.service.ToScanDetailResponse(scan))
case "quick":
// Quick scan: requires targets list
if len(req.Targets) == 0 {
dto.BadRequest(c, "targets is required for quick mode")
return
}
// TODO: Implement when worker integration is ready
dto.Error(c, http.StatusNotImplemented, "NOT_IMPLEMENTED", "Quick scan is not yet implemented")
default:
dto.BadRequest(c, "Invalid mode, must be 'normal' or 'quick'")
}
}