401 lines
13 KiB
Go
401 lines
13 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/creativenoz/aurganize-v62/backend/internal/config"
|
||
"github.com/creativenoz/aurganize-v62/backend/internal/handlers"
|
||
"github.com/creativenoz/aurganize-v62/backend/internal/middleware"
|
||
"github.com/creativenoz/aurganize-v62/backend/internal/repositories"
|
||
"github.com/creativenoz/aurganize-v62/backend/internal/routes"
|
||
"github.com/creativenoz/aurganize-v62/backend/internal/services"
|
||
"github.com/creativenoz/aurganize-v62/backend/jobs"
|
||
"github.com/creativenoz/aurganize-v62/backend/pkg/logger"
|
||
"github.com/go-playground/validator/v10"
|
||
"github.com/jmoiron/sqlx"
|
||
"github.com/labstack/echo/v4"
|
||
|
||
echomiddleware "github.com/labstack/echo/v4/middleware"
|
||
"github.com/rs/zerolog/log"
|
||
)
|
||
|
||
func main() {
|
||
// =========================================================================
|
||
// Loading Configuration
|
||
// =========================================================================
|
||
cfg, err := config.Load()
|
||
if err != nil {
|
||
// we are not using logger here, since we need config information to set the log level
|
||
fmt.Fprintf(os.Stderr, "Failed to load configurations : %v\n", err)
|
||
// hence when config load fails we exit application, cause have not point in continuing further
|
||
os.Exit(1)
|
||
}
|
||
// =========================================================================
|
||
// Initializing Logger
|
||
// =========================================================================
|
||
logger.Init(cfg.Server.Environment)
|
||
log.Info().
|
||
Str("Version", "0.6.2").
|
||
Str("environment", cfg.Server.Environment).
|
||
Msg("Starting Aurganize v6.2 API server")
|
||
|
||
// =========================================================================
|
||
// Database Connection
|
||
// =========================================================================
|
||
log.Info().
|
||
Str("host", cfg.DatabaseDSN())
|
||
db, err := sqlx.Connect("postgres", cfg.DatabaseDSN())
|
||
if err != nil {
|
||
log.Fatal().
|
||
Err(err).
|
||
Str("dsn", cfg.DatabaseDSN()).
|
||
Msg("failed to connect to database")
|
||
}
|
||
|
||
defer db.Close()
|
||
|
||
db.SetMaxOpenConns(cfg.Database.MaxOpenConns)
|
||
db.SetMaxIdleConns(cfg.Database.MaxIdleConns)
|
||
db.SetConnMaxLifetime(cfg.Database.ConnMaxLifetime)
|
||
|
||
if err := db.Ping(); err != nil {
|
||
log.Fatal().
|
||
Err(err).
|
||
Str("dsn", cfg.DatabaseDSN()).
|
||
Msg("failed to ping database")
|
||
}
|
||
log.Info().
|
||
Str("host", cfg.Database.Host).
|
||
Str("database", cfg.Database.DBName).
|
||
Msg("database connected successfully")
|
||
|
||
// =========================================================================
|
||
// Initialize Repositories
|
||
// =========================================================================
|
||
userRepo := repositories.NewUserRepository(db)
|
||
sessionRepo := repositories.NewSessionRepository(db)
|
||
tenantRepo := repositories.NewTenantRepository(db)
|
||
log.Info().
|
||
Msg("repositories initialized")
|
||
|
||
// =========================================================================
|
||
// Initialize Handlers
|
||
// =========================================================================
|
||
|
||
authService := services.NewAuthService(cfg, sessionRepo, userRepo)
|
||
userService := services.NewUserService(userRepo)
|
||
tenantService := services.NewTenantService(cfg, tenantRepo, userRepo, db)
|
||
|
||
log.Info().
|
||
Msg("services initialized")
|
||
|
||
// =========================================================================
|
||
// Initialize Handlers
|
||
// =========================================================================
|
||
|
||
authHandler := handlers.NewAuthHandler(cfg, authService, userService)
|
||
userHandler := handlers.NewUserRegisterHandler(cfg, authService, userService, tenantService)
|
||
tenantHandler := handlers.NewTenantHanlder(tenantService)
|
||
|
||
log.Info().
|
||
Msg("handlers initialized")
|
||
|
||
// =========================================================================
|
||
// Initialize Middleware
|
||
// =========================================================================
|
||
authMiddleware := middleware.NewAuthMiddleware(authService)
|
||
globalrateLimitterMiddleware := middleware.NewRateLimiter(5, time.Minute)
|
||
log.Info().
|
||
Msg("middleware initialized")
|
||
|
||
// =========================================================================
|
||
// Background Jobs
|
||
// =========================================================================
|
||
sessionCleanUpJob := jobs.NewSessionCleanUpJob(sessionRepo)
|
||
|
||
go func() {
|
||
sessionCleanUpJob.Start(context.Background())
|
||
}()
|
||
|
||
log.Info().
|
||
Dur("interval", 12*time.Hour).
|
||
Msg("session clean up job started")
|
||
|
||
// =========================================================================
|
||
// Create Echo Instance
|
||
// =========================================================================
|
||
e := echo.New()
|
||
e.HideBanner = true
|
||
e.HidePort = true
|
||
e.Validator = &customValidator{validator: validator.New()}
|
||
e.HTTPErrorHandler = customHTTPErrorHandler // we are using a custom error handler
|
||
|
||
e.Server.ReadTimeout = cfg.Server.ReadTimeout
|
||
e.Server.WriteTimeout = cfg.Server.WriteTimeout
|
||
|
||
log.Info().Msg("Echo server instance created")
|
||
// =========================================================================
|
||
// Middleware Pipeline
|
||
// =========================================================================
|
||
|
||
// Setting safe recover middleware
|
||
e.Use(echomiddleware.Recover())
|
||
// Middleware catches panic
|
||
// Returns 500 Internal Server Error
|
||
// Server keeps running
|
||
// -------------------------------------------------------------------------
|
||
// Setting request ID middleware
|
||
e.Use(echomiddleware.RequestID())
|
||
// Trace request through entire system
|
||
// Link frontend error to backend logs
|
||
// This adds a header : X-Request-ID: abc-123-def-456
|
||
// ------------------------------------------------------------------------
|
||
// Setting Logger format
|
||
e.Use(echomiddleware.LoggerWithConfig(echomiddleware.LoggerConfig{
|
||
Format: `{"time":"${time_rfc3339}","method":"${method}","uri":"${uri}",` +
|
||
`"status":${status},"latency_ms":${latency_ms},"error":"${error}"}` + "\n",
|
||
Output: log.Logger,
|
||
}))
|
||
// We are setting a custom log format, which is consisten with our logger format
|
||
// {
|
||
// "time": "2025-11-26T10:30:45Z",
|
||
// "method": "POST",
|
||
// "uri": "/api/v1/login",
|
||
// "status": 200,
|
||
// "latency_ms": 45,
|
||
// "error": ""
|
||
// }
|
||
// -----------------------------------------------------------------------
|
||
// Setting CORS (Cross-Origin Resource Sharing) middleware
|
||
e.Use(middleware.NewCORSMiddleware())
|
||
// Prevents malicious sites from calling your API
|
||
// ----------------------------------------------------------------------
|
||
// Setting Security Headers middleware
|
||
e.Use(echomiddleware.SecureWithConfig(echomiddleware.SecureConfig{
|
||
XSSProtection: "1; mode=block",
|
||
ContentTypeNosniff: "nosniff",
|
||
XFrameOptions: "SAMEORIGIN",
|
||
HSTSMaxAge: 31536000,
|
||
HSTSExcludeSubdomains: false,
|
||
ContentSecurityPolicy: "default-src 'self'",
|
||
}))
|
||
// X-XSS-Protection:
|
||
// - Blocks cross-site scripting attacks
|
||
// - Browser detects XSS and blocks page
|
||
|
||
// X-Content-Type-Options: nosniff:
|
||
// - Prevents MIME-type sniffing
|
||
// - Browser trusts Content-Type header
|
||
// - Prevents executing scripts as HTML
|
||
|
||
// X-Frame-Options: SAMEORIGIN:
|
||
// - Prevents clickjacking
|
||
// - Page can't be embedded in iframe (except same origin)
|
||
// - Protects against UI redress attacks
|
||
|
||
// Strict-Transport-Security (HSTS):
|
||
// - Forces HTTPS for 1 year
|
||
// - Prevents downgrade attacks
|
||
// - Can't be disabled by user
|
||
|
||
// Content-Security-Policy:
|
||
// - Only load resources from same origin
|
||
// - Prevents loading malicious scripts
|
||
// - Additional layer of XSS protection
|
||
// -------------------------------------------------------------------
|
||
// Setting Gzip compression middleware
|
||
e.Use(echomiddleware.Gzip())
|
||
|
||
// TODO : Rate Limiting middleware (planning to use redis for custom rate limiter)
|
||
|
||
log.Info().Msg("Middleware configured")
|
||
|
||
// =========================================================================
|
||
// Route Mapping
|
||
// =========================================================================
|
||
|
||
e.GET("/health", healthCheckHandler(cfg)) // (Public - health check)
|
||
api := e.Group("/api/v6.2")
|
||
|
||
api.GET("/ping", func(c echo.Context) error { // (Public - connectivity test)
|
||
return c.JSON(http.StatusOK, map[string]string{
|
||
"message": "pika pikaaa --- PIKAAA CHUUUUU",
|
||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||
"version": "0.6.2",
|
||
})
|
||
})
|
||
|
||
routes.SetUpRoutes(
|
||
e,
|
||
authHandler,
|
||
userHandler,
|
||
tenantHandler,
|
||
authMiddleware,
|
||
globalrateLimitterMiddleware,
|
||
)
|
||
|
||
log.Info().Msg("Routes configured")
|
||
// =========================================================================
|
||
// Start Server in a new thread
|
||
// =========================================================================
|
||
|
||
serverAddr := fmt.Sprintf(":%s", cfg.Server.Port)
|
||
|
||
go func() {
|
||
log.Info().
|
||
Str("address", serverAddr).
|
||
Str("environment", cfg.Server.Environment).
|
||
Msg("Server starting")
|
||
|
||
if err := e.Start(serverAddr); err != nil && err != http.ErrServerClosed {
|
||
log.Fatal().
|
||
Err(err).
|
||
Msg("Failed to start server")
|
||
}
|
||
}()
|
||
// =========================================================================
|
||
// Shutdown Logic
|
||
// =========================================================================
|
||
|
||
quit := make(chan os.Signal, 1)
|
||
// Creates a channel named quit that can carry values of type os.Signal.
|
||
// The 1 means it’s a buffered channel with capacity 1 → it can hold one signal without blocking.
|
||
// This channel will be used to receive OS signals like Ctrl+C or kill.
|
||
|
||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||
// Whenever the process receives any of these signals, send them into the quit channel.”
|
||
// os.Interrupt → typically the signal sent when you press Ctrl+C in the terminal.
|
||
// syscall.SIGTERM → the “please terminate” signal, used by process managers / Docker / Kubernetes,
|
||
|
||
<-quit // Blocks until we get a signal in this channel
|
||
// This is a receive operation on the channel.
|
||
// The code blocks here and does nothing until:
|
||
// the OS sends os.Interrupt or SIGTERM,
|
||
// which signal.Notify pushes into quit.
|
||
// When a signal arrives, <-quit unblocks and the program continues.
|
||
|
||
log.Info().Msg("Shutting down server gracefully..")
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
// Creates a context with timeout of 10 seconds.
|
||
// This context is passed to e.Shutdown(ctx)
|
||
// Echo will have at most 10 seconds to shut down gracefully.
|
||
// After 10 seconds, the context is cancelled, and shutdown will be forced.
|
||
|
||
if err := e.Shutdown(ctx); err != nil {
|
||
log.Error().
|
||
Err(err).
|
||
Msg("Server forced to shutdown, graceful shutdown failed")
|
||
}
|
||
// Calls Echo’s Shutdown method with your timeout context.
|
||
// e.Shutdown(ctx):
|
||
// -- stops accepting new requests,
|
||
// -- waits for in-flight requests to finish,
|
||
// -- closes the server gracefully (within the timeout).
|
||
// If something goes wrong (e.g., it can’t shut down in time), err is non-nil:
|
||
// -- logs an error saying graceful shutdown failed and it had to force close.
|
||
|
||
log.Info().Msg("API Server exited")
|
||
|
||
}
|
||
|
||
type customValidator struct {
|
||
validator *validator.Validate
|
||
}
|
||
|
||
func (cv *customValidator) Validate(i interface{}) error {
|
||
if err := cv.validator.Struct(i); err != nil {
|
||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// HealthCheck Handler function
|
||
// This endpoint is to be used by:
|
||
// - Load balancers to determine if instance is healthy
|
||
// - Kubernetes for liveness and readiness probes
|
||
// - Monitoring systems for uptime checks
|
||
func healthCheckHandler(c *config.Config) echo.HandlerFunc {
|
||
return func(e echo.Context) error {
|
||
// TODO : we need to add health check for
|
||
// - Database connection
|
||
// - Redis connection if we are using it
|
||
// - NATS connection
|
||
|
||
// For now we are just returning OK, since most of the service dependecies are not implemented yet.
|
||
response := map[string]interface{}{
|
||
"status": "healthy",
|
||
"version": "0.6.2",
|
||
"environment": c.Server.Environment,
|
||
"timestamp": time.Now().UTC(),
|
||
"checks": map[string]string{
|
||
"server": "ok",
|
||
"database": "not setup",
|
||
"redis": "not setup",
|
||
"nats": "not setup",
|
||
},
|
||
"uptime": 999999999999, // logic yet to be implemented
|
||
}
|
||
return e.JSON(http.StatusOK, response)
|
||
}
|
||
}
|
||
|
||
// CustomHTTPErrorHandler
|
||
// Used to provide consistent error response
|
||
// Converts Echo erros to JSON format, that can be parsed at frontend
|
||
func customHTTPErrorHandler(err error, c echo.Context) {
|
||
|
||
// Setting default values
|
||
code := http.StatusInternalServerError
|
||
message := "internal server error"
|
||
|
||
// Checking if the error is and Echo error
|
||
// We do this through a type assertion err.(*echo.HTTPError) [ basetype.(type to assert to)]
|
||
if he, ok := err.(*echo.HTTPError); ok {
|
||
code = he.Code
|
||
// Then we again check with a type assertion
|
||
if msg, ok := he.Message.(string); ok {
|
||
message = msg
|
||
}
|
||
}
|
||
|
||
log.Debug().
|
||
Err(err).
|
||
Int("status", code).
|
||
Str("method", c.Request().Method).
|
||
Str("path", c.Request().URL.Path).
|
||
Msg("HTTP error")
|
||
|
||
if code >= 500 {
|
||
log.Error().
|
||
Err(err).
|
||
Int("status", code).
|
||
Str("method", c.Request().Method).
|
||
Str("path", c.Request().URL.Path).
|
||
Msg("HTTP error")
|
||
}
|
||
|
||
// If the response is not already written
|
||
// Then we don't have to again log
|
||
if !c.Response().Committed {
|
||
c.JSON(code, map[string]interface{}{
|
||
"error": map[string]interface{}{
|
||
"code": code,
|
||
"message": message,
|
||
"timestamp": time.Now().UTC(),
|
||
"path": c.Request().URL.Path,
|
||
"request_id": c.Response().Header().Get(echo.HeaderXRequestID),
|
||
},
|
||
})
|
||
}
|
||
|
||
}
|