aurganize-backend/backend/cmd/api/main.go

401 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 its 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 Echos 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 cant 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),
},
})
}
}