first commit
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user