chore: clean .gitignore cache
This commit is contained in:
@@ -1,294 +0,0 @@
|
||||
// Package main is the entry point for the Charon backend API.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/charon/backend/internal/api/middleware"
|
||||
"github.com/Wikid82/charon/backend/internal/api/routes"
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/database"
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/server"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/Wikid82/charon/backend/internal/version"
|
||||
_ "github.com/Wikid82/charon/backend/pkg/dnsprovider/builtin" // Register built-in DNS providers
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// parsePluginSignatures reads the CHARON_PLUGIN_SIGNATURES environment variable
|
||||
// and returns the parsed signature allowlist for plugin verification.
|
||||
//
|
||||
// Modes:
|
||||
// - nil return (permissive): Env var unset/empty — all plugins allowed
|
||||
// - empty map (strict): Env var set to "{}" — no external plugins allowed
|
||||
// - populated map: Only plugins with matching signatures are allowed
|
||||
func parsePluginSignatures() map[string]string {
|
||||
envVal := os.Getenv("CHARON_PLUGIN_SIGNATURES")
|
||||
if envVal == "" {
|
||||
logger.Log().Info("Plugin signature verification: PERMISSIVE mode (CHARON_PLUGIN_SIGNATURES not set)")
|
||||
return nil
|
||||
}
|
||||
|
||||
var signatures map[string]string
|
||||
if err := json.Unmarshal([]byte(envVal), &signatures); err != nil {
|
||||
logger.Log().WithError(err).Error("Failed to parse CHARON_PLUGIN_SIGNATURES JSON — falling back to permissive mode")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate all signatures have sha256: prefix
|
||||
for name, sig := range signatures {
|
||||
if !strings.HasPrefix(sig, "sha256:") {
|
||||
logger.Log().Errorf("Invalid signature for plugin %q: must have sha256: prefix — falling back to permissive mode", name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(signatures) == 0 {
|
||||
logger.Log().Info("Plugin signature verification: STRICT mode (empty allowlist — no external plugins permitted)")
|
||||
} else {
|
||||
logger.Log().Infof("Plugin signature verification: STRICT mode (%d plugin(s) in allowlist)", len(signatures))
|
||||
}
|
||||
|
||||
return signatures
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Setup logging with rotation
|
||||
logDir := "/app/data/logs"
|
||||
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||
// Fallback to local directory if /app/data fails (e.g. local dev)
|
||||
logDir = "data/logs"
|
||||
_ = os.MkdirAll(logDir, 0o755)
|
||||
}
|
||||
|
||||
logFile := filepath.Join(logDir, "charon.log")
|
||||
rotator := &lumberjack.Logger{
|
||||
Filename: logFile,
|
||||
MaxSize: 10, // megabytes
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28, // days
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
// Ensure legacy cpmp.log exists as symlink for compatibility (cpmp is a legacy name for Charon)
|
||||
legacyLog := filepath.Join(logDir, "cpmp.log")
|
||||
if _, err := os.Lstat(legacyLog); os.IsNotExist(err) {
|
||||
_ = os.Symlink(logFile, legacyLog) // ignore errors
|
||||
}
|
||||
|
||||
// Log to both stdout and file
|
||||
mw := io.MultiWriter(os.Stdout, rotator)
|
||||
log.SetOutput(mw)
|
||||
gin.DefaultWriter = mw
|
||||
// Initialize a basic logger so CLI and early code can log.
|
||||
logger.Init(false, mw)
|
||||
|
||||
// Handle CLI commands
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "migrate":
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Connect(cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("connect database: %v", err)
|
||||
}
|
||||
|
||||
logger.Log().Info("Running database migrations for all models...")
|
||||
if err := db.AutoMigrate(
|
||||
// Core models
|
||||
&models.ProxyHost{},
|
||||
&models.Location{},
|
||||
&models.CaddyConfig{},
|
||||
&models.RemoteServer{},
|
||||
&models.SSLCertificate{},
|
||||
&models.AccessList{},
|
||||
&models.SecurityHeaderProfile{},
|
||||
&models.User{},
|
||||
&models.Setting{},
|
||||
&models.ImportSession{},
|
||||
&models.Notification{},
|
||||
&models.NotificationProvider{},
|
||||
&models.NotificationTemplate{},
|
||||
&models.NotificationConfig{},
|
||||
&models.UptimeMonitor{},
|
||||
&models.UptimeHeartbeat{},
|
||||
&models.UptimeHost{},
|
||||
&models.UptimeNotificationEvent{},
|
||||
&models.Domain{},
|
||||
&models.UserPermittedHost{},
|
||||
// Security models
|
||||
&models.SecurityConfig{},
|
||||
&models.SecurityDecision{},
|
||||
&models.SecurityAudit{},
|
||||
&models.SecurityRuleSet{},
|
||||
&models.CrowdsecPresetEvent{},
|
||||
&models.CrowdsecConsoleEnrollment{},
|
||||
// DNS Provider models (Issue #21)
|
||||
&models.DNSProvider{},
|
||||
&models.DNSProviderCredential{},
|
||||
// Plugin model (Phase 5)
|
||||
&models.Plugin{},
|
||||
); err != nil {
|
||||
log.Fatalf("migration failed: %v", err)
|
||||
}
|
||||
|
||||
logger.Log().Info("Migration completed successfully")
|
||||
return
|
||||
|
||||
case "reset-password":
|
||||
if len(os.Args) != 4 {
|
||||
log.Fatalf("Usage: %s reset-password <email> <new-password>", os.Args[0])
|
||||
}
|
||||
email := os.Args[2]
|
||||
newPassword := os.Args[3]
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Connect(cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("connect database: %v", err)
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
log.Fatalf("user not found: %v", err)
|
||||
}
|
||||
|
||||
if err := user.SetPassword(newPassword); err != nil {
|
||||
log.Fatalf("failed to hash password: %v", err)
|
||||
}
|
||||
|
||||
// Unlock account if locked
|
||||
user.LockedUntil = nil
|
||||
user.FailedLoginAttempts = 0
|
||||
|
||||
if err := db.Save(&user).Error; err != nil {
|
||||
log.Fatalf("failed to save user: %v", err)
|
||||
}
|
||||
|
||||
logger.Log().Infof("Password updated successfully for user %s", email)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Infof("starting %s backend on version %s", version.Name, version.Full())
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Connect(cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("connect database: %v", err)
|
||||
}
|
||||
|
||||
// Note: All database migrations are centralized in routes.Register()
|
||||
// This ensures migrations run exactly once and in the correct order.
|
||||
// DO NOT add AutoMigrate calls here - they cause "duplicate column" errors.
|
||||
|
||||
// Reconcile CrowdSec state after migrations, before HTTP server starts
|
||||
// This ensures CrowdSec is running if user preference was to have it enabled
|
||||
crowdsecBinPath := os.Getenv("CHARON_CROWDSEC_BIN")
|
||||
if crowdsecBinPath == "" {
|
||||
crowdsecBinPath = "/usr/local/bin/crowdsec"
|
||||
}
|
||||
crowdsecDataDir := os.Getenv("CHARON_CROWDSEC_DATA")
|
||||
if crowdsecDataDir == "" {
|
||||
crowdsecDataDir = "/app/data/crowdsec"
|
||||
}
|
||||
|
||||
crowdsecExec := handlers.NewDefaultCrowdsecExecutor()
|
||||
services.ReconcileCrowdSecOnStartup(db, crowdsecExec, crowdsecBinPath, crowdsecDataDir)
|
||||
|
||||
// Initialize plugin loader and load external DNS provider plugins (Phase 5)
|
||||
logger.Log().Info("Initializing DNS provider plugin system...")
|
||||
pluginDir := os.Getenv("CHARON_PLUGINS_DIR")
|
||||
if pluginDir == "" {
|
||||
pluginDir = "/app/plugins"
|
||||
}
|
||||
pluginLoader := services.NewPluginLoaderService(db, pluginDir, parsePluginSignatures())
|
||||
if err := pluginLoader.LoadAllPlugins(); err != nil {
|
||||
logger.Log().WithError(err).Warn("Failed to load external DNS provider plugins")
|
||||
}
|
||||
logger.Log().Info("Plugin system initialized")
|
||||
|
||||
router := server.NewRouter(cfg.FrontendDir)
|
||||
// Initialize structured logger with same writer as stdlib log so both capture logs
|
||||
logger.Init(cfg.Debug, mw)
|
||||
// Request ID middleware must run before recovery so the recover logs include the request id
|
||||
router.Use(middleware.RequestID())
|
||||
// Log requests with request-scoped logger
|
||||
router.Use(middleware.RequestLogger())
|
||||
// Attach a recovery middleware that logs stack traces when debug is enabled
|
||||
router.Use(middleware.Recovery(cfg.Debug))
|
||||
|
||||
// Pass config to routes for auth service and certificate service
|
||||
if err := routes.Register(router, db, cfg); err != nil {
|
||||
log.Fatalf("register routes: %v", err)
|
||||
}
|
||||
|
||||
// Register import handler with config dependencies
|
||||
routes.RegisterImportHandler(router, db, cfg.CaddyBinary, cfg.ImportDir, cfg.ImportCaddyfile)
|
||||
|
||||
// Check for mounted Caddyfile on startup
|
||||
if err := handlers.CheckMountedImport(db, cfg.ImportCaddyfile, cfg.CaddyBinary, cfg.ImportDir); err != nil {
|
||||
logger.Log().WithError(err).Warn("WARNING: failed to process mounted Caddyfile")
|
||||
}
|
||||
|
||||
// Initialize emergency server (Tier 2 break glass)
|
||||
emergencyServer := server.NewEmergencyServer(db, cfg.Emergency)
|
||||
if err := emergencyServer.Start(); err != nil {
|
||||
logger.Log().WithError(err).Fatal("Failed to start emergency server")
|
||||
}
|
||||
|
||||
// Setup graceful shutdown
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Start main HTTP server in goroutine
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%s", cfg.HTTPPort)
|
||||
logger.Log().Infof("starting %s backend on %s", version.Name, addr)
|
||||
|
||||
if err := router.Run(addr); err != nil {
|
||||
logger.Log().WithError(err).Fatal("server error")
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal
|
||||
sig := <-quit
|
||||
logger.Log().Infof("Received signal %v, initiating graceful shutdown...", sig)
|
||||
|
||||
// Graceful shutdown with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Stop emergency server
|
||||
if err := emergencyServer.Stop(ctx); err != nil {
|
||||
logger.Log().WithError(err).Error("Emergency server shutdown error")
|
||||
}
|
||||
|
||||
logger.Log().Info("Server shutdown complete")
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/database"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestResetPasswordCommand_Succeeds(t *testing.T) {
|
||||
if os.Getenv("CHARON_TEST_RUN_MAIN") == "1" {
|
||||
// Child process: emulate CLI args and run main().
|
||||
email := os.Getenv("CHARON_TEST_EMAIL")
|
||||
newPassword := os.Getenv("CHARON_TEST_NEW_PASSWORD")
|
||||
os.Args = []string{"charon", "reset-password", email, newPassword}
|
||||
main()
|
||||
return
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
dbPath := filepath.Join(tmp, "data", "test.db")
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir db dir: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Connect(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("connect db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil {
|
||||
t.Fatalf("automigrate: %v", err)
|
||||
}
|
||||
|
||||
email := "user@example.com"
|
||||
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: "admin", Enabled: true}
|
||||
user.PasswordHash = "$2a$10$example_hashed_password"
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatalf("seed user: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestResetPasswordCommand_Succeeds") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe
|
||||
cmd.Dir = tmp
|
||||
cmd.Env = append(os.Environ(),
|
||||
"CHARON_TEST_RUN_MAIN=1",
|
||||
"CHARON_TEST_EMAIL="+email,
|
||||
"CHARON_TEST_NEW_PASSWORD=new-password",
|
||||
"CHARON_DB_PATH="+dbPath,
|
||||
"CHARON_CADDY_CONFIG_DIR="+filepath.Join(tmp, "caddy"),
|
||||
"CHARON_IMPORT_DIR="+filepath.Join(tmp, "imports"),
|
||||
)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("expected exit 0; err=%v; output=%s", err, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateCommand_Succeeds(t *testing.T) {
|
||||
if os.Getenv("CHARON_TEST_RUN_MAIN") == "1" {
|
||||
// Child process: emulate CLI args and run main().
|
||||
os.Args = []string{"charon", "migrate"}
|
||||
main()
|
||||
return
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
dbPath := filepath.Join(tmp, "data", "test.db")
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir db dir: %v", err)
|
||||
}
|
||||
|
||||
// Create database without security tables
|
||||
db, err := database.Connect(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("connect db: %v", err)
|
||||
}
|
||||
// Only migrate User table to simulate old database
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil {
|
||||
t.Fatalf("automigrate user: %v", err)
|
||||
}
|
||||
|
||||
// Verify security tables don't exist
|
||||
if db.Migrator().HasTable(&models.SecurityConfig{}) {
|
||||
t.Fatal("SecurityConfig table should not exist yet")
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestMigrateCommand_Succeeds") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe
|
||||
cmd.Dir = tmp
|
||||
cmd.Env = append(os.Environ(),
|
||||
"CHARON_TEST_RUN_MAIN=1",
|
||||
"CHARON_DB_PATH="+dbPath,
|
||||
"CHARON_CADDY_CONFIG_DIR="+filepath.Join(tmp, "caddy"),
|
||||
"CHARON_IMPORT_DIR="+filepath.Join(tmp, "imports"),
|
||||
)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("expected exit 0; err=%v; output=%s", err, string(out))
|
||||
}
|
||||
|
||||
// Reconnect and verify security tables were created
|
||||
db2, err := database.Connect(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("reconnect db: %v", err)
|
||||
}
|
||||
|
||||
securityModels := []any{
|
||||
&models.SecurityConfig{},
|
||||
&models.SecurityDecision{},
|
||||
&models.SecurityAudit{},
|
||||
&models.SecurityRuleSet{},
|
||||
&models.CrowdsecPresetEvent{},
|
||||
&models.CrowdsecConsoleEnrollment{},
|
||||
}
|
||||
|
||||
for _, model := range securityModels {
|
||||
if !db2.Migrator().HasTable(model) {
|
||||
t.Errorf("Table for %T was not created by migrate command", model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartupVerification_MissingTables(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
dbPath := filepath.Join(tmp, "data", "test.db")
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir db dir: %v", err)
|
||||
}
|
||||
|
||||
// Create database without security tables
|
||||
db, err := database.Connect(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("connect db: %v", err)
|
||||
}
|
||||
// Only migrate User table to simulate old database
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil {
|
||||
t.Fatalf("automigrate user: %v", err)
|
||||
}
|
||||
|
||||
// Verify security tables don't exist
|
||||
if db.Migrator().HasTable(&models.SecurityConfig{}) {
|
||||
t.Fatal("SecurityConfig table should not exist yet")
|
||||
}
|
||||
|
||||
// Close and reopen to simulate startup scenario
|
||||
sqlDB, _ := db.DB()
|
||||
_ = sqlDB.Close()
|
||||
|
||||
db, err = database.Connect(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("reconnect db: %v", err)
|
||||
}
|
||||
|
||||
// Simulate startup verification logic from main.go
|
||||
securityModels := []any{
|
||||
&models.SecurityConfig{},
|
||||
&models.SecurityDecision{},
|
||||
&models.SecurityAudit{},
|
||||
&models.SecurityRuleSet{},
|
||||
&models.CrowdsecPresetEvent{},
|
||||
&models.CrowdsecConsoleEnrollment{},
|
||||
}
|
||||
|
||||
missingTables := false
|
||||
for _, model := range securityModels {
|
||||
if !db.Migrator().HasTable(model) {
|
||||
missingTables = true
|
||||
t.Logf("Missing table for model %T", model)
|
||||
}
|
||||
}
|
||||
|
||||
if !missingTables {
|
||||
t.Fatal("Expected to find missing tables but all were present")
|
||||
}
|
||||
|
||||
// Run auto-migration (simulating startup verification logic)
|
||||
if err := db.AutoMigrate(securityModels...); err != nil {
|
||||
t.Fatalf("failed to migrate security tables: %v", err)
|
||||
}
|
||||
|
||||
// Verify all tables now exist
|
||||
for _, model := range securityModels {
|
||||
if !db.Migrator().HasTable(model) {
|
||||
t.Errorf("Table for %T was not created by auto-migration", model)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/logger"
|
||||
"github.com/Wikid82/charon/backend/internal/util"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
gormlogger "gorm.io/gorm/logger"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
)
|
||||
|
||||
func logSeedResult(entry *logrus.Entry, result *gorm.DB, errorMessage string, logCreated func(), existsMessage string) {
|
||||
switch {
|
||||
case result.Error != nil:
|
||||
entry.WithError(result.Error).Error(errorMessage)
|
||||
case result.RowsAffected > 0:
|
||||
logCreated()
|
||||
default:
|
||||
entry.Info(existsMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Connect to database
|
||||
// Initialize simple logger to stdout
|
||||
mw := io.MultiWriter(os.Stdout)
|
||||
logger.Init(false, mw)
|
||||
|
||||
// Configure GORM logger to ignore "record not found" errors
|
||||
// These are expected during seed operations when checking if records exist
|
||||
gormLog := gormlogger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
gormlogger.Config{
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
LogLevel: gormlogger.Warn,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: false,
|
||||
},
|
||||
)
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("./data/charon.db"), &gorm.Config{
|
||||
Logger: gormLog,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Log().WithError(err).Fatal("Failed to connect to database")
|
||||
}
|
||||
|
||||
// Auto migrate
|
||||
if err := db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.ProxyHost{},
|
||||
&models.CaddyConfig{},
|
||||
&models.RemoteServer{},
|
||||
&models.SSLCertificate{},
|
||||
&models.AccessList{},
|
||||
&models.Setting{},
|
||||
&models.ImportSession{},
|
||||
); err != nil {
|
||||
logger.Log().WithError(err).Fatal("Failed to migrate database")
|
||||
}
|
||||
|
||||
logger.Log().Info("✓ Database migrated successfully")
|
||||
|
||||
// Seed Remote Servers
|
||||
remoteServers := []models.RemoteServer{
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Local Docker Registry",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 5000,
|
||||
Scheme: "http",
|
||||
Description: "Local Docker container registry",
|
||||
Enabled: true,
|
||||
Reachable: false,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Development API Server",
|
||||
Provider: "generic",
|
||||
Host: "192.168.1.100",
|
||||
Port: 8080,
|
||||
Scheme: "http",
|
||||
Description: "Main development API backend",
|
||||
Enabled: true,
|
||||
Reachable: false,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Staging Web App",
|
||||
Provider: "vm",
|
||||
Host: "staging.internal",
|
||||
Port: 3000,
|
||||
Scheme: "http",
|
||||
Description: "Staging environment web application",
|
||||
Enabled: true,
|
||||
Reachable: false,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Database Admin",
|
||||
Provider: "docker",
|
||||
Host: "localhost",
|
||||
Port: 8081,
|
||||
Scheme: "http",
|
||||
Description: "PhpMyAdmin or similar DB management tool",
|
||||
Enabled: false,
|
||||
Reachable: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, server := range remoteServers {
|
||||
result := db.Where("host = ? AND port = ?", server.Host, server.Port).FirstOrCreate(&server)
|
||||
logEntry := logger.Log().WithField("server", server.Name)
|
||||
logSeedResult(
|
||||
logEntry,
|
||||
result,
|
||||
"Failed to seed remote server",
|
||||
func() {
|
||||
logEntry.Infof("✓ Created remote server: %s (%s:%d)", server.Name, server.Host, server.Port)
|
||||
},
|
||||
"Remote server already exists",
|
||||
)
|
||||
}
|
||||
|
||||
// Seed Proxy Hosts
|
||||
proxyHosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Development App",
|
||||
DomainNames: "app.local.dev",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 3000,
|
||||
SSLForced: false,
|
||||
WebsocketSupport: true,
|
||||
HSTSEnabled: false,
|
||||
BlockExploits: true,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "API Server",
|
||||
DomainNames: "api.local.dev",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "192.168.1.100",
|
||||
ForwardPort: 8080,
|
||||
SSLForced: false,
|
||||
WebsocketSupport: false,
|
||||
HSTSEnabled: false,
|
||||
BlockExploits: true,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Docker Registry",
|
||||
DomainNames: "docker.local.dev",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 5000,
|
||||
SSLForced: false,
|
||||
WebsocketSupport: false,
|
||||
HSTSEnabled: false,
|
||||
BlockExploits: true,
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, host := range proxyHosts {
|
||||
result := db.Where("domain_names = ?", host.DomainNames).FirstOrCreate(&host)
|
||||
logEntry := logger.Log().WithField("host", util.SanitizeForLog(host.DomainNames))
|
||||
logSeedResult(
|
||||
logEntry,
|
||||
result,
|
||||
"Failed to seed proxy host",
|
||||
func() {
|
||||
logEntry.Infof("✓ Created proxy host: %s -> %s://%s:%d", host.DomainNames, host.ForwardScheme, host.ForwardHost, host.ForwardPort)
|
||||
},
|
||||
"Proxy host already exists",
|
||||
)
|
||||
}
|
||||
|
||||
// Seed Settings
|
||||
settings := []models.Setting{
|
||||
{
|
||||
Key: "app_name",
|
||||
Value: "Charon",
|
||||
Type: "string",
|
||||
Category: "general",
|
||||
},
|
||||
{
|
||||
Key: "default_scheme",
|
||||
Value: "http",
|
||||
Type: "string",
|
||||
Category: "general",
|
||||
},
|
||||
{
|
||||
Key: "enable_ssl_by_default",
|
||||
Value: "false",
|
||||
Type: "bool",
|
||||
Category: "security",
|
||||
},
|
||||
}
|
||||
|
||||
for _, setting := range settings {
|
||||
result := db.Where("key = ?", setting.Key).FirstOrCreate(&setting)
|
||||
logEntry := logger.Log().WithField("setting", setting.Key)
|
||||
logSeedResult(
|
||||
logEntry,
|
||||
result,
|
||||
"Failed to seed setting",
|
||||
func() {
|
||||
logEntry.Infof("✓ Created setting: %s = %s", setting.Key, setting.Value)
|
||||
},
|
||||
"Setting already exists",
|
||||
)
|
||||
}
|
||||
|
||||
// Seed default admin user (for future authentication)
|
||||
defaultAdminEmail := os.Getenv("CHARON_DEFAULT_ADMIN_EMAIL")
|
||||
if defaultAdminEmail == "" {
|
||||
defaultAdminEmail = "admin@localhost"
|
||||
}
|
||||
defaultAdminPassword := os.Getenv("CHARON_DEFAULT_ADMIN_PASSWORD")
|
||||
// If a default password is not specified, leave the hashed placeholder (non-loginable)
|
||||
forceAdmin := os.Getenv("CHARON_FORCE_DEFAULT_ADMIN") == "1"
|
||||
|
||||
user := models.User{
|
||||
UUID: uuid.NewString(),
|
||||
Email: defaultAdminEmail,
|
||||
Name: "Administrator",
|
||||
Role: "admin",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// If a default password provided, use SetPassword to generate a proper bcrypt hash
|
||||
if defaultAdminPassword != "" {
|
||||
if err := user.SetPassword(defaultAdminPassword); err != nil {
|
||||
logger.Log().WithError(err).Error("Failed to hash default admin password")
|
||||
}
|
||||
} else {
|
||||
// Keep previous behavior: using example hashed password (not valid)
|
||||
user.PasswordHash = "$2a$10$example_hashed_password"
|
||||
}
|
||||
|
||||
var existing models.User
|
||||
// Find by email first - use Take instead of First to avoid GORM's "record not found" log
|
||||
result := db.Where("email = ?", user.Email).Take(&existing)
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// Not found -> create new user
|
||||
createResult := db.Create(&user)
|
||||
if createResult.Error != nil {
|
||||
logger.Log().WithError(createResult.Error).Error("Failed to seed user")
|
||||
} else if createResult.RowsAffected > 0 {
|
||||
logger.Log().WithField("user", user.Email).Infof("✓ Created default user: %s", user.Email)
|
||||
}
|
||||
} else {
|
||||
// Unexpected error
|
||||
logger.Log().WithError(result.Error).Error("Failed to query for existing user")
|
||||
}
|
||||
} else {
|
||||
// Found existing user - optionally update if forced
|
||||
if forceAdmin {
|
||||
existing.Email = user.Email
|
||||
existing.Name = user.Name
|
||||
existing.Role = user.Role
|
||||
existing.Enabled = user.Enabled
|
||||
if defaultAdminPassword != "" {
|
||||
if err := existing.SetPassword(defaultAdminPassword); err == nil {
|
||||
db.Save(&existing)
|
||||
logger.Log().WithField("user", existing.Email).Infof("✓ Updated existing admin user password for: %s", existing.Email)
|
||||
} else {
|
||||
logger.Log().WithError(err).Error("Failed to update existing admin password")
|
||||
}
|
||||
} else {
|
||||
db.Save(&existing)
|
||||
logger.Log().WithField("user", existing.Email).Info("User already exists")
|
||||
}
|
||||
} else {
|
||||
logger.Log().WithField("user", existing.Email).Info("User already exists")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log().Info("\n✓ Database seeding completed successfully!")
|
||||
logger.Log().Info(" You can now start the application and see sample data.")
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSeedMain_CreatesDatabaseFile(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd: %v", err)
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(wd) })
|
||||
|
||||
if err := os.MkdirAll("data", 0o755); err != nil {
|
||||
t.Fatalf("mkdir data: %v", err)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
dbPath := filepath.Join("data", "charon.db")
|
||||
info, err := os.Stat(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("expected db file to exist at %s: %v", dbPath, err)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
t.Fatalf("expected db file to be non-empty")
|
||||
}
|
||||
}
|
||||
package main
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} } t.Fatalf("expected db file to be non-empty") if info.Size() == 0 { } t.Fatalf("expected db file to exist at %s: %v", dbPath, err) if err != nil { info, err := os.Stat(dbPath) dbPath := filepath.Join("data", "charon.db") main() } t.Fatalf("mkdir data: %v", err) if err := os.MkdirAll("data", 0o755); err != nil { t.Cleanup(func() { _ = os.Chdir(wd) }) } t.Fatalf("chdir: %v", err) if err := os.Chdir(tmp); err != nil { tmp := t.TempDir() } t.Fatalf("getwd: %v", err) if err != nil { wd, err := os.Getwd() t.Parallel()func TestSeedMain_CreatesDatabaseFile(t *testing.T) {) "testing" "path/filepath" "os"
|
||||
@@ -1,31 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSeedMain_Smoke(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd: %v", err)
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(wd) })
|
||||
|
||||
if err := os.MkdirAll("data", 0o755); err != nil {
|
||||
t.Fatalf("mkdir data: %v", err)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
p := filepath.Join("data", "charon.db")
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
t.Fatalf("expected db file to exist: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user