Files
go-simple-api/internal/middleware/require_auth.go
T

93 lines
3.8 KiB
Go
Raw Normal View History

2026-07-16 10:13:46 +03:30
package middleware
import (
"context"
"log/slog"
"net/http"
"github.com/alexedwards/scs/v2"
"git.hamidsoltani.com/hamid/go-simple-api/internal/models"
"git.hamidsoltani.com/hamid/go-simple-api/internal/session"
)
// contextKey is a private, unexported type used as the type of our context
// key below. This is a well-known Go idiom to avoid key collisions: since
// context.WithValue keys are compared by BOTH type and value, using our
// own named type (instead of a plain string) guarantees userContextKey can
// never accidentally collide with a key defined by another package, even
// if the underlying text happened to be identical.
type contextKey string
const userContextKey contextKey = "current_user"
// RequireAuth is a middleware factory (same three-layer shape as
// RequestLogger) that protects a route: it checks the caller's session for
// a logged-in user ID, loads the full user from the database, and - only
// if that all succeeds - stores the user in the request's context and lets
// the request continue. If anything fails, it responds 401 immediately and
// the wrapped handler never runs at all.
func RequireAuth(sessions *scs.SessionManager, userRepo *models.UserRepository, logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetInt returns the zero value (0) if the key was never set
// in the session - which is exactly what happens for a
// visitor who never logged in.
userID := sessions.GetInt(r.Context(), session.UserIDKey)
if userID == 0 {
writeUnauthorized(w)
return
}
user, err := userRepo.FindByID(r.Context(), userID)
if err != nil {
// Covers both "no such user" (e.g. the account was
// deleted after this session was created) and genuine
// database errors - either way, this request cannot
// proceed as authenticated.
logger.Error("require auth: find user failed", "error", err, "user_id", userID)
writeUnauthorized(w)
return
}
// context.WithValue returns a NEW context wrapping the old
// one plus our key/value pair - contexts are immutable, you
// can't add to an existing one in place. Similarly,
// r.WithContext returns a NEW *http.Request carrying that
// context; we pass that new request onward so downstream
// handlers can read the user back out.
ctx := context.WithValue(r.Context(), userContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// CurrentUser lets handlers retrieve the authenticated user that
// RequireAuth already loaded and stashed in the request's context.
// Handlers never need to know about userContextKey directly (it's
// unexported - only this file can create or read that specific key) - they
// just call this function.
func CurrentUser(r *http.Request) *models.User {
// Value() returns `any`, so we need a type assertion to get back a
// concrete *models.User. The two-value form (`user, ok := ...`) is the
// SAFE version: ok is false if the assertion fails (wrong type, or the
// key simply isn't present) instead of panicking - always prefer this
// form when the value's presence isn't 100% guaranteed.
user, ok := r.Context().Value(userContextKey).(*models.User)
if !ok {
return nil
}
return user
}
// writeUnauthorized writes a plain {"error":"unauthorized"} 401 response.
// Written by hand (instead of reusing handlers.writeError) because
// internal/middleware and internal/handlers are separate packages, and
// writeError is unexported in the handlers package - a deliberate package
// boundary, not an oversight.
func writeUnauthorized(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"unauthorized"}`))
}