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>
This commit is contained in:
180
CTO_FIX_TRACKER.md
Normal file
180
CTO_FIX_TRACKER.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# CTO Fix Tracker — Post-Audit Action Plan
|
||||
|
||||
> Generated: 2026-03-13 | Source: Per-service code audit (24 SERVICE_DOCS.md)
|
||||
> Status: IN PROGRESS
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Audit 24 microservices phát hiện **6 loại vấn đề cross-cutting** và **nhiều bug cụ thể per-service**.
|
||||
Ưu tiên theo impact: Security > Runtime Bugs > Code Quality > Tech Debt.
|
||||
|
||||
---
|
||||
|
||||
## P0 — CRITICAL (Security & Runtime Failures)
|
||||
|
||||
### P0-1: Missing Authentication/Authorization
|
||||
**Impact**: Tất cả endpoints public, bất kỳ ai cũng gọi được API
|
||||
**Affected**: 18/24 services (chỉ IAM + merchant có auth đầy đủ)
|
||||
|
||||
| Service | Status | Fix |
|
||||
|---------|--------|-----|
|
||||
| catalog-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| order-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| booking-service-net | Public endpoints (only admin has auth) | Add [Authorize] to public controllers |
|
||||
| fnb-engine-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| inventory-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| social-service-net | No JWT middleware in pipeline | Add UseAuthentication/UseAuthorization |
|
||||
| mining-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| chat-service-net | Has [Authorize] ✅ | OK |
|
||||
| membership-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| wallet-service-net | Has [Authorize] ✅ | OK |
|
||||
| storage-service-net | Has [Authorize] ✅ | OK |
|
||||
| ads-manager-service-net | No auth middleware | Add UseAuthentication/UseAuthorization |
|
||||
| ads-serving-service-net | No auth middleware | Add UseAuthentication/UseAuthorization |
|
||||
| ads-billing-service-net | No auth middleware | Add UseAuthentication/UseAuthorization |
|
||||
| ads-tracking-service-net | No auth middleware | Add UseAuthentication/UseAuthorization |
|
||||
| ads-analytics-service-net | No auth middleware | Add UseAuthentication/UseAuthorization |
|
||||
| promotion-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| mission-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| mkt-facebook-service-net | No auth middleware | Add UseAuthentication/UseAuthorization |
|
||||
| mkt-whatsapp-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| mkt-x-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
| mkt-zalo-service-net | No [Authorize] | Add auth middleware + attributes |
|
||||
|
||||
### P0-2: Template Artifacts (Runtime Failures)
|
||||
**Impact**: Services connect to wrong database or fail to build Docker image
|
||||
|
||||
| Service | Issue | Fix |
|
||||
|---------|-------|-----|
|
||||
| mission-service-net | DB name `myservice_db` instead of `mission_service` | Fix appsettings connection string |
|
||||
| mkt-facebook-service-net | Dockerfile references `MyService.API` | Rename to FacebookService.API |
|
||||
| mkt-whatsapp-service-net | DbContext file named `MyServiceContext.cs`, DB `myservice_db` | Rename file + fix connection string |
|
||||
| promotion-service-net | docker-compose uses template naming | Fix service naming |
|
||||
|
||||
### P0-3: Critical Handler Bugs
|
||||
|
||||
| Service | Bug | Impact |
|
||||
|---------|-----|--------|
|
||||
| ads-tracking-service-net | `TrackPixelEventCommandHandler` creates PixelEvent but NEVER persists | All tracking data lost |
|
||||
| ads-tracking-service-net | `RecordConversionCommand` has handler but NO controller exposes it | Dead code |
|
||||
| booking-service-net | `UpdateResourceCommand` accepts Name/Capacity but only applies IsActive | Silent data loss |
|
||||
| ads-manager-service-net | `ListPendingAdsQuery` filters "Pending" but enum is "pending_review" | Always returns empty |
|
||||
| mining-service-net | `BanMinerCommand` calls Suspend() not Ban(); `ResetMinerStreakCommand` is no-op | Admin actions broken |
|
||||
| order-service-net | Missing DB columns referenced by Dapper queries | Runtime SQL errors |
|
||||
| mkt-x-service-net | Only ISampleRepository in DI; 8 other repos missing registration | Runtime DI failures |
|
||||
|
||||
---
|
||||
|
||||
## P1 — HIGH (Data Integrity & Correctness)
|
||||
|
||||
### P1-1: Missing FluentValidation Validators
|
||||
**Impact**: Invalid data enters system without validation
|
||||
|
||||
| Service | Commands without validators |
|
||||
|---------|---------------------------|
|
||||
| ads-manager-service-net | ALL 10 commands |
|
||||
| ads-serving-service-net | ALL queries (no commands exist) |
|
||||
| ads-billing-service-net | ALL 3 commands |
|
||||
| ads-tracking-service-net | 2/3 commands |
|
||||
| ads-analytics-service-net | ALL commands |
|
||||
| mining-service-net | ALL commands |
|
||||
| mission-service-net | ALL 4 commands |
|
||||
| promotion-service-net | ALL 12 commands |
|
||||
| social-service-net | ALL 8 commands |
|
||||
|
||||
### P1-2: Missing Command/Query Handlers
|
||||
|
||||
| Service | Missing Handler |
|
||||
|---------|----------------|
|
||||
| promotion-service-net | ExchangeVoucherCommand, PurchaseVoucherCommand (no handlers) |
|
||||
| promotion-service-net | SearchVouchersQuery, GetCampaignStatisticsQuery, GetCampaignVouchersQuery (no handlers) |
|
||||
| mission-service-net | GetUserMissionProgressQuery (no handler) |
|
||||
| mkt-facebook-service-net | GetConversationsQuery, GetCustomersQuery (no handlers) |
|
||||
| mkt-whatsapp-service-net | GetConversationsQuery (no handler, controller queries repo directly) |
|
||||
| ads-manager-service-net | Audience query handlers missing |
|
||||
|
||||
### P1-3: Repository Pattern Violations
|
||||
|
||||
| Service | Issue |
|
||||
|---------|-------|
|
||||
| catalog-service-net | Category handlers use DbContext directly, bypass repository |
|
||||
| booking-service-net | 3 repo interfaces in Infrastructure instead of Domain |
|
||||
| ads-billing-service-net | No repository pattern at all, direct DbContext |
|
||||
| ads-analytics-service-net | No repository pattern |
|
||||
| ads-serving-service-net | No repository pattern |
|
||||
|
||||
---
|
||||
|
||||
## P2 — MEDIUM (Code Quality & Conventions)
|
||||
|
||||
### P2-1: Response Format Inconsistency
|
||||
Standard: `{ success: bool, data: T }` — Many services return raw DTOs
|
||||
|
||||
| Service | Issue |
|
||||
|---------|-------|
|
||||
| chat-service-net | Returns raw DTOs |
|
||||
| membership-service-net | Mixed (Members raw, StampCards wrapped) |
|
||||
| social-service-net | Returns raw DTOs |
|
||||
| ads-* services | Returns raw DTOs |
|
||||
| booking-service-net | Returns raw DTOs |
|
||||
|
||||
### P2-2: Domain Events Defined but No Handlers
|
||||
|
||||
| Service | Unused Events |
|
||||
|---------|--------------|
|
||||
| membership-service-net | MembershipLevelChangedDomainEvent (never raised) |
|
||||
| social-service-net | UserUnblockedDomainEvent (never raised) |
|
||||
| ads-manager-service-net | All events dispatched but no handlers |
|
||||
| promotion-service-net | VoucherRedeemedDomainEvent (never consumed) |
|
||||
| booking-service-net | Events defined but unused |
|
||||
|
||||
### P2-3: Missing EF Migrations
|
||||
|
||||
| Service | Issue |
|
||||
|---------|-------|
|
||||
| mkt-facebook-service-net | No migrations exist |
|
||||
| ads-billing-service-net | Spurious InvoiceId1 FK column |
|
||||
| ads-analytics-service-net | ClientRequest table missing from migration |
|
||||
|
||||
### P2-4: Unused Dependencies (Tech Debt)
|
||||
Redis, Dapper, Polly registered but unused in: booking, social, mining, mission, promotion, ads-* services
|
||||
|
||||
---
|
||||
|
||||
## Fix Execution Plan
|
||||
|
||||
### Wave 1 — P0 Security + Template (Parallel Agents)
|
||||
- Agent 1: Fix auth for core services (catalog, order, booking, fnb-engine, inventory)
|
||||
- Agent 2: Fix auth for social services (social, mining, membership, mission)
|
||||
- Agent 3: Fix auth for ads services (ads-manager, ads-serving, ads-billing, ads-tracking, ads-analytics)
|
||||
- Agent 4: Fix auth for mkt services (mkt-facebook, mkt-whatsapp, mkt-x, mkt-zalo, promotion)
|
||||
- Agent 5: Fix template artifacts (mission, mkt-facebook, mkt-whatsapp, promotion)
|
||||
- Agent 6: Fix critical handler bugs (ads-tracking, booking, ads-manager, mining, mkt-x)
|
||||
|
||||
### Wave 2 — P1 Validators + Missing Handlers
|
||||
- Agent 7-12: Add FluentValidation per service group
|
||||
- Agent 13-15: Implement missing handlers
|
||||
|
||||
### Wave 3 — P2 Code Quality
|
||||
- Response format standardization
|
||||
- Migration fixes
|
||||
- Cleanup unused dependencies
|
||||
|
||||
---
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
| Wave | Task | Status | Agent | Commit |
|
||||
|------|------|--------|-------|--------|
|
||||
| 1 | Auth: core services | 🔄 TODO | — | — |
|
||||
| 1 | Auth: social services | 🔄 TODO | — | — |
|
||||
| 1 | Auth: ads services | 🔄 TODO | — | — |
|
||||
| 1 | Auth: mkt services | 🔄 TODO | — | — |
|
||||
| 1 | Template artifacts | 🔄 TODO | — | — |
|
||||
| 1 | Critical handler bugs | 🔄 TODO | — | — |
|
||||
| 2 | Validators | 🔄 TODO | — | — |
|
||||
| 2 | Missing handlers | 🔄 TODO | — | — |
|
||||
| 3 | Response format | 🔄 TODO | — | — |
|
||||
| 3 | Migration fixes | 🔄 TODO | — | — |
|
||||
@@ -37,6 +37,9 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
|
||||
|
||||
<!-- EN: JWT Bearer authentication / VI: JWT Bearer authentication -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsAnalyticsService.API.Application.DTOs;
|
||||
|
||||
@@ -9,6 +10,7 @@ namespace AdsAnalyticsService.API.Controllers;
|
||||
/// VI: API Controller phân tích breakdown.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-analytics/campaigns")]
|
||||
[Produces("application/json")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsAnalyticsService.API.Application.DTOs;
|
||||
|
||||
@@ -9,6 +10,7 @@ namespace AdsAnalyticsService.API.Controllers;
|
||||
/// VI: API Controller insights và khuyến nghị.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-analytics/insights")]
|
||||
[Produces("application/json")]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsAnalyticsService.API.Application.Queries;
|
||||
|
||||
@@ -10,6 +11,7 @@ namespace AdsAnalyticsService.API.Controllers;
|
||||
/// VI: API Controller cho metrics phân tích quảng cáo.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-analytics")]
|
||||
[Produces("application/json")]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsAnalyticsService.API.Application.Commands;
|
||||
using AdsAnalyticsService.API.Application.DTOs;
|
||||
@@ -13,6 +14,7 @@ namespace AdsAnalyticsService.API.Controllers;
|
||||
/// VI: API Controller quản lý báo cáo.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-analytics/reports")]
|
||||
[Produces("application/json")]
|
||||
|
||||
@@ -86,6 +86,29 @@ try
|
||||
name: "postgresql",
|
||||
tags: ["db", "postgresql"]);
|
||||
|
||||
// EN: Add JWT Bearer authentication via IAM IdentityServer OIDC discovery
|
||||
// VI: Thêm JWT Bearer authentication qua IAM IdentityServer OIDC discovery
|
||||
var jwtAuthority = builder.Configuration["Jwt:Authority"] ?? "http://localhost:5001";
|
||||
builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = jwtAuthority;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
// EN: In Development, skip signature validation to allow Docker IAM tokens
|
||||
// VI: Trong Development, bỏ qua validate signature để chấp nhận token từ Docker IAM
|
||||
ValidateIssuerSigningKey = builder.Environment.IsDevelopment() ? false : true,
|
||||
SignatureValidator = builder.Environment.IsDevelopment()
|
||||
? (token, _) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add CORS / VI: Thêm CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -116,13 +139,17 @@ try
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
// 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 (anonymous) / VI: Map health check endpoints (không cần xác thực)
|
||||
app.MapHealthChecks("/health").AllowAnonymous();
|
||||
app.MapHealthChecks("/health/live", new()
|
||||
{
|
||||
Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
|
||||
});
|
||||
app.MapHealthChecks("/health/ready");
|
||||
}).AllowAnonymous();
|
||||
app.MapHealthChecks("/health/ready").AllowAnonymous();
|
||||
|
||||
// EN: Map controllers / VI: Map controllers
|
||||
app.MapControllers();
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
|
||||
|
||||
<!-- EN: JWT Bearer authentication / VI: JWT Bearer authentication -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsBillingService.API.Application.Commands;
|
||||
using AdsBillingService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsBillingService.API.Controllers;
|
||||
@@ -10,6 +11,7 @@ namespace AdsBillingService.API.Controllers;
|
||||
/// VI: API Controller quản lý tài khoản billing.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-billing/accounts")]
|
||||
[Produces("application/json")]
|
||||
public class BillingAccountsController : ControllerBase
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using AdsBillingService.Infrastructure;
|
||||
@@ -9,6 +10,7 @@ namespace AdsBillingService.API.Controllers;
|
||||
/// VI: API Controller quản lý hạn mức tín dụng.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-billing/credit-lines")]
|
||||
[Produces("application/json")]
|
||||
public class CreditLinesController : ControllerBase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AdsBillingService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsBillingService.API.Controllers;
|
||||
@@ -9,6 +10,7 @@ namespace AdsBillingService.API.Controllers;
|
||||
/// VI: API Controller quản lý hóa đơn.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-billing/invoices")]
|
||||
[Produces("application/json")]
|
||||
public class InvoicesController : ControllerBase
|
||||
|
||||
@@ -86,6 +86,29 @@ try
|
||||
name: "postgresql",
|
||||
tags: ["db", "postgresql"]);
|
||||
|
||||
// EN: Add JWT Bearer authentication via IAM IdentityServer OIDC discovery
|
||||
// VI: Thêm JWT Bearer authentication qua IAM IdentityServer OIDC discovery
|
||||
var jwtAuthority = builder.Configuration["Jwt:Authority"] ?? "http://localhost:5001";
|
||||
builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = jwtAuthority;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
// EN: In Development, skip signature validation to allow Docker IAM tokens
|
||||
// VI: Trong Development, bỏ qua validate signature để chấp nhận token từ Docker IAM
|
||||
ValidateIssuerSigningKey = builder.Environment.IsDevelopment() ? false : true,
|
||||
SignatureValidator = builder.Environment.IsDevelopment()
|
||||
? (token, _) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add CORS / VI: Thêm CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -116,13 +139,17 @@ try
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
// 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 (anonymous) / VI: Map health check endpoints (không cần xác thực)
|
||||
app.MapHealthChecks("/health").AllowAnonymous();
|
||||
app.MapHealthChecks("/health/live", new()
|
||||
{
|
||||
Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
|
||||
});
|
||||
app.MapHealthChecks("/health/ready");
|
||||
}).AllowAnonymous();
|
||||
app.MapHealthChecks("/health/ready").AllowAnonymous();
|
||||
|
||||
// EN: Map controllers / VI: Map controllers
|
||||
app.MapControllers();
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
|
||||
|
||||
<!-- EN: JWT Bearer authentication / VI: JWT Bearer authentication -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using AdsManagerService.Domain.AggregatesModel.AdAggregate;
|
||||
using AdsManagerService.Infrastructure;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -20,8 +21,10 @@ public class ListPendingAdsQueryHandler : IRequestHandler<ListPendingAdsQuery, L
|
||||
|
||||
public async Task<List<AdDto>> Handle(ListPendingAdsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// EN: Filter by ReviewStatusId matching AdReviewStatus.PendingReview (id=2, name="pending_review").
|
||||
// VI: Lọc theo ReviewStatusId tương ứng AdReviewStatus.PendingReview (id=2, name="pending_review").
|
||||
var ads = await _context.Ads
|
||||
.Where(a => a.ReviewStatus.Name == "Pending")
|
||||
.Where(a => a.ReviewStatusId == AdReviewStatus.PendingReview.Id)
|
||||
.OrderBy(a => a.CreatedAt)
|
||||
.Skip((request.Page - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
@@ -32,7 +35,7 @@ public class ListPendingAdsQueryHandler : IRequestHandler<ListPendingAdsQuery, L
|
||||
Name = a.Name,
|
||||
Format = a.Format.Name,
|
||||
Status = a.Status.Name,
|
||||
ReviewStatus = a.ReviewStatus.Name,
|
||||
ReviewStatus = AdReviewStatus.PendingReview.Name,
|
||||
Headline = a.Headline,
|
||||
PrimaryText = a.PrimaryText,
|
||||
Description = a.Description,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsManagerService.API.Application.Commands;
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -10,6 +11,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller quản lý ad sets.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-manager/adsets")]
|
||||
[Produces("application/json")]
|
||||
public class AdSetsController : ControllerBase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsManagerService.API.Application.Commands;
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -10,6 +11,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller Admin cho duyệt và kiểm duyệt quảng cáo.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/admin/ads-manager/ads")]
|
||||
[Produces("application/json")]
|
||||
public class AdminAdsController : ControllerBase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -9,6 +10,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller Admin cho quản lý và giám sát chiến dịch.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/admin/ads-manager/campaigns")]
|
||||
[Produces("application/json")]
|
||||
public class AdminCampaignsController : ControllerBase
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -8,6 +9,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller Admin cho báo cáo và phân tích.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/admin/ads-manager/reports")]
|
||||
[Produces("application/json")]
|
||||
public class AdminReportsController : ControllerBase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsManagerService.API.Application.Commands;
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -10,6 +11,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller quản lý quảng cáo.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-manager/ads")]
|
||||
[Produces("application/json")]
|
||||
public class AdsController : ControllerBase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -9,6 +10,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller quản lý đối tượng mục tiêu.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-manager/audiences")]
|
||||
[Produces("application/json")]
|
||||
public class AudiencesController : ControllerBase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsManagerService.API.Application.Commands;
|
||||
using AdsManagerService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsManagerService.API.Controllers;
|
||||
@@ -10,6 +11,7 @@ namespace AdsManagerService.API.Controllers;
|
||||
/// VI: API Controller quản lý chiến dịch quảng cáo.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads-manager/campaigns")]
|
||||
[Produces("application/json")]
|
||||
public class CampaignsController : ControllerBase
|
||||
|
||||
@@ -86,6 +86,29 @@ try
|
||||
name: "postgresql",
|
||||
tags: ["db", "postgresql"]);
|
||||
|
||||
// EN: Add JWT Bearer authentication via IAM IdentityServer OIDC discovery
|
||||
// VI: Thêm JWT Bearer authentication qua IAM IdentityServer OIDC discovery
|
||||
var jwtAuthority = builder.Configuration["Jwt:Authority"] ?? "http://localhost:5001";
|
||||
builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = jwtAuthority;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
// EN: In Development, skip signature validation to allow Docker IAM tokens
|
||||
// VI: Trong Development, bỏ qua validate signature để chấp nhận token từ Docker IAM
|
||||
ValidateIssuerSigningKey = builder.Environment.IsDevelopment() ? false : true,
|
||||
SignatureValidator = builder.Environment.IsDevelopment()
|
||||
? (token, _) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add CORS / VI: Thêm CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -116,13 +139,17 @@ try
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
// 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 (anonymous) / VI: Map health check endpoints (không cần xác thực)
|
||||
app.MapHealthChecks("/health").AllowAnonymous();
|
||||
app.MapHealthChecks("/health/live", new()
|
||||
{
|
||||
Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
|
||||
});
|
||||
app.MapHealthChecks("/health/ready");
|
||||
}).AllowAnonymous();
|
||||
app.MapHealthChecks("/health/ready").AllowAnonymous();
|
||||
|
||||
// EN: Map controllers / VI: Map controllers
|
||||
app.MapControllers();
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
|
||||
|
||||
<!-- EN: JWT Bearer authentication / VI: JWT Bearer authentication -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
|
||||
<!-- EN: EF Core Design for migrations / VI: EF Core Design cho migrations -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AdsServingService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsServingService.API.Controllers;
|
||||
@@ -9,6 +10,7 @@ namespace AdsServingService.API.Controllers;
|
||||
/// VI: API Controller Admin để quản lý và giám sát các phiên đấu giá.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/admin/auctions")]
|
||||
[Produces("application/json")]
|
||||
public class AdminAuctionsController : ControllerBase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsServingService.API.Application.Queries;
|
||||
using AdsServingService.Infrastructure;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -11,6 +12,7 @@ namespace AdsServingService.API.Controllers;
|
||||
/// VI: API Controller Admin để quản lý điều tiết ngân sách.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/admin/budget")]
|
||||
[Produces("application/json")]
|
||||
public class AdminBudgetController : ControllerBase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AdsServingService.Domain.AggregatesModel.FrequencyAggregate;
|
||||
using AdsServingService.Infrastructure;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -10,6 +11,7 @@ namespace AdsServingService.API.Controllers;
|
||||
/// VI: API Controller Admin để quản lý frequency caps.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/admin/frequency")]
|
||||
[Produces("application/json")]
|
||||
public class AdminFrequencyController : ControllerBase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AdsServingService.API.Application.Queries;
|
||||
using AdsServingService.API.Application.Events;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AdsServingService.API.Controllers;
|
||||
@@ -10,6 +11,7 @@ namespace AdsServingService.API.Controllers;
|
||||
/// VI: API Controller serve quảng cáo theo thời gian thực.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/ads")]
|
||||
[Produces("application/json")]
|
||||
public class AdsController : ControllerBase
|
||||
|
||||
@@ -91,6 +91,29 @@ try
|
||||
name: "postgresql",
|
||||
tags: ["db", "postgresql"]);
|
||||
|
||||
// EN: Add JWT Bearer authentication via IAM IdentityServer OIDC discovery
|
||||
// VI: Thêm JWT Bearer authentication qua IAM IdentityServer OIDC discovery
|
||||
var jwtAuthority = builder.Configuration["Jwt:Authority"] ?? "http://localhost:5001";
|
||||
builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = jwtAuthority;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
// EN: In Development, skip signature validation to allow Docker IAM tokens
|
||||
// VI: Trong Development, bỏ qua validate signature để chấp nhận token từ Docker IAM
|
||||
ValidateIssuerSigningKey = builder.Environment.IsDevelopment() ? false : true,
|
||||
SignatureValidator = builder.Environment.IsDevelopment()
|
||||
? (token, _) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add CORS / VI: Thêm CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -121,13 +144,17 @@ try
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
// 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 (anonymous) / VI: Map health check endpoints (không cần xác thực)
|
||||
app.MapHealthChecks("/health").AllowAnonymous();
|
||||
app.MapHealthChecks("/health/live", new()
|
||||
{
|
||||
Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
|
||||
});
|
||||
app.MapHealthChecks("/health/ready");
|
||||
}).AllowAnonymous();
|
||||
app.MapHealthChecks("/health/ready").AllowAnonymous();
|
||||
|
||||
// EN: Map controllers / VI: Map controllers
|
||||
app.MapControllers();
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
|
||||
|
||||
<!-- EN: JWT Bearer authentication / VI: JWT Bearer authentication -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using MediatR;
|
||||
using AdsTrackingService.Domain.AggregatesModel.TrackingPixelAggregate;
|
||||
using AdsTrackingService.Infrastructure;
|
||||
|
||||
namespace AdsTrackingService.API.Application.Commands;
|
||||
|
||||
@@ -10,13 +11,16 @@ namespace AdsTrackingService.API.Application.Commands;
|
||||
public class TrackPixelEventCommandHandler : IRequestHandler<TrackPixelEventCommand, bool>
|
||||
{
|
||||
private readonly ITrackingPixelRepository _repository;
|
||||
private readonly AdsTrackingServiceContext _context;
|
||||
private readonly ILogger<TrackPixelEventCommandHandler> _logger;
|
||||
|
||||
public TrackPixelEventCommandHandler(
|
||||
ITrackingPixelRepository repository,
|
||||
AdsTrackingServiceContext context,
|
||||
ILogger<TrackPixelEventCommandHandler> logger)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -37,8 +41,8 @@ public class TrackPixelEventCommandHandler : IRequestHandler<TrackPixelEventComm
|
||||
return false;
|
||||
}
|
||||
|
||||
// EN: Create pixel event
|
||||
// VI: Tạo pixel event
|
||||
// EN: Create pixel event and persist to database
|
||||
// VI: Tạo pixel event và lưu vào database
|
||||
var pixelEvent = new PixelEvent(
|
||||
pixel.Id,
|
||||
request.AdId,
|
||||
@@ -48,10 +52,11 @@ public class TrackPixelEventCommandHandler : IRequestHandler<TrackPixelEventComm
|
||||
request.IpAddress
|
||||
);
|
||||
|
||||
// EN: Note: In a real implementation, you would add PixelEvent to a repository
|
||||
// VI: Lưu ý: Trong triển khai thực tế, bạn sẽ thêm PixelEvent vào repository
|
||||
// For now, we just log it as PixelEvent is not an aggregate root
|
||||
|
||||
// EN: Add pixel event to DbContext and save
|
||||
// VI: Thêm pixel event vào DbContext và lưu
|
||||
await _context.PixelEvents.AddAsync(pixelEvent, ct);
|
||||
await _context.SaveEntitiesAsync(ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Tracked pixel event: PixelCode={PixelCode}, AdId={AdId}, UserId={UserId}, EventType={EventType}",
|
||||
request.PixelCode, request.AdId, request.UserId, request.EventType);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsTrackingService.API.Application.Commands;
|
||||
using AdsTrackingService.API.Application.Queries;
|
||||
|
||||
namespace AdsTrackingService.API.Controllers;
|
||||
@@ -10,6 +12,7 @@ namespace AdsTrackingService.API.Controllers;
|
||||
/// VI: Controller theo dõi conversion và attribution.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-tracking/conversions")]
|
||||
public class ConversionsController : ControllerBase
|
||||
@@ -23,6 +26,35 @@ public class ConversionsController : ControllerBase
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Record a new conversion event.
|
||||
/// VI: Ghi nhận sự kiện conversion mới.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ConversionResult), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> RecordConversion(
|
||||
[FromBody] RecordConversionRequest request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var command = new RecordConversionCommand(
|
||||
request.AdvertiserId,
|
||||
request.CampaignId,
|
||||
request.UserId,
|
||||
request.ConversionType,
|
||||
request.ConversionValue,
|
||||
request.Currency ?? "VND"
|
||||
);
|
||||
|
||||
var result = await _mediator.Send(command, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Recorded conversion: Id={ConversionId}, Campaign={CampaignId}, Type={ConversionType}",
|
||||
result.ConversionId, request.CampaignId, request.ConversionType);
|
||||
|
||||
return CreatedAtAction(null, new { success = true, data = result });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get conversions with optional filtering.
|
||||
/// VI: Lấy danh sách conversions với bộ lọc tùy chọn.
|
||||
@@ -66,3 +98,16 @@ public class ConversionsController : ControllerBase
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request to record a conversion event.
|
||||
/// VI: Request ghi nhận sự kiện conversion.
|
||||
/// </summary>
|
||||
public record RecordConversionRequest(
|
||||
Guid AdvertiserId,
|
||||
Guid CampaignId,
|
||||
Guid UserId,
|
||||
string ConversionType,
|
||||
decimal ConversionValue,
|
||||
string? Currency
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsTrackingService.API.Application.Commands;
|
||||
using AdsTrackingService.Domain.AggregatesModel.TrackingPixelAggregate;
|
||||
@@ -11,6 +12,7 @@ namespace AdsTrackingService.API.Controllers;
|
||||
/// VI: Controller theo dõi sự kiện pixel.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-tracking/events")]
|
||||
public class EventsController : ControllerBase
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdsTrackingService.API.Application.Commands;
|
||||
using AdsTrackingService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace AdsTrackingService.API.Controllers;
|
||||
/// VI: Controller quản lý tracking pixel.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/ads-tracking/pixels")]
|
||||
public class PixelsController : ControllerBase
|
||||
|
||||
@@ -86,6 +86,29 @@ try
|
||||
name: "postgresql",
|
||||
tags: ["db", "postgresql"]);
|
||||
|
||||
// EN: Add JWT Bearer authentication via IAM IdentityServer OIDC discovery
|
||||
// VI: Thêm JWT Bearer authentication qua IAM IdentityServer OIDC discovery
|
||||
var jwtAuthority = builder.Configuration["Jwt:Authority"] ?? "http://localhost:5001";
|
||||
builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = jwtAuthority;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
// EN: In Development, skip signature validation to allow Docker IAM tokens
|
||||
// VI: Trong Development, bỏ qua validate signature để chấp nhận token từ Docker IAM
|
||||
ValidateIssuerSigningKey = builder.Environment.IsDevelopment() ? false : true,
|
||||
SignatureValidator = builder.Environment.IsDevelopment()
|
||||
? (token, _) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token)
|
||||
: null,
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add CORS / VI: Thêm CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -116,13 +139,17 @@ try
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
// 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 (anonymous) / VI: Map health check endpoints (không cần xác thực)
|
||||
app.MapHealthChecks("/health").AllowAnonymous();
|
||||
app.MapHealthChecks("/health/live", new()
|
||||
{
|
||||
Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
|
||||
});
|
||||
app.MapHealthChecks("/health/ready");
|
||||
}).AllowAnonymous();
|
||||
app.MapHealthChecks("/health/ready").AllowAnonymous();
|
||||
|
||||
// EN: Map controllers / VI: Map controllers
|
||||
app.MapControllers();
|
||||
|
||||
@@ -31,8 +31,13 @@ public class UpdateResourceCommandHandler : IRequestHandler<UpdateResourceComman
|
||||
throw new DomainException($"Resource {request.ResourceId} not found");
|
||||
}
|
||||
|
||||
// EN: Update via domain methods
|
||||
// VI: Cập nhật qua domain methods
|
||||
// EN: Update name and capacity via domain methods
|
||||
// VI: Cập nhật tên và sức chứa qua domain methods
|
||||
resource.UpdateName(request.Name);
|
||||
resource.UpdateCapacity(request.Capacity);
|
||||
|
||||
// EN: Update active status
|
||||
// VI: Cập nhật trạng thái hoạt động
|
||||
if (request.IsActive)
|
||||
resource.Activate();
|
||||
else
|
||||
|
||||
@@ -7,6 +7,7 @@ using BookingService.API.Application.Queries;
|
||||
using BookingService.API.Models.Requests;
|
||||
using BookingService.API.Models.Responses;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookingService.API.Controllers;
|
||||
@@ -14,6 +15,7 @@ namespace BookingService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/appointments")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class AppointmentsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -7,6 +7,7 @@ using BookingService.API.Application.Queries;
|
||||
using BookingService.API.Models.Requests;
|
||||
using BookingService.API.Models.Responses;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookingService.API.Controllers;
|
||||
@@ -14,6 +15,7 @@ namespace BookingService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/resources")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ResourcesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -7,6 +7,7 @@ using BookingService.API.Models.Responses;
|
||||
using BookingService.Domain.AggregatesModel.StaffAggregate;
|
||||
using BookingService.Infrastructure.Repositories;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookingService.API.Controllers;
|
||||
@@ -14,6 +15,7 @@ namespace BookingService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/schedules")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class SchedulesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -6,6 +6,7 @@ using BookingService.API.Application.Queries;
|
||||
using BookingService.API.Models.Requests;
|
||||
using BookingService.API.Models.Responses;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookingService.API.Controllers;
|
||||
@@ -13,6 +14,7 @@ namespace BookingService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/slots")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class SlotsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -7,6 +7,7 @@ using BookingService.API.Application.Queries;
|
||||
using BookingService.API.Models.Requests;
|
||||
using BookingService.API.Models.Responses;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookingService.API.Controllers;
|
||||
@@ -14,6 +15,7 @@ namespace BookingService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/staff/{staffId:guid}/schedule")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class StaffSchedulesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -7,6 +7,7 @@ using BookingService.API.Application.Queries.Therapist;
|
||||
using BookingService.API.Models.Requests;
|
||||
using BookingService.API.Models.Responses;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BookingService.API.Controllers;
|
||||
@@ -18,6 +19,7 @@ namespace BookingService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/therapists")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class TherapistsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -50,4 +50,28 @@ public class Resource : Entity, IAggregateRoot
|
||||
|
||||
public void Activate() => _isActive = true;
|
||||
public void Deactivate() => _isActive = false;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update resource name.
|
||||
/// VI: Cập nhật tên tài nguyên.
|
||||
/// </summary>
|
||||
public void UpdateName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new DomainException("Resource name cannot be empty / Tên tài nguyên không được trống");
|
||||
|
||||
_name = name.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update resource capacity.
|
||||
/// VI: Cập nhật sức chứa tài nguyên.
|
||||
/// </summary>
|
||||
public void UpdateCapacity(int capacity)
|
||||
{
|
||||
if (capacity < 0)
|
||||
throw new DomainException("Capacity cannot be negative / Sức chứa không được âm");
|
||||
|
||||
_capacity = capacity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CatalogService.API.Application.Commands;
|
||||
using CatalogService.API.Application.DTOs;
|
||||
@@ -17,6 +18,7 @@ namespace CatalogService.API.Controllers;
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/categories")]
|
||||
[Authorize]
|
||||
public class CategoriesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CatalogService.API.Application.Commands;
|
||||
using CatalogService.API.Application.DTOs;
|
||||
@@ -17,6 +18,7 @@ namespace CatalogService.API.Controllers;
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/products")]
|
||||
[Authorize]
|
||||
public class ProductsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller cho hang doi pha che barista.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FnbEngine.API.Application.Commands;
|
||||
using FnbEngine.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace FnbEngine.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/fnb/barista")]
|
||||
[Authorize]
|
||||
public class BaristaController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller cho hệ thống hiển thị bếp.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FnbEngine.API.Application.Commands;
|
||||
using FnbEngine.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace FnbEngine.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/kitchen")]
|
||||
[Authorize]
|
||||
public class KitchenController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller quản lý đặt bàn.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FnbEngine.API.Application.Commands;
|
||||
using FnbEngine.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace FnbEngine.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/reservations")]
|
||||
[Authorize]
|
||||
public class ReservationsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller quản lý phiên.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FnbEngine.API.Application.Commands;
|
||||
using FnbEngine.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace FnbEngine.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/sessions")]
|
||||
[Authorize]
|
||||
public class SessionsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller quản lý bàn.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FnbEngine.API.Application.Commands;
|
||||
using FnbEngine.API.Application.Queries;
|
||||
@@ -15,6 +16,7 @@ namespace FnbEngine.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/tables")]
|
||||
[Authorize]
|
||||
public class TablesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
@@ -131,6 +133,7 @@ public class TablesController : ControllerBase
|
||||
/// EN: Get table by QR token (public, no auth).
|
||||
/// VI: Lấy bàn theo QR token (public, không cần auth).
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("by-token/{token}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), 200)]
|
||||
[ProducesResponseType(404)]
|
||||
|
||||
@@ -6,6 +6,7 @@ using InventoryService.API.Application.Commands.DeductInventory;
|
||||
using InventoryService.API.Application.DTOs;
|
||||
using InventoryService.API.Application.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
@@ -18,6 +19,7 @@ namespace InventoryService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/inventory")]
|
||||
[SwaggerTag("Inventory Management - Stock operations, reservations, and tracking")]
|
||||
[Authorize]
|
||||
public class InventoryController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -60,8 +60,10 @@ public class BanMinerCommandHandler : IRequestHandler<BanMinerCommand, BanMinerR
|
||||
if (miner == null)
|
||||
throw new MinerNotFoundException(request.MinerId);
|
||||
|
||||
miner.Suspend();
|
||||
// In a real implementation, might have separate Ban status
|
||||
// EN: Call Ban() to permanently ban the miner account
|
||||
// VI: Gọi Ban() để cấm vĩnh viễn tài khoản thợ đào
|
||||
miner.Ban();
|
||||
_minerRepository.Update(miner);
|
||||
await _minerRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return new BanMinerResult(true, $"Miner {request.MinerId} banned successfully");
|
||||
@@ -119,8 +121,10 @@ public class ResetMinerStreakCommandHandler : IRequestHandler<ResetMinerStreakCo
|
||||
if (miner == null)
|
||||
throw new MinerNotFoundException(request.MinerId);
|
||||
|
||||
// Streak is a value object, need to create new one or add method to Miner
|
||||
// For now, just save and return success
|
||||
// EN: Reset the miner's streak via domain method
|
||||
// VI: Reset streak của thợ đào qua domain method
|
||||
miner.ResetStreak();
|
||||
_minerRepository.Update(miner);
|
||||
await _minerRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return new ResetStreakResult(true, $"Streak reset for miner {request.MinerId}");
|
||||
|
||||
@@ -288,6 +288,16 @@ public class Miner : Entity, IAggregateRoot
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Reset miner's mining streak (admin action).
|
||||
/// VI: Reset streak đào của thợ đào (hành động admin).
|
||||
/// </summary>
|
||||
public void ResetStreak()
|
||||
{
|
||||
Streak = Streak.Reset();
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Points Management
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
**Port**: 5000 (development, via launchSettings.json), 8080 (Docker/production)
|
||||
|
||||
**Database**: PostgreSQL (`myservice_db` default in appsettings.json, configurable via `ConnectionStrings:DefaultConnection` or `DATABASE_URL`)
|
||||
**Database**: PostgreSQL (`mission_service` default in appsettings.json, configurable via `ConnectionStrings:DefaultConnection` or `DATABASE_URL`)
|
||||
|
||||
**Framework**: .NET 10.0, C# 14, Clean Architecture + CQRS
|
||||
|
||||
@@ -458,7 +458,7 @@ The `Domain/Events/` directory referenced in the template pattern does not exist
|
||||
|
||||
| Key | Default Value | Description |
|
||||
|-----|---------------|-------------|
|
||||
| `ConnectionStrings:DefaultConnection` | `Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres` | PostgreSQL connection string |
|
||||
| `ConnectionStrings:DefaultConnection` | `Host=localhost;Port=5432;Database=mission_service;Username=postgres;Password=postgres` | PostgreSQL connection string |
|
||||
| `Redis:ConnectionString` | `localhost:6379` | Redis connection string |
|
||||
| `Jwt:Secret` | `your-super-secret-key-min-32-characters` | JWT signing key |
|
||||
| `Jwt:Issuer` | `goodgo-platform` | JWT issuer |
|
||||
@@ -512,5 +512,5 @@ The `Domain/Events/` directory referenced in the template pattern does not exist
|
||||
5. **UserReward aggregate**: Has a full repository and EF configuration but is not used by any command handler — rewards are tracked via `UserTask.RewardClaimed` flag only.
|
||||
6. **Idempotency**: `IRequestManager`/`RequestManager` are registered but not used by any handler or behavior.
|
||||
7. **Redis**: Package referenced and health check configured, but no caching logic exists in the codebase.
|
||||
8. **Database name**: Default in appsettings is `myservice_db` (template default, not renamed to `mission_service`).
|
||||
8. **Database name**: Default in appsettings is `mission_service` (renamed from template default).
|
||||
9. **JWT audience validation**: Disabled (`ValidateAudience = false`, `ValidateIssuer = false`) — relies on IAM service for token signing.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<AssemblyName>MissionService.API</AssemblyName>
|
||||
<RootNamespace>MissionService.API</RootNamespace>
|
||||
<Description>Web API layer with CQRS pattern</Description>
|
||||
<UserSecretsId>myservice-api</UserSecretsId>
|
||||
<UserSecretsId>mission-service-api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
]
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres"
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=mission_service;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "localhost:6379"
|
||||
|
||||
@@ -3,7 +3,7 @@ ASPNETCORE_ENVIRONMENT=Development
|
||||
|
||||
# Database / Cơ Sở Dữ Liệu
|
||||
# PostgreSQL connection string (Neon or local)
|
||||
DATABASE_URL=Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
|
||||
DATABASE_URL=Host=localhost;Port=5432;Database=facebook_service;Username=postgres;Password=postgres
|
||||
|
||||
# Redis Cache
|
||||
REDIS_URL=localhost:6379
|
||||
@@ -18,11 +18,11 @@ JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
|
||||
|
||||
# API Configuration / Cấu Hình API
|
||||
API_PORT=5000
|
||||
API_BASE_PATH=/api/v1/myservice
|
||||
API_BASE_PATH=/api/v1/facebook
|
||||
|
||||
# Observability / Quan Sát
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
OTEL_SERVICE_NAME=myservice
|
||||
OTEL_SERVICE_NAME=facebook-service
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=Information
|
||||
|
||||
@@ -4,14 +4,14 @@ WORKDIR /src
|
||||
|
||||
# EN: Copy project files for layer caching
|
||||
# VI: Sao chép các file project để tận dụng layer caching
|
||||
COPY ["src/MyService.API/MyService.API.csproj", "src/MyService.API/"]
|
||||
COPY ["src/MyService.Domain/MyService.Domain.csproj", "src/MyService.Domain/"]
|
||||
COPY ["src/MyService.Infrastructure/MyService.Infrastructure.csproj", "src/MyService.Infrastructure/"]
|
||||
COPY ["src/FacebookService.API/FacebookService.API.csproj", "src/FacebookService.API/"]
|
||||
COPY ["src/FacebookService.Domain/FacebookService.Domain.csproj", "src/FacebookService.Domain/"]
|
||||
COPY ["src/FacebookService.Infrastructure/FacebookService.Infrastructure.csproj", "src/FacebookService.Infrastructure/"]
|
||||
COPY ["Directory.Build.props", "./"]
|
||||
|
||||
# EN: Restore dependencies
|
||||
# VI: Khôi phục dependencies
|
||||
RUN dotnet restore "src/MyService.API/MyService.API.csproj"
|
||||
RUN dotnet restore "src/FacebookService.API/FacebookService.API.csproj"
|
||||
|
||||
# EN: Copy all source code
|
||||
# VI: Sao chép toàn bộ source code
|
||||
@@ -19,12 +19,12 @@ COPY src/ ./src/
|
||||
|
||||
# EN: Build the application
|
||||
# VI: Build ứng dụng
|
||||
WORKDIR "/src/src/MyService.API"
|
||||
RUN dotnet build "MyService.API.csproj" -c Release -o /app/build --no-restore
|
||||
WORKDIR "/src/src/FacebookService.API"
|
||||
RUN dotnet build "FacebookService.API.csproj" -c Release -o /app/build --no-restore
|
||||
|
||||
# Publish stage / Giai đoạn publish
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MyService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
RUN dotnet publish "FacebookService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||
|
||||
# Runtime stage / Giai đoạn runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
@@ -63,4 +63,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
|
||||
# EN: Start the application
|
||||
# VI: Khởi động ứng dụng
|
||||
ENTRYPOINT ["dotnet", "MyService.API.dll"]
|
||||
ENTRYPOINT ["dotnet", "FacebookService.API.dll"]
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
**Port**: `5000` (Development, from launchSettings.json), `8080` (Docker/Production)
|
||||
|
||||
**Database**: PostgreSQL (`myservice_db` default from appsettings; actual DB name configured via `ConnectionStrings:DefaultConnection` or `DATABASE_URL` env var)
|
||||
**Database**: PostgreSQL (`facebook_service` default from appsettings; actual DB name configured via `ConnectionStrings:DefaultConnection` or `DATABASE_URL` env var)
|
||||
|
||||
**Framework**: .NET 10.0, C# 14, Clean Architecture + CQRS (MediatR)
|
||||
|
||||
@@ -405,7 +405,7 @@ Domain events are dispatched in-process only via `FacebookServiceContext.Dispatc
|
||||
|
||||
| Key | Description | Default |
|
||||
|-----|-------------|---------|
|
||||
| `ConnectionStrings:DefaultConnection` | PostgreSQL connection string | `Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres` |
|
||||
| `ConnectionStrings:DefaultConnection` | PostgreSQL connection string | `Host=localhost;Port=5432;Database=facebook_service;Username=postgres;Password=postgres` |
|
||||
| `DATABASE_URL` | Fallback PostgreSQL connection string (env var) | - |
|
||||
|
||||
### Facebook Configuration
|
||||
@@ -450,7 +450,7 @@ The pipeline executes in order:
|
||||
- Non-root user: `dotnetuser` (UID/GID 1001)
|
||||
- Port: 8080
|
||||
- Healthcheck: `curl -f http://localhost:8080/health/live` (30s interval, 3 retries)
|
||||
- **Note**: Dockerfile still references `MyService.API` naming (template not fully renamed)
|
||||
- **Note**: Dockerfile references `FacebookService.API` naming (fixed from template)
|
||||
|
||||
### Migrations
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ version: '3.8'
|
||||
# VI: Docker Compose cho phát triển local
|
||||
|
||||
services:
|
||||
myservice-api:
|
||||
facebook-service-api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: myservice-api
|
||||
container_name: facebook-service-api
|
||||
ports:
|
||||
- "5000:8080"
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=facebook_service;Username=postgres;Password=postgres
|
||||
- REDIS_URL=redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- myservice-network
|
||||
- facebook-service-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
|
||||
interval: 30s
|
||||
@@ -31,17 +31,17 @@ services:
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: myservice-postgres
|
||||
container_name: facebook-service-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: myservice_db
|
||||
POSTGRES_DB: facebook_service
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- myservice-network
|
||||
- facebook-service-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
@@ -50,13 +50,13 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: myservice-redis
|
||||
container_name: facebook-service-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- myservice-network
|
||||
- facebook-service-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
@@ -68,5 +68,5 @@ volumes:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
myservice-network:
|
||||
facebook-service-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FacebookService.API.Application.Commands;
|
||||
using FacebookService.API.Application.Queries;
|
||||
@@ -15,6 +16,7 @@ namespace FacebookService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/chatbots")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ChatbotsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FacebookService.API.Application.Queries;
|
||||
using FacebookService.API.Application.Dtos;
|
||||
@@ -14,6 +15,7 @@ namespace FacebookService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/conversations")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ConversationsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FacebookService.API.Application.Commands;
|
||||
using FacebookService.API.Application.Queries;
|
||||
@@ -15,6 +16,7 @@ namespace FacebookService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/customers")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class CustomersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using FacebookService.API.Application.Commands;
|
||||
using System.Security.Cryptography;
|
||||
@@ -14,6 +15,7 @@ namespace FacebookService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/webhooks/facebook")]
|
||||
[Produces("application/json")]
|
||||
[AllowAnonymous]
|
||||
public class WebhooksController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<AssemblyName>FacebookService.API</AssemblyName>
|
||||
<RootNamespace>FacebookService.API</RootNamespace>
|
||||
<Description>Web API layer with CQRS pattern</Description>
|
||||
<UserSecretsId>myservice-api</UserSecretsId>
|
||||
<UserSecretsId>facebook-service-api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using FacebookService.API.Application.Behaviors;
|
||||
using FacebookService.Infrastructure;
|
||||
using Serilog;
|
||||
@@ -96,6 +98,23 @@ try
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add JWT Authentication / VI: Thêm JWT Authentication
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Jwt:Authority"];
|
||||
options.Audience = builder.Configuration["Jwt:Audience"];
|
||||
options.RequireHttpsMetadata = builder.Configuration.GetValue<bool>("Jwt:RequireHttpsMetadata", false);
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
|
||||
@@ -114,6 +133,8 @@ try
|
||||
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
]
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres"
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=facebook_service;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "localhost:6379"
|
||||
|
||||
@@ -3,7 +3,7 @@ ASPNETCORE_ENVIRONMENT=Development
|
||||
|
||||
# Database / Cơ Sở Dữ Liệu
|
||||
# PostgreSQL connection string (Neon or local)
|
||||
DATABASE_URL=Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
|
||||
DATABASE_URL=Host=localhost;Port=5432;Database=whatsapp_service;Username=postgres;Password=postgres
|
||||
|
||||
# Redis Cache
|
||||
REDIS_URL=localhost:6379
|
||||
@@ -18,11 +18,11 @@ JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
|
||||
|
||||
# API Configuration / Cấu Hình API
|
||||
API_PORT=5000
|
||||
API_BASE_PATH=/api/v1/myservice
|
||||
API_BASE_PATH=/api/v1/whatsapp
|
||||
|
||||
# Observability / Quan Sát
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
OTEL_SERVICE_NAME=myservice
|
||||
OTEL_SERVICE_NAME=whatsapp-service
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=Information
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
**Docker Port**: 8080 (`ASPNETCORE_URLS=http://+:8080`)
|
||||
|
||||
**Database**: PostgreSQL (connection string key: `ConnectionStrings:DefaultConnection` or `DATABASE_URL`)
|
||||
- Default DB name in config: `myservice_db` (placeholder from template)
|
||||
- Default DB name in config: `whatsapp_service`
|
||||
|
||||
**DbContext**: `WhatsAppServiceContext` (file named `MyServiceContext.cs`)
|
||||
**DbContext**: `WhatsAppServiceContext` (file: `WhatsAppServiceContext.cs`)
|
||||
|
||||
**Health Checks**:
|
||||
- `/health` - Full health (includes PostgreSQL check)
|
||||
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
- "5000:8080"
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=whatsapp-service_db;Username=postgres;Password=postgres
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=whatsapp_service;Username=postgres;Password=postgres
|
||||
- REDIS_URL=redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: whatsapp-service_db
|
||||
POSTGRES_DB: whatsapp_service
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WhatsAppService.API.Application.Commands;
|
||||
using WhatsAppService.Domain.AggregatesModel.ConversationAggregate;
|
||||
@@ -12,6 +13,7 @@ namespace WhatsAppService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ConversationsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WhatsAppService.Domain.AggregatesModel.CustomerAggregate;
|
||||
|
||||
@@ -11,6 +12,7 @@ namespace WhatsAppService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class CustomersController : ControllerBase
|
||||
{
|
||||
private readonly ICustomerRepository _repository;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WhatsAppService.API.Application.Commands;
|
||||
using WhatsAppService.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace WhatsAppService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class SamplesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WhatsAppService.API.Application.Commands;
|
||||
|
||||
@@ -13,6 +14,7 @@ namespace WhatsAppService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/webhooks")]
|
||||
[AllowAnonymous]
|
||||
public class WebhooksController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WhatsAppService.API.Application.Commands;
|
||||
using WhatsAppService.Domain.AggregatesModel.WhatsAppAccountAggregate;
|
||||
@@ -12,6 +13,7 @@ namespace WhatsAppService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class WhatsAppAccountsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using WhatsAppService.API.Application.Behaviors;
|
||||
using WhatsAppService.Infrastructure;
|
||||
using Serilog;
|
||||
@@ -96,6 +98,23 @@ try
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add JWT Authentication / VI: Thêm JWT Authentication
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Jwt:Authority"];
|
||||
options.Audience = builder.Configuration["Jwt:Audience"];
|
||||
options.RequireHttpsMetadata = builder.Configuration.GetValue<bool>("Jwt:RequireHttpsMetadata", false);
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
|
||||
@@ -114,6 +133,8 @@ try
|
||||
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<AssemblyName>WhatsAppService.API</AssemblyName>
|
||||
<RootNamespace>WhatsAppService.API</RootNamespace>
|
||||
<Description>Web API layer with CQRS pattern</Description>
|
||||
<UserSecretsId>myservice-api</UserSecretsId>
|
||||
<UserSecretsId>whatsapp-service-api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
]
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres"
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=whatsapp_service;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "localhost:6379"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MktXService.API.Application.Commands;
|
||||
using MktXService.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class AccountsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MktXService.API.Application.Commands;
|
||||
using MktXService.API.Application.Queries;
|
||||
@@ -15,6 +16,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class CampaignsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MktXService.API.Application.Queries;
|
||||
|
||||
@@ -13,6 +14,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ContactsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MktXService.API.Application.Commands;
|
||||
using MktXService.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class ConversationsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MktXService.API.Application.Commands;
|
||||
using MktXService.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class SamplesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MktXService.API.Application.Queries;
|
||||
|
||||
@@ -13,6 +14,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class TemplatesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -15,6 +16,7 @@ namespace MktXService.API.Controllers;
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/webhooks/twitter")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class WebhooksController : ControllerBase
|
||||
{
|
||||
private readonly ITwitterApiClient _twitterClient;
|
||||
@@ -36,6 +38,7 @@ public class WebhooksController : ControllerBase
|
||||
/// 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)
|
||||
{
|
||||
@@ -51,6 +54,7 @@ public class WebhooksController : ControllerBase
|
||||
/// VI: Nhận events từ Twitter webhook.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ReceiveWebhookEvent([FromBody] TwitterWebhookPayload payload)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MktXService.API.Application.Behaviors;
|
||||
using MktXService.Infrastructure;
|
||||
using Serilog;
|
||||
@@ -96,6 +98,23 @@ try
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add JWT Authentication / VI: Thêm JWT Authentication
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Jwt:Authority"];
|
||||
options.Audience = builder.Configuration["Jwt:Audience"];
|
||||
options.RequireHttpsMetadata = builder.Configuration.GetValue<bool>("Jwt:RequireHttpsMetadata", false);
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
|
||||
@@ -114,6 +133,8 @@ try
|
||||
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MktXService.Domain.AggregatesModel.AIConversationSessionAggregate;
|
||||
using MktXService.Domain.AggregatesModel.AutomationFlowAggregate;
|
||||
using MktXService.Domain.AggregatesModel.CampaignAggregate;
|
||||
using MktXService.Domain.AggregatesModel.ContactAggregate;
|
||||
using MktXService.Domain.AggregatesModel.ConversationAggregate;
|
||||
using MktXService.Domain.AggregatesModel.SampleAggregate;
|
||||
using MktXService.Domain.AggregatesModel.SegmentAggregate;
|
||||
using MktXService.Domain.AggregatesModel.TemplateAggregate;
|
||||
using MktXService.Domain.AggregatesModel.TwitterAccountAggregate;
|
||||
using MktXService.Infrastructure.Idempotency;
|
||||
using MktXService.Infrastructure.Repositories;
|
||||
|
||||
@@ -48,6 +56,14 @@ public static class DependencyInjection
|
||||
|
||||
// EN: Register repositories / VI: Đăng ký repositories
|
||||
services.AddScoped<ISampleRepository, SampleRepository>();
|
||||
services.AddScoped<ITwitterAccountRepository, TwitterAccountRepository>();
|
||||
services.AddScoped<IContactRepository, ContactRepository>();
|
||||
services.AddScoped<IConversationRepository, ConversationRepository>();
|
||||
services.AddScoped<ITemplateRepository, TemplateRepository>();
|
||||
services.AddScoped<ICampaignRepository, CampaignRepository>();
|
||||
services.AddScoped<ISegmentRepository, SegmentRepository>();
|
||||
services.AddScoped<IAutomationFlowRepository, AutomationFlowRepository>();
|
||||
services.AddScoped<IAIConversationSessionRepository, AIConversationSessionRepository>();
|
||||
|
||||
// EN: Register idempotency services / VI: Đăng ký idempotency services
|
||||
services.AddScoped<IRequestManager, RequestManager>();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MktZaloService.API.Application.Behaviors;
|
||||
using MktZaloService.API.Application.Services;
|
||||
using MktZaloService.Infrastructure;
|
||||
@@ -100,6 +102,23 @@ try
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add JWT Authentication / VI: Thêm JWT Authentication
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Jwt:Authority"];
|
||||
options.Audience = builder.Configuration["Jwt:Audience"];
|
||||
options.RequireHttpsMetadata = builder.Configuration.GetValue<bool>("Jwt:RequireHttpsMetadata", false);
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
|
||||
@@ -118,6 +137,8 @@ try
|
||||
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller REST API cho Admin Orders.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrderService.API.Application.DTOs;
|
||||
using OrderService.API.Application.Queries;
|
||||
@@ -14,6 +15,7 @@ namespace OrderService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/admin/orders")]
|
||||
[Authorize]
|
||||
public class AdminOrdersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller REST API cho Orders.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrderService.API.Application.Commands;
|
||||
using OrderService.API.Application.DTOs;
|
||||
@@ -15,6 +16,7 @@ namespace OrderService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/orders")]
|
||||
[Authorize]
|
||||
public class OrdersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// VI: Controller REST API cho Reports.
|
||||
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OrderService.API.Application.Commands.Reports;
|
||||
using OrderService.API.Application.Queries;
|
||||
@@ -15,6 +16,7 @@ namespace OrderService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/reports")]
|
||||
[Authorize]
|
||||
public class ReportsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace PromotionService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class CampaignsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
@@ -30,7 +31,6 @@ public class CampaignsController : ControllerBase
|
||||
/// VI: Tạo chiến dịch mới.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(CampaignDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<CampaignDto>> CreateCampaign([FromBody] CreateCampaignCommand command)
|
||||
@@ -72,7 +72,6 @@ public class CampaignsController : ControllerBase
|
||||
/// VI: Kích hoạt chiến dịch.
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/activate")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ActivateCampaign(Guid id)
|
||||
@@ -86,7 +85,6 @@ public class CampaignsController : ControllerBase
|
||||
/// VI: Tạm dừng chiến dịch.
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/pause")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> PauseCampaign(Guid id)
|
||||
@@ -100,7 +98,6 @@ public class CampaignsController : ControllerBase
|
||||
/// VI: Hủy chiến dịch.
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/cancel")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> CancelCampaign(Guid id)
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace PromotionService.API.Controllers;
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Produces("application/json")]
|
||||
[Authorize]
|
||||
public class VouchersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
@@ -30,7 +31,6 @@ public class VouchersController : ControllerBase
|
||||
/// VI: Nhận voucher miễn phí.
|
||||
/// </summary>
|
||||
[HttpPost("claim")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(VoucherDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<VoucherDto>> ClaimVoucher([FromBody] ClaimVoucherCommand command)
|
||||
@@ -44,7 +44,6 @@ public class VouchersController : ControllerBase
|
||||
/// VI: Xác thực mã voucher.
|
||||
/// </summary>
|
||||
[HttpGet("validate/{code}")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(VoucherValidationDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<VoucherValidationDto>> ValidateVoucher(string code, [FromQuery] Guid userId)
|
||||
{
|
||||
@@ -57,7 +56,6 @@ public class VouchersController : ControllerBase
|
||||
/// VI: Sử dụng voucher.
|
||||
/// </summary>
|
||||
[HttpPost("redeem")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(RedemptionDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<RedemptionDto>> RedeemVoucher([FromBody] RedeemVoucherCommand command)
|
||||
@@ -71,7 +69,6 @@ public class VouchersController : ControllerBase
|
||||
/// VI: Lấy voucher của người dùng.
|
||||
/// </summary>
|
||||
[HttpGet("user/{userId:guid}")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(IEnumerable<VoucherSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<VoucherSummaryDto>>> GetUserVouchers(Guid userId)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<AssemblyName>PromotionService.API</AssemblyName>
|
||||
<RootNamespace>PromotionService.API</RootNamespace>
|
||||
<Description>Web API layer with CQRS pattern</Description>
|
||||
<UserSecretsId>myservice-api</UserSecretsId>
|
||||
<UserSecretsId>promotion-service-api</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SocialService.API.Application.Commands;
|
||||
using SocialService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace SocialService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/admin/social")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SocialService.API.Application.Commands;
|
||||
|
||||
@@ -10,6 +11,7 @@ namespace SocialService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Authorize]
|
||||
public class BlocksController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SocialService.API.Application.Commands;
|
||||
using SocialService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace SocialService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Authorize]
|
||||
public class RelationshipsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -2,6 +2,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using SocialService.API.Application.Behaviors;
|
||||
using SocialService.Infrastructure;
|
||||
using Serilog;
|
||||
@@ -103,6 +105,22 @@ try
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add JWT Authentication / VI: Thêm JWT Authentication
|
||||
builder.Services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer("Bearer", options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Jwt:Authority"] ?? "http://iam-service-net:8080";
|
||||
options.Audience = builder.Configuration["Jwt:Audience"] ?? "goodgo-api";
|
||||
options.RequireHttpsMetadata = false; // EN: Development only / VI: Chỉ development
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false, // EN: IAM service validates / VI: IAM service xác thực
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add health checks / VI: Thêm health checks
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddNpgSql(
|
||||
@@ -142,6 +160,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");
|
||||
app.MapHealthChecks("/health/live", new()
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
Reference in New Issue
Block a user