mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
- Remove seed data generation command (cmd/seed/main.go) - Consolidate database migrations into single init schema file - Rename ip_address DTO to host_port for consistency - Add host_port_snapshot DTO and model for snapshot tracking - Rename host_port handler and repository files for clarity - Implement host_port_snapshot service layer with CRUD operations - Update website_snapshot service to work with new host_port structure - Enhance terminal login UI with focus state tracking and Tab key navigation - Update docker-compose configuration for development environment - Refactor directory and website snapshot DTOs for improved data structure - Add comprehensive test coverage for model and handler changes - Simplify database schema by consolidating related migrations into single initialization file
196 lines
4.7 KiB
Go
196 lines
4.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/xingrin/go-backend/internal/dto"
|
|
"github.com/xingrin/go-backend/internal/model"
|
|
"github.com/xingrin/go-backend/internal/pkg/csv"
|
|
"github.com/xingrin/go-backend/internal/service"
|
|
)
|
|
|
|
// WebsiteSnapshotHandler handles website snapshot endpoints
|
|
type WebsiteSnapshotHandler struct {
|
|
svc *service.WebsiteSnapshotService
|
|
}
|
|
|
|
// NewWebsiteSnapshotHandler creates a new website snapshot handler
|
|
func NewWebsiteSnapshotHandler(svc *service.WebsiteSnapshotService) *WebsiteSnapshotHandler {
|
|
return &WebsiteSnapshotHandler{svc: svc}
|
|
}
|
|
|
|
// BulkUpsert creates website snapshots and syncs to asset table
|
|
// POST /api/scans/:id/websites/bulk-upsert
|
|
func (h *WebsiteSnapshotHandler) BulkUpsert(c *gin.Context) {
|
|
scanID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
dto.BadRequest(c, "Invalid scan ID")
|
|
return
|
|
}
|
|
|
|
var req dto.BulkUpsertWebsiteSnapshotsRequest
|
|
if !dto.BindJSON(c, &req) {
|
|
return
|
|
}
|
|
|
|
snapshotCount, assetCount, err := h.svc.SaveAndSync(scanID, req.TargetID, req.Websites)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrScanNotFoundForSnapshot) {
|
|
dto.NotFound(c, "Scan not found")
|
|
return
|
|
}
|
|
dto.InternalError(c, "Failed to save snapshots")
|
|
return
|
|
}
|
|
|
|
dto.Success(c, dto.BulkUpsertWebsiteSnapshotsResponse{
|
|
SnapshotCount: int(snapshotCount),
|
|
AssetCount: int(assetCount),
|
|
})
|
|
}
|
|
|
|
// List returns paginated website snapshots for a scan
|
|
// GET /api/scans/:id/websites/
|
|
func (h *WebsiteSnapshotHandler) List(c *gin.Context) {
|
|
scanID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
dto.BadRequest(c, "Invalid scan ID")
|
|
return
|
|
}
|
|
|
|
var query dto.WebsiteSnapshotListQuery
|
|
if !dto.BindQuery(c, &query) {
|
|
return
|
|
}
|
|
|
|
snapshots, total, err := h.svc.ListByScan(scanID, &query)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrScanNotFoundForSnapshot) {
|
|
dto.NotFound(c, "Scan not found")
|
|
return
|
|
}
|
|
dto.InternalError(c, "Failed to list snapshots")
|
|
return
|
|
}
|
|
|
|
// Convert to response
|
|
var resp []dto.WebsiteSnapshotResponse
|
|
for _, s := range snapshots {
|
|
resp = append(resp, toWebsiteSnapshotResponse(&s))
|
|
}
|
|
|
|
dto.Paginated(c, resp, total, query.GetPage(), query.GetPageSize())
|
|
}
|
|
|
|
// Export exports website snapshots as CSV
|
|
// GET /api/scans/:id/websites/export/
|
|
func (h *WebsiteSnapshotHandler) Export(c *gin.Context) {
|
|
scanID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
dto.BadRequest(c, "Invalid scan ID")
|
|
return
|
|
}
|
|
|
|
// Get count for progress estimation
|
|
count, err := h.svc.CountByScan(scanID)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrScanNotFoundForSnapshot) {
|
|
dto.NotFound(c, "Scan not found")
|
|
return
|
|
}
|
|
dto.InternalError(c, "Failed to export snapshots")
|
|
return
|
|
}
|
|
|
|
rows, err := h.svc.StreamByScan(scanID)
|
|
if err != nil {
|
|
dto.InternalError(c, "Failed to export snapshots")
|
|
return
|
|
}
|
|
|
|
headers := []string{
|
|
"url", "host", "location", "title", "status_code",
|
|
"content_length", "content_type", "webserver", "tech",
|
|
"response_body", "response_headers", "vhost", "created_at",
|
|
}
|
|
|
|
filename := fmt.Sprintf("scan-%d-websites.csv", scanID)
|
|
|
|
mapper := func(rows *sql.Rows) ([]string, error) {
|
|
snapshot, err := h.svc.ScanRow(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
statusCode := ""
|
|
if snapshot.StatusCode != nil {
|
|
statusCode = strconv.Itoa(*snapshot.StatusCode)
|
|
}
|
|
|
|
contentLength := ""
|
|
if snapshot.ContentLength != nil {
|
|
contentLength = strconv.Itoa(*snapshot.ContentLength)
|
|
}
|
|
|
|
vhost := ""
|
|
if snapshot.Vhost != nil {
|
|
vhost = strconv.FormatBool(*snapshot.Vhost)
|
|
}
|
|
|
|
tech := ""
|
|
if len(snapshot.Tech) > 0 {
|
|
tech = strings.Join(snapshot.Tech, "|")
|
|
}
|
|
|
|
return []string{
|
|
snapshot.URL,
|
|
snapshot.Host,
|
|
snapshot.Location,
|
|
snapshot.Title,
|
|
statusCode,
|
|
contentLength,
|
|
snapshot.ContentType,
|
|
snapshot.Webserver,
|
|
tech,
|
|
snapshot.ResponseBody,
|
|
snapshot.ResponseHeaders,
|
|
vhost,
|
|
snapshot.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
}, nil
|
|
}
|
|
|
|
if err := csv.StreamCSV(c, rows, headers, filename, mapper, count); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// toWebsiteSnapshotResponse converts model to response DTO
|
|
func toWebsiteSnapshotResponse(s *model.WebsiteSnapshot) dto.WebsiteSnapshotResponse {
|
|
tech := s.Tech
|
|
if tech == nil {
|
|
tech = []string{}
|
|
}
|
|
return dto.WebsiteSnapshotResponse{
|
|
ID: s.ID,
|
|
ScanID: s.ScanID,
|
|
URL: s.URL,
|
|
Host: s.Host,
|
|
Title: s.Title,
|
|
StatusCode: s.StatusCode,
|
|
ContentLength: s.ContentLength,
|
|
Location: s.Location,
|
|
Webserver: s.Webserver,
|
|
ContentType: s.ContentType,
|
|
Tech: tech,
|
|
ResponseBody: s.ResponseBody,
|
|
Vhost: s.Vhost,
|
|
ResponseHeaders: s.ResponseHeaders,
|
|
CreatedAt: s.CreatedAt,
|
|
}
|
|
}
|