add env files
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

This commit is contained in:
aovantsev
2025-10-03 13:33:22 +03:00
parent 324e861218
commit d0f14dfca2
22 changed files with 468 additions and 41 deletions

5
.env Normal file
View File

@@ -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

7
.env.development Normal file
View File

@@ -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

7
.env.production Normal file
View File

@@ -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

7
.env.staging Normal file
View File

@@ -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

12
.gitignore vendored
View File

@@ -17,12 +17,12 @@
# Go workspace file # Go workspace file
go.work go.work
# Environment variables # Environment variables (keeping all env files in git as requested)
.env # .env
.env.local # .env.local
.env.development.local # .env.development.local
.env.test.local # .env.test.local
.env.production.local # .env.production.local
# Node.js # Node.js
node_modules/ node_modules/

View File

@@ -9,8 +9,9 @@ COPY go.mod go.sum ./
# Download dependencies # Download dependencies
RUN go mod download RUN go mod download
# Copy source code # Copy source code and environment files
COPY *.go ./ COPY *.go ./
COPY .env* ./
# Build the application # Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . 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 the Go binary from go-builder stage
COPY --from=go-builder /app/main . 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 the React build from frontend-builder stage
COPY --from=frontend-builder /app/frontend/build ./frontend/build COPY --from=frontend-builder /app/frontend/build ./frontend/build

View File

@@ -17,7 +17,7 @@ import (
var jwtSecret []byte var jwtSecret []byte
// InitJWT initializes JWT secret // InitJWT initializes JWT secret (legacy function)
func InitJWT() { func InitJWT() {
secret := os.Getenv("JWT_SECRET") secret := os.Getenv("JWT_SECRET")
if secret == "" { if secret == "" {
@@ -28,6 +28,11 @@ func InitJWT() {
jwtSecret = []byte(secret) jwtSecret = []byte(secret)
} }
// InitJWTWithConfig initializes JWT secret with configuration
func InitJWTWithConfig(config *Config) {
jwtSecret = []byte(config.JWTSecret)
}
// generateRandomSecret generates a random secret for JWT // generateRandomSecret generates a random secret for JWT
func generateRandomSecret() string { func generateRandomSecret() string {
b := make([]byte, 32) b := make([]byte, 32)

216
config.go Normal file
View File

@@ -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:]
}

View File

@@ -11,7 +11,7 @@ import (
var db *sql.DB var db *sql.DB
// InitDB initializes the database connection // InitDB initializes the database connection (legacy function)
func InitDB() error { func InitDB() error {
// Get database URL from environment variable // Get database URL from environment variable
dbURL := os.Getenv("DATABASE_URL") dbURL := os.Getenv("DATABASE_URL")
@@ -19,6 +19,16 @@ func InitDB() error {
// Default for local development // Default for local development
dbURL = "postgres://postgres:password@localhost:5432/counter_db?sslmode=disable" 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 var err error
db, err = sql.Open("postgres", dbURL) db, err = sql.Open("postgres", dbURL)
@@ -31,7 +41,7 @@ func InitDB() error {
return fmt.Errorf("failed to ping database: %w", err) return fmt.Errorf("failed to ping database: %w", err)
} }
log.Println("Database connection established") log.Println("Database connection established successfully")
return nil return nil
} }
@@ -72,7 +82,7 @@ func CreateTables() error {
} }
} }
log.Println("Database tables created successfully") log.Println("Database tables created successfully")
return nil return nil
} }

6
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,6 @@
services:
app:
environment:
- ENVIRONMENT=production
env_file:
- .env.production

View File

@@ -0,0 +1,6 @@
services:
app:
environment:
- ENVIRONMENT=staging
env_file:
- .env.staging

View File

@@ -20,9 +20,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
environment: environment:
DATABASE_URL: postgres://postgres:password@postgres:5432/counter_db?sslmode=disable - ENVIRONMENT=development
JWT_SECRET: your-super-secret-jwt-key-change-in-production env_file:
GIN_MODE: release - .env.development
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

4
frontend/.env Normal file
View File

@@ -0,0 +1,4 @@
# Base Frontend Configuration
REACT_APP_API_URL=http://localhost:8080/api/v1
REACT_APP_ENVIRONMENT=development
REACT_APP_DEBUG=true

View File

@@ -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

5
frontend/.env.production Normal file
View File

@@ -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

5
frontend/.env.staging Normal file
View File

@@ -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

View File

@@ -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,
});
}
};

View File

@@ -12,8 +12,11 @@ import {
CounterStats, CounterStats,
User, User,
} from '../types'; } 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 // Create axios instance
const api = axios.create({ const api = axios.create({

52
main.go
View File

@@ -2,24 +2,23 @@ package main
import ( import (
"log" "log"
"os"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/joho/godotenv"
) )
func main() { func main() {
// Load environment variables // Load configuration with environment file precedence
if err := godotenv.Load(); err != nil { config := LoadConfig()
log.Println("No .env file found, using system environment variables")
}
// Initialize JWT // Set Gin mode based on configuration
InitJWT() gin.SetMode(config.GinMode)
// Initialize database // Initialize JWT with configuration
if err := InitDB(); err != nil { InitJWTWithConfig(config)
// Initialize database with configuration
if err := InitDBWithConfig(config); err != nil {
log.Fatal("Failed to initialize database:", err) log.Fatal("Failed to initialize database:", err)
} }
@@ -28,21 +27,16 @@ func main() {
log.Fatal("Failed to create tables:", err) log.Fatal("Failed to create tables:", err)
} }
// Set Gin mode
if os.Getenv("GIN_MODE") == "release" {
gin.SetMode(gin.ReleaseMode)
}
// Create Gin router // Create Gin router
r := gin.Default() r := gin.Default()
// Configure CORS // Configure CORS
config := cors.DefaultConfig() corsConfig := cors.DefaultConfig()
config.AllowOrigins = []string{"http://localhost:3000", "http://localhost:5173"} // React dev servers corsConfig.AllowOrigins = []string{"http://localhost:3000", "http://localhost:5173"} // React dev servers
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"}
config.AllowCredentials = true corsConfig.AllowCredentials = true
r.Use(cors.New(config)) r.Use(cors.New(corsConfig))
// Health check endpoint // Health check endpoint
r.GET("/health", func(c *gin.Context) { r.GET("/health", func(c *gin.Context) {
@@ -83,11 +77,17 @@ func main() {
}) })
// Start server // Start server
port := os.Getenv("PORT") port := config.Port
if port == "" {
port = "8080" 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)) log.Fatal(r.Run(":" + port))
} }

10
scripts/dev.sh Executable file
View File

@@ -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

10
scripts/prod.sh Executable file
View File

@@ -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

10
scripts/staging.sh Executable file
View File

@@ -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