// Package router is where the whole application gets wired together: it // receives already-constructed shared dependencies (logger, db, sessions, // config) from main.go, builds the handlers and middleware that need them, // and registers every route on a chi.Mux. package router import ( "database/sql" "log/slog" "time" "github.com/alexedwards/scs/v2" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/httprate" "git.hamidsoltani.com/hamid/go-simple-api/internal/config" "git.hamidsoltani.com/hamid/go-simple-api/internal/handlers" "git.hamidsoltani.com/hamid/go-simple-api/internal/middleware" "git.hamidsoltani.com/hamid/go-simple-api/internal/models" "git.hamidsoltani.com/hamid/go-simple-api/internal/oauth" ) // New builds and returns the fully configured chi router for the // application. *chi.Mux implements http.Handler (it has a ServeHTTP // method), so the return value can be passed directly to http.Server as // its Handler - see cmd/api/main.go. func New(logger *slog.Logger, db *sql.DB, sessions *scs.SessionManager, cfg config.Config) *chi.Mux { r := chi.NewRouter() // --- Global middleware stack --- // Order matters here: each middleware wraps everything registered // after it, so requests flow through this list top-to-bottom on the // way in, and bottom-to-top on the way out. // Tags every request with a unique ID, retrievable later via // chimw.GetReqID(ctx) - used by our RequestLogger below, and useful // for correlating log lines to one specific request once shipped to // Loki. r.Use(chimw.RequestID) // Our own structured JSON request logger (internal/middleware), // replacing chi's built-in plain-text Logger. r.Use(middleware.RequestLogger(logger)) // Recovers from panics in any handler, returning a 500 instead of // crashing the whole server process for one bad request. r.Use(chimw.Recoverer) // Cancels a request's context if it runs longer than this, so a slow // downstream call (e.g. a hung database query) can't hold a // connection open forever. r.Use(chimw.Timeout(60 * time.Second)) // CORS: controls which browser-based frontends (running on a // different origin than this API) are allowed to call it with // credentials (cookies) attached. This does NOT protect against // non-browser callers (curl, mobile apps, server-to-server) - CORS is // a browser-enforced rule, not a server-side security boundary. r.Use(cors.Handler(cors.Options{ AllowedOrigins: cfg.AllowedOrigins, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedHeaders: []string{"Content-Type"}, AllowCredentials: true, // required for the session cookie to be sent cross-origin })) // A generous, global rate limit - mostly a safety net against // runaway scripts/bots hitting the API in general. 100 requests per // IP per minute. r.Use(httprate.LimitByIP(100, time.Minute)) // scs's own middleware: loads session data (from Redis) into the // request's context before the handler runs, and saves any changes // back (plus sets/refreshes the cookie) after the handler finishes. // Every route below this line can use sessions.Get/Put/Destroy. r.Use(sessions.LoadAndSave) // --- Public routes --- r.Get("/health", handlers.Health) userRepo := models.NewUserRepository(db) authHandler := handlers.NewAuthHandler(userRepo, sessions, logger) requireAuth := middleware.RequireAuth(sessions, userRepo, logger) // Register/Login get a MUCH stricter rate limit than the global one, // since these are exactly the endpoints a credential-stuffing / // brute-force script would target: 5 requests per IP per minute. // r.Group scopes this middleware to only the routes registered inside // the closure - it does not affect any route registered outside it. r.Group(func(r chi.Router) { r.Use(httprate.LimitByIP(5, time.Minute)) r.Post("/register", authHandler.Register) r.Post("/login", authHandler.Login) }) // Deliberately OUTSIDE the strict-rate-limit group above - we don't // want to rate-limit a legitimate logged-in user trying to log out. r.Post("/logout", authHandler.Logout) // --- Protected routes --- // Every route registered inside this group first passes through // requireAuth; if the caller isn't authenticated, requireAuth responds // 401 and the route handler never runs at all. Add future // authenticated-only routes inside this same group. r.Group(func(r chi.Router) { r.Use(requireAuth) r.Get("/me", authHandler.Me) }) // --- Google OAuth2 routes --- googleConfig := oauth.NewGoogleConfig(cfg) googleHandler := handlers.NewGoogleOAuthHandler(googleConfig, userRepo, sessions, logger) r.Get("/auth/google/login", googleHandler.Login) r.Get("/auth/google/callback", googleHandler.Callback) return r }