using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Infrastructure.Data; using PowderCoating.Shared.Constants; namespace PowderCoating.Web.Controllers; /// /// Receives platform-level Stripe webhook events (subscription lifecycle, checkout completion, etc.) /// from the Stripe platform account. This is distinct from PaymentController.ConnectWebhook /// which handles Stripe Connect account-level events (payments, refunds, disputes on tenant /// accounts). Raw body reading is required — Stripe's signature verification computes an HMAC over /// the exact bytes received, so any middleware that re-encodes the body (e.g. JSON middleware) /// would break signature validation. The request body size limit is 5 MB; Stripe events are always /// much smaller, but this cap prevents memory exhaustion from malformed oversized POST requests. /// [AllowAnonymous] [IgnoreAntiforgeryToken] [RequestSizeLimit(5 * 1024 * 1024)] // 5 MB — Stripe events are never larger than this [EnableRateLimiting(AppConstants.RateLimitPolicies.Webhook)] public class StripeWebhookController : Controller { private readonly IStripeService _stripeService; private readonly ApplicationDbContext _db; private readonly ILogger _logger; public StripeWebhookController( IStripeService stripeService, ApplicationDbContext db, ILogger logger) { _stripeService = stripeService; _db = db; _logger = logger; } /// /// Receives, verifies, and processes a Stripe webhook event at POST /stripe/webhook. /// The body is read as raw text rather than via model binding so the HMAC signature computed /// by Stripe over the exact byte sequence remains valid. The event ID and type are parsed /// structurally (before signature check) purely for logging — this pre-parse does not trust the /// payload; the authoritative verification happens inside /// IStripeService.HandleWebhookAsync. The StripeWebhookEvent audit record is /// written AFTER signature verification to prevent unauthenticated callers from flooding the /// events table. On a processing error after verification, the event is still recorded with /// Status = Failed so SuperAdmins can investigate in StripeEventsController. /// Returns HTTP 500 on processing failure — Stripe will retry the event up to its retry policy /// limit, which is the desired behaviour for transient DB or service errors. /// [HttpPost("/stripe/webhook")] public async Task Webhook() { // Read raw body using var reader = new StreamReader(Request.Body); var json = await reader.ReadToEndAsync(); var stripeSignature = Request.Headers["Stripe-Signature"].ToString(); if (string.IsNullOrEmpty(stripeSignature)) { _logger.LogWarning("Stripe webhook received without signature header"); return BadRequest("Missing Stripe-Signature header"); } // Parse event id/type for logging (no signature check yet — just structural parse) string eventId = string.Empty; string eventType = string.Empty; try { var evt = Stripe.EventUtility.ParseEvent(json); eventId = evt.Id ?? string.Empty; eventType = evt.Type ?? string.Empty; } catch { /* invalid JSON — HandleWebhookAsync will reject it below */ } // Process first (signature verification happens inside HandleWebhookAsync). // Only persist a log record after we know the request is authentic — this prevents // unauthenticated callers from flooding the StripeWebhookEvents table. try { await _stripeService.HandleWebhookAsync(json, stripeSignature); // Signature verified and event processed — now record it var logEntry = new StripeWebhookEvent { EventId = eventId, EventType = eventType, RawJson = json, Status = StripeWebhookEventStatus.Processed, ReceivedAt = DateTime.UtcNow, ProcessedAt = DateTime.UtcNow }; _db.StripeWebhookEvents.Add(logEntry); await _db.SaveChangesAsync(); return Ok(); } catch (Stripe.StripeException ex) { // Signature invalid or event malformed — log warning, do NOT save to DB _logger.LogWarning("Stripe webhook rejected (invalid signature or payload): {Message}", ex.Message); return BadRequest("Invalid Stripe signature"); } catch (Exception ex) { _logger.LogError(ex, "Error processing Stripe webhook {EventType} {EventId}", eventType, eventId); // Processing failed after signature was verified — record it for debugging var logEntry = new StripeWebhookEvent { EventId = eventId, EventType = eventType, RawJson = json, Status = StripeWebhookEventStatus.Failed, ReceivedAt = DateTime.UtcNow, ProcessedAt = DateTime.UtcNow, ErrorMessage = ex.Message }; _db.StripeWebhookEvents.Add(logEntry); await _db.SaveChangesAsync(); return StatusCode(500, "Webhook processing error"); } } }