From b99866db548390e0274aa7ae37d8157e3dc9d379 Mon Sep 17 00:00:00 2001 From: rizzOn Date: Sat, 29 Nov 2025 20:52:17 +0530 Subject: [PATCH] feat(echo server): Define backend api server using echo framework API server (backend/cmd/api): - we load configuration - init logger - create echo server instance - we define the middleware pipeline --- recover --- request_id --- logger_format --- cors (ross-Origin Resource Sharing) --- security_headers --- gzip_compresssion - route_mapping - star_server - graceful_shutdown add-ons: - healthCheckHandler() - customHTTPErrorHandler() This commit establishes the foundation for all future development. Story: E1-002 - Backend Project Initialization (main echo server) --- backend/cmd/api/main.go | 310 +++++++++++++++++++++++++++++++++++++++- backend/go.mod | 3 + backend/go.sum | 5 + 3 files changed, 316 insertions(+), 2 deletions(-) diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 0c40d87..fa1ab43 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -1,7 +1,313 @@ package main -import "fmt" +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/creativenoz/aurganize-v62/backend/internal/config" + "github.com/creativenoz/aurganize-v62/backend/pkg/logger" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/rs/zerolog/log" +) func main() { - fmt.Println("Aurganize") + // ========================================================================= + // 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") + + // ========================================================================= + // Create Echo Instance + // ========================================================================= + e := echo.New() + e.HideBanner = true + e.HidePort = true + + 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(middleware.Recover()) + // Middleware catches panic + // Returns 500 Internal Server Error + // Server keeps running + // ------------------------------------------------------------------------- + // Setting request ID middleware + e.Use(middleware.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(middleware.LoggerWithConfig(middleware.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.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{ + "http://localhost:5173", // (Development) Svelte dev server : this is the port suggested to be used with front-end + "http://localhost:3000", // (Developement) Alternative dev port : this is an alternative port kept aside + "https://app.aurganize.com", // (Production) Production frontend : we can use this subdomain itself for front-end + }, + AllowMethods: []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + http.MethodPatch, + http.MethodOptions, + }, + AllowHeaders: []string{ + "Origin", + "Content-Type", + "Accept", + "Authorization", + "X-Request-ID", + }, + + AllowCredentials: true, // Not sure about why are using this option + + MaxAge: 3600, // 1 hour in seconds + })) + // Prevents malicious sites from calling your API + // ---------------------------------------------------------------------- + // Setting Security Headers middleware + e.Use(middleware.SecureWithConfig(middleware.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(middleware.Gzip()) + + // TODO : Rate Limiting middleware (planning to use redis for custom rate limiter) + + log.Info().Msg("Middleware configured") + + // ========================================================================= + // Middleware Pipeline + // ========================================================================= + + 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", + "timestamp": time.Now().UTC().Format(time.RFC3339), + "version": "0.6.2", + }) + }) + + 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") + +} + +// 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), + }, + }) + } + } diff --git a/backend/go.mod b/backend/go.mod index 258d0b3..a2881b4 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,9 +2,12 @@ module github.com/creativenoz/aurganize-v62/backend go 1.25.2 + + require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-playground/locales v0.14.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 128d8aa..cda58ac 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -4,6 +4,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= @@ -35,6 +37,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -77,6 +81,7 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=