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>
183 lines
5.5 KiB
C#
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
|