diff --git a/.env b/.env new file mode 100644 index 0000000..b1baa65 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# Base Configuration +DATABASE_URL=postgres://postgres:password@localhost:5432/counter_db?sslmode=disable +PORT=8080 +LOG_LEVEL=info +GIN_MODE=debug diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..5d286aa --- /dev/null +++ b/.env.development @@ -0,0 +1,7 @@ +# Development Environment +ENVIRONMENT=development +DATABASE_URL=postgres://postgres:password@postgres:5432/counter_db?sslmode=disable +JWT_SECRET=dev-secret-key-change-in-production +PORT=8080 +GIN_MODE=debug +LOG_LEVEL=debug diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..3512ca5 --- /dev/null +++ b/.env.production @@ -0,0 +1,7 @@ +# Production Environment +ENVIRONMENT=production +DATABASE_URL=postgres://postgres:password@postgres:5432/counter_db?sslmode=disable +JWT_SECRET=super-secure-production-secret-change-this +PORT=8080 +GIN_MODE=release +LOG_LEVEL=warn diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..84bc4b7 --- /dev/null +++ b/.env.staging @@ -0,0 +1,7 @@ +# Staging Environment +ENVIRONMENT=staging +DATABASE_URL=postgres://postgres:password@postgres:5432/counter_db?sslmode=disable +JWT_SECRET=staging-secret-key-change-this +PORT=8080 +GIN_MODE=release +LOG_LEVEL=info diff --git a/.gitignore b/.gitignore index 89a34c3..f4f1ef1 100644 --- a/.gitignore +++ b/.gitignore @@ -17,12 +17,12 @@ # Go workspace file go.work -# Environment variables -.env -.env.local -.env.development.local -.env.test.local -.env.production.local +# Environment variables (keeping all env files in git as requested) +# .env +# .env.local +# .env.development.local +# .env.test.local +# .env.production.local # Node.js node_modules/ diff --git a/Dockerfile b/Dockerfile index 8f2e668..e727491 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,9 @@ COPY go.mod go.sum ./ # Download dependencies RUN go mod download -# Copy source code +# Copy source code and environment files COPY *.go ./ +COPY .env* ./ # Build the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . @@ -43,6 +44,9 @@ WORKDIR /root/ # Copy the Go binary from go-builder stage COPY --from=go-builder /app/main . +# Copy environment files from go-builder stage +COPY --from=go-builder /app/.env* ./ + # Copy the React build from frontend-builder stage COPY --from=frontend-builder /app/frontend/build ./frontend/build diff --git a/auth.go b/auth.go index cfc2743..a7396d6 100644 --- a/auth.go +++ b/auth.go @@ -17,7 +17,7 @@ import ( var jwtSecret []byte -// InitJWT initializes JWT secret +// InitJWT initializes JWT secret (legacy function) func InitJWT() { secret := os.Getenv("JWT_SECRET") if secret == "" { @@ -28,6 +28,11 @@ func InitJWT() { jwtSecret = []byte(secret) } +// InitJWTWithConfig initializes JWT secret with configuration +func InitJWTWithConfig(config *Config) { + jwtSecret = []byte(config.JWTSecret) +} + // generateRandomSecret generates a random secret for JWT func generateRandomSecret() string { b := make([]byte, 32) diff --git a/config.go b/config.go new file mode 100644 index 0000000..264591f --- /dev/null +++ b/config.go @@ -0,0 +1,216 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/joho/godotenv" +) + +type Environment string + +const ( + Development Environment = "development" + Staging Environment = "staging" + Production Environment = "production" +) + +type Config struct { + Environment Environment + DatabaseURL string + JWTSecret string + Port string + GinMode string + LogLevel string + Debug bool +} + +// LoadConfig loads configuration with proper environment file precedence +func LoadConfig() *Config { + // Load environment files in priority order + loadEnvironmentFiles() + + // Get environment + env := getEnvironment() + + // Load configuration + config := &Config{ + Environment: env, + DatabaseURL: getEnv("DATABASE_URL", getDefaultDatabaseURL(env)), + JWTSecret: getRequiredEnv("JWT_SECRET"), + Port: getEnv("PORT", "8080"), + GinMode: getGinMode(env), + LogLevel: getLogLevel(env), + Debug: env == Development, + } + + // Log configuration (without sensitive data) + logConfig(config) + + return config +} + +// loadEnvironmentFiles loads environment files in priority order +func loadEnvironmentFiles() { + // Get environment first (from system env or default) + env := getEnvironmentFromSystem() + + log.Printf("🔍 Detected environment: %s", env) + + // Define file loading order (later files override earlier ones) + files := []string{ + ".env", // Base configuration + fmt.Sprintf(".env.%s", env), // Environment-specific + } + + // Load files in order + for _, file := range files { + if _, err := os.Stat(file); err == nil { + if err := godotenv.Load(file); err != nil { + log.Printf("⚠️ Warning: Could not load %s: %v", file, err) + } else { + log.Printf("✅ Loaded: %s", file) + } + } else { + log.Printf("❌ Not found: %s", file) + } + } +} + +// getEnvironmentFromSystem gets environment from system variables only +func getEnvironmentFromSystem() string { + // Check if ENVIRONMENT is already set + if env := os.Getenv("ENVIRONMENT"); env != "" { + return strings.ToLower(env) + } + + // Fallback detection + if ginMode := os.Getenv("GIN_MODE"); ginMode == "release" { + return "production" + } + + return "development" +} + +// getEnvironment gets the current environment +func getEnvironment() Environment { + env := strings.ToLower(getEnv("ENVIRONMENT", "development")) + + switch env { + case "development", "dev": + return Development + case "staging", "stage": + return Staging + case "production", "prod": + return Production + default: + log.Printf("⚠️ Unknown environment '%s', defaulting to development", env) + return Development + } +} + +// getGinMode returns the appropriate Gin mode for the environment +func getGinMode(env Environment) string { + switch env { + case Production, Staging: + return "release" + case Development: + return "debug" + default: + return "debug" + } +} + +// getLogLevel returns the appropriate log level for the environment +func getLogLevel(env Environment) string { + switch env { + case Production: + return "warn" + case Staging: + return "info" + case Development: + return "debug" + default: + return "debug" + } +} + +// getDefaultDatabaseURL returns default database URL for environment +func getDefaultDatabaseURL(env Environment) string { + switch env { + case Development: + return "postgres://postgres:password@localhost:5432/counter_db?sslmode=disable" + case Staging: + return "postgres://postgres:password@postgres:5432/counter_db?sslmode=disable" + case Production: + return "postgres://postgres:password@postgres:5432/counter_db?sslmode=require" + default: + return "postgres://postgres:password@localhost:5432/counter_db?sslmode=disable" + } +} + +// getEnv gets environment variable with default value +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// getRequiredEnv gets required environment variable +func getRequiredEnv(key string) string { + value := os.Getenv(key) + if value == "" { + log.Fatalf("❌ Required environment variable %s is not set", key) + } + return value +} + +// logConfig logs configuration (without sensitive data) +func logConfig(config *Config) { + // Environment banner + log.Printf("") + log.Printf("╔══════════════════════════════════════════════════════════════╗") + log.Printf("║ COUNTER APPLICATION ║") + log.Printf("║ ║") + log.Printf("║ 🌍 ENVIRONMENT: %-15s ║", strings.ToUpper(string(config.Environment))) + log.Printf("║ 🚀 MODE: %-20s ║", config.GinMode) + log.Printf("║ 🔧 DEBUG: %-20s ║", fmt.Sprintf("%t", config.Debug)) + log.Printf("║ 📊 LOG LEVEL: %-15s ║", config.LogLevel) + log.Printf("║ 🌐 PORT: %-20s ║", config.Port) + log.Printf("║ ║") + log.Printf("║ 📁 Configuration Files Loaded: ║") + log.Printf("║ • .env (base configuration) ║") + log.Printf("║ • .env.%s (environment-specific) ║", config.Environment) + log.Printf("║ ║") + log.Printf("║ 🔐 Security: ║") + log.Printf("║ • Database: %s ║", maskDatabaseURL(config.DatabaseURL)) + log.Printf("║ • JWT Secret: %s ║", maskSecret(config.JWTSecret)) + log.Printf("║ ║") + log.Printf("╚══════════════════════════════════════════════════════════════╝") + log.Printf("") +} + +// maskDatabaseURL masks sensitive parts of database URL +func maskDatabaseURL(url string) string { + // Simple masking - replace password with *** + if strings.Contains(url, "://") { + parts := strings.Split(url, "://") + if len(parts) == 2 { + // Replace password in connection string + masked := strings.Replace(parts[1], ":", ":***@", 1) + return parts[0] + "://" + masked + } + } + return "***" +} + +// maskSecret masks JWT secret for logging +func maskSecret(secret string) string { + if len(secret) <= 8 { + return "***" + } + return secret[:4] + "***" + secret[len(secret)-4:] +} diff --git a/database.go b/database.go index d1dc564..b2369f0 100644 --- a/database.go +++ b/database.go @@ -11,7 +11,7 @@ import ( var db *sql.DB -// InitDB initializes the database connection +// InitDB initializes the database connection (legacy function) func InitDB() error { // Get database URL from environment variable dbURL := os.Getenv("DATABASE_URL") @@ -19,6 +19,16 @@ func InitDB() error { // Default for local development dbURL = "postgres://postgres:password@localhost:5432/counter_db?sslmode=disable" } + return initDBWithURL(dbURL) +} + +// InitDBWithConfig initializes the database connection with configuration +func InitDBWithConfig(config *Config) error { + return initDBWithURL(config.DatabaseURL) +} + +// initDBWithURL initializes the database connection with a specific URL +func initDBWithURL(dbURL string) error { var err error db, err = sql.Open("postgres", dbURL) @@ -31,7 +41,7 @@ func InitDB() error { return fmt.Errorf("failed to ping database: %w", err) } - log.Println("Database connection established") + log.Println("✅ Database connection established successfully") return nil } @@ -72,7 +82,7 @@ func CreateTables() error { } } - log.Println("Database tables created successfully") + log.Println("✅ Database tables created successfully") return nil } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..53e7a75 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,6 @@ +services: + app: + environment: + - ENVIRONMENT=production + env_file: + - .env.production diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..f57deea --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,6 @@ +services: + app: + environment: + - ENVIRONMENT=staging + env_file: + - .env.staging diff --git a/docker-compose.yml b/docker-compose.yml index f8d059f..57b8d60 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,9 +20,9 @@ services: ports: - "8080:8080" environment: - DATABASE_URL: postgres://postgres:password@postgres:5432/counter_db?sslmode=disable - JWT_SECRET: your-super-secret-jwt-key-change-in-production - GIN_MODE: release + - ENVIRONMENT=development + env_file: + - .env.development depends_on: postgres: condition: service_healthy diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..627e4db --- /dev/null +++ b/frontend/.env @@ -0,0 +1,4 @@ +# Base Frontend Configuration +REACT_APP_API_URL=http://localhost:8080/api/v1 +REACT_APP_ENVIRONMENT=development +REACT_APP_DEBUG=true diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..422eeb7 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,5 @@ +# Development Frontend Configuration +REACT_APP_API_URL=http://localhost:8080/api/v1 +REACT_APP_ENVIRONMENT=development +REACT_APP_DEBUG=true +REACT_APP_LOG_LEVEL=debug diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..768366e --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,5 @@ +# Production Frontend Configuration +REACT_APP_API_URL=/api/v1 +REACT_APP_ENVIRONMENT=production +REACT_APP_DEBUG=false +REACT_APP_LOG_LEVEL=warn diff --git a/frontend/.env.staging b/frontend/.env.staging new file mode 100644 index 0000000..9befba9 --- /dev/null +++ b/frontend/.env.staging @@ -0,0 +1,5 @@ +# Staging Frontend Configuration +REACT_APP_API_URL=https://staging-api.yourdomain.com/api/v1 +REACT_APP_ENVIRONMENT=staging +REACT_APP_DEBUG=false +REACT_APP_LOG_LEVEL=info diff --git a/frontend/src/config/environment.ts b/frontend/src/config/environment.ts new file mode 100644 index 0000000..3343440 --- /dev/null +++ b/frontend/src/config/environment.ts @@ -0,0 +1,102 @@ +// Environment configuration for React frontend +export type Environment = 'development' | 'staging' | 'production'; + +export interface AppConfig { + environment: Environment; + apiUrl: string; + debug: boolean; + logLevel: string; +} + +// Get environment from process.env +export const getEnvironment = (): Environment => { + const env = process.env.REACT_APP_ENVIRONMENT as Environment; + + // Fallback to NODE_ENV if REACT_APP_ENVIRONMENT is not set + if (!env) { + const nodeEnv = process.env.NODE_ENV; + switch (nodeEnv) { + case 'production': + return 'production'; + case 'development': + return 'development'; + default: + return 'development'; + } + } + + return env; +}; + +// Get API URL based on environment +export const getApiUrl = (): string => { + const apiUrl = process.env.REACT_APP_API_URL; + + if (apiUrl) { + return apiUrl; + } + + // Fallback based on environment + const env = getEnvironment(); + switch (env) { + case 'production': + return '/api/v1'; // Relative URL for production + case 'staging': + return 'https://staging-api.yourdomain.com/api/v1'; + case 'development': + default: + return 'http://localhost:8080/api/v1'; + } +}; + +// Get debug flag +export const isDebugMode = (): boolean => { + const debug = process.env.REACT_APP_DEBUG; + if (debug !== undefined) { + return debug === 'true'; + } + + // Fallback based on environment + return getEnvironment() === 'development'; +}; + +// Get log level +export const getLogLevel = (): string => { + return process.env.REACT_APP_LOG_LEVEL || 'info'; +}; + +// Get complete app configuration +export const getAppConfig = (): AppConfig => { + return { + environment: getEnvironment(), + apiUrl: getApiUrl(), + debug: isDebugMode(), + logLevel: getLogLevel(), + }; +}; + +// Environment checks +export const isDevelopment = (): boolean => { + return getEnvironment() === 'development'; +}; + +export const isStaging = (): boolean => { + return getEnvironment() === 'staging'; +}; + +export const isProduction = (): boolean => { + return getEnvironment() === 'production'; +}; + +// Log configuration (only in development) +export const logConfig = (): void => { + if (isDevelopment()) { + const config = getAppConfig(); + console.log('🚀 Frontend Configuration:', { + environment: config.environment, + apiUrl: config.apiUrl, + debug: config.debug, + logLevel: config.logLevel, + }); + } +}; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index a713a02..d5eeaa6 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -12,8 +12,11 @@ import { CounterStats, User, } from '../types'; +import { getApiUrl, logConfig } from '../config/environment'; -const API_BASE_URL = process.env.REACT_APP_API_URL || '/api/v1'; +// Initialize configuration +logConfig(); +const API_BASE_URL = getApiUrl(); // Create axios instance const api = axios.create({ diff --git a/main.go b/main.go index 187bb88..5c6d218 100644 --- a/main.go +++ b/main.go @@ -2,24 +2,23 @@ package main import ( "log" - "os" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/joho/godotenv" ) func main() { - // Load environment variables - if err := godotenv.Load(); err != nil { - log.Println("No .env file found, using system environment variables") - } + // Load configuration with environment file precedence + config := LoadConfig() - // Initialize JWT - InitJWT() + // Set Gin mode based on configuration + gin.SetMode(config.GinMode) - // Initialize database - if err := InitDB(); err != nil { + // Initialize JWT with configuration + InitJWTWithConfig(config) + + // Initialize database with configuration + if err := InitDBWithConfig(config); err != nil { log.Fatal("Failed to initialize database:", err) } @@ -28,21 +27,16 @@ func main() { log.Fatal("Failed to create tables:", err) } - // Set Gin mode - if os.Getenv("GIN_MODE") == "release" { - gin.SetMode(gin.ReleaseMode) - } - // Create Gin router r := gin.Default() // Configure CORS - config := cors.DefaultConfig() - config.AllowOrigins = []string{"http://localhost:3000", "http://localhost:5173"} // React dev servers - config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} - config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} - config.AllowCredentials = true - r.Use(cors.New(config)) + corsConfig := cors.DefaultConfig() + corsConfig.AllowOrigins = []string{"http://localhost:3000", "http://localhost:5173"} // React dev servers + corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} + corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} + corsConfig.AllowCredentials = true + r.Use(cors.New(corsConfig)) // Health check endpoint r.GET("/health", func(c *gin.Context) { @@ -83,11 +77,17 @@ func main() { }) // Start server - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } + port := config.Port + + log.Printf("") + log.Printf("🚀 Starting Counter Application Server...") + log.Printf(" 🌐 Listening on: http://localhost:%s", port) + log.Printf(" 📊 Health check: http://localhost:%s/health", port) + log.Printf(" 🔗 API endpoint: http://localhost:%s/api/v1", port) + log.Printf(" 🎨 Frontend: http://localhost:%s/", port) + log.Printf("") + log.Printf("✅ Server is ready and accepting connections!") + log.Printf("") - log.Printf("Server starting on port %s", port) log.Fatal(r.Run(":" + port)) } diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..7c2574d --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Development environment script + +echo "🚀 Starting Counter app in development mode..." + +# Set environment +export ENVIRONMENT=development + +# Start with development configuration +docker-compose -f docker-compose.yml up --build diff --git a/scripts/prod.sh b/scripts/prod.sh new file mode 100755 index 0000000..323fe62 --- /dev/null +++ b/scripts/prod.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Production environment script + +echo "🚀 Starting Counter app in production mode..." + +# Set environment +export ENVIRONMENT=production + +# Start with production configuration +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build -d diff --git a/scripts/staging.sh b/scripts/staging.sh new file mode 100755 index 0000000..08cd9de --- /dev/null +++ b/scripts/staging.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Staging environment script + +echo "🚀 Starting Counter app in staging mode..." + +# Set environment +export ENVIRONMENT=staging + +# Start with staging configuration +docker-compose -f docker-compose.yml -f docker-compose.staging.yml up --build -d