Files
xingrin/go-backend/cmd/server/main.go
yyhuni a5c48fe4d4 feat(frontend,backend): implement IP address management and export functionality
- Add IP address DTO, handler, service, and repository layers in Go backend
- Implement IP address bulk delete endpoint at /ip-addresses/bulk-delete/
- Add IP address export endpoint with optional IP filtering by target
- Simplify IP address hosts column display using ExpandableCell component
- Update IP address export to support filtering selected IPs for download
- Add error handling and toast notifications for export operations
- Internationalize IP address column labels and tooltips in Chinese
- Update IP address service to support filtered exports with comma-separated IPs
- Add host-port mapping seeding for test data generation
- Refactor scope filter and repository queries to support IP address operations
2026-01-13 16:42:57 +08:00

311 lines
10 KiB
Go

package main
import (
"context"
"embed"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/xingrin/go-backend/internal/auth"
"github.com/xingrin/go-backend/internal/config"
"github.com/xingrin/go-backend/internal/database"
"github.com/xingrin/go-backend/internal/handler"
"github.com/xingrin/go-backend/internal/middleware"
"github.com/xingrin/go-backend/internal/pkg"
pkgvalidator "github.com/xingrin/go-backend/internal/pkg/validator"
"github.com/xingrin/go-backend/internal/repository"
"github.com/xingrin/go-backend/internal/service"
"go.uber.org/zap"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
fmt.Printf("Failed to load config: %v\n", err)
os.Exit(1)
}
// Initialize logger
if err := pkg.InitLogger(&pkg.LogConfig{
Level: cfg.Log.Level,
Format: cfg.Log.Format,
}); err != nil {
fmt.Printf("Failed to initialize logger: %v\n", err)
os.Exit(1)
}
defer pkg.Sync()
pkg.Info("Starting server",
zap.Int("port", cfg.Server.Port),
zap.String("mode", cfg.Server.Mode),
)
// Initialize custom validator with translations
if err := pkgvalidator.Init(); err != nil {
pkg.Fatal("Failed to initialize validator", zap.Error(err))
}
pkg.Info("Validator initialized with custom translations")
// Initialize database
db, err := database.NewDatabase(&cfg.Database)
if err != nil {
pkg.Fatal("Failed to connect to database", zap.Error(err))
}
pkg.Info("Database connected",
zap.String("host", cfg.Database.Host),
zap.Int("port", cfg.Database.Port),
zap.String("name", cfg.Database.Name),
)
// Run SQL migrations
database.MigrationsFS = migrationsFS
database.MigrationsPath = "migrations"
sqlDB, err := db.DB()
if err != nil {
pkg.Fatal("Failed to get underlying sql.DB", zap.Error(err))
}
if err := database.RunMigrations(sqlDB); err != nil {
pkg.Fatal("Failed to run database migrations", zap.Error(err))
}
// Initialize Redis (optional)
var redisClient *redis.Client
if cfg.Redis.Host != "" {
redisClient = redis.NewClient(&redis.Options{
Addr: cfg.Redis.Addr(),
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := redisClient.Ping(ctx).Err(); err != nil {
pkg.Warn("Failed to connect to Redis, continuing without Redis",
zap.Error(err),
)
redisClient = nil
} else {
pkg.Info("Redis connected",
zap.String("addr", cfg.Redis.Addr()),
)
}
}
// Initialize JWT manager
jwtManager := auth.NewJWTManager(
cfg.JWT.Secret,
cfg.JWT.AccessExpire,
cfg.JWT.RefreshExpire,
)
// Set Gin mode
gin.SetMode(cfg.Server.Mode)
// Create Gin router
router := gin.New()
// Disable automatic redirect for trailing slash
// This prevents 301/307 redirects when URL has/doesn't have trailing slash
router.RedirectTrailingSlash = false
router.RedirectFixedPath = false
// Add middleware
router.Use(middleware.Recovery())
router.Use(middleware.Logger())
// Create repositories
userRepo := repository.NewUserRepository(db)
orgRepo := repository.NewOrganizationRepository(db)
targetRepo := repository.NewTargetRepository(db)
engineRepo := repository.NewEngineRepository(db)
websiteRepo := repository.NewWebsiteRepository(db)
subdomainRepo := repository.NewSubdomainRepository(db)
endpointRepo := repository.NewEndpointRepository(db)
directoryRepo := repository.NewDirectoryRepository(db)
ipAddressRepo := repository.NewIPAddressRepository(db)
// Create services
userSvc := service.NewUserService(userRepo)
orgSvc := service.NewOrganizationService(orgRepo)
targetSvc := service.NewTargetService(targetRepo, orgRepo)
engineSvc := service.NewEngineService(engineRepo)
websiteSvc := service.NewWebsiteService(websiteRepo, targetRepo)
subdomainSvc := service.NewSubdomainService(subdomainRepo, targetRepo)
endpointSvc := service.NewEndpointService(endpointRepo, targetRepo)
directorySvc := service.NewDirectoryService(directoryRepo, targetRepo)
ipAddressSvc := service.NewIPAddressService(ipAddressRepo, targetRepo)
// Create handlers
healthHandler := handler.NewHealthHandler(db, redisClient)
authHandler := handler.NewAuthHandler(db, jwtManager)
userHandler := handler.NewUserHandler(userSvc)
orgHandler := handler.NewOrganizationHandler(orgSvc)
targetHandler := handler.NewTargetHandler(targetSvc)
engineHandler := handler.NewEngineHandler(engineSvc)
websiteHandler := handler.NewWebsiteHandler(websiteSvc)
subdomainHandler := handler.NewSubdomainHandler(subdomainSvc)
endpointHandler := handler.NewEndpointHandler(endpointSvc)
directoryHandler := handler.NewDirectoryHandler(directorySvc)
ipAddressHandler := handler.NewIPAddressHandler(ipAddressSvc)
// Register health routes
router.GET("/health", healthHandler.Check)
router.GET("/health/live", healthHandler.Liveness)
router.GET("/health/ready", healthHandler.Readiness)
// API routes
api := router.Group("/api")
{
// Auth routes (public)
authGroup := api.Group("/auth")
{
authGroup.POST("/login", authHandler.Login)
authGroup.POST("/refresh", authHandler.RefreshToken)
}
// Protected routes
protected := api.Group("")
protected.Use(middleware.AuthMiddleware(jwtManager))
{
// Auth
protected.GET("/auth/me", authHandler.GetCurrentUser)
// Users
protected.POST("/users", userHandler.Create)
protected.GET("/users", userHandler.List)
protected.PUT("/users/password", userHandler.UpdatePassword)
// Organizations
protected.POST("/organizations", orgHandler.Create)
protected.POST("/organizations/bulk-delete", orgHandler.BulkDelete)
protected.GET("/organizations", orgHandler.List)
protected.GET("/organizations/:id", orgHandler.GetByID)
protected.GET("/organizations/:id/targets", orgHandler.ListTargets)
protected.POST("/organizations/:id/link_targets", orgHandler.LinkTargets)
protected.POST("/organizations/:id/unlink_targets", orgHandler.UnlinkTargets)
protected.PUT("/organizations/:id", orgHandler.Update)
protected.DELETE("/organizations/:id", orgHandler.Delete)
// Targets
protected.POST("/targets", targetHandler.Create)
protected.POST("/targets/batch_create", targetHandler.BatchCreate)
protected.POST("/targets/bulk-delete", targetHandler.BulkDelete)
protected.GET("/targets", targetHandler.List)
protected.GET("/targets/:id", targetHandler.GetByID)
protected.PUT("/targets/:id", targetHandler.Update)
protected.DELETE("/targets/:id", targetHandler.Delete)
// Websites (nested under targets)
protected.GET("/targets/:id/websites", websiteHandler.List)
protected.GET("/targets/:id/websites/export", websiteHandler.Export)
protected.POST("/targets/:id/websites/bulk-create", websiteHandler.BulkCreate)
protected.POST("/targets/:id/websites/bulk-upsert", websiteHandler.BulkUpsert)
// Websites (standalone)
protected.GET("/websites/:id", websiteHandler.GetByID)
protected.DELETE("/websites/:id", websiteHandler.Delete)
protected.POST("/websites/bulk-delete", websiteHandler.BulkDelete)
// Subdomains (nested under targets)
protected.GET("/targets/:id/subdomains", subdomainHandler.List)
protected.GET("/targets/:id/subdomains/export", subdomainHandler.Export)
protected.POST("/targets/:id/subdomains/bulk-create", subdomainHandler.BulkCreate)
// Subdomains (standalone)
protected.POST("/subdomains/bulk-delete", subdomainHandler.BulkDelete)
// Endpoints (nested under targets)
protected.GET("/targets/:id/endpoints", endpointHandler.List)
protected.GET("/targets/:id/endpoints/export", endpointHandler.Export)
protected.POST("/targets/:id/endpoints/bulk-create", endpointHandler.BulkCreate)
protected.POST("/targets/:id/endpoints/bulk-upsert", endpointHandler.BulkUpsert)
// Endpoints (standalone)
protected.GET("/endpoints/:id", endpointHandler.GetByID)
protected.DELETE("/endpoints/:id", endpointHandler.Delete)
protected.POST("/endpoints/bulk-delete", endpointHandler.BulkDelete)
// Directories (nested under targets)
protected.GET("/targets/:id/directories", directoryHandler.List)
protected.GET("/targets/:id/directories/export", directoryHandler.Export)
protected.POST("/targets/:id/directories/bulk-create", directoryHandler.BulkCreate)
protected.POST("/targets/:id/directories/bulk-upsert", directoryHandler.BulkUpsert)
// Directories (standalone)
protected.POST("/directories/bulk-delete", directoryHandler.BulkDelete)
// IP Addresses (nested under targets)
protected.GET("/targets/:id/ip-addresses", ipAddressHandler.List)
protected.GET("/targets/:id/ip-addresses/export", ipAddressHandler.Export)
protected.POST("/targets/:id/ip-addresses/bulk-upsert", ipAddressHandler.BulkUpsert)
// IP Addresses (standalone)
protected.POST("/ip-addresses/bulk-delete", ipAddressHandler.BulkDelete)
// Engines
protected.POST("/engines", engineHandler.Create)
protected.GET("/engines", engineHandler.List)
protected.GET("/engines/:id", engineHandler.GetByID)
protected.PUT("/engines/:id", engineHandler.Update)
protected.DELETE("/engines/:id", engineHandler.Delete)
}
}
// Create HTTP server with trailing slash normalization
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
Handler: middleware.NormalizeTrailingSlash(router), // Wrap router to strip trailing slashes
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
pkg.Info("Server listening", zap.String("addr", srv.Addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
pkg.Fatal("Failed to start server", zap.Error(err))
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
pkg.Info("Shutting down server...")
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
pkg.Error("Server forced to shutdown", zap.Error(err))
}
// Close database connection
if sqlDB, err := db.DB(); err == nil {
sqlDB.Close()
}
// Close Redis connection
if redisClient != nil {
redisClient.Close()
}
pkg.Info("Server exited")
}