Files
go-simple-api/internal/router/router.go
T

122 lines
4.7 KiB
Go
Raw Normal View History

2026-07-16 10:13:46 +03:30
// 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
}