// Package middleware provides HTTP middleware for authentication and context injection. package middleware import ( "context" "encoding/json" "log" "net/http" "strings" fbauth "github.com/kjannette/koin-ping/backend/internal/firebase" "github.com/kjannette/koin-ping/backend/internal/models" ) type contextKey string const ( UserIDKey contextKey = "user_id" UserEmailKey contextKey = "user_email" UserTierKey contextKey = "user_tier" ) type errorResponse struct { Error string `json:"error"` Message string `json:"message"` } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) //nolint:errcheck } // Authenticate verifies the Firebase ID token and auto-provisions a local user // record. The local user UUID (not the Firebase UID) is placed into context so // all downstream handlers use it as the canonical user identifier. func Authenticate(userModel *models.UserModel) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { log.Println("No Authorization header or invalid format") writeJSON(w, http.StatusUnauthorized, errorResponse{ Error: "UNAUTHORIZED", Message: "No authentication token provided", }) return } token := strings.TrimPrefix(authHeader, "Bearer ") if token == "" { log.Println("Empty token") writeJSON(w, http.StatusUnauthorized, errorResponse{ Error: "UNAUTHORIZED", Message: "Invalid token format", }) return } log.Println("Verifying Firebase token...") decoded, err := fbauth.Auth().VerifyIDToken(r.Context(), token) if err != nil { log.Printf("Token verification failed: %v", err) errMsg := err.Error() if strings.Contains(errMsg, "expired") { writeJSON(w, http.StatusUnauthorized, errorResponse{ Error: "TOKEN_EXPIRED", Message: "Authentication token has expired", }) return } writeJSON(w, http.StatusUnauthorized, errorResponse{ Error: "UNAUTHORIZED", Message: "Failed to verify authentication token", }) return } firebaseUID := decoded.UID email, _ := decoded.Claims["email"].(string) user, err := userModel.FindOrCreateByFirebaseUID(r.Context(), firebaseUID, email) if err != nil { log.Printf("Failed to provision local user for Firebase UID %s: %v", firebaseUID, err) writeJSON(w, http.StatusInternalServerError, errorResponse{ Error: "INTERNAL_ERROR", Message: "Failed to initialize user account", }) return } log.Printf("Token verified! User UUID: %s, Email: %s", user.ID, email) ctx := context.WithValue(r.Context(), UserIDKey, user.ID) ctx = context.WithValue(ctx, UserEmailKey, email) ctx = context.WithValue(ctx, UserTierKey, string(user.SubscriptionTier)) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // RequireSubscription blocks requests from users without an active subscription. // Must be applied after Authenticate. func RequireSubscription(userModel *models.UserModel) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID := GetUserID(r.Context()) if userID == "" { writeJSON(w, http.StatusUnauthorized, errorResponse{ Error: "UNAUTHORIZED", Message: "Authentication required", }) return } user, err := userModel.GetByID(r.Context(), userID) if err != nil || user == nil { log.Printf("RequireSubscription: failed to load user %s: %v", userID, err) writeJSON(w, http.StatusInternalServerError, errorResponse{ Error: "INTERNAL_ERROR", Message: "Failed to verify subscription", }) return } if user.SubscriptionStatus != "active" && user.SubscriptionStatus != "trialing" { writeJSON(w, http.StatusForbidden, errorResponse{ Error: "SUBSCRIPTION_REQUIRED", Message: "An active subscription is required to use this feature", }) return } next.ServeHTTP(w, r) }) } } func GetUserID(ctx context.Context) string { if v, ok := ctx.Value(UserIDKey).(string); ok { return v } return "" } func GetUserEmail(ctx context.Context) string { if v, ok := ctx.Value(UserEmailKey).(string); ok { return v } return "" } func GetUserTier(ctx context.Context) string { if v, ok := ctx.Value(UserTierKey).(string); ok { return v } return "free" }