122 lines
4.7 KiB
Go
122 lines
4.7 KiB
Go
// 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
|
|
}
|