mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
feat(go-backend): complete all Go models
- Add scan-related models: ScanLog, ScanInputTarget, ScheduledScan, SubfinderProviderSettings - Add engine models: Wordlist, NucleiTemplateRepo - Add notification models: Notification, NotificationSettings - Add config model: BlacklistRule - Add statistics models: AssetStatistics, StatisticsHistory - Add auth models: User (auth_user), Session (django_session) - Add shopspring/decimal dependency for Vulnerability model - Update model_test.go with all 33 model table name tests - All tests passing
This commit is contained in:
24
go-backend/.env.example
Normal file
24
go-backend/.env.example
Normal file
@@ -0,0 +1,24 @@
|
||||
# Server Configuration
|
||||
SERVER_PORT=8888
|
||||
GIN_MODE=release
|
||||
|
||||
# Database Configuration (PostgreSQL)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=your_password
|
||||
DB_NAME=xingrin
|
||||
DB_SSLMODE=disable
|
||||
DB_MAX_OPEN_CONNS=25
|
||||
DB_MAX_IDLE_CONNS=5
|
||||
DB_CONN_MAX_LIFETIME=300
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
40
go-backend/Makefile
Normal file
40
go-backend/Makefile
Normal file
@@ -0,0 +1,40 @@
|
||||
.PHONY: build run test lint clean
|
||||
|
||||
# Build the server binary
|
||||
build:
|
||||
go build -o bin/server ./cmd/server
|
||||
|
||||
# Run the server
|
||||
run:
|
||||
go run ./cmd/server
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
go test -v -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# Run linter
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf bin/
|
||||
rm -f coverage.out coverage.html
|
||||
|
||||
# Download dependencies
|
||||
deps:
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
# Format code
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
# Generate mocks (if needed)
|
||||
generate:
|
||||
go generate ./...
|
||||
BIN
go-backend/bin/server
Executable file
BIN
go-backend/bin/server
Executable file
Binary file not shown.
142
go-backend/cmd/server/main.go
Normal file
142
go-backend/cmd/server/main.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
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 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),
|
||||
)
|
||||
|
||||
// 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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set Gin mode
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
|
||||
// Create Gin router
|
||||
router := gin.New()
|
||||
|
||||
// Add middleware
|
||||
router.Use(middleware.Recovery())
|
||||
router.Use(middleware.Logger())
|
||||
|
||||
// Create handlers
|
||||
healthHandler := handler.NewHealthHandler(db, redisClient)
|
||||
|
||||
// Register routes
|
||||
router.GET("/health", healthHandler.Check)
|
||||
router.GET("/health/live", healthHandler.Liveness)
|
||||
router.GET("/health/ready", healthHandler.Readiness)
|
||||
|
||||
// Create HTTP server
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
||||
Handler: router,
|
||||
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")
|
||||
}
|
||||
66
go-backend/go.mod
Normal file
66
go-backend/go.mod
Normal file
@@ -0,0 +1,66 @@
|
||||
module github.com/xingrin/go-backend
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/zap v1.26.0
|
||||
gorm.io/datatypes v1.2.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.4.7 // indirect
|
||||
)
|
||||
187
go-backend/go.sum
Normal file
187
go-backend/go.sum
Normal file
@@ -0,0 +1,187 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
|
||||
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
|
||||
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
|
||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
|
||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
|
||||
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
163
go-backend/internal/config/config.go
Normal file
163
go-backend/internal/config/config.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config holds all configuration for the application
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
Redis RedisConfig
|
||||
Log LogConfig
|
||||
}
|
||||
|
||||
// ServerConfig holds server-related configuration
|
||||
type ServerConfig struct {
|
||||
Port int `mapstructure:"SERVER_PORT"`
|
||||
Mode string `mapstructure:"GIN_MODE"`
|
||||
}
|
||||
|
||||
// DatabaseConfig holds database-related configuration
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"DB_HOST"`
|
||||
Port int `mapstructure:"DB_PORT"`
|
||||
User string `mapstructure:"DB_USER"`
|
||||
Password string `mapstructure:"DB_PASSWORD"`
|
||||
Name string `mapstructure:"DB_NAME"`
|
||||
SSLMode string `mapstructure:"DB_SSLMODE"`
|
||||
MaxOpenConns int `mapstructure:"DB_MAX_OPEN_CONNS"`
|
||||
MaxIdleConns int `mapstructure:"DB_MAX_IDLE_CONNS"`
|
||||
ConnMaxLifetime int `mapstructure:"DB_CONN_MAX_LIFETIME"`
|
||||
}
|
||||
|
||||
// RedisConfig holds Redis-related configuration
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"REDIS_HOST"`
|
||||
Port int `mapstructure:"REDIS_PORT"`
|
||||
Password string `mapstructure:"REDIS_PASSWORD"`
|
||||
DB int `mapstructure:"REDIS_DB"`
|
||||
}
|
||||
|
||||
// LogConfig holds logging-related configuration
|
||||
type LogConfig struct {
|
||||
Level string `mapstructure:"LOG_LEVEL"`
|
||||
Format string `mapstructure:"LOG_FORMAT"`
|
||||
}
|
||||
|
||||
|
||||
// Load reads configuration from environment variables
|
||||
func Load() (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// Set default values
|
||||
setDefaults(v)
|
||||
|
||||
// Read from environment variables
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
cfg := &Config{}
|
||||
|
||||
// Server config
|
||||
cfg.Server.Port = v.GetInt("SERVER_PORT")
|
||||
cfg.Server.Mode = v.GetString("GIN_MODE")
|
||||
|
||||
// Database config
|
||||
cfg.Database.Host = v.GetString("DB_HOST")
|
||||
cfg.Database.Port = v.GetInt("DB_PORT")
|
||||
cfg.Database.User = v.GetString("DB_USER")
|
||||
cfg.Database.Password = v.GetString("DB_PASSWORD")
|
||||
cfg.Database.Name = v.GetString("DB_NAME")
|
||||
cfg.Database.SSLMode = v.GetString("DB_SSLMODE")
|
||||
cfg.Database.MaxOpenConns = v.GetInt("DB_MAX_OPEN_CONNS")
|
||||
cfg.Database.MaxIdleConns = v.GetInt("DB_MAX_IDLE_CONNS")
|
||||
cfg.Database.ConnMaxLifetime = v.GetInt("DB_CONN_MAX_LIFETIME")
|
||||
|
||||
// Redis config
|
||||
cfg.Redis.Host = v.GetString("REDIS_HOST")
|
||||
cfg.Redis.Port = v.GetInt("REDIS_PORT")
|
||||
cfg.Redis.Password = v.GetString("REDIS_PASSWORD")
|
||||
cfg.Redis.DB = v.GetInt("REDIS_DB")
|
||||
|
||||
// Log config
|
||||
cfg.Log.Level = v.GetString("LOG_LEVEL")
|
||||
cfg.Log.Format = v.GetString("LOG_FORMAT")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// setDefaults sets default values for configuration
|
||||
func setDefaults(v *viper.Viper) {
|
||||
// Server defaults
|
||||
v.SetDefault("SERVER_PORT", 8888)
|
||||
v.SetDefault("GIN_MODE", "release")
|
||||
|
||||
// Database defaults
|
||||
v.SetDefault("DB_HOST", "localhost")
|
||||
v.SetDefault("DB_PORT", 5432)
|
||||
v.SetDefault("DB_USER", "postgres")
|
||||
v.SetDefault("DB_PASSWORD", "")
|
||||
v.SetDefault("DB_NAME", "xingrin")
|
||||
v.SetDefault("DB_SSLMODE", "disable")
|
||||
v.SetDefault("DB_MAX_OPEN_CONNS", 25)
|
||||
v.SetDefault("DB_MAX_IDLE_CONNS", 5)
|
||||
v.SetDefault("DB_CONN_MAX_LIFETIME", 300)
|
||||
|
||||
// Redis defaults
|
||||
v.SetDefault("REDIS_HOST", "localhost")
|
||||
v.SetDefault("REDIS_PORT", 6379)
|
||||
v.SetDefault("REDIS_PASSWORD", "")
|
||||
v.SetDefault("REDIS_DB", 0)
|
||||
|
||||
// Log defaults
|
||||
v.SetDefault("LOG_LEVEL", "info")
|
||||
v.SetDefault("LOG_FORMAT", "json")
|
||||
}
|
||||
|
||||
// DSN returns the database connection string
|
||||
func (c *DatabaseConfig) DSN() string {
|
||||
return fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
c.Host, c.Port, c.User, c.Password, c.Name, c.SSLMode,
|
||||
)
|
||||
}
|
||||
|
||||
// RedisAddr returns the Redis address
|
||||
func (c *RedisConfig) Addr() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
// GetDefaults returns a Config with all default values (for testing)
|
||||
func GetDefaults() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: 8888,
|
||||
Mode: "release",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Password: "",
|
||||
Name: "xingrin",
|
||||
SSLMode: "disable",
|
||||
MaxOpenConns: 25,
|
||||
MaxIdleConns: 5,
|
||||
ConnMaxLifetime: 300,
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
},
|
||||
Log: LogConfig{
|
||||
Level: "info",
|
||||
Format: "json",
|
||||
},
|
||||
}
|
||||
}
|
||||
147
go-backend/internal/config/config_test.go
Normal file
147
go-backend/internal/config/config_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestConfigDefaults tests that default values are correctly set
|
||||
// Property 4: 配置默认值正确性
|
||||
// *对于任意* 缺失的环境变量,配置系统应返回预定义的默认值。
|
||||
// **验证: 需求 2.4**
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
// Clear all relevant environment variables
|
||||
envVars := []string{
|
||||
"SERVER_PORT", "GIN_MODE",
|
||||
"DB_HOST", "DB_PORT", "DB_USER", "DB_PASSWORD", "DB_NAME", "DB_SSLMODE",
|
||||
"DB_MAX_OPEN_CONNS", "DB_MAX_IDLE_CONNS", "DB_CONN_MAX_LIFETIME",
|
||||
"REDIS_HOST", "REDIS_PORT", "REDIS_PASSWORD", "REDIS_DB",
|
||||
"LOG_LEVEL", "LOG_FORMAT",
|
||||
}
|
||||
for _, env := range envVars {
|
||||
os.Unsetenv(env)
|
||||
}
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
defaults := GetDefaults()
|
||||
|
||||
// Test Server defaults
|
||||
if cfg.Server.Port != defaults.Server.Port {
|
||||
t.Errorf("Server.Port: expected %d, got %d", defaults.Server.Port, cfg.Server.Port)
|
||||
}
|
||||
if cfg.Server.Mode != defaults.Server.Mode {
|
||||
t.Errorf("Server.Mode: expected %s, got %s", defaults.Server.Mode, cfg.Server.Mode)
|
||||
}
|
||||
|
||||
// Test Database defaults
|
||||
if cfg.Database.Host != defaults.Database.Host {
|
||||
t.Errorf("Database.Host: expected %s, got %s", defaults.Database.Host, cfg.Database.Host)
|
||||
}
|
||||
if cfg.Database.Port != defaults.Database.Port {
|
||||
t.Errorf("Database.Port: expected %d, got %d", defaults.Database.Port, cfg.Database.Port)
|
||||
}
|
||||
if cfg.Database.User != defaults.Database.User {
|
||||
t.Errorf("Database.User: expected %s, got %s", defaults.Database.User, cfg.Database.User)
|
||||
}
|
||||
if cfg.Database.Name != defaults.Database.Name {
|
||||
t.Errorf("Database.Name: expected %s, got %s", defaults.Database.Name, cfg.Database.Name)
|
||||
}
|
||||
if cfg.Database.SSLMode != defaults.Database.SSLMode {
|
||||
t.Errorf("Database.SSLMode: expected %s, got %s", defaults.Database.SSLMode, cfg.Database.SSLMode)
|
||||
}
|
||||
if cfg.Database.MaxOpenConns != defaults.Database.MaxOpenConns {
|
||||
t.Errorf("Database.MaxOpenConns: expected %d, got %d", defaults.Database.MaxOpenConns, cfg.Database.MaxOpenConns)
|
||||
}
|
||||
if cfg.Database.MaxIdleConns != defaults.Database.MaxIdleConns {
|
||||
t.Errorf("Database.MaxIdleConns: expected %d, got %d", defaults.Database.MaxIdleConns, cfg.Database.MaxIdleConns)
|
||||
}
|
||||
if cfg.Database.ConnMaxLifetime != defaults.Database.ConnMaxLifetime {
|
||||
t.Errorf("Database.ConnMaxLifetime: expected %d, got %d", defaults.Database.ConnMaxLifetime, cfg.Database.ConnMaxLifetime)
|
||||
}
|
||||
|
||||
// Test Redis defaults
|
||||
if cfg.Redis.Host != defaults.Redis.Host {
|
||||
t.Errorf("Redis.Host: expected %s, got %s", defaults.Redis.Host, cfg.Redis.Host)
|
||||
}
|
||||
if cfg.Redis.Port != defaults.Redis.Port {
|
||||
t.Errorf("Redis.Port: expected %d, got %d", defaults.Redis.Port, cfg.Redis.Port)
|
||||
}
|
||||
if cfg.Redis.DB != defaults.Redis.DB {
|
||||
t.Errorf("Redis.DB: expected %d, got %d", defaults.Redis.DB, cfg.Redis.DB)
|
||||
}
|
||||
|
||||
// Test Log defaults
|
||||
if cfg.Log.Level != defaults.Log.Level {
|
||||
t.Errorf("Log.Level: expected %s, got %s", defaults.Log.Level, cfg.Log.Level)
|
||||
}
|
||||
if cfg.Log.Format != defaults.Log.Format {
|
||||
t.Errorf("Log.Format: expected %s, got %s", defaults.Log.Format, cfg.Log.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigFromEnv tests that environment variables override defaults
|
||||
func TestConfigFromEnv(t *testing.T) {
|
||||
// Set custom environment variables
|
||||
os.Setenv("SERVER_PORT", "9999")
|
||||
os.Setenv("DB_HOST", "custom-host")
|
||||
os.Setenv("DB_PORT", "5433")
|
||||
os.Setenv("LOG_LEVEL", "debug")
|
||||
defer func() {
|
||||
os.Unsetenv("SERVER_PORT")
|
||||
os.Unsetenv("DB_HOST")
|
||||
os.Unsetenv("DB_PORT")
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
}()
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server.Port != 9999 {
|
||||
t.Errorf("Server.Port: expected 9999, got %d", cfg.Server.Port)
|
||||
}
|
||||
if cfg.Database.Host != "custom-host" {
|
||||
t.Errorf("Database.Host: expected custom-host, got %s", cfg.Database.Host)
|
||||
}
|
||||
if cfg.Database.Port != 5433 {
|
||||
t.Errorf("Database.Port: expected 5433, got %d", cfg.Database.Port)
|
||||
}
|
||||
if cfg.Log.Level != "debug" {
|
||||
t.Errorf("Log.Level: expected debug, got %s", cfg.Log.Level)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDatabaseDSN tests the DSN generation
|
||||
func TestDatabaseDSN(t *testing.T) {
|
||||
cfg := &DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Password: "secret",
|
||||
Name: "testdb",
|
||||
SSLMode: "disable",
|
||||
}
|
||||
|
||||
expected := "host=localhost port=5432 user=postgres password=secret dbname=testdb sslmode=disable"
|
||||
if cfg.DSN() != expected {
|
||||
t.Errorf("DSN: expected %s, got %s", expected, cfg.DSN())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedisAddr tests the Redis address generation
|
||||
func TestRedisAddr(t *testing.T) {
|
||||
cfg := &RedisConfig{
|
||||
Host: "localhost",
|
||||
Port: 6379,
|
||||
}
|
||||
|
||||
expected := "localhost:6379"
|
||||
if cfg.Addr() != expected {
|
||||
t.Errorf("Addr: expected %s, got %s", expected, cfg.Addr())
|
||||
}
|
||||
}
|
||||
84
go-backend/internal/database/database.go
Normal file
84
go-backend/internal/database/database.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/xingrin/go-backend/internal/config"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// DB is the global database instance
|
||||
var DB *gorm.DB
|
||||
|
||||
// NewDatabase creates a new database connection
|
||||
func NewDatabase(cfg *config.DatabaseConfig) (*gorm.DB, error) {
|
||||
dsn := cfg.DSN()
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
SingularTable: true, // Use singular table names (e.g., "target" not "targets")
|
||||
},
|
||||
// Disable auto-migration to prevent creating new tables
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Get underlying sql.DB to configure connection pool
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
|
||||
|
||||
// Verify connection
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// InitDatabase initializes the global database instance
|
||||
func InitDatabase(cfg *config.DatabaseConfig) error {
|
||||
var err error
|
||||
DB, err = NewDatabase(cfg)
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func Close() error {
|
||||
if DB != nil {
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping checks if the database connection is alive
|
||||
func Ping() error {
|
||||
if DB == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
sqlDB, err := DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Ping()
|
||||
}
|
||||
|
||||
// GetDB returns the global database instance
|
||||
func GetDB() *gorm.DB {
|
||||
return DB
|
||||
}
|
||||
106
go-backend/internal/handler/health.go
Normal file
106
go-backend/internal/handler/health.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HealthHandler handles health check requests
|
||||
type HealthHandler struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
// HealthResponse represents the health check response
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Database string `json:"database"`
|
||||
Redis string `json:"redis"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// NewHealthHandler creates a new health handler
|
||||
func NewHealthHandler(db *gorm.DB, redis *redis.Client) *HealthHandler {
|
||||
return &HealthHandler{
|
||||
db: db,
|
||||
redis: redis,
|
||||
}
|
||||
}
|
||||
|
||||
// Check handles GET /health
|
||||
func (h *HealthHandler) Check(c *gin.Context) {
|
||||
resp := HealthResponse{
|
||||
Status: "healthy",
|
||||
Database: "connected",
|
||||
Redis: "connected",
|
||||
Details: make(map[string]string),
|
||||
}
|
||||
|
||||
// Check database connection
|
||||
if h.db != nil {
|
||||
sqlDB, err := h.db.DB()
|
||||
if err != nil {
|
||||
resp.Status = "unhealthy"
|
||||
resp.Database = "error"
|
||||
resp.Details["database_error"] = err.Error()
|
||||
} else if err := sqlDB.Ping(); err != nil {
|
||||
resp.Status = "unhealthy"
|
||||
resp.Database = "disconnected"
|
||||
resp.Details["database_error"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
resp.Database = "not_configured"
|
||||
}
|
||||
|
||||
// Check Redis connection
|
||||
if h.redis != nil {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.redis.Ping(ctx).Err(); err != nil {
|
||||
resp.Status = "unhealthy"
|
||||
resp.Redis = "disconnected"
|
||||
resp.Details["redis_error"] = err.Error()
|
||||
}
|
||||
} else {
|
||||
resp.Redis = "not_configured"
|
||||
}
|
||||
|
||||
// Return appropriate status code
|
||||
if resp.Status == "healthy" {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
} else {
|
||||
c.JSON(http.StatusServiceUnavailable, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// Liveness handles GET /health/live (for Kubernetes liveness probe)
|
||||
func (h *HealthHandler) Liveness(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "alive",
|
||||
})
|
||||
}
|
||||
|
||||
// Readiness handles GET /health/ready (for Kubernetes readiness probe)
|
||||
func (h *HealthHandler) Readiness(c *gin.Context) {
|
||||
// Check if database is ready
|
||||
if h.db != nil {
|
||||
sqlDB, err := h.db.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"status": "not_ready",
|
||||
"reason": "database_unavailable",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ready",
|
||||
})
|
||||
}
|
||||
78
go-backend/internal/middleware/logger.go
Normal file
78
go-backend/internal/middleware/logger.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/xingrin/go-backend/internal/pkg"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
// RequestIDHeader is the header name for request ID
|
||||
RequestIDHeader = "X-Request-ID"
|
||||
// RequestIDKey is the context key for request ID
|
||||
RequestIDKey = "requestId"
|
||||
)
|
||||
|
||||
// Logger returns a gin middleware for logging requests
|
||||
func Logger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Generate or get request ID
|
||||
requestID := c.GetHeader(RequestIDHeader)
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
}
|
||||
c.Set(RequestIDKey, requestID)
|
||||
c.Header(RequestIDHeader, requestID)
|
||||
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Calculate latency
|
||||
latency := time.Since(start)
|
||||
|
||||
// Log request
|
||||
fields := []zap.Field{
|
||||
zap.String("requestId", requestID),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", path),
|
||||
zap.String("query", query),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
zap.String("userAgent", c.Request.UserAgent()),
|
||||
zap.Duration("latency", latency),
|
||||
zap.Int("bodySize", c.Writer.Size()),
|
||||
}
|
||||
|
||||
// Add error if exists
|
||||
if len(c.Errors) > 0 {
|
||||
fields = append(fields, zap.String("error", c.Errors.String()))
|
||||
}
|
||||
|
||||
// Log based on status code
|
||||
status := c.Writer.Status()
|
||||
switch {
|
||||
case status >= 500:
|
||||
pkg.Error("Server error", fields...)
|
||||
case status >= 400:
|
||||
pkg.Warn("Client error", fields...)
|
||||
default:
|
||||
pkg.Info("Request completed", fields...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetRequestID returns the request ID from context
|
||||
func GetRequestID(c *gin.Context) string {
|
||||
if requestID, exists := c.Get(RequestIDKey); exists {
|
||||
return requestID.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
43
go-backend/internal/middleware/recovery.go
Normal file
43
go-backend/internal/middleware/recovery.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/xingrin/go-backend/internal/pkg"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Recovery returns a gin middleware for recovering from panics
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// Get request ID for tracing
|
||||
requestID := GetRequestID(c)
|
||||
|
||||
// Get stack trace
|
||||
stack := string(debug.Stack())
|
||||
|
||||
// Log the panic with full stack trace
|
||||
pkg.Error("Panic recovered",
|
||||
zap.String("requestId", requestID),
|
||||
zap.Any("error", err),
|
||||
zap.String("stack", stack),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
)
|
||||
|
||||
// Return 500 error
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"requestId": requestID,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
35
go-backend/internal/model/asset_statistics.go
Normal file
35
go-backend/internal/model/asset_statistics.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AssetStatistics represents asset statistics (singleton)
|
||||
type AssetStatistics struct {
|
||||
ID int `gorm:"primaryKey" json:"id"`
|
||||
|
||||
// Current statistics
|
||||
TotalTargets int `gorm:"column:total_targets;default:0" json:"totalTargets"`
|
||||
TotalSubdomains int `gorm:"column:total_subdomains;default:0" json:"totalSubdomains"`
|
||||
TotalIPs int `gorm:"column:total_ips;default:0" json:"totalIps"`
|
||||
TotalEndpoints int `gorm:"column:total_endpoints;default:0" json:"totalEndpoints"`
|
||||
TotalWebsites int `gorm:"column:total_websites;default:0" json:"totalWebsites"`
|
||||
TotalVulns int `gorm:"column:total_vulns;default:0" json:"totalVulns"`
|
||||
TotalAssets int `gorm:"column:total_assets;default:0" json:"totalAssets"`
|
||||
|
||||
// Previous statistics (for trend calculation)
|
||||
PrevTargets int `gorm:"column:prev_targets;default:0" json:"prevTargets"`
|
||||
PrevSubdomains int `gorm:"column:prev_subdomains;default:0" json:"prevSubdomains"`
|
||||
PrevIPs int `gorm:"column:prev_ips;default:0" json:"prevIps"`
|
||||
PrevEndpoints int `gorm:"column:prev_endpoints;default:0" json:"prevEndpoints"`
|
||||
PrevWebsites int `gorm:"column:prev_websites;default:0" json:"prevWebsites"`
|
||||
PrevVulns int `gorm:"column:prev_vulns;default:0" json:"prevVulns"`
|
||||
PrevAssets int `gorm:"column:prev_assets;default:0" json:"prevAssets"`
|
||||
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for AssetStatistics
|
||||
func (AssetStatistics) TableName() string {
|
||||
return "asset_statistics"
|
||||
}
|
||||
38
go-backend/internal/model/blacklist_rule.go
Normal file
38
go-backend/internal/model/blacklist_rule.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BlacklistRule represents a blacklist rule
|
||||
type BlacklistRule struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Pattern string `gorm:"column:pattern;size:255" json:"pattern"`
|
||||
RuleType string `gorm:"column:rule_type;size:20" json:"ruleType"`
|
||||
Scope string `gorm:"column:scope;size:20;index" json:"scope"`
|
||||
TargetID *int `gorm:"column:target_id;index" json:"targetId"`
|
||||
Description string `gorm:"column:description;size:500" json:"description"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for BlacklistRule
|
||||
func (BlacklistRule) TableName() string {
|
||||
return "blacklist_rule"
|
||||
}
|
||||
|
||||
// RuleType constants
|
||||
const (
|
||||
RuleTypeDomain = "domain"
|
||||
RuleTypeIP = "ip"
|
||||
RuleTypeCIDR = "cidr"
|
||||
RuleTypeKeyword = "keyword"
|
||||
)
|
||||
|
||||
// Scope constants
|
||||
const (
|
||||
ScopeGlobal = "global"
|
||||
ScopeTarget = "target"
|
||||
)
|
||||
27
go-backend/internal/model/directory.go
Normal file
27
go-backend/internal/model/directory.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Directory represents a discovered directory
|
||||
type Directory struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
URL string `gorm:"column:url;size:2000;not null" json:"url"`
|
||||
Status *int `gorm:"column:status" json:"status"`
|
||||
ContentLength *int64 `gorm:"column:content_length" json:"contentLength"`
|
||||
Words *int `gorm:"column:words" json:"words"`
|
||||
Lines *int `gorm:"column:lines" json:"lines"`
|
||||
ContentType string `gorm:"column:content_type;size:200" json:"contentType"`
|
||||
Duration *int64 `gorm:"column:duration" json:"duration"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Directory
|
||||
func (Directory) TableName() string {
|
||||
return "directory"
|
||||
}
|
||||
27
go-backend/internal/model/directory_snapshot.go
Normal file
27
go-backend/internal/model/directory_snapshot.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DirectorySnapshot represents a directory discovered in a scan
|
||||
type DirectorySnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
URL string `gorm:"column:url;size:2000" json:"url"`
|
||||
Status *int `gorm:"column:status" json:"status"`
|
||||
ContentLength *int64 `gorm:"column:content_length" json:"contentLength"`
|
||||
Words *int `gorm:"column:words" json:"words"`
|
||||
Lines *int `gorm:"column:lines" json:"lines"`
|
||||
ContentType string `gorm:"column:content_type;size:200" json:"contentType"`
|
||||
Duration *int64 `gorm:"column:duration" json:"duration"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for DirectorySnapshot
|
||||
func (DirectorySnapshot) TableName() string {
|
||||
return "directory_snapshot"
|
||||
}
|
||||
35
go-backend/internal/model/endpoint.go
Normal file
35
go-backend/internal/model/endpoint.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Endpoint represents a discovered endpoint
|
||||
type Endpoint struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
URL string `gorm:"column:url;type:text" json:"url"`
|
||||
Host string `gorm:"column:host;size:253" json:"host"`
|
||||
Location string `gorm:"column:location;type:text" json:"location"`
|
||||
Title string `gorm:"column:title;type:text" json:"title"`
|
||||
Webserver string `gorm:"column:webserver;type:text" json:"webserver"`
|
||||
ResponseBody string `gorm:"column:response_body;type:text" json:"responseBody"`
|
||||
ContentType string `gorm:"column:content_type;type:text" json:"contentType"`
|
||||
Tech pq.StringArray `gorm:"column:tech;type:varchar(100)[]" json:"tech"`
|
||||
StatusCode *int `gorm:"column:status_code" json:"statusCode"`
|
||||
ContentLength *int `gorm:"column:content_length" json:"contentLength"`
|
||||
Vhost *bool `gorm:"column:vhost" json:"vhost"`
|
||||
MatchedGFPatterns pq.StringArray `gorm:"column:matched_gf_patterns;type:varchar(100)[]" json:"matchedGfPatterns"`
|
||||
ResponseHeaders string `gorm:"column:response_headers;type:text" json:"responseHeaders"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Endpoint
|
||||
func (Endpoint) TableName() string {
|
||||
return "endpoint"
|
||||
}
|
||||
35
go-backend/internal/model/endpoint_snapshot.go
Normal file
35
go-backend/internal/model/endpoint_snapshot.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// EndpointSnapshot represents an endpoint discovered in a scan
|
||||
type EndpointSnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
URL string `gorm:"column:url;type:text" json:"url"`
|
||||
Host string `gorm:"column:host;size:253" json:"host"`
|
||||
Title string `gorm:"column:title;type:text" json:"title"`
|
||||
StatusCode *int `gorm:"column:status_code" json:"statusCode"`
|
||||
ContentLength *int `gorm:"column:content_length" json:"contentLength"`
|
||||
Location string `gorm:"column:location;type:text" json:"location"`
|
||||
Webserver string `gorm:"column:webserver;type:text" json:"webserver"`
|
||||
ContentType string `gorm:"column:content_type;type:text" json:"contentType"`
|
||||
Tech pq.StringArray `gorm:"column:tech;type:varchar(100)[]" json:"tech"`
|
||||
ResponseBody string `gorm:"column:response_body;type:text" json:"responseBody"`
|
||||
Vhost *bool `gorm:"column:vhost" json:"vhost"`
|
||||
MatchedGFPatterns pq.StringArray `gorm:"column:matched_gf_patterns;type:varchar(100)[]" json:"matchedGfPatterns"`
|
||||
ResponseHeaders string `gorm:"column:response_headers;type:text" json:"responseHeaders"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for EndpointSnapshot
|
||||
func (EndpointSnapshot) TableName() string {
|
||||
return "endpoint_snapshot"
|
||||
}
|
||||
23
go-backend/internal/model/host_port_mapping.go
Normal file
23
go-backend/internal/model/host_port_mapping.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// HostPortMapping represents a host-IP-port mapping
|
||||
type HostPortMapping struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
Host string `gorm:"column:host;size:1000;not null" json:"host"`
|
||||
IP string `gorm:"column:ip;type:inet;not null" json:"ip"`
|
||||
Port int `gorm:"column:port;not null" json:"port"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for HostPortMapping
|
||||
func (HostPortMapping) TableName() string {
|
||||
return "host_port_mapping"
|
||||
}
|
||||
23
go-backend/internal/model/host_port_mapping_snapshot.go
Normal file
23
go-backend/internal/model/host_port_mapping_snapshot.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// HostPortMappingSnapshot represents a host-IP-port mapping discovered in a scan
|
||||
type HostPortMappingSnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
Host string `gorm:"column:host;size:1000;not null" json:"host"`
|
||||
IP string `gorm:"column:ip;type:inet;not null" json:"ip"`
|
||||
Port int `gorm:"column:port;not null" json:"port"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for HostPortMappingSnapshot
|
||||
func (HostPortMappingSnapshot) TableName() string {
|
||||
return "host_port_mapping_snapshot"
|
||||
}
|
||||
250
go-backend/internal/model/model_test.go
Normal file
250
go-backend/internal/model/model_test.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TableNamer interface for models with TableName method
|
||||
type TableNamer interface {
|
||||
TableName() string
|
||||
}
|
||||
|
||||
// TestTableNames tests that all models return correct table names
|
||||
// Property 1: 数据库表名映射正确性
|
||||
// *对于任意* Go 模型,其 TableName() 方法返回的表名应与 Django 模型的 db_table 一致。
|
||||
// **验证: 需求 4.1**
|
||||
func TestTableNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
model TableNamer
|
||||
expected string
|
||||
}{
|
||||
// Base models
|
||||
{Organization{}, "organization"},
|
||||
{Target{}, "target"},
|
||||
{Scan{}, "scan"},
|
||||
{Subdomain{}, "subdomain"},
|
||||
{WebSite{}, "website"},
|
||||
{WorkerNode{}, "worker_node"},
|
||||
{ScanEngine{}, "scan_engine"},
|
||||
// Asset models
|
||||
{Endpoint{}, "endpoint"},
|
||||
{Directory{}, "directory"},
|
||||
{HostPortMapping{}, "host_port_mapping"},
|
||||
{Vulnerability{}, "vulnerability"},
|
||||
{Screenshot{}, "screenshot"},
|
||||
// Snapshot models
|
||||
{SubdomainSnapshot{}, "subdomain_snapshot"},
|
||||
{WebsiteSnapshot{}, "website_snapshot"},
|
||||
{EndpointSnapshot{}, "endpoint_snapshot"},
|
||||
{DirectorySnapshot{}, "directory_snapshot"},
|
||||
{HostPortMappingSnapshot{}, "host_port_mapping_snapshot"},
|
||||
{VulnerabilitySnapshot{}, "vulnerability_snapshot"},
|
||||
{ScreenshotSnapshot{}, "screenshot_snapshot"},
|
||||
// Scan-related models
|
||||
{ScanLog{}, "scan_log"},
|
||||
{ScanInputTarget{}, "scan_input_target"},
|
||||
{ScheduledScan{}, "scheduled_scan"},
|
||||
{SubfinderProviderSettings{}, "subfinder_provider_settings"},
|
||||
// Engine models
|
||||
{Wordlist{}, "wordlist"},
|
||||
{NucleiTemplateRepo{}, "nuclei_template_repo"},
|
||||
// Notification models
|
||||
{Notification{}, "notification"},
|
||||
{NotificationSettings{}, "notification_settings"},
|
||||
// Config models
|
||||
{BlacklistRule{}, "blacklist_rule"},
|
||||
// Statistics models
|
||||
{AssetStatistics{}, "asset_statistics"},
|
||||
{StatisticsHistory{}, "statistics_history"},
|
||||
// Auth models
|
||||
{User{}, "auth_user"},
|
||||
{Session{}, "django_session"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
if got := tt.model.TableName(); got != tt.expected {
|
||||
t.Errorf("TableName() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONFieldNames tests that JSON field names are camelCase
|
||||
// Property 2: JSON 字段名转换正确性
|
||||
// *对于任意* Go 模型序列化为 JSON,所有字段名应为 camelCase 格式。
|
||||
// **验证: 需求 4.6**
|
||||
func TestJSONFieldNames(t *testing.T) {
|
||||
// Test Target model
|
||||
target := Target{
|
||||
ID: 1,
|
||||
Name: "test.com",
|
||||
Type: "domain",
|
||||
CreatedAt: time.Now(),
|
||||
LastScannedAt: nil,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(target)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal Target: %v", err)
|
||||
}
|
||||
jsonStr := string(jsonBytes)
|
||||
|
||||
// Should contain camelCase
|
||||
camelCaseFields := []string{"id", "name", "type", "createdAt", "lastScannedAt"}
|
||||
for _, field := range camelCaseFields {
|
||||
if !strings.Contains(jsonStr, `"`+field+`"`) {
|
||||
t.Errorf("JSON should contain camelCase field %q, got: %s", field, jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Should NOT contain snake_case
|
||||
snakeCaseFields := []string{"created_at", "last_scanned_at", "organization_id"}
|
||||
for _, field := range snakeCaseFields {
|
||||
if strings.Contains(jsonStr, `"`+field+`"`) {
|
||||
t.Errorf("JSON should NOT contain snake_case field %q, got: %s", field, jsonStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanJSONFieldNames tests Scan model JSON serialization
|
||||
func TestScanJSONFieldNames(t *testing.T) {
|
||||
scan := Scan{
|
||||
ID: 1,
|
||||
TargetID: 1,
|
||||
Status: "running",
|
||||
Progress: 50,
|
||||
CurrentStage: "subdomain_discovery",
|
||||
CachedSubdomainsCount: 100,
|
||||
CachedWebsitesCount: 50,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(scan)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal Scan: %v", err)
|
||||
}
|
||||
jsonStr := string(jsonBytes)
|
||||
|
||||
// Should contain camelCase
|
||||
camelCaseFields := []string{
|
||||
"targetId", "status", "progress", "currentStage",
|
||||
"cachedSubdomainsCount", "cachedWebsitesCount",
|
||||
}
|
||||
for _, field := range camelCaseFields {
|
||||
if !strings.Contains(jsonStr, `"`+field+`"`) {
|
||||
t.Errorf("JSON should contain camelCase field %q, got: %s", field, jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Should NOT contain snake_case
|
||||
snakeCaseFields := []string{
|
||||
"target_id", "current_stage", "cached_subdomains_count",
|
||||
}
|
||||
for _, field := range snakeCaseFields {
|
||||
if strings.Contains(jsonStr, `"`+field+`"`) {
|
||||
t.Errorf("JSON should NOT contain snake_case field %q, got: %s", field, jsonStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TestGORMColumnTags tests that GORM column tags use snake_case
|
||||
// Property 3: 数据库字段映射正确性
|
||||
// *对于任意* Go 模型字段,其 gorm column tag 应与数据库实际列名(snake_case)一致。
|
||||
// **验证: 需求 4.2**
|
||||
func TestGORMColumnTags(t *testing.T) {
|
||||
// Test Target model
|
||||
targetType := reflect.TypeOf(Target{})
|
||||
expectedColumns := map[string]string{
|
||||
"ID": "", // primaryKey, no explicit column
|
||||
"Name": "name",
|
||||
"Type": "type",
|
||||
"OrganizationID": "organization_id",
|
||||
"CreatedAt": "created_at",
|
||||
"LastScannedAt": "last_scanned_at",
|
||||
"DeletedAt": "deleted_at",
|
||||
}
|
||||
|
||||
for fieldName, expectedColumn := range expectedColumns {
|
||||
field, found := targetType.FieldByName(fieldName)
|
||||
if !found {
|
||||
t.Errorf("Field %s not found in Target", fieldName)
|
||||
continue
|
||||
}
|
||||
|
||||
gormTag := field.Tag.Get("gorm")
|
||||
if expectedColumn != "" && !strings.Contains(gormTag, "column:"+expectedColumn) {
|
||||
t.Errorf("Field %s: expected gorm column:%s, got tag: %s", fieldName, expectedColumn, gormTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanGORMColumnTags tests Scan model GORM tags
|
||||
func TestScanGORMColumnTags(t *testing.T) {
|
||||
scanType := reflect.TypeOf(Scan{})
|
||||
expectedColumns := map[string]string{
|
||||
"TargetID": "target_id",
|
||||
"EngineIDs": "engine_ids",
|
||||
"EngineNames": "engine_names",
|
||||
"YamlConfiguration": "yaml_configuration",
|
||||
"ScanMode": "scan_mode",
|
||||
"Status": "status",
|
||||
"ResultsDir": "results_dir",
|
||||
"ContainerIDs": "container_ids",
|
||||
"WorkerID": "worker_id",
|
||||
"ErrorMessage": "error_message",
|
||||
"Progress": "progress",
|
||||
"CurrentStage": "current_stage",
|
||||
"StageProgress": "stage_progress",
|
||||
"CreatedAt": "created_at",
|
||||
"StoppedAt": "stopped_at",
|
||||
"DeletedAt": "deleted_at",
|
||||
"CachedSubdomainsCount": "cached_subdomains_count",
|
||||
"CachedWebsitesCount": "cached_websites_count",
|
||||
"CachedEndpointsCount": "cached_endpoints_count",
|
||||
"CachedIPsCount": "cached_ips_count",
|
||||
"CachedVulnsTotal": "cached_vulns_total",
|
||||
}
|
||||
|
||||
for fieldName, expectedColumn := range expectedColumns {
|
||||
field, found := scanType.FieldByName(fieldName)
|
||||
if !found {
|
||||
t.Errorf("Field %s not found in Scan", fieldName)
|
||||
continue
|
||||
}
|
||||
|
||||
gormTag := field.Tag.Get("gorm")
|
||||
if !strings.Contains(gormTag, "column:"+expectedColumn) {
|
||||
t.Errorf("Field %s: expected gorm column:%s, got tag: %s", fieldName, expectedColumn, gormTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWorkerNodePasswordHidden tests that password is hidden from JSON
|
||||
func TestWorkerNodePasswordHidden(t *testing.T) {
|
||||
worker := WorkerNode{
|
||||
ID: 1,
|
||||
Name: "worker-1",
|
||||
IPAddress: "192.168.1.1",
|
||||
Password: "secret123",
|
||||
Status: "connected",
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(worker)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal WorkerNode: %v", err)
|
||||
}
|
||||
jsonStr := string(jsonBytes)
|
||||
|
||||
// Password should NOT appear in JSON
|
||||
if strings.Contains(jsonStr, "secret123") {
|
||||
t.Errorf("Password should be hidden from JSON, got: %s", jsonStr)
|
||||
}
|
||||
if strings.Contains(jsonStr, `"password"`) {
|
||||
t.Errorf("Password field should not appear in JSON, got: %s", jsonStr)
|
||||
}
|
||||
}
|
||||
38
go-backend/internal/model/notification.go
Normal file
38
go-backend/internal/model/notification.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification represents a notification entry
|
||||
type Notification struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Category string `gorm:"column:category;size:20;index" json:"category"`
|
||||
Level string `gorm:"column:level;size:20;index" json:"level"`
|
||||
Title string `gorm:"column:title;size:200" json:"title"`
|
||||
Message string `gorm:"column:message;size:2000" json:"message"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index" json:"createdAt"`
|
||||
IsRead bool `gorm:"column:is_read;default:false;index" json:"isRead"`
|
||||
ReadAt *time.Time `gorm:"column:read_at" json:"readAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Notification
|
||||
func (Notification) TableName() string {
|
||||
return "notification"
|
||||
}
|
||||
|
||||
// NotificationCategory constants
|
||||
const (
|
||||
NotificationCategoryScan = "scan"
|
||||
NotificationCategoryVulnerability = "vulnerability"
|
||||
NotificationCategoryAsset = "asset"
|
||||
NotificationCategorySystem = "system"
|
||||
)
|
||||
|
||||
// NotificationLevel constants
|
||||
const (
|
||||
NotificationLevelLow = "low"
|
||||
NotificationLevelMedium = "medium"
|
||||
NotificationLevelHigh = "high"
|
||||
NotificationLevelCritical = "critical"
|
||||
)
|
||||
34
go-backend/internal/model/notification_settings.go
Normal file
34
go-backend/internal/model/notification_settings.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// NotificationSettings represents notification settings (singleton)
|
||||
type NotificationSettings struct {
|
||||
ID int `gorm:"primaryKey" json:"id"`
|
||||
DiscordEnabled bool `gorm:"column:discord_enabled;default:false" json:"discordEnabled"`
|
||||
DiscordWebhookURL string `gorm:"column:discord_webhook_url;size:500" json:"discordWebhookUrl"`
|
||||
WecomEnabled bool `gorm:"column:wecom_enabled;default:false" json:"wecomEnabled"`
|
||||
WecomWebhookURL string `gorm:"column:wecom_webhook_url;size:500" json:"wecomWebhookUrl"`
|
||||
Categories datatypes.JSON `gorm:"column:categories;type:jsonb" json:"categories"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for NotificationSettings
|
||||
func (NotificationSettings) TableName() string {
|
||||
return "notification_settings"
|
||||
}
|
||||
|
||||
// DefaultCategories returns the default category configuration
|
||||
func DefaultCategories() map[string]bool {
|
||||
return map[string]bool{
|
||||
"scan": true,
|
||||
"vulnerability": true,
|
||||
"asset": true,
|
||||
"system": false,
|
||||
}
|
||||
}
|
||||
22
go-backend/internal/model/nuclei_template_repo.go
Normal file
22
go-backend/internal/model/nuclei_template_repo.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// NucleiTemplateRepo represents a nuclei template git repository
|
||||
type NucleiTemplateRepo struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:200;uniqueIndex" json:"name"`
|
||||
RepoURL string `gorm:"column:repo_url;size:500" json:"repoUrl"`
|
||||
LocalPath string `gorm:"column:local_path;size:500" json:"localPath"`
|
||||
CommitHash string `gorm:"column:commit_hash;size:40" json:"commitHash"`
|
||||
LastSyncedAt *time.Time `gorm:"column:last_synced_at" json:"lastSyncedAt"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for NucleiTemplateRepo
|
||||
func (NucleiTemplateRepo) TableName() string {
|
||||
return "nuclei_template_repo"
|
||||
}
|
||||
19
go-backend/internal/model/organization.go
Normal file
19
go-backend/internal/model/organization.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Organization represents an organization in the system
|
||||
type Organization struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:300" json:"name"`
|
||||
Description string `gorm:"column:description;size:1000" json:"description"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Organization
|
||||
func (Organization) TableName() string {
|
||||
return "organization"
|
||||
}
|
||||
67
go-backend/internal/model/scan.go
Normal file
67
go-backend/internal/model/scan.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// Scan represents a scan job
|
||||
type Scan struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
EngineIDs pq.Int64Array `gorm:"column:engine_ids;type:integer[]" json:"engineIds"`
|
||||
EngineNames datatypes.JSON `gorm:"column:engine_names;type:jsonb" json:"engineNames"`
|
||||
YamlConfiguration string `gorm:"column:yaml_configuration;type:text" json:"yamlConfiguration"`
|
||||
ScanMode string `gorm:"column:scan_mode;size:10;default:'full'" json:"scanMode"`
|
||||
Status string `gorm:"column:status;size:20;default:'initiated'" json:"status"`
|
||||
ResultsDir string `gorm:"column:results_dir;size:100" json:"resultsDir"`
|
||||
ContainerIDs pq.StringArray `gorm:"column:container_ids;type:varchar(100)[]" json:"containerIds"`
|
||||
WorkerID *int `gorm:"column:worker_id" json:"workerId"`
|
||||
ErrorMessage string `gorm:"column:error_message;size:2000" json:"errorMessage"`
|
||||
Progress int `gorm:"column:progress;default:0" json:"progress"`
|
||||
CurrentStage string `gorm:"column:current_stage;size:50" json:"currentStage"`
|
||||
StageProgress datatypes.JSON `gorm:"column:stage_progress;type:jsonb" json:"stageProgress"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
StoppedAt *time.Time `gorm:"column:stopped_at" json:"stoppedAt"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
|
||||
|
||||
// Cached statistics
|
||||
CachedSubdomainsCount int `gorm:"column:cached_subdomains_count" json:"cachedSubdomainsCount"`
|
||||
CachedWebsitesCount int `gorm:"column:cached_websites_count" json:"cachedWebsitesCount"`
|
||||
CachedEndpointsCount int `gorm:"column:cached_endpoints_count" json:"cachedEndpointsCount"`
|
||||
CachedIPsCount int `gorm:"column:cached_ips_count" json:"cachedIpsCount"`
|
||||
CachedDirectoriesCount int `gorm:"column:cached_directories_count" json:"cachedDirectoriesCount"`
|
||||
CachedScreenshotsCount int `gorm:"column:cached_screenshots_count" json:"cachedScreenshotsCount"`
|
||||
CachedVulnsTotal int `gorm:"column:cached_vulns_total" json:"cachedVulnsTotal"`
|
||||
CachedVulnsCritical int `gorm:"column:cached_vulns_critical" json:"cachedVulnsCritical"`
|
||||
CachedVulnsHigh int `gorm:"column:cached_vulns_high" json:"cachedVulnsHigh"`
|
||||
CachedVulnsMedium int `gorm:"column:cached_vulns_medium" json:"cachedVulnsMedium"`
|
||||
CachedVulnsLow int `gorm:"column:cached_vulns_low" json:"cachedVulnsLow"`
|
||||
StatsUpdatedAt *time.Time `gorm:"column:stats_updated_at" json:"statsUpdatedAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Scan
|
||||
func (Scan) TableName() string {
|
||||
return "scan"
|
||||
}
|
||||
|
||||
// ScanStatus constants
|
||||
const (
|
||||
ScanStatusInitiated = "initiated"
|
||||
ScanStatusRunning = "running"
|
||||
ScanStatusCompleted = "completed"
|
||||
ScanStatusFailed = "failed"
|
||||
ScanStatusStopped = "stopped"
|
||||
ScanStatusPending = "pending"
|
||||
)
|
||||
|
||||
// ScanMode constants
|
||||
const (
|
||||
ScanModeFull = "full"
|
||||
ScanModeIncremental = "incremental"
|
||||
)
|
||||
19
go-backend/internal/model/scan_engine.go
Normal file
19
go-backend/internal/model/scan_engine.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScanEngine represents a scan engine configuration
|
||||
type ScanEngine struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:200;uniqueIndex" json:"name"`
|
||||
Configuration string `gorm:"column:configuration;size:10000" json:"configuration"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ScanEngine
|
||||
func (ScanEngine) TableName() string {
|
||||
return "scan_engine"
|
||||
}
|
||||
30
go-backend/internal/model/scan_input_target.go
Normal file
30
go-backend/internal/model/scan_input_target.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScanInputTarget represents a scan input target entry
|
||||
type ScanInputTarget struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null;index" json:"scanId"`
|
||||
Value string `gorm:"column:value;size:2000" json:"value"`
|
||||
InputType string `gorm:"column:input_type;size:10" json:"inputType"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ScanInputTarget
|
||||
func (ScanInputTarget) TableName() string {
|
||||
return "scan_input_target"
|
||||
}
|
||||
|
||||
// InputType constants
|
||||
const (
|
||||
InputTypeDomain = "domain"
|
||||
InputTypeIP = "ip"
|
||||
InputTypeCIDR = "cidr"
|
||||
InputTypeURL = "url"
|
||||
)
|
||||
29
go-backend/internal/model/scan_log.go
Normal file
29
go-backend/internal/model/scan_log.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScanLog represents a scan log entry
|
||||
type ScanLog struct {
|
||||
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null;index" json:"scanId"`
|
||||
Level string `gorm:"column:level;size:10;default:'info'" json:"level"`
|
||||
Content string `gorm:"column:content;type:text" json:"content"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ScanLog
|
||||
func (ScanLog) TableName() string {
|
||||
return "scan_log"
|
||||
}
|
||||
|
||||
// ScanLogLevel constants
|
||||
const (
|
||||
ScanLogLevelInfo = "info"
|
||||
ScanLogLevelWarning = "warning"
|
||||
ScanLogLevelError = "error"
|
||||
)
|
||||
35
go-backend/internal/model/scheduled_scan.go
Normal file
35
go-backend/internal/model/scheduled_scan.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// ScheduledScan represents a scheduled scan task
|
||||
type ScheduledScan struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:200" json:"name"`
|
||||
EngineIDs pq.Int64Array `gorm:"column:engine_ids;type:integer[]" json:"engineIds"`
|
||||
EngineNames datatypes.JSON `gorm:"column:engine_names;type:jsonb" json:"engineNames"`
|
||||
YamlConfiguration string `gorm:"column:yaml_configuration;type:text" json:"yamlConfiguration"`
|
||||
OrganizationID *int `gorm:"column:organization_id" json:"organizationId"`
|
||||
TargetID *int `gorm:"column:target_id" json:"targetId"`
|
||||
CronExpression string `gorm:"column:cron_expression;size:100;default:'0 2 * * *'" json:"cronExpression"`
|
||||
IsEnabled bool `gorm:"column:is_enabled;default:true;index" json:"isEnabled"`
|
||||
RunCount int `gorm:"column:run_count;default:0" json:"runCount"`
|
||||
LastRunTime *time.Time `gorm:"column:last_run_time" json:"lastRunTime"`
|
||||
NextRunTime *time.Time `gorm:"column:next_run_time" json:"nextRunTime"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
|
||||
// Relationships
|
||||
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ScheduledScan
|
||||
func (ScheduledScan) TableName() string {
|
||||
return "scheduled_scan"
|
||||
}
|
||||
24
go-backend/internal/model/screenshot.go
Normal file
24
go-backend/internal/model/screenshot.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Screenshot represents a screenshot asset
|
||||
type Screenshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
URL string `gorm:"column:url;type:text" json:"url"`
|
||||
StatusCode *int16 `gorm:"column:status_code" json:"statusCode"`
|
||||
Image []byte `gorm:"column:image;type:bytea" json:"-"` // Hidden from JSON
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Screenshot
|
||||
func (Screenshot) TableName() string {
|
||||
return "screenshot"
|
||||
}
|
||||
23
go-backend/internal/model/screenshot_snapshot.go
Normal file
23
go-backend/internal/model/screenshot_snapshot.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScreenshotSnapshot represents a screenshot captured in a scan
|
||||
type ScreenshotSnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
URL string `gorm:"column:url;type:text" json:"url"`
|
||||
StatusCode *int16 `gorm:"column:status_code" json:"statusCode"`
|
||||
Image []byte `gorm:"column:image;type:bytea" json:"-"` // Hidden from JSON
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ScreenshotSnapshot
|
||||
func (ScreenshotSnapshot) TableName() string {
|
||||
return "screenshot_snapshot"
|
||||
}
|
||||
17
go-backend/internal/model/session.go
Normal file
17
go-backend/internal/model/session.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session represents a Django django_session compatible model
|
||||
type Session struct {
|
||||
SessionKey string `gorm:"column:session_key;size:40;primaryKey" json:"sessionKey"`
|
||||
SessionData string `gorm:"column:session_data;type:text" json:"sessionData"`
|
||||
ExpireDate time.Time `gorm:"column:expire_date;index" json:"expireDate"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Session (Django django_session)
|
||||
func (Session) TableName() string {
|
||||
return "django_session"
|
||||
}
|
||||
25
go-backend/internal/model/statistics_history.go
Normal file
25
go-backend/internal/model/statistics_history.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatisticsHistory represents daily statistics history
|
||||
type StatisticsHistory struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Date time.Time `gorm:"column:date;type:date;uniqueIndex" json:"date"`
|
||||
TotalTargets int `gorm:"column:total_targets;default:0" json:"totalTargets"`
|
||||
TotalSubdomains int `gorm:"column:total_subdomains;default:0" json:"totalSubdomains"`
|
||||
TotalIPs int `gorm:"column:total_ips;default:0" json:"totalIps"`
|
||||
TotalEndpoints int `gorm:"column:total_endpoints;default:0" json:"totalEndpoints"`
|
||||
TotalWebsites int `gorm:"column:total_websites;default:0" json:"totalWebsites"`
|
||||
TotalVulns int `gorm:"column:total_vulns;default:0" json:"totalVulns"`
|
||||
TotalAssets int `gorm:"column:total_assets;default:0" json:"totalAssets"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for StatisticsHistory
|
||||
func (StatisticsHistory) TableName() string {
|
||||
return "statistics_history"
|
||||
}
|
||||
21
go-backend/internal/model/subdomain.go
Normal file
21
go-backend/internal/model/subdomain.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Subdomain represents a discovered subdomain
|
||||
type Subdomain struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
Name string `gorm:"column:name;size:1000" json:"name"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Subdomain
|
||||
func (Subdomain) TableName() string {
|
||||
return "subdomain"
|
||||
}
|
||||
21
go-backend/internal/model/subdomain_snapshot.go
Normal file
21
go-backend/internal/model/subdomain_snapshot.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// SubdomainSnapshot represents a subdomain discovered in a scan
|
||||
type SubdomainSnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
Name string `gorm:"column:name;size:1000" json:"name"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for SubdomainSnapshot
|
||||
func (SubdomainSnapshot) TableName() string {
|
||||
return "subdomain_snapshot"
|
||||
}
|
||||
34
go-backend/internal/model/subfinder_provider_settings.go
Normal file
34
go-backend/internal/model/subfinder_provider_settings.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// SubfinderProviderSettings represents subfinder provider settings (singleton)
|
||||
type SubfinderProviderSettings struct {
|
||||
ID int `gorm:"primaryKey" json:"id"`
|
||||
Providers datatypes.JSON `gorm:"column:providers;type:jsonb" json:"providers"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for SubfinderProviderSettings
|
||||
func (SubfinderProviderSettings) TableName() string {
|
||||
return "subfinder_provider_settings"
|
||||
}
|
||||
|
||||
// DefaultProviders returns the default provider configuration
|
||||
func DefaultProviders() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"fofa": map[string]interface{}{"enabled": false, "email": "", "api_key": ""},
|
||||
"hunter": map[string]interface{}{"enabled": false, "api_key": ""},
|
||||
"shodan": map[string]interface{}{"enabled": false, "api_key": ""},
|
||||
"censys": map[string]interface{}{"enabled": false, "api_id": "", "api_secret": ""},
|
||||
"zoomeye": map[string]interface{}{"enabled": false, "api_key": ""},
|
||||
"securitytrails": map[string]interface{}{"enabled": false, "api_key": ""},
|
||||
"threatbook": map[string]interface{}{"enabled": false, "api_key": ""},
|
||||
"quake": map[string]interface{}{"enabled": false, "api_key": ""},
|
||||
}
|
||||
}
|
||||
24
go-backend/internal/model/target.go
Normal file
24
go-backend/internal/model/target.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Target represents a scan target (domain, IP, etc.)
|
||||
type Target struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:300" json:"name"`
|
||||
Type string `gorm:"column:type;size:20;default:'domain'" json:"type"`
|
||||
OrganizationID *int `gorm:"column:organization_id" json:"organizationId"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
LastScannedAt *time.Time `gorm:"column:last_scanned_at" json:"lastScannedAt"`
|
||||
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
|
||||
|
||||
// Relationships
|
||||
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Target
|
||||
func (Target) TableName() string {
|
||||
return "target"
|
||||
}
|
||||
25
go-backend/internal/model/user.go
Normal file
25
go-backend/internal/model/user.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User represents a Django auth_user compatible model
|
||||
type User struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Password string `gorm:"column:password;size:128" json:"-"`
|
||||
LastLogin *time.Time `gorm:"column:last_login" json:"lastLogin"`
|
||||
IsSuperuser bool `gorm:"column:is_superuser;default:false" json:"isSuperuser"`
|
||||
Username string `gorm:"column:username;size:150;uniqueIndex" json:"username"`
|
||||
FirstName string `gorm:"column:first_name;size:150" json:"firstName"`
|
||||
LastName string `gorm:"column:last_name;size:150" json:"lastName"`
|
||||
Email string `gorm:"column:email;size:254" json:"email"`
|
||||
IsStaff bool `gorm:"column:is_staff;default:false" json:"isStaff"`
|
||||
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
|
||||
DateJoined time.Time `gorm:"column:date_joined;autoCreateTime" json:"dateJoined"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for User (Django auth_user)
|
||||
func (User) TableName() string {
|
||||
return "auth_user"
|
||||
}
|
||||
40
go-backend/internal/model/vulnerability.go
Normal file
40
go-backend/internal/model/vulnerability.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// Vulnerability represents a discovered vulnerability
|
||||
type Vulnerability struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
URL string `gorm:"column:url;size:2000" json:"url"`
|
||||
VulnType string `gorm:"column:vuln_type;size:100" json:"vulnType"`
|
||||
Severity string `gorm:"column:severity;size:20;default:'unknown'" json:"severity"`
|
||||
Source string `gorm:"column:source;size:50" json:"source"`
|
||||
CVSSScore *decimal.Decimal `gorm:"column:cvss_score;type:decimal(3,1)" json:"cvssScore"`
|
||||
Description string `gorm:"column:description;type:text" json:"description"`
|
||||
RawOutput datatypes.JSON `gorm:"column:raw_output;type:jsonb" json:"rawOutput"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Vulnerability
|
||||
func (Vulnerability) TableName() string {
|
||||
return "vulnerability"
|
||||
}
|
||||
|
||||
// VulnSeverity constants
|
||||
const (
|
||||
VulnSeverityUnknown = "unknown"
|
||||
VulnSeverityInfo = "info"
|
||||
VulnSeverityLow = "low"
|
||||
VulnSeverityMedium = "medium"
|
||||
VulnSeverityHigh = "high"
|
||||
VulnSeverityCritical = "critical"
|
||||
)
|
||||
30
go-backend/internal/model/vulnerability_snapshot.go
Normal file
30
go-backend/internal/model/vulnerability_snapshot.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// VulnerabilitySnapshot represents a vulnerability discovered in a scan
|
||||
type VulnerabilitySnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
URL string `gorm:"column:url;size:2000" json:"url"`
|
||||
VulnType string `gorm:"column:vuln_type;size:100" json:"vulnType"`
|
||||
Severity string `gorm:"column:severity;size:20;default:'unknown'" json:"severity"`
|
||||
Source string `gorm:"column:source;size:50" json:"source"`
|
||||
CVSSScore *decimal.Decimal `gorm:"column:cvss_score;type:decimal(3,1)" json:"cvssScore"`
|
||||
Description string `gorm:"column:description;type:text" json:"description"`
|
||||
RawOutput datatypes.JSON `gorm:"column:raw_output;type:jsonb" json:"rawOutput"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for VulnerabilitySnapshot
|
||||
func (VulnerabilitySnapshot) TableName() string {
|
||||
return "vulnerability_snapshot"
|
||||
}
|
||||
34
go-backend/internal/model/website.go
Normal file
34
go-backend/internal/model/website.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// WebSite represents a discovered website
|
||||
type WebSite struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
|
||||
URL string `gorm:"column:url;type:text" json:"url"`
|
||||
Host string `gorm:"column:host;size:253" json:"host"`
|
||||
Location string `gorm:"column:location;type:text" json:"location"`
|
||||
Title string `gorm:"column:title;type:text" json:"title"`
|
||||
Webserver string `gorm:"column:webserver;type:text" json:"webserver"`
|
||||
ResponseBody string `gorm:"column:response_body;type:text" json:"responseBody"`
|
||||
ContentType string `gorm:"column:content_type;type:text" json:"contentType"`
|
||||
Tech pq.StringArray `gorm:"column:tech;type:varchar(100)[]" json:"tech"`
|
||||
StatusCode *int `gorm:"column:status_code" json:"statusCode"`
|
||||
ContentLength *int64 `gorm:"column:content_length" json:"contentLength"`
|
||||
Vhost *bool `gorm:"column:vhost" json:"vhost"`
|
||||
ResponseHeaders string `gorm:"column:response_headers;type:text" json:"responseHeaders"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Target *Target `gorm:"foreignKey:TargetID" json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for WebSite
|
||||
func (WebSite) TableName() string {
|
||||
return "website"
|
||||
}
|
||||
34
go-backend/internal/model/website_snapshot.go
Normal file
34
go-backend/internal/model/website_snapshot.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// WebsiteSnapshot represents a website discovered in a scan
|
||||
type WebsiteSnapshot struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ScanID int `gorm:"column:scan_id;not null" json:"scanId"`
|
||||
URL string `gorm:"column:url;type:text" json:"url"`
|
||||
Host string `gorm:"column:host;size:253" json:"host"`
|
||||
Title string `gorm:"column:title;type:text" json:"title"`
|
||||
StatusCode *int `gorm:"column:status_code" json:"statusCode"`
|
||||
ContentLength *int64 `gorm:"column:content_length" json:"contentLength"`
|
||||
Location string `gorm:"column:location;type:text" json:"location"`
|
||||
Webserver string `gorm:"column:webserver;type:text" json:"webserver"`
|
||||
ContentType string `gorm:"column:content_type;type:text" json:"contentType"`
|
||||
Tech pq.StringArray `gorm:"column:tech;type:varchar(100)[]" json:"tech"`
|
||||
ResponseBody string `gorm:"column:response_body;type:text" json:"responseBody"`
|
||||
Vhost *bool `gorm:"column:vhost" json:"vhost"`
|
||||
ResponseHeaders string `gorm:"column:response_headers;type:text" json:"responseHeaders"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
|
||||
// Relationships
|
||||
Scan *Scan `gorm:"foreignKey:ScanID" json:"scan,omitempty"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for WebsiteSnapshot
|
||||
func (WebsiteSnapshot) TableName() string {
|
||||
return "website_snapshot"
|
||||
}
|
||||
23
go-backend/internal/model/wordlist.go
Normal file
23
go-backend/internal/model/wordlist.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Wordlist represents a wordlist file
|
||||
type Wordlist struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:200;uniqueIndex" json:"name"`
|
||||
Description string `gorm:"column:description;size:200" json:"description"`
|
||||
FilePath string `gorm:"column:file_path;size:500" json:"filePath"`
|
||||
FileSize int64 `gorm:"column:file_size;default:0" json:"fileSize"`
|
||||
LineCount int `gorm:"column:line_count;default:0" json:"lineCount"`
|
||||
FileHash string `gorm:"column:file_hash;size:64" json:"fileHash"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;index" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Wordlist
|
||||
func (Wordlist) TableName() string {
|
||||
return "wordlist"
|
||||
}
|
||||
32
go-backend/internal/model/worker_node.go
Normal file
32
go-backend/internal/model/worker_node.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// WorkerNode represents a worker node for executing scans
|
||||
type WorkerNode struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;size:100;uniqueIndex" json:"name"`
|
||||
IPAddress string `gorm:"column:ip_address;type:inet" json:"ipAddress"`
|
||||
SSHPort int `gorm:"column:ssh_port;default:22" json:"sshPort"`
|
||||
Username string `gorm:"column:username;size:50;default:'root'" json:"username"`
|
||||
Password string `gorm:"column:password;size:200" json:"-"` // Hidden from JSON
|
||||
IsLocal bool `gorm:"column:is_local;default:false" json:"isLocal"`
|
||||
Status string `gorm:"column:status;size:20;default:'pending'" json:"status"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for WorkerNode
|
||||
func (WorkerNode) TableName() string {
|
||||
return "worker_node"
|
||||
}
|
||||
|
||||
// WorkerStatus constants
|
||||
const (
|
||||
WorkerStatusPending = "pending"
|
||||
WorkerStatusConnected = "connected"
|
||||
WorkerStatusDisconnected = "disconnected"
|
||||
WorkerStatusError = "error"
|
||||
)
|
||||
130
go-backend/internal/pkg/logger.go
Normal file
130
go-backend/internal/pkg/logger.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
// Logger is the global logger instance
|
||||
Logger *zap.Logger
|
||||
// Sugar is the sugared logger for convenience
|
||||
Sugar *zap.SugaredLogger
|
||||
)
|
||||
|
||||
// LogConfig holds logging configuration
|
||||
type LogConfig struct {
|
||||
Level string
|
||||
Format string
|
||||
}
|
||||
|
||||
// InitLogger initializes the global logger
|
||||
func InitLogger(cfg *LogConfig) error {
|
||||
level := parseLogLevel(cfg.Level)
|
||||
|
||||
var config zap.Config
|
||||
if strings.ToLower(cfg.Format) == "json" {
|
||||
config = zap.NewProductionConfig()
|
||||
} else {
|
||||
config = zap.NewDevelopmentConfig()
|
||||
}
|
||||
|
||||
config.Level = zap.NewAtomicLevelAt(level)
|
||||
config.EncoderConfig.TimeKey = "timestamp"
|
||||
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
|
||||
var err error
|
||||
Logger, err = config.Build(
|
||||
zap.AddCallerSkip(1),
|
||||
zap.AddStacktrace(zapcore.ErrorLevel),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Sugar = Logger.Sugar()
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLogLevel converts string level to zapcore.Level
|
||||
func parseLogLevel(level string) zapcore.Level {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
return zapcore.DebugLevel
|
||||
case "info":
|
||||
return zapcore.InfoLevel
|
||||
case "warn", "warning":
|
||||
return zapcore.WarnLevel
|
||||
case "error":
|
||||
return zapcore.ErrorLevel
|
||||
case "fatal":
|
||||
return zapcore.FatalLevel
|
||||
default:
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sync flushes any buffered log entries
|
||||
func Sync() {
|
||||
if Logger != nil {
|
||||
_ = Logger.Sync()
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func Debug(msg string, fields ...zap.Field) {
|
||||
Logger.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func Info(msg string, fields ...zap.Field) {
|
||||
Logger.Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func Warn(msg string, fields ...zap.Field) {
|
||||
Logger.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func Error(msg string, fields ...zap.Field) {
|
||||
Logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Fatal logs a fatal message and exits
|
||||
func Fatal(msg string, fields ...zap.Field) {
|
||||
Logger.Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
// With creates a child logger with additional fields
|
||||
func With(fields ...zap.Field) *zap.Logger {
|
||||
return Logger.With(fields...)
|
||||
}
|
||||
|
||||
// WithRequestID creates a logger with request ID field
|
||||
func WithRequestID(requestID string) *zap.Logger {
|
||||
return Logger.With(zap.String("requestId", requestID))
|
||||
}
|
||||
|
||||
// NewNopLogger returns a no-op logger for testing
|
||||
func NewNopLogger() *zap.Logger {
|
||||
return zap.NewNop()
|
||||
}
|
||||
|
||||
// InitTestLogger initializes a test logger that writes to stdout
|
||||
func InitTestLogger() {
|
||||
Logger = zap.NewExample()
|
||||
Sugar = Logger.Sugar()
|
||||
}
|
||||
|
||||
// InitDefaultLogger initializes logger with default settings
|
||||
func InitDefaultLogger() error {
|
||||
return InitLogger(&LogConfig{
|
||||
Level: os.Getenv("LOG_LEVEL"),
|
||||
Format: os.Getenv("LOG_FORMAT"),
|
||||
})
|
||||
}
|
||||
139
go-backend/internal/pkg/response.go
Normal file
139
go-backend/internal/pkg/response.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response represents a standard API response
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error *ErrorInfo `json:"error,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorInfo represents error information
|
||||
type ErrorInfo struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// Meta represents pagination metadata
|
||||
type Meta struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
}
|
||||
|
||||
// OK sends a successful response with data
|
||||
func OK(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// OKWithMeta sends a successful response with data and pagination
|
||||
func OKWithMeta(c *gin.Context, data interface{}, meta *Meta) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Meta: meta,
|
||||
})
|
||||
}
|
||||
|
||||
// Created sends a 201 Created response
|
||||
func Created(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// NoContent sends a 204 No Content response
|
||||
func NoContent(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// BadRequest sends a 400 Bad Request response
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusBadRequest, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: "BAD_REQUEST",
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Unauthorized sends a 401 Unauthorized response
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: "UNAUTHORIZED",
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Forbidden sends a 403 Forbidden response
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusForbidden, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: "FORBIDDEN",
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// NotFound sends a 404 Not Found response
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusNotFound, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: "NOT_FOUND",
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// InternalError sends a 500 Internal Server Error response
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusInternalServerError, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: "INTERNAL_ERROR",
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ValidationError sends a 422 Unprocessable Entity response
|
||||
func ValidationError(c *gin.Context, message string, details string) {
|
||||
c.JSON(http.StatusUnprocessableEntity, Response{
|
||||
Success: false,
|
||||
Error: &ErrorInfo{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: message,
|
||||
Details: details,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// CalculateTotalPages calculates total pages from count and page size
|
||||
func CalculateTotalPages(totalCount int64, pageSize int) int {
|
||||
if pageSize <= 0 {
|
||||
return 0
|
||||
}
|
||||
pages := int(totalCount) / pageSize
|
||||
if int(totalCount)%pageSize > 0 {
|
||||
pages++
|
||||
}
|
||||
return pages
|
||||
}
|
||||
Reference in New Issue
Block a user