From c6bcc8d0df259fba6af58e9776aa469207f6e427 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 17 Jan 2026 18:08:46 +0700 Subject: [PATCH] feat: Implement JWT authentication, SignalR, external service clients in MiningService, and update documentation across services. --- .../src/ChatService.API/Program.cs | 27 ++ .../Controllers/AdminController.cs | 2 + .../Controllers/CirclesController.cs | 2 + .../Controllers/MiningController.cs | 2 + .../Controllers/ReferralsController.cs | 2 + .../Extensions/ExternalServicesExtensions.cs | 61 +++ .../ExternalServices/IamServiceClient.cs | 60 +++ .../ExternalServices/SocialServiceClient.cs | 66 ++++ .../ExternalServices/WalletServiceClient.cs | 67 ++++ .../Handlers/IntegrationEventHandlers.cs | 105 +++++ .../IntegrationEvents/IntegrationEvents.cs | 67 ++++ .../MiningService.API.csproj | 2 + .../src/MiningService.API/Program.cs | 99 ++++- .../docs/en/ARCHITECTURE.md | 265 ++++++++++++- .../mission-service-net/docs/en/README.md | 2 + .../docs/vi/ARCHITECTURE.md | 369 ++++++++++++++++++ .../mission-service-net/docs/vi/README.md | 269 ++++++++++++- .../src/SocialService.API/Program.cs | 26 ++ 18 files changed, 1477 insertions(+), 16 deletions(-) create mode 100644 services/mining-service-net/src/MiningService.API/Extensions/ExternalServicesExtensions.cs create mode 100644 services/mining-service-net/src/MiningService.API/ExternalServices/IamServiceClient.cs create mode 100644 services/mining-service-net/src/MiningService.API/ExternalServices/SocialServiceClient.cs create mode 100644 services/mining-service-net/src/MiningService.API/ExternalServices/WalletServiceClient.cs create mode 100644 services/mining-service-net/src/MiningService.API/IntegrationEvents/Handlers/IntegrationEventHandlers.cs create mode 100644 services/mining-service-net/src/MiningService.API/IntegrationEvents/IntegrationEvents.cs diff --git a/services/chat-service-net/src/ChatService.API/Program.cs b/services/chat-service-net/src/ChatService.API/Program.cs index c7f0ab31..1cc38264 100644 --- a/services/chat-service-net/src/ChatService.API/Program.cs +++ b/services/chat-service-net/src/ChatService.API/Program.cs @@ -109,6 +109,33 @@ try Version = "v1", Description = "Real-time Chat Service with SignalR, AI Integration / Chat Service thời gian thực với SignalR, tích hợp AI" }); + + // EN: Add JWT Bearer security definition / VI: Thêm định nghĩa bảo mật JWT Bearer + options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Description = "JWT Authorization header using Bearer scheme. Example: \"Bearer {token}\" / Header Authorization JWT sử dụng scheme Bearer. Ví dụ: \"Bearer {token}\"", + Name = "Authorization", + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }); + + // EN: Add security requirement / VI: Thêm yêu cầu bảo mật + options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); }); // EN: Add health checks / VI: Thêm health checks diff --git a/services/mining-service-net/src/MiningService.API/Controllers/AdminController.cs b/services/mining-service-net/src/MiningService.API/Controllers/AdminController.cs index e442a477..ee0963d6 100644 --- a/services/mining-service-net/src/MiningService.API/Controllers/AdminController.cs +++ b/services/mining-service-net/src/MiningService.API/Controllers/AdminController.cs @@ -1,4 +1,5 @@ using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiningService.API.Application.Commands; using MiningService.API.Application.Queries; @@ -11,6 +12,7 @@ namespace MiningService.API.Controllers; /// [ApiController] [Route("api/v1/admin")] +[Authorize(Roles = "Admin")] public class AdminController : ControllerBase { private readonly IMediator _mediator; diff --git a/services/mining-service-net/src/MiningService.API/Controllers/CirclesController.cs b/services/mining-service-net/src/MiningService.API/Controllers/CirclesController.cs index 4196e480..c314800e 100644 --- a/services/mining-service-net/src/MiningService.API/Controllers/CirclesController.cs +++ b/services/mining-service-net/src/MiningService.API/Controllers/CirclesController.cs @@ -1,4 +1,5 @@ using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiningService.API.Application.Commands; using MiningService.API.Application.Queries; @@ -11,6 +12,7 @@ namespace MiningService.API.Controllers; /// [ApiController] [Route("api/v1/[controller]")] +[Authorize] public class CirclesController : ControllerBase { private readonly IMediator _mediator; diff --git a/services/mining-service-net/src/MiningService.API/Controllers/MiningController.cs b/services/mining-service-net/src/MiningService.API/Controllers/MiningController.cs index 5e550f15..dc033223 100644 --- a/services/mining-service-net/src/MiningService.API/Controllers/MiningController.cs +++ b/services/mining-service-net/src/MiningService.API/Controllers/MiningController.cs @@ -1,4 +1,5 @@ using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiningService.API.Application.Commands; using MiningService.API.Application.Queries; @@ -11,6 +12,7 @@ namespace MiningService.API.Controllers; /// [ApiController] [Route("api/v1/[controller]")] +[Authorize] public class MiningController : ControllerBase { private readonly IMediator _mediator; diff --git a/services/mining-service-net/src/MiningService.API/Controllers/ReferralsController.cs b/services/mining-service-net/src/MiningService.API/Controllers/ReferralsController.cs index 9757176f..e2f4a2e6 100644 --- a/services/mining-service-net/src/MiningService.API/Controllers/ReferralsController.cs +++ b/services/mining-service-net/src/MiningService.API/Controllers/ReferralsController.cs @@ -1,4 +1,5 @@ using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiningService.API.Application.Commands; using MiningService.API.Application.Queries; @@ -11,6 +12,7 @@ namespace MiningService.API.Controllers; /// [ApiController] [Route("api/v1/[controller]")] +[Authorize] public class ReferralsController : ControllerBase { private readonly IMediator _mediator; diff --git a/services/mining-service-net/src/MiningService.API/Extensions/ExternalServicesExtensions.cs b/services/mining-service-net/src/MiningService.API/Extensions/ExternalServicesExtensions.cs new file mode 100644 index 00000000..a51ca179 --- /dev/null +++ b/services/mining-service-net/src/MiningService.API/Extensions/ExternalServicesExtensions.cs @@ -0,0 +1,61 @@ +using MiningService.API.ExternalServices; +using Polly; +using Polly.Extensions.Http; + +namespace MiningService.API.Extensions; + +/// +/// EN: Extension methods for registering external service clients. +/// VI: Extension methods để đăng ký các external service clients. +/// +public static class ExternalServicesExtensions +{ + public static IServiceCollection AddExternalServices(this IServiceCollection services, IConfiguration configuration) + { + // IAM Service Client + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(configuration["ExternalServices:IamService:BaseUrl"] ?? "http://iam-service-net:8080"); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.Timeout = TimeSpan.FromSeconds(30); + }) + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + // Wallet Service Client + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(configuration["ExternalServices:WalletService:BaseUrl"] ?? "http://wallet-service-net:8080"); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.Timeout = TimeSpan.FromSeconds(30); + }) + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + // Social Service Client + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(configuration["ExternalServices:SocialService:BaseUrl"] ?? "http://social-service-net:8080"); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.Timeout = TimeSpan.FromSeconds(30); + }) + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + return services; + } + + private static IAsyncPolicy GetRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + } + + private static IAsyncPolicy GetCircuitBreakerPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)); + } +} diff --git a/services/mining-service-net/src/MiningService.API/ExternalServices/IamServiceClient.cs b/services/mining-service-net/src/MiningService.API/ExternalServices/IamServiceClient.cs new file mode 100644 index 00000000..de879828 --- /dev/null +++ b/services/mining-service-net/src/MiningService.API/ExternalServices/IamServiceClient.cs @@ -0,0 +1,60 @@ +namespace MiningService.API.ExternalServices; + +/// +/// EN: Client interface for IAM Service communication. +/// VI: Interface client giao tiếp với IAM Service. +/// +public interface IIamServiceClient +{ + Task GetUserInfoAsync(Guid userId, CancellationToken cancellationToken = default); + Task ValidateUserAsync(Guid userId, CancellationToken cancellationToken = default); +} + +public record UserInfo( + Guid UserId, + string Email, + string DisplayName, + bool IsEmailVerified, + bool IsKycVerified, + DateTime CreatedAt); + +/// +/// EN: HTTP client implementation for IAM Service. +/// VI: Triển khai HTTP client cho IAM Service. +/// +public class IamServiceClient : IIamServiceClient +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public IamServiceClient(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task GetUserInfoAsync(Guid userId, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.GetAsync($"/api/v1/users/{userId}", cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to get user info for {UserId}: {StatusCode}", userId, response.StatusCode); + return null; + } + return await response.Content.ReadFromJsonAsync(cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling IAM Service for user {UserId}", userId); + return null; + } + } + + public async Task ValidateUserAsync(Guid userId, CancellationToken cancellationToken = default) + { + var userInfo = await GetUserInfoAsync(userId, cancellationToken); + return userInfo != null && userInfo.IsEmailVerified; + } +} diff --git a/services/mining-service-net/src/MiningService.API/ExternalServices/SocialServiceClient.cs b/services/mining-service-net/src/MiningService.API/ExternalServices/SocialServiceClient.cs new file mode 100644 index 00000000..323c139f --- /dev/null +++ b/services/mining-service-net/src/MiningService.API/ExternalServices/SocialServiceClient.cs @@ -0,0 +1,66 @@ +namespace MiningService.API.ExternalServices; + +/// +/// EN: Client interface for Social Service communication. +/// VI: Interface client giao tiếp với Social Service. +/// +public interface ISocialServiceClient +{ + Task> GetFriendSuggestionsAsync(Guid userId, int limit = 10, CancellationToken cancellationToken = default); + Task AreFriendsAsync(Guid userId1, Guid userId2, CancellationToken cancellationToken = default); +} + +public record FriendSuggestion(Guid UserId, string DisplayName, int MutualFriends, decimal TrustScore); + +/// +/// EN: HTTP client implementation for Social Service. +/// VI: Triển khai HTTP client cho Social Service. +/// +public class SocialServiceClient : ISocialServiceClient +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public SocialServiceClient(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task> GetFriendSuggestionsAsync(Guid userId, int limit = 10, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.GetAsync($"/api/v1/social/friends/{userId}/suggestions?limit={limit}", cancellationToken); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to get friend suggestions for {UserId}: {StatusCode}", userId, response.StatusCode); + return new List(); + } + return await response.Content.ReadFromJsonAsync>(cancellationToken) ?? new(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Social Service for user {UserId}", userId); + return new List(); + } + } + + public async Task AreFriendsAsync(Guid userId1, Guid userId2, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.GetAsync($"/api/v1/social/friends/{userId1}/check/{userId2}", cancellationToken); + if (!response.IsSuccessStatusCode) return false; + var result = await response.Content.ReadFromJsonAsync(cancellationToken); + return result?.AreFriends ?? false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking friendship between {UserId1} and {UserId2}", userId1, userId2); + return false; + } + } + + private record FriendCheckResult(bool AreFriends); +} diff --git a/services/mining-service-net/src/MiningService.API/ExternalServices/WalletServiceClient.cs b/services/mining-service-net/src/MiningService.API/ExternalServices/WalletServiceClient.cs new file mode 100644 index 00000000..29e4247a --- /dev/null +++ b/services/mining-service-net/src/MiningService.API/ExternalServices/WalletServiceClient.cs @@ -0,0 +1,67 @@ +namespace MiningService.API.ExternalServices; + +/// +/// EN: Client interface for Wallet Service communication. +/// VI: Interface client giao tiếp với Wallet Service. +/// +public interface IWalletServiceClient +{ + Task TransferPointsAsync(Guid userId, decimal amount, string description, CancellationToken cancellationToken = default); + Task GetBalanceAsync(Guid userId, CancellationToken cancellationToken = default); +} + +public record WalletBalance(Guid WalletId, Guid UserId, decimal AvailableBalance, decimal PendingBalance); + +/// +/// EN: HTTP client implementation for Wallet Service. +/// VI: Triển khai HTTP client cho Wallet Service. +/// +public class WalletServiceClient : IWalletServiceClient +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public WalletServiceClient(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task TransferPointsAsync(Guid userId, decimal amount, string description, CancellationToken cancellationToken = default) + { + try + { + var request = new { UserId = userId, Amount = amount, Description = description, Source = "Mining" }; + var response = await _httpClient.PostAsJsonAsync("/api/v1/points/add", request, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to transfer points for {UserId}: {StatusCode}", userId, response.StatusCode); + return false; + } + + _logger.LogInformation("Transferred {Amount} points to user {UserId}", amount, userId); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Wallet Service for user {UserId}", userId); + return false; + } + } + + public async Task GetBalanceAsync(Guid userId, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.GetAsync($"/api/v1/wallets/user/{userId}/balance", cancellationToken); + if (!response.IsSuccessStatusCode) return null; + return await response.Content.ReadFromJsonAsync(cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting wallet balance for user {UserId}", userId); + return null; + } + } +} diff --git a/services/mining-service-net/src/MiningService.API/IntegrationEvents/Handlers/IntegrationEventHandlers.cs b/services/mining-service-net/src/MiningService.API/IntegrationEvents/Handlers/IntegrationEventHandlers.cs new file mode 100644 index 00000000..304e85eb --- /dev/null +++ b/services/mining-service-net/src/MiningService.API/IntegrationEvents/Handlers/IntegrationEventHandlers.cs @@ -0,0 +1,105 @@ +using MediatR; +using MiningService.Domain.AggregatesModel.MinerAggregate; +using MiningService.Domain.AggregatesModel.ReferralAggregate; + +namespace MiningService.API.IntegrationEvents.Handlers; + +/// +/// EN: Handler for UserRegisteredIntegrationEvent - creates a new Miner when user registers. +/// VI: Handler cho UserRegisteredIntegrationEvent - tạo Miner mới khi user đăng ký. +/// +public class UserRegisteredIntegrationEventHandler : INotificationHandler +{ + private readonly IMinerRepository _minerRepository; + private readonly IReferralRepository _referralRepository; + private readonly ILogger _logger; + + public UserRegisteredIntegrationEventHandler( + IMinerRepository minerRepository, + IReferralRepository referralRepository, + ILogger logger) + { + _minerRepository = minerRepository; + _referralRepository = referralRepository; + _logger = logger; + } + + public async Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken) + { + var @event = notification.Event; + + // Check if miner already exists + var existingMiner = await _minerRepository.GetByUserIdAsync(@event.UserId, cancellationToken); + if (existingMiner != null) + { + _logger.LogWarning("Miner already exists for user {UserId}", @event.UserId); + return; + } + + // Create new miner + var miner = Miner.Create(@event.UserId); + _minerRepository.Add(miner); + + // If referral code provided, create referral relationship + if (!string.IsNullOrEmpty(@event.ReferralCode)) + { + var referrer = await _minerRepository.GetByReferralCodeAsync(@event.ReferralCode, cancellationToken); + if (referrer != null) + { + var referral = Referral.Create(referrer.Id, miner.Id, @event.ReferralCode); + _referralRepository.Add(referral); + _logger.LogInformation("Created referral from {ReferrerId} to {ReferredId}", referrer.Id, miner.Id); + } + } + + await _minerRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + _logger.LogInformation("Created miner for user {UserId} with referral code {ReferralCode}", + @event.UserId, miner.ReferralCode); + } +} + +/// +/// EN: Handler for UserKycCompletedIntegrationEvent - activates referral when user completes KYC. +/// VI: Handler cho UserKycCompletedIntegrationEvent - kích hoạt referral khi user hoàn thành KYC. +/// +public class UserKycCompletedIntegrationEventHandler : INotificationHandler +{ + private readonly IMinerRepository _minerRepository; + private readonly IReferralRepository _referralRepository; + private readonly ILogger _logger; + + public UserKycCompletedIntegrationEventHandler( + IMinerRepository minerRepository, + IReferralRepository referralRepository, + ILogger logger) + { + _minerRepository = minerRepository; + _referralRepository = referralRepository; + _logger = logger; + } + + public async Task Handle(UserKycCompletedNotification notification, CancellationToken cancellationToken) + { + var @event = notification.Event; + + var miner = await _minerRepository.GetByUserIdAsync(@event.UserId, cancellationToken); + if (miner == null) + { + _logger.LogWarning("Miner not found for user {UserId} during KYC completion", @event.UserId); + return; + } + + // Find inactive referral for this user + var referral = await _referralRepository.GetByReferredIdAsync(miner.Id, cancellationToken); + if (referral != null && !referral.IsActive) + { + referral.Activate(); + _logger.LogInformation("Activated referral {ReferralId} for user {UserId}", referral.Id, @event.UserId); + await _referralRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); + } + } +} + +// MediatR notification wrappers +public record UserRegisteredNotification(UserRegisteredIntegrationEvent Event) : INotification; +public record UserKycCompletedNotification(UserKycCompletedIntegrationEvent Event) : INotification; diff --git a/services/mining-service-net/src/MiningService.API/IntegrationEvents/IntegrationEvents.cs b/services/mining-service-net/src/MiningService.API/IntegrationEvents/IntegrationEvents.cs new file mode 100644 index 00000000..54d65b37 --- /dev/null +++ b/services/mining-service-net/src/MiningService.API/IntegrationEvents/IntegrationEvents.cs @@ -0,0 +1,67 @@ +namespace MiningService.API.IntegrationEvents; + +/// +/// EN: Base interface for integration events. +/// VI: Interface cơ sở cho integration events. +/// +public interface IIntegrationEvent +{ + Guid EventId { get; } + DateTime OccurredOn { get; } +} + +/// +/// EN: Event published when points are mined and should be credited to wallet. +/// VI: Event được publish khi điểm được đào và cần credit vào ví. +/// +public record PointsMinedIntegrationEvent( + Guid EventId, + DateTime OccurredOn, + Guid UserId, + Guid MinerId, + decimal Points, + string Source, + int StreakDays) : IIntegrationEvent; + +/// +/// EN: Event published when a referral is activated (user completed KYC). +/// VI: Event được publish khi referral được kích hoạt (user đã KYC). +/// +public record ReferralActivatedIntegrationEvent( + Guid EventId, + DateTime OccurredOn, + Guid ReferrerId, + Guid ReferredUserId, + decimal BonusRate) : IIntegrationEvent; + +/// +/// EN: Event published when a security circle is completed (3+ members). +/// VI: Event được publish khi vòng tròn an toàn hoàn thành (3+ thành viên). +/// +public record CircleCompletedIntegrationEvent( + Guid EventId, + DateTime OccurredOn, + Guid CircleId, + Guid OwnerId, + int MemberCount, + decimal BonusMultiplier) : IIntegrationEvent; + +/// +/// EN: Event consumed when user registered in IAM Service. +/// VI: Event được consume khi user đăng ký trong IAM Service. +/// +public record UserRegisteredIntegrationEvent( + Guid EventId, + DateTime OccurredOn, + Guid UserId, + string Email, + string? ReferralCode) : IIntegrationEvent; + +/// +/// EN: Event consumed when user completed KYC verification. +/// VI: Event được consume khi user hoàn thành xác thực KYC. +/// +public record UserKycCompletedIntegrationEvent( + Guid EventId, + DateTime OccurredOn, + Guid UserId) : IIntegrationEvent; diff --git a/services/mining-service-net/src/MiningService.API/MiningService.API.csproj b/services/mining-service-net/src/MiningService.API/MiningService.API.csproj index 850353a1..bdbde56d 100644 --- a/services/mining-service-net/src/MiningService.API/MiningService.API.csproj +++ b/services/mining-service-net/src/MiningService.API/MiningService.API.csproj @@ -14,10 +14,12 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/services/mining-service-net/src/MiningService.API/Program.cs b/services/mining-service-net/src/MiningService.API/Program.cs index d8212050..8271c365 100644 --- a/services/mining-service-net/src/MiningService.API/Program.cs +++ b/services/mining-service-net/src/MiningService.API/Program.cs @@ -1,9 +1,15 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.SignalR; +using Microsoft.IdentityModel.Tokens; using MiningService.API.Application.Behaviors; +using MiningService.API.Extensions; +using MiningService.API.Hubs; using MiningService.Infrastructure; using Serilog; +using System.Text; // EN: Configure Serilog early / VI: Cấu hình Serilog sớm Log.Logger = new LoggerConfiguration() @@ -26,6 +32,9 @@ try // EN: Add Infrastructure services / VI: Thêm Infrastructure services builder.Services.AddInfrastructure(builder.Configuration); + // EN: Add External Service clients (IAM, Wallet, Social) / VI: Thêm External Service clients + builder.Services.AddExternalServices(builder.Configuration); + // EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors builder.Services.AddMediatR(cfg => { @@ -38,6 +47,51 @@ try // EN: Add FluentValidation / VI: Thêm FluentValidation builder.Services.AddValidatorsFromAssemblyContaining(); + // EN: Add JWT Authentication / VI: Thêm JWT Authentication + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "your-super-secret-key-min-32-characters-long"; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? "goodgo-platform", + ValidAudience = builder.Configuration["Jwt:Audience"] ?? "goodgo-services", + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)) + }; + + // EN: Support JWT in SignalR query string / VI: Hỗ trợ JWT trong query string SignalR + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); + builder.Services.AddAuthorization(); + + // EN: Add SignalR for real-time updates / VI: Thêm SignalR cho cập nhật thời gian thực + builder.Services.AddSignalR(options => + { + options.EnableDetailedErrors = builder.Environment.IsDevelopment(); + options.KeepAliveInterval = TimeSpan.FromSeconds(15); + }); + builder.Services.AddSingleton(); + // EN: Add API versioning / VI: Thêm API versioning builder.Services.AddApiVersioning(options => { @@ -64,7 +118,7 @@ try builder.Environment.IsDevelopment(); }); - // EN: Add Swagger / VI: Thêm Swagger + // EN: Add Swagger with JWT Bearer / VI: Thêm Swagger với JWT Bearer builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { @@ -72,7 +126,33 @@ try { Title = "MiningService API", Version = "v1", - Description = "MiningService microservice API / API microservice MiningService" + Description = "Pi Network-style Point Mining Service / Dịch vụ đào điểm kiểu Pi Network" + }); + + // EN: Add JWT Bearer security definition / VI: Thêm định nghĩa bảo mật JWT Bearer + options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Description = "JWT Authorization header. Example: \"Bearer {token}\"", + Name = "Authorization", + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }); + + options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } }); }); @@ -90,9 +170,12 @@ try { options.AddDefaultPolicy(policy => { - policy.AllowAnyOrigin() + policy.WithOrigins( + builder.Configuration.GetSection("AllowedOrigins").Get() + ?? ["http://localhost:3000", "http://localhost:5173"]) .AllowAnyMethod() - .AllowAnyHeader(); + .AllowAnyHeader() + .AllowCredentials(); // EN: Required for SignalR / VI: Bắt buộc cho SignalR }); }); @@ -114,6 +197,10 @@ try app.UseCors(); app.UseRouting(); + + // EN: Add Authentication & Authorization middleware / VI: Thêm middleware xác thực & phân quyền + app.UseAuthentication(); + app.UseAuthorization(); // EN: Map health check endpoints / VI: Map health check endpoints app.MapHealthChecks("/health"); @@ -125,6 +212,9 @@ try // EN: Map controllers / VI: Map controllers app.MapControllers(); + + // EN: Map SignalR Hub / VI: Map SignalR Hub + app.MapHub("/hubs/mining"); // EN: Run the application / VI: Chạy ứng dụng app.Run(); @@ -142,3 +232,4 @@ finally // EN: Make Program class accessible for integration tests // VI: Làm cho class Program có thể truy cập cho integration tests public partial class Program { } + diff --git a/services/mission-service-net/docs/en/ARCHITECTURE.md b/services/mission-service-net/docs/en/ARCHITECTURE.md index 703226db..6761aa5b 100644 --- a/services/mission-service-net/docs/en/ARCHITECTURE.md +++ b/services/mission-service-net/docs/en/ARCHITECTURE.md @@ -2,6 +2,8 @@ Technical architecture documentation for the Mission Service. +📖 **See also:** [README - Service Overview](./README.md) + ## High-Level Architecture ```mermaid @@ -196,6 +198,129 @@ classDiagram UserCheckIn --> StreakBonus : applies ``` +### Reward Aggregate + +```mermaid +%%{init: {'theme':'dark'}}%% +classDiagram + class UserReward { + +Guid Id + +Guid UserId + +Guid SourceId + +RewardType Type + +RewardStatus Status + +RewardAmount Amount + +DateTime EarnedAt + +DateTime ClaimedAt + +Claim() + +Expire() + } + + class RewardAmount { + +decimal Points + +decimal BonusPoints + +string Currency + +TotalPoints() + } + + class RewardType { + <> + MISSION_COMPLETE + CHECKIN_DAILY + CHECKIN_MILESTONE + REFERRAL_BONUS + SOCIAL_ACTION + } + + UserReward --> RewardAmount : has + UserReward --> RewardType : uses +``` + +--- + +## Value Objects + +### MissionReward + +```csharp +public record MissionReward : ValueObject +{ + public decimal Points { get; init; } + public decimal MiningBoostPercent { get; init; } + public int ExperiencePoints { get; init; } + public string? BadgeId { get; init; } + + public static MissionReward Create( + decimal points, + decimal miningBoost = 0, + int xp = 0, + string? badge = null) + { + Guard.Against.Negative(points, nameof(points)); + Guard.Against.Negative(miningBoost, nameof(miningBoost)); + + return new MissionReward + { + Points = points, + MiningBoostPercent = miningBoost, + ExperiencePoints = xp, + BadgeId = badge + }; + } +} +``` + +### TaskProgress + +```csharp +public record TaskProgress : ValueObject +{ + public int CurrentValue { get; init; } + public int TargetValue { get; init; } + public DateTime LastUpdated { get; init; } + + public decimal PercentComplete => + TargetValue > 0 + ? Math.Min(100, (CurrentValue * 100m) / TargetValue) + : 0; + + public bool IsComplete => CurrentValue >= TargetValue; + + public TaskProgress UpdateProgress(int newValue) + { + return this with + { + CurrentValue = Math.Min(newValue, TargetValue), + LastUpdated = DateTime.UtcNow + }; + } +} +``` + +### TaskEvidence + +```csharp +public record TaskEvidence : ValueObject +{ + public EvidenceType Type { get; init; } + public string Data { get; init; } = string.Empty; + public string? ScreenshotUrl { get; init; } + public string? VideoUrl { get; init; } + public DateTime CapturedAt { get; init; } + + public enum EvidenceType + { + WatchDuration, // Video watch duration + ClickData, // Click information + UploadedContent, // Uploaded content URL + SocialProof, // Screenshot of social action + InviteCode // Used invite code + } +} +``` + +--- + ## Database Schema ### ER Diagram @@ -360,9 +485,38 @@ flowchart LR style Wallet fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px ``` +### Events Consumed + +```mermaid +%%{init: {'theme':'dark'}}%% +flowchart RL + subgraph Publishers["📤 Publishers"] + IAM[IAM Service] + Mining[Mining Service] + end + + subgraph Events["📨 Integration Events"] + E1[UserRegisteredEvent] + E2[UserDeletedEvent] + E3[ReferralActivatedEvent] + end + + subgraph Mission["📋 Mission Service"] + M[Events Consumer] + end + + IAM --> E1 --> M + IAM --> E2 --> M + Mining --> E3 --> M + + style Events fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px + style Mission fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:3px +``` + ### Event Payloads ```csharp +// Published Events public record MissionCompletedEvent( Guid TaskId, Guid UserId, @@ -382,15 +536,116 @@ public record CheckInCompletedEvent( DateTime CheckedInAt ); -public record RewardClaimedEvent( - Guid TaskId, +// Consumed Events from IAM +public record UserRegisteredEvent( Guid UserId, - decimal PointsAmount, - string RewardType, - DateTime ClaimedAt + string Email, + string DisplayName, + DateTime RegisteredAt +); + +// Consumed Events from Mining +public record ReferralActivatedEvent( + Guid InviterId, + Guid InviteeId, + string InviteCode, + DateTime ActivatedAt ); ``` +--- + +## IAM Service Integration + +### JWT Authentication Flow + +```mermaid +%%{init: {'theme':'dark'}}%% +sequenceDiagram + participant Client as 📱 Client + participant API as 🌐 Mission API + participant Auth as 🔐 JWT Middleware + + Client->>API: Request + Bearer Token + API->>Auth: Validate Token + Auth->>Auth: Verify Signature + Auth->>Auth: Check Expiration + Auth->>Auth: Extract Claims + + alt Token Valid + Auth->>API: UserId, Roles, Permissions + API->>API: Process Request + API-->>Client: 200 OK Response + else Token Invalid + Auth-->>Client: 401 Unauthorized + end +``` + +### IAM Service Client + +```csharp +public interface IIamServiceClient +{ + Task GetUserInfoAsync(Guid userId, CancellationToken ct); + Task> GetUsersByIdsAsync(List userIds, CancellationToken ct); +} + +public record UserInfo( + Guid Id, + string Email, + string DisplayName, + string? AvatarUrl, + DateTime CreatedAt +); +``` + +--- + +## Storage Service Integration + +### Upload Flow (Pay Per Upload) + +```mermaid +%%{init: {'theme':'dark'}}%% +sequenceDiagram + participant Client as 📱 Client + participant API as 🌐 Mission API + participant Storage as 📦 Storage Service + participant AI as 🤖 Content Moderation + + Client->>API: POST /tasks/{id}/upload + API->>Storage: POST /api/v1/upload + Storage-->>API: { fileId, url } + API->>AI: Moderate Content (async) + API-->>Client: 202 Accepted + + Note over AI: Background Processing + AI-->>API: Moderation Result +``` + +### Storage Service Client + +```csharp +public interface IStorageServiceClient +{ + Task UploadFileAsync( + Stream fileStream, + string fileName, + string contentType, + CancellationToken ct); + + Task GetSignedUrlAsync( + Guid fileId, + TimeSpan ttl, + CancellationToken ct); +} + +public record UploadResult(Guid FileId, string Url, long SizeBytes); +public record SignedUrlResult(string SignedUrl, DateTime ExpiresAt); +``` + +--- + ## Service Dependencies ```mermaid diff --git a/services/mission-service-net/docs/en/README.md b/services/mission-service-net/docs/en/README.md index 8cf5fc1d..58d2da93 100644 --- a/services/mission-service-net/docs/en/README.md +++ b/services/mission-service-net/docs/en/README.md @@ -3,6 +3,8 @@ > **EN**: Mission & Task gamification service for GoodGo Platform. > **VI**: Dịch vụ gamification Mission & Task cho GoodGo Platform. +📖 **See also:** [Detailed Architecture Documentation](./ARCHITECTURE.md) + ## Overview The **Mission Service** manages gamification missions and tasks, enabling users to earn rewards through various activities on the GoodGo platform. diff --git a/services/mission-service-net/docs/vi/ARCHITECTURE.md b/services/mission-service-net/docs/vi/ARCHITECTURE.md index 82626e57..239831cf 100644 --- a/services/mission-service-net/docs/vi/ARCHITECTURE.md +++ b/services/mission-service-net/docs/vi/ARCHITECTURE.md @@ -2,6 +2,8 @@ Tài liệu kiến trúc kỹ thuật cho Mission Service. +📖 **Xem thêm:** [README - Tổng Quan Dịch Vụ](./README.md) + ## Kiến Trúc Tổng Quan ```mermaid @@ -196,6 +198,128 @@ classDiagram UserCheckIn --> StreakBonus : áp dụng ``` +### Reward Aggregate + +```mermaid +%%{init: {'theme':'dark'}}%% +classDiagram + class UserReward { + +Guid Id + +Guid UserId + +Guid SourceId + +RewardType Type + +RewardStatus Status + +RewardAmount Amount + +DateTime EarnedAt + +DateTime ClaimedAt + +Claim() + +Expire() + } + + class RewardAmount { + +decimal Points + +decimal BonusPoints + +string Currency + +TotalPoints() + } + + class RewardType { + <> + MISSION_COMPLETE + CHECKIN_DAILY + CHECKIN_MILESTONE + REFERRAL_BONUS + SOCIAL_ACTION + } + + UserReward --> RewardAmount : has + UserReward --> RewardType : uses +``` + +--- + +## Value Objects + +### MissionReward + +```csharp +public record MissionReward : ValueObject +{ + public decimal Points { get; init; } + public decimal MiningBoostPercent { get; init; } + public int ExperiencePoints { get; init; } + public string? BadgeId { get; init; } + + public static MissionReward Create( + decimal points, + decimal miningBoost = 0, + int xp = 0, + string? badge = null) + { + Guard.Against.Negative(points, nameof(points)); + Guard.Against.Negative(miningBoost, nameof(miningBoost)); + + return new MissionReward + { + Points = points, + MiningBoostPercent = miningBoost, + ExperiencePoints = xp, + BadgeId = badge + }; + } +} +``` + +### TaskProgress + +```csharp +public record TaskProgress : ValueObject +{ + public int CurrentValue { get; init; } + public int TargetValue { get; init; } + public DateTime LastUpdated { get; init; } + + public decimal PercentComplete => + TargetValue > 0 + ? Math.Min(100, (CurrentValue * 100m) / TargetValue) + : 0; + + public bool IsComplete => CurrentValue >= TargetValue; + + public TaskProgress UpdateProgress(int newValue) + { + return this with + { + CurrentValue = Math.Min(newValue, TargetValue), + LastUpdated = DateTime.UtcNow + }; + } +} +``` + +### TaskEvidence + +```csharp +public record TaskEvidence : ValueObject +{ + public EvidenceType Type { get; init; } + public string Data { get; init; } = string.Empty; + public string? ScreenshotUrl { get; init; } + public string? VideoUrl { get; init; } + public DateTime CapturedAt { get; init; } + + public enum EvidenceType + { + WatchDuration, // Video xem được bao lâu + ClickData, // Thông tin click + UploadedContent, // URL nội dung tải lên + SocialProof, // Screenshot hành động xã hội + InviteCode // Mã mời đã sử dụng + } +} +``` + +--- ## Database Schema ### ER Diagram @@ -360,9 +484,38 @@ flowchart LR style Wallet fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px ``` +### Các Event Được Tiêu Thụ + +```mermaid +%%{init: {'theme':'dark'}}%% +flowchart RL + subgraph Publishers["📤 Nhà Xuất Bản"] + IAM[IAM Service] + Mining[Mining Service] + end + + subgraph Events["📨 Integration Events"] + E1[UserRegisteredEvent] + E2[UserDeletedEvent] + E3[ReferralActivatedEvent] + end + + subgraph Mission["📋 Mission Service"] + M[Events Consumer] + end + + IAM --> E1 --> M + IAM --> E2 --> M + Mining --> E3 --> M + + style Events fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px + style Mission fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:3px +``` + ### Event Payloads ```csharp +// Events Xuất Bản public record MissionCompletedEvent( Guid TaskId, Guid UserId, @@ -389,8 +542,224 @@ public record RewardClaimedEvent( string RewardType, DateTime ClaimedAt ); + +// Events Tiêu Thụ từ IAM +public record UserRegisteredEvent( + Guid UserId, + string Email, + string DisplayName, + DateTime RegisteredAt +); + +public record UserDeletedEvent( + Guid UserId, + DateTime DeletedAt +); + +// Events Tiêu Thụ từ Mining +public record ReferralActivatedEvent( + Guid InviterId, + Guid InviteeId, + string InviteCode, + DateTime ActivatedAt +); ``` +--- + +## Tích Hợp IAM Service + +### Luồng Xác Thực JWT + +```mermaid +%%{init: {'theme':'dark'}}%% +sequenceDiagram + participant Client as 📱 Client + participant API as 🌐 Mission API + participant Auth as 🔐 JWT Middleware + participant IAM as 👤 IAM Service + + Client->>API: Request + Bearer Token + API->>Auth: Validate Token + Auth->>Auth: Verify Signature (public key) + Auth->>Auth: Check Expiration + Auth->>Auth: Extract Claims + + alt Token Hợp Lệ + Auth->>API: UserId, Roles, Permissions + API->>API: Xử lý Request + API-->>Client: 200 OK Response + else Token Không Hợp Lệ + Auth-->>Client: 401 Unauthorized + else Token Hết Hạn + Auth-->>Client: 401 Token Expired + end +``` + +### Handler UserRegisteredEvent + +```csharp +public class UserRegisteredEventConsumer : IConsumer +{ + private readonly IMissionDbContext _dbContext; + private readonly ILogger _logger; + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + + // Tạo hồ sơ check-in cho user mới + var userCheckIn = new UserCheckIn( + userId: @event.UserId, + currentStreak: 0, + longestStreak: 0, + totalCheckIns: 0 + ); + + _dbContext.UserCheckIns.Add(userCheckIn); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "Created check-in profile for new user {UserId}", + @event.UserId); + } +} +``` + +### IAM Service Client + +```csharp +public interface IIamServiceClient +{ + Task GetUserInfoAsync(Guid userId, CancellationToken ct); + Task> GetUsersByIdsAsync(List userIds, CancellationToken ct); +} + +public record UserInfo( + Guid Id, + string Email, + string DisplayName, + string? AvatarUrl, + DateTime CreatedAt +); +``` + +--- + +## Tích Hợp Storage Service + +### Luồng Upload Nội Dung (Pay Per Upload) + +```mermaid +%%{init: {'theme':'dark'}}%% +sequenceDiagram + participant Client as 📱 Client + participant API as 🌐 Mission API + participant Storage as 📦 Storage Service + participant AI as 🤖 Content Moderation + participant DB as 💾 PostgreSQL + + Client->>API: POST /tasks/{id}/upload + API->>API: Validate Task Status + + API->>Storage: POST /api/v1/upload (multipart) + Storage->>Storage: Validate File (type, size) + Storage->>Storage: Store File + Storage-->>API: { fileId, url, metadata } + + API->>AI: Moderate Content (async) + + API->>DB: Save TaskEvidence { fileUrl } + API-->>Client: 202 Accepted { evidenceId } + + Note over AI,DB: Background Processing + AI-->>API: Moderation Result + API->>DB: Update Verification Status +``` + +### Luồng Lấy Video cho Mission + +```mermaid +%%{init: {'theme':'dark'}}%% +sequenceDiagram + participant Client as 📱 Client + participant API as 🌐 Mission API + participant Storage as 📦 Storage Service + + Client->>API: GET /missions/{id} + API->>API: Get Mission Details + + alt Mission có Video + API->>Storage: GET /api/v1/files/{fileId}/signed-url + Storage->>Storage: Generate Signed URL (TTL: 1h) + Storage-->>API: { signedUrl, expiresAt } + API-->>Client: Mission { ..., videoUrl } + else Mission không có Video + API-->>Client: Mission { ... } + end +``` + +### Storage Service Client + +```csharp +public interface IStorageServiceClient +{ + /// + /// Upload file lên Storage Service + /// + Task UploadFileAsync( + Stream fileStream, + string fileName, + string contentType, + string folder, + CancellationToken ct); + + /// + /// Lấy signed URL để truy cập file + /// + Task GetSignedUrlAsync( + Guid fileId, + TimeSpan ttl, + CancellationToken ct); + + /// + /// Xóa file + /// + Task DeleteFileAsync(Guid fileId, CancellationToken ct); +} + +public record UploadResult( + Guid FileId, + string Url, + string ContentType, + long SizeBytes, + DateTime UploadedAt +); + +public record SignedUrlResult( + string SignedUrl, + DateTime ExpiresAt +); +``` + +### Cấu Hình Storage + +```csharp +public class StorageServiceOptions +{ + public string BaseUrl { get; set; } = "http://storage-service:8080"; + public int TimeoutSeconds { get; set; } = 30; + public long MaxFileSizeBytes { get; set; } = 50 * 1024 * 1024; // 50MB + public string[] AllowedContentTypes { get; set; } = + { + "image/jpeg", "image/png", "image/webp", + "video/mp4", "video/webm" + }; +} +``` + +--- + ## Phụ Thuộc Dịch Vụ ```mermaid diff --git a/services/mission-service-net/docs/vi/README.md b/services/mission-service-net/docs/vi/README.md index 18cda367..e920a865 100644 --- a/services/mission-service-net/docs/vi/README.md +++ b/services/mission-service-net/docs/vi/README.md @@ -3,6 +3,8 @@ > **EN**: Mission & Task gamification service for GoodGo Platform. > **VI**: Dịch vụ gamification Mission & Task cho GoodGo Platform. +📖 **Xem thêm:** [Tài liệu Kiến Trúc Chi Tiết](./ARCHITECTURE.md) + ## Tổng Quan **Mission Service** quản lý các nhiệm vụ (missions) và tác vụ (tasks) gamification, cho phép người dùng kiếm phần thưởng thông qua các hoạt động trên nền tảng GoodGo. @@ -178,6 +180,173 @@ flowchart LR --- +## Chi Tiết Từng Loại Mission + +### 🎬 Xem Video + +```mermaid +%%{init: {'theme':'dark'}}%% +sequenceDiagram + participant U as 📱 User + participant A as 🌐 Mission API + participant V as 🎬 Video Player + participant W as 💰 Wallet + + U->>A: GET /missions?type=VIDEO + A-->>U: Danh sách video missions + U->>A: POST /tasks/start {missionId} + A-->>U: {taskId, videoUrl, requiredDuration} + + U->>V: Bắt đầu xem video + V->>A: POST /tasks/{id}/progress {watchedSeconds} + Note over V,A: Cập nhật định kỳ (mỗi 5s) + + V->>A: POST /tasks/{id}/complete {totalWatched} + A->>A: Xác thực thời lượng xem ≥ yêu cầu + A-->>U: Task hoàn thành! + + U->>A: POST /tasks/{id}/claim + A->>W: GrantPoints(userId, amount) + A-->>U: Đã nhận thưởng! +``` + +| Tham Số | Giá Trị | Mô Tả | +|---------|---------|-------| +| `minWatchPercent` | 80% | Tỷ lệ xem tối thiểu | +| `skipDetection` | true | Phát hiện tua nhanh | +| `antiBot` | true | CAPTCHA cho hoạt động đáng ngờ | +| `reward` | 5-20 MP | Tùy độ dài video | + +--- + +### 💰 Pay Per Click + +```mermaid +%%{init: {'theme':'dark'}}%% +flowchart LR + U[📱 User] --> M[📋 Xem Mission] + M --> C{🖱️ Nhấn Link/Ads} + C --> V[✅ Xác Thực Click] + V --> R[🎁 Nhận Thưởng] + + style R fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px +``` + +| Hành Động | Điểm | Giới Hạn | +|-----------|------|----------| +| Xem quảng cáo | 1 MP | 20/ngày | +| Nhấn link ngoài | 2 MP | 10/ngày | +| Cài đặt app (CPI) | 50 MP | 2/ngày | + +**Quy Tắc Chống Gian Lận:** +- Rate Limit: 10 clicks/ngày +- Cooldown: 30 giây giữa các lần +- Chỉ URL unique + +--- + +### 📤 Pay Per Upload (UGC) + +```mermaid +%%{init: {'theme':'dark'}}%% +flowchart TD + U[📱 Upload Nội Dung] --> T{Loại Nội Dung} + T -->|Ảnh| P[Xem Xét Ảnh] + T -->|Video| V[Xem Xét Video] + T -->|Review| R[Xem Xét Text] + + P --> AI[🤖 AI Moderation] + V --> AI + R --> AI + + AI -->|Pass| A[✅ Duyệt] + AI -->|Flag| M[👨‍💻 Xem Xét Thủ Công] + M --> A + M --> X[❌ Từ Chối] + + A --> RW[🎁 Cấp Thưởng] + + style A fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px + style X fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px +``` + +| Loại Nội Dung | Yêu Cầu | Review | Thưởng | +|---------------|---------|--------|--------| +| Ảnh | 720p, <5MB | AI + Manual | 5-20 MP | +| Video | 480p, 15-60s | AI + Manual | 20-100 MP | +| Text Review | 50-500 ký tự | AI | 3-10 MP | + +--- + +### 👥 Mời Bạn Bè + +```mermaid +%%{init: {'theme':'dark'}}%% +flowchart LR + subgraph Invite["👥 Mời Bạn Bè"] + A[Chia Sẻ Code] --> B[Bạn Đăng Ký] + B --> C[Bạn Hoàn Thành Mission] + C --> D[✅ Cả Hai Nhận Thưởng] + end + + style D fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px +``` + +| Số Bạn Mời | Thưởng/Bạn | Bonus | +|------------|------------|-------| +| 1-5 người | 10 MP | - | +| 6-20 người | 15 MP | +50% | +| 21+ người | 20 MP | +100% | + +**Tích hợp Mining Service:** +- Đồng bộ với `ReferralAggregate` +- Bonus stacking: Mission reward + Mining rate boost + +--- + +### ❤️ Tương Tác Xã Hội + +```mermaid +%%{init: {'theme':'dark'}}%% +flowchart TD + subgraph Actions["Hành Động"] + L[❤️ Like Post] + S[🔄 Share Content] + F[➕ Follow/Subscribe] + C[💬 Comment] + end + + subgraph Platforms["Nền Tảng Hỗ Trợ"] + FB[Facebook] + TW[Twitter/X] + IG[Instagram] + YT[YouTube] + TK[TikTok] + end + + Actions --> V{Xác Thực} + V -->|OAuth| API[Platform API] + V -->|Screenshot| AI[AI Verify] + V -->|URL Check| SC[Scraper] + + style V fill:#E67E22,color:#ECF0F1,stroke:#D35400,stroke-width:2px +``` + +| Phương Thức Xác Thực | Độ Chính Xác | Tốc Độ | Chi Phí | +|---------------------|--------------|--------|---------| +| OAuth API | Cao | Nhanh | Trung bình | +| Screenshot AI | Trung bình | TB | Thấp | +| URL Scraping | Thấp | Nhanh | Miễn phí | + +| Hành Động | Điểm | Giới Hạn | +|-----------|------|----------| +| Like | 1 MP | 10/ngày | +| Share | 3 MP | 5/ngày | +| Follow/Subscribe | 5 MP | 3/ngày | +| Comment | 2 MP | 10/ngày | + +--- + ## Hệ Thống Điểm Danh Hàng Ngày ### 🔥 Thưởng Streak @@ -365,13 +534,99 @@ flowchart LR ### Biến Môi Trường -| Biến | Mô Tả | Bắt Buộc | -|------|-------|----------| -| `DATABASE_URL` | Kết nối PostgreSQL | Có | -| `REDIS_URL` | Kết nối Redis | Có | -| `RABBITMQ_URL` | Kết nối RabbitMQ | Có | -| `JWT_AUTHORITY` | URL phát hành JWT | Có | -| `MAX_DAILY_TASKS` | Số task tối đa/ngày | Không | +| Biến | Mô Tả | Bắt Buộc | Mặc Định | +|------|-------|----------|----------| +| `DATABASE_URL` | Kết nối PostgreSQL | Có | - | +| `REDIS_URL` | Kết nối Redis | Có | - | +| `RABBITMQ_URL` | Kết nối RabbitMQ | Có | - | +| `JWT_AUTHORITY` | URL phát hành JWT | Có | - | +| `MAX_DAILY_TASKS` | Số task tối đa/ngày | Không | 50 | +| `CHECKIN_STREAK_ENABLED` | Bật thưởng streak | Không | true | +| `CONTENT_MODERATION_API` | API AI moderation | Không | - | + +--- + +## Admin Configuration Entity + +### MissionConfiguration Aggregate + +```mermaid +%%{init: {'theme':'dark'}}%% +classDiagram + class MissionConfiguration { + +Guid Id + +bool IsGloballyEnabled + +int MaxDailyTasks + +int TaskCooldownSeconds + +DateTime UpdatedAt + +Guid UpdatedBy + } + + class CheckInConfiguration { + +Guid Id + +List~StreakTier~ Tiers + +int BasePoints + +bool StreakEnabled + } + + class StreakTier { + +int MinDays + +int MaxDays + +int DailyPoints + +int MilestoneBonus + +string BadgeName + } + + class RewardConfiguration { + +Guid Id + +decimal VideoRewardMin + +decimal VideoRewardMax + +decimal ClickReward + +decimal UploadRewardMin + +decimal UploadRewardMax + +decimal InviteReward + +decimal SocialReward + } + + MissionConfiguration --> CheckInConfiguration + MissionConfiguration --> RewardConfiguration + CheckInConfiguration --> "*" StreakTier +``` + +### Giá Trị Cấu Hình Mặc Định + +| Danh Mục | Tham Số | Mặc Định | Mô Tả | +|----------|---------|----------|-------| +| **Hệ Thống** | Bật Toàn Cục | true | Công tắc khẩn cấp | +| **Hệ Thống** | Task Tối Đa/Ngày | 50 | Giới hạn tasks | +| **Hệ Thống** | Cooldown Tasks | 60 giây | Thời gian chờ | +| **Video** | Thưởng Min | 5 MP | Video ngắn | +| **Video** | Thưởng Max | 20 MP | Video dài | +| **Video** | Tỷ Lệ Xem Min | 80% | Phần trăm xem tối thiểu | +| **Click** | Thưởng/Click | 1-2 MP | Tùy loại link | +| **Click** | Giới Hạn/Ngày | 20 | Số clicks tối đa | +| **Upload** | Thưởng Ảnh | 5-20 MP | Tùy chất lượng | +| **Upload** | Thưởng Video | 20-100 MP | Tùy độ dài | +| **Invite** | Thưởng/Bạn | 10-20 MP | Tùy số lượng | +| **Social** | Like/Share | 1-3 MP | Tùy hành động | +| **Check-in** | Điểm Ngày 1-6 | 2 MP | Điểm cơ bản | +| **Check-in** | Bonus Ngày 7 | 20 MP | Mốc tuần đầu | +| **Check-in** | Bonus Ngày 30 | 100 MP | Mốc tháng | + +### Nhật Ký Cấu Hình (Audit Log) + +```csharp +public record ConfigAuditLog( + Guid Id, + Guid AdminUserId, + string ConfigType, // "Mission" | "CheckIn" | "Reward" + string PreviousValue, // JSON cấu hình cũ + string NewValue, // JSON cấu hình mới + string Reason, // Lý do admin thay đổi + DateTime CreatedAt, + string IpAddress +); +``` --- diff --git a/services/social-service-net/src/SocialService.API/Program.cs b/services/social-service-net/src/SocialService.API/Program.cs index 7f875345..b750dca9 100644 --- a/services/social-service-net/src/SocialService.API/Program.cs +++ b/services/social-service-net/src/SocialService.API/Program.cs @@ -74,6 +74,32 @@ try Version = "v1", Description = "SocialService microservice API / API microservice SocialService" }); + + // EN: Add JWT Bearer authentication / VI: Thêm xác thực JWT Bearer + options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'", + Name = "Authorization", + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }); + + options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); }); // EN: Add health checks / VI: Thêm health checks