feat: use router to group routes
This commit is contained in:
parent
a3f4661456
commit
280cb93648
12 changed files with 222 additions and 198 deletions
|
|
@ -8,9 +8,9 @@ import (
|
|||
)
|
||||
|
||||
// AuthMiddleware checks for JWT token and adds user to context if valid
|
||||
func AuthMiddleware(authService *service.AuthService, userService *service.UserService) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
func AuthMiddleware(authService *service.AuthService, userService *service.UserService) Middleware {
|
||||
return func(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get JWT from cookie
|
||||
cookie, err := r.Cookie("auth_token")
|
||||
if err != nil {
|
||||
|
|
@ -50,12 +50,12 @@ func AuthMiddleware(authService *service.AuthService, userService *service.UserS
|
|||
// Add user to context
|
||||
ctx := ctxkeys.WithUser(r.Context(), user)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireGuest ensures request is not authenticated
|
||||
func RequireGuest(next http.HandlerFunc) http.HandlerFunc {
|
||||
func RequireGuest(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
if user != nil {
|
||||
|
|
@ -67,7 +67,7 @@ func RequireGuest(next http.HandlerFunc) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// RequireAuth ensures the user is authenticated and has completed onboarding
|
||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
func RequireAuth(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
if user == nil {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import "net/http"
|
|||
// CacheStatic wraps a handler to set long-lived cache headers for static assets.
|
||||
// Assets use query-string cache busting (?v=<timestamp>), so it's safe to cache
|
||||
// them indefinitely — the URL changes when the content changes.
|
||||
func CacheStatic(h http.Handler) http.Handler {
|
||||
func CacheStatic(h http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
h.ServeHTTP(w, r)
|
||||
|
|
@ -15,7 +15,7 @@ func CacheStatic(h http.Handler) http.Handler {
|
|||
// NoCacheDynamic sets Cache-Control: no-cache on responses so browsers always
|
||||
// revalidate with the server. This prevents stale HTML from being shown after
|
||||
// navigation (e.g. back button) while still allowing conditional requests.
|
||||
func NoCacheDynamic(next http.Handler) http.Handler {
|
||||
func NoCacheDynamic(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip static assets — they're handled by CacheStatic.
|
||||
if len(r.URL.Path) >= 8 && r.URL.Path[:8] == "/assets/" {
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Chain applies multiple middleware in order (first to last)
|
||||
// The middleware are executed in the order they are provided
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// handler := Chain(mux,
|
||||
// AuthMiddleware(...), // Executes first
|
||||
// WithURLPath, // Executes second
|
||||
// Config(...), // Executes third
|
||||
// )
|
||||
func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
|
||||
// Apply middleware in reverse order so they execute in the order provided
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
h = middlewares[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
|
@ -9,11 +9,11 @@ import (
|
|||
|
||||
// Config middleware adds the sanitized app configuration to the request context.
|
||||
// Sensitive values like JWTSecret and DBPath are excluded for security.
|
||||
func Config(cfg *config.Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
func Config(cfg *config.Config) Middleware {
|
||||
return func(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := ctxkeys.WithConfig(r.Context(), cfg.Sanitized())
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const (
|
|||
)
|
||||
|
||||
// CSRFProtection validates CSRF tokens on all state-changing requests
|
||||
func CSRFProtection(next http.Handler) http.Handler {
|
||||
func CSRFProtection(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip CSRF check for safe methods (GET, HEAD, OPTIONS)
|
||||
if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ var skipLoggingPaths = []string{
|
|||
|
||||
// RequestLogging logs HTTP requests with method, path, status, and duration
|
||||
// Skips logging for paths defined in skipLoggingPaths
|
||||
func RequestLogging(next http.Handler) http.Handler {
|
||||
func RequestLogging(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip logging for configured paths
|
||||
for _, prefix := range skipLoggingPaths {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,26 @@ import (
|
|||
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
|
||||
)
|
||||
|
||||
type Middleware func(http.Handler) http.HandlerFunc
|
||||
|
||||
// Chain applies multiple middleware in order (first to last)
|
||||
// The middleware are executed in the order they are provided
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// handler := Chain(mux,
|
||||
// AuthMiddleware(...), // Executes first
|
||||
// WithURLPath, // Executes second
|
||||
// Config(...), // Executes third
|
||||
// )
|
||||
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
|
||||
// Apply middleware in reverse order so they execute in the order provided
|
||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||
h = middlewares[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Redirect handles both HTMX and regular HTTP redirects.
|
||||
// For HTMX requests, it sets the HX-Redirect header; for regular requests,
|
||||
// it uses http.Redirect.
|
||||
|
|
@ -97,18 +97,12 @@ func (rl *RateLimiter) cleanup() {
|
|||
}
|
||||
}
|
||||
|
||||
// RateLimitAuth creates middleware for auth endpoints
|
||||
// Limits: 5 requests per 15 minutes per IP
|
||||
func RateLimitAuth() func(http.HandlerFunc) http.HandlerFunc {
|
||||
limiter := NewRateLimiter(5, 15*time.Minute)
|
||||
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
// Middleware returns a Middleware that enforces this rate limiter per client IP.
|
||||
func (rl *RateLimiter) Middleware() Middleware {
|
||||
return func(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get real IP (handle proxies)
|
||||
ip := getClientIP(r)
|
||||
|
||||
// Check rate limit
|
||||
if !limiter.Allow(ip) {
|
||||
if !rl.Allow(ip) {
|
||||
slog.Warn("rate limit exceeded",
|
||||
"ip", ip,
|
||||
"path", r.URL.Path,
|
||||
|
|
@ -116,32 +110,8 @@ func RateLimitAuth() func(http.HandlerFunc) http.HandlerFunc {
|
|||
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimitCRUD creates middleware for state-changing CRUD endpoints.
|
||||
// Limits: 60 requests per minute per IP.
|
||||
func RateLimitCRUD() func(http.Handler) http.Handler {
|
||||
limiter := NewRateLimiter(60, 1*time.Minute)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := getClientIP(r)
|
||||
|
||||
if !limiter.Allow(ip) {
|
||||
slog.Warn("CRUD rate limit exceeded",
|
||||
"ip", ip,
|
||||
"path", r.URL.Path,
|
||||
)
|
||||
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,26 +6,24 @@ import (
|
|||
|
||||
// SecurityHeaders sets common security response headers on every response.
|
||||
// Note: HSTS is handled by Caddy at the reverse proxy layer.
|
||||
func SecurityHeaders() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
func SecurityHeaders(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
|
||||
h.Set("Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"style-src 'self' 'unsafe-inline'; "+
|
||||
"img-src 'self' data:; "+
|
||||
"font-src 'self'; "+
|
||||
"frame-ancestors 'none'; "+
|
||||
"base-uri 'self'; "+
|
||||
"form-action 'self'")
|
||||
h.Set("Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"style-src 'self' 'unsafe-inline'; "+
|
||||
"img-src 'self' data:; "+
|
||||
"font-src 'self'; "+
|
||||
"frame-ancestors 'none'; "+
|
||||
"base-uri 'self'; "+
|
||||
"form-action 'self'")
|
||||
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()")
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import (
|
|||
)
|
||||
|
||||
// WithURLPath adds the current URL's path to the context
|
||||
func WithURLPath(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
func WithURLPath(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctxWithPath := ctxkeys.WithURLPath(r.Context(), r.URL.Path)
|
||||
next.ServeHTTP(w, r.WithContext(ctxWithPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue