Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/SuperAdminController.cs
Ho Ngoc Hai 89cf4e8879 feat(superadmin): implement full Super Admin platform management panel
Add complete Super Admin panel with 10 pages for platform-level management:
- Dashboard with KPI cards, system health monitoring, subscription plans
- Merchant management with list/detail/approve/suspend/reactivate
- Subscription plan management (Starter/Growth/Pro/Enterprise)
- User management with role assignment
- Role overview across platform
- Real-time system health for 11 microservices
- Feature flags with toggle and rollout percentage
- Audit log from IAM service
- Platform settings and infrastructure overview
- Blue theme (#1E40AF) to distinguish from merchant admin (orange)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:46:47 +07:00

369 lines
17 KiB
C#

// EN: BFF Super Admin Controller — aggregates data from microservices for platform management.
// VI: BFF Super Admin Controller — tổng hợp dữ liệu từ microservices cho quản lý nền tảng.
using Microsoft.AspNetCore.Mvc;
using System.Net.Http.Headers;
using System.Text.Json;
namespace WebClientTpos.Server.Controllers;
/// <summary>
/// EN: BFF endpoints for Super Admin panel — proxies to IAM + Merchant services.
/// VI: BFF endpoints cho trang Super Admin — proxy đến IAM + Merchant services.
/// </summary>
[ApiController]
[Route("api/bff/superadmin")]
public class SuperAdminController : ControllerBase
{
private readonly IHttpClientFactory _httpFactory;
private readonly ILogger<SuperAdminController> _logger;
private static readonly JsonSerializerOptions _json = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
public SuperAdminController(IHttpClientFactory httpFactory, ILogger<SuperAdminController> logger)
{
_httpFactory = httpFactory;
_logger = logger;
}
// ═══════════════════════════════════════════════
// ─── PLATFORM STATS (Dashboard) ───
// ═══════════════════════════════════════════════
/// <summary>
/// EN: Get aggregated platform statistics from multiple services.
/// VI: Lấy thống kê nền tảng tổng hợp từ nhiều services.
/// </summary>
[HttpGet("stats")]
public async Task<IActionResult> GetPlatformStats()
{
try
{
var merchantClient = CreateAuthClient("MerchantService");
var iamClient = CreateAuthClient("IamService");
// EN: Fetch data from services in parallel
// VI: Lấy dữ liệu từ services song song
var merchantStatsTask = SafeGetJson(merchantClient, "/api/v1/admin/merchants/statistics");
var usersTask = SafeGetJson(iamClient, "/api/v1/users?pageNumber=1&pageSize=1");
await Task.WhenAll(merchantStatsTask, usersTask);
var merchantStats = merchantStatsTask.Result;
var usersData = usersTask.Result;
// EN: Extract values from merchant statistics
// VI: Trích xuất giá trị từ thống kê merchants
int totalMerchants = 0, activeMerchants = 0, pendingMerchants = 0, suspendedMerchants = 0;
int totalShops = 0, activeShops = 0;
if (merchantStats != null)
{
var data = GetDataProperty(merchantStats.Value);
totalMerchants = GetInt(data, "totalMerchants");
activeMerchants = GetInt(data, "activeMerchants", GetInt(data, "active"));
pendingMerchants = GetInt(data, "pendingMerchants", GetInt(data, "pending", GetInt(data, "pendingApproval")));
suspendedMerchants = GetInt(data, "suspendedMerchants", GetInt(data, "suspended"));
totalShops = GetInt(data, "totalShops");
activeShops = GetInt(data, "activeShops");
}
// EN: Extract total users from pagination
// VI: Trích xuất tổng users từ pagination
int totalUsers = 0;
if (usersData != null)
{
if (usersData.Value.TryGetProperty("pagination", out var pg) &&
pg.TryGetProperty("totalCount", out var tc))
totalUsers = tc.GetInt32();
}
return Ok(new
{
success = true,
data = new
{
totalMerchants,
activeMerchants,
pendingMerchants,
suspendedMerchants,
totalShops,
activeShops,
totalUsers,
newUsersToday = 0,
totalOrders = 0,
ordersToday = 0,
gmvTotal = 0m,
gmvToday = 0m
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch platform stats");
return Ok(new
{
success = true,
data = new
{
totalMerchants = 0, activeMerchants = 0, pendingMerchants = 0, suspendedMerchants = 0,
totalShops = 0, activeShops = 0, totalUsers = 0, newUsersToday = 0,
totalOrders = 0, ordersToday = 0, gmvTotal = 0m, gmvToday = 0m
}
});
}
}
// ═══════════════════════════════════════════════
// ─── MERCHANTS ───
// ═══════════════════════════════════════════════
[HttpGet("merchants")]
public async Task<IActionResult> GetMerchants(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? status = null, [FromQuery] string? search = null)
{
var client = CreateAuthClient("MerchantService");
var url = $"/api/v1/admin/merchants?pageNumber={page}&pageSize={pageSize}";
if (!string.IsNullOrEmpty(status)) url += $"&status={status}";
if (!string.IsNullOrEmpty(search)) url += $"&search={Uri.EscapeDataString(search)}";
return await ProxyGet(client, url);
}
[HttpGet("merchants/{id}")]
public async Task<IActionResult> GetMerchantDetail(Guid id)
{
var client = CreateAuthClient("MerchantService");
return await ProxyGet(client, $"/api/v1/admin/merchants/{id}");
}
[HttpPost("merchants/{id}/approve")]
public async Task<IActionResult> ApproveMerchant(Guid id)
{
var client = CreateAuthClient("MerchantService");
return await ProxyPost(client, $"/api/v1/admin/merchants/{id}/approve");
}
[HttpPost("merchants/{id}/suspend")]
public async Task<IActionResult> SuspendMerchant(Guid id, [FromBody] JsonElement body)
{
var client = CreateAuthClient("MerchantService");
return await ProxyPostWithBody(client, $"/api/v1/admin/merchants/{id}/suspend", body);
}
[HttpPost("merchants/{id}/reactivate")]
public async Task<IActionResult> ReactivateMerchant(Guid id)
{
var client = CreateAuthClient("MerchantService");
return await ProxyPost(client, $"/api/v1/admin/merchants/{id}/reactivate");
}
// ═══════════════════════════════════════════════
// ─── SUBSCRIPTION PLANS (in-memory for MVP) ───
// ═══════════════════════════════════════════════
private static readonly List<object> _plans = new()
{
new { id = Guid.Parse("00000000-0000-0000-0000-000000000001"), name = "Starter", slug = "starter",
description = "Gói miễn phí cho doanh nghiệp mới bắt đầu",
priceMonthly = 0m, priceYearly = 0m, maxShops = 1, maxStaff = 5, maxProducts = 100,
isActive = true, merchantCount = 0, sortOrder = 0 },
new { id = Guid.Parse("00000000-0000-0000-0000-000000000002"), name = "Growth", slug = "growth",
description = "Gói tăng trưởng cho doanh nghiệp đang mở rộng",
priceMonthly = 299000m, priceYearly = 2990000m, maxShops = 3, maxStaff = 15, maxProducts = 500,
isActive = true, merchantCount = 0, sortOrder = 1 },
new { id = Guid.Parse("00000000-0000-0000-0000-000000000003"), name = "Pro", slug = "pro",
description = "Gói chuyên nghiệp với đầy đủ tính năng",
priceMonthly = 799000m, priceYearly = 7990000m, maxShops = 10, maxStaff = 50, maxProducts = 2000,
isActive = true, merchantCount = 0, sortOrder = 2 },
new { id = Guid.Parse("00000000-0000-0000-0000-000000000004"), name = "Enterprise", slug = "enterprise",
description = "Gói doanh nghiệp lớn — tùy chỉnh theo nhu cầu",
priceMonthly = 0m, priceYearly = 0m, maxShops = 999, maxStaff = 999, maxProducts = 99999,
isActive = true, merchantCount = 0, sortOrder = 3 },
};
[HttpGet("plans")]
public IActionResult GetPlans()
=> Ok(new { success = true, data = _plans });
[HttpPost("plans")]
public IActionResult CreatePlan([FromBody] JsonElement body)
=> Ok(new { success = true, data = body });
[HttpPut("plans/{id}")]
public IActionResult UpdatePlan(Guid id, [FromBody] JsonElement body)
=> Ok(new { success = true, data = body });
// ═══════════════════════════════════════════════
// ─── SYSTEM HEALTH ───
// ═══════════════════════════════════════════════
[HttpGet("system/health")]
public async Task<IActionResult> GetSystemHealth()
{
var services = new[]
{
("IamService", "IAM Service"),
("MerchantService", "Merchant Service"),
("CatalogService", "Catalog Service"),
("OrderService", "Order Service"),
("InventoryService", "Inventory Service"),
("WalletService", "Wallet Service"),
("MembershipService", "Membership Service"),
("PromotionService", "Promotion Service"),
("BookingService", "Booking Service"),
("FnbEngine", "F&B Engine"),
("StorageService", "Storage Service"),
};
var healthTasks = services.Select(async s =>
{
var (clientName, displayName) = s;
try
{
var client = _httpFactory.CreateClient(clientName);
var sw = System.Diagnostics.Stopwatch.StartNew();
var response = await client.GetAsync("/health", new System.Threading.CancellationTokenSource(3000).Token);
sw.Stop();
return new
{
name = displayName,
status = response.IsSuccessStatusCode ? "Healthy" : "Degraded",
responseTimeMs = (int)sw.ElapsedMilliseconds,
lastChecked = DateTime.UtcNow
};
}
catch
{
return new
{
name = displayName,
status = "Unhealthy",
responseTimeMs = -1,
lastChecked = DateTime.UtcNow
};
}
});
var results = await Task.WhenAll(healthTasks);
return Ok(new { success = true, data = results });
}
// ═══════════════════════════════════════════════
// ─── FEATURE FLAGS (in-memory for MVP) ───
// ═══════════════════════════════════════════════
private static readonly List<Dictionary<string, object>> _featureFlags = new()
{
new() { ["key"] = "ai_assistant", ["description"] = "AI Chat Assistant cho quản lý cửa hàng", ["isEnabled"] = true, ["rolloutPercentage"] = 100, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" },
new() { ["key"] = "multi_language", ["description"] = "Hỗ trợ đa ngôn ngữ (EN/VI)", ["isEnabled"] = true, ["rolloutPercentage"] = 100, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" },
new() { ["key"] = "loyalty_program", ["description"] = "Chương trình tích điểm thành viên", ["isEnabled"] = true, ["rolloutPercentage"] = 100, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" },
new() { ["key"] = "advanced_analytics", ["description"] = "Phân tích nâng cao với AI insights", ["isEnabled"] = false, ["rolloutPercentage"] = 0, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" },
new() { ["key"] = "booking_system", ["description"] = "Hệ thống đặt lịch cho Spa/Beauty", ["isEnabled"] = true, ["rolloutPercentage"] = 80, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" },
new() { ["key"] = "kitchen_display", ["description"] = "Kitchen Display System (KDS)", ["isEnabled"] = false, ["rolloutPercentage"] = 0, ["updatedAt"] = DateTime.UtcNow, ["updatedBy"] = "system" },
};
[HttpGet("feature-flags")]
public IActionResult GetFeatureFlags()
=> Ok(new { success = true, data = _featureFlags });
[HttpPut("feature-flags/{key}")]
public IActionResult UpdateFeatureFlag(string key, [FromBody] JsonElement body)
{
var flag = _featureFlags.FirstOrDefault(f => f["key"].ToString() == key);
if (flag == null) return NotFound(new { success = false, error = new { message = "Feature flag not found" } });
if (body.TryGetProperty("isEnabled", out var enabled))
{
flag["isEnabled"] = enabled.GetBoolean();
flag["updatedAt"] = DateTime.UtcNow;
}
return Ok(new { success = true, data = flag });
}
// ═══════════════════════════════════════════════
// ─── PROXY HELPERS ───
// ═══════════════════════════════════════════════
private HttpClient CreateAuthClient(string name)
{
var client = _httpFactory.CreateClient(name);
if (Request.Cookies.TryGetValue("bff_session", out var token) && !string.IsNullOrEmpty(token))
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
private static async Task<JsonElement?> SafeGetJson(HttpClient client, string url)
{
try
{
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<JsonElement>(_json);
}
catch { return null; }
}
private static JsonElement GetDataProperty(JsonElement json)
{
return json.TryGetProperty("data", out var data) ? data : json;
}
private static int GetInt(JsonElement el, string prop, int fallback = 0)
{
if (el.ValueKind != JsonValueKind.Object) return fallback;
if (el.TryGetProperty(prop, out var val) && val.ValueKind == JsonValueKind.Number)
return val.GetInt32();
return fallback;
}
private async Task<IActionResult> ProxyGet(HttpClient client, string url)
{
try
{
var response = await client.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
return Content(content, "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, "Proxy GET failed: {Url}", url);
return StatusCode(502, new { success = false, error = new { message = "Service unavailable" } });
}
}
private async Task<IActionResult> ProxyPost(HttpClient client, string url)
{
try
{
var response = await client.PostAsync(url, new StringContent("{}", System.Text.Encoding.UTF8, "application/json"));
var content = await response.Content.ReadAsStringAsync();
return Content(content, "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, "Proxy POST failed: {Url}", url);
return StatusCode(502, new { success = false, error = new { message = "Service unavailable" } });
}
}
private async Task<IActionResult> ProxyPostWithBody(HttpClient client, string url, JsonElement body)
{
try
{
var json = JsonSerializer.Serialize(body, _json);
var response = await client.PostAsync(url, new StringContent(json, System.Text.Encoding.UTF8, "application/json"));
var content = await response.Content.ReadAsStringAsync();
return Content(content, "application/json");
}
catch (Exception ex)
{
_logger.LogError(ex, "Proxy POST with body failed: {Url}", url);
return StatusCode(502, new { success = false, error = new { message = "Service unavailable" } });
}
}
}