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"}`)) }