Files
pos-system/services/mkt-x-service-net/src/MktXService.API/Controllers/WebhooksController.cs
Ho Ngoc Hai f8606e0447 fix(P0): security hardening + critical bug fixes across 22 services
Wave 1 — 6 parallel agents fixing P0 issues from code audit:

Auth (18 services secured):
- Added JWT Bearer auth + [Authorize] to all unprotected controllers
- Webhook endpoints (Facebook/WhatsApp/Zalo/X) stay [AllowAnonymous]
- Health checks remain public for Docker/K8s probes
- Services: catalog, order, booking, fnb-engine, inventory, social,
  ads-manager, ads-serving, ads-billing, ads-tracking, ads-analytics,
  mkt-facebook, mkt-whatsapp, mkt-x, mkt-zalo, promotion

Template artifacts (4 services):
- mission-service: myservice_db → mission_service
- mkt-facebook: Dockerfile MyService.API → FacebookService.API
- mkt-whatsapp: MyServiceContext.cs → WhatsAppServiceContext.cs
- promotion: UserSecretsId fixed

Critical handler bugs (7 fixes):
- ads-tracking: TrackPixelEventHandler now persists to DB
- ads-tracking: RecordConversion endpoint exposed via controller
- booking: UpdateResource now applies Name + Capacity changes
- ads-manager: ListPendingAds uses correct enum (pending_review)
- mining: BanMiner calls Ban() not Suspend()
- mining: ResetMinerStreak now actually resets streak
- mkt-x: 8 missing repository DI registrations added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:18:09 +07:00

183 lines
5.5 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MktXService.Infrastructure.ExternalServices.Twitter;
namespace MktXService.API.Controllers;
/// <summary>
/// EN: Controller for Twitter Webhook handling.
/// VI: Controller xử lý Twitter Webhook.
/// </summary>
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/webhooks/twitter")]
[Produces("application/json")]
[Authorize]
public class WebhooksController : ControllerBase
{
private readonly ITwitterApiClient _twitterClient;
private readonly TwitterApiOptions _options;
private readonly ILogger<WebhooksController> _logger;
public WebhooksController(
ITwitterApiClient twitterClient,
IOptions<TwitterApiOptions> options,
ILogger<WebhooksController> logger)
{
_twitterClient = twitterClient;
_options = options.Value;
_logger = logger;
}
/// <summary>
/// EN: CRC token verification for Twitter webhook.
/// VI: Xác thực CRC token cho Twitter webhook.
/// </summary>
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult VerifyCrcToken([FromQuery(Name = "crc_token")] string crcToken)
{
_logger.LogInformation("Received CRC challenge: {Token}", crcToken);
var response = _twitterClient.GenerateCrcResponse(crcToken);
return Ok(new { response_token = response });
}
/// <summary>
/// EN: Receive Twitter webhook events.
/// VI: Nhận events từ Twitter webhook.
/// </summary>
[HttpPost]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> ReceiveWebhookEvent([FromBody] TwitterWebhookPayload payload)
{
_logger.LogInformation("Received webhook event for user: {UserId}", payload.ForUserId);
// EN: Process Direct Message events
// VI: Xử lý events Direct Message
if (payload.DirectMessageEvents?.Any() == true)
{
foreach (var dmEvent in payload.DirectMessageEvents)
{
_logger.LogInformation("Received DM from {SenderId}: {MessageId}",
dmEvent.MessageCreate?.SenderId, dmEvent.Id);
// EN: In production, queue for async processing
// VI: Trong production, đưa vào hàng đợi để xử lý bất đồng bộ
}
}
// EN: Process follow events
// VI: Xử lý events follow
if (payload.FollowEvents?.Any() == true)
{
foreach (var followEvent in payload.FollowEvents)
{
_logger.LogInformation("New follower: {FollowerId}",
followEvent.Source?.Id);
}
}
return Ok();
}
/// <summary>
/// EN: Register webhook for an account.
/// VI: Đăng ký webhook cho tài khoản.
/// </summary>
[HttpPost("register")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> RegisterWebhook([FromBody] RegisterWebhookRequest request)
{
try
{
var webhookId = await _twitterClient.RegisterWebhookAsync(request.WebhookUrl);
return Ok(new { success = true, webhookId });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register webhook");
return BadRequest(new { success = false, error = ex.Message });
}
}
/// <summary>
/// EN: Subscribe to webhook events.
/// VI: Đăng ký nhận webhook events.
/// </summary>
[HttpPost("subscribe")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> SubscribeToWebhook()
{
try
{
await _twitterClient.SubscribeToWebhookAsync();
return Ok(new { success = true, message = "Subscribed to webhook events" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to subscribe to webhook");
return BadRequest(new { success = false, error = ex.Message });
}
}
}
#region Webhook DTOs
public class TwitterWebhookPayload
{
public string? ForUserId { get; set; }
public List<DirectMessageEvent>? DirectMessageEvents { get; set; }
public List<FollowEvent>? FollowEvents { get; set; }
public Dictionary<string, TwitterUserData>? Users { get; set; }
}
public class DirectMessageEvent
{
public string? Id { get; set; }
public string? Type { get; set; }
public string? CreatedTimestamp { get; set; }
public MessageCreate? MessageCreate { get; set; }
}
public class MessageCreate
{
public string? SenderId { get; set; }
public string? Target { get; set; }
public MessageData? MessageData { get; set; }
}
public class MessageData
{
public string? Text { get; set; }
public List<object>? Entities { get; set; }
}
public class FollowEvent
{
public string? Type { get; set; }
public string? CreatedTimestamp { get; set; }
public TwitterUserData? Source { get; set; }
public TwitterUserData? Target { get; set; }
}
public class TwitterUserData
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? ScreenName { get; set; }
}
public record RegisterWebhookRequest(string WebhookUrl);
#endregion