feat(membership): Introduce Membership Service with IAM integration
- Added the Membership Service configuration to the local docker-compose.yml, replacing the previous Social Service setup. - Implemented IAM Service client with caching and health check capabilities in the Membership Service. - Created Dependency Injection for IAM Service settings and registered the HttpClient for communication. - Removed the outdated docker-compose.yml for the previous Social Service. - Enhanced IAM Service client functionality to validate users, retrieve roles, and manage permissions.
This commit is contained in:
@@ -117,20 +117,28 @@ services:
|
||||
- "traefik.http.services.storage-service.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
|
||||
# Social Service .NET - Social Graph Management
|
||||
social-service:
|
||||
# Membership Service .NET - Membership Management
|
||||
membership-service-net:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/social-service-net/Dockerfile
|
||||
container_name: social-service-local
|
||||
context: ../../services/membership-service-net
|
||||
dockerfile: Dockerfile
|
||||
image: goodgo/membership-service-net:latest
|
||||
container_name: membership-service-net-local
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ConnectionStrings__DefaultConnection=${SOCIAL_DATABASE_URL:-Host=localhost;Port=5432;Database=social_db;Username=postgres;Password=postgres}
|
||||
- ASPNETCORE_URLS=http://+:8080
|
||||
# EN: Database - Neon PostgreSQL
|
||||
# VI: Cơ sở dữ liệu - Neon PostgreSQL
|
||||
- ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Port=5432;Database=membership_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require
|
||||
# EN: IAM Service Communication
|
||||
# VI: Giao tiếp IAM Service
|
||||
- IamService__BaseUrl=http://iam-service-net:8080
|
||||
- IamService__ServiceName=social-service
|
||||
- IamService__ServiceName=membership-service
|
||||
ports:
|
||||
- "5003:8080"
|
||||
depends_on:
|
||||
iam-service-net:
|
||||
condition: service_healthy
|
||||
traefik:
|
||||
condition: service_started
|
||||
networks:
|
||||
@@ -144,11 +152,11 @@ services:
|
||||
start_period: 40s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.social-service.rule=PathPrefix(`/api/v1/relationships`) || PathPrefix(`/api/v1/blocks`)"
|
||||
- "traefik.http.routers.social-service.entrypoints=web"
|
||||
- "traefik.http.services.social-service.loadbalancer.server.port=8080"
|
||||
- "traefik.http.services.social-service.loadbalancer.healthcheck.path=/health/live"
|
||||
- "traefik.http.services.social-service.loadbalancer.healthcheck.interval=10s"
|
||||
- "traefik.http.routers.membership-service.rule=PathPrefix(`/api/v1/memberships`) || PathPrefix(`/api/v1/subscriptions`)"
|
||||
- "traefik.http.routers.membership-service.entrypoints=web"
|
||||
- "traefik.http.services.membership-service.loadbalancer.server.port=8080"
|
||||
- "traefik.http.services.membership-service.loadbalancer.healthcheck.path=/health/live"
|
||||
- "traefik.http.services.membership-service.loadbalancer.healthcheck.interval=10s"
|
||||
|
||||
# IAM Service .NET - Identity and Access Management (Duende IdentityServer)
|
||||
iam-service-net:
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
# EN: Docker Compose for local development
|
||||
# VI: Docker Compose cho phát triển local
|
||||
|
||||
services:
|
||||
myservice-api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: myservice-api
|
||||
ports:
|
||||
- "5000:8080"
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
|
||||
- REDIS_URL=redis:6379
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- myservice-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: myservice-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: myservice_db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- myservice-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: myservice-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- myservice-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
myservice-network:
|
||||
driver: bridge
|
||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MembershipService.Domain.AggregatesModel.MemberAggregate;
|
||||
using MembershipService.Infrastructure.ExternalServices;
|
||||
using MembershipService.Infrastructure.Idempotency;
|
||||
using MembershipService.Infrastructure.Repositories;
|
||||
|
||||
@@ -52,6 +53,28 @@ public static class DependencyInjection
|
||||
// EN: Register idempotency services / VI: Đăng ký idempotency services
|
||||
services.AddScoped<IRequestManager, RequestManager>();
|
||||
|
||||
// EN: Configure IAM Service Client / VI: Cấu hình IAM Service Client
|
||||
services.Configure<IamServiceSettings>(
|
||||
configuration.GetSection(IamServiceSettings.SectionName));
|
||||
|
||||
// EN: Register HttpClient for IAM Service / VI: Đăng ký HttpClient cho IAM Service
|
||||
services.AddHttpClient<IIamServiceClient, HttpIamServiceClient>((sp, client) =>
|
||||
{
|
||||
var settings = configuration.GetSection(IamServiceSettings.SectionName)
|
||||
.Get<IamServiceSettings>() ?? new IamServiceSettings();
|
||||
|
||||
client.BaseAddress = new Uri(settings.BaseUrl);
|
||||
client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
// EN: Allow self-signed certificates in development
|
||||
// VI: Cho phép self-signed certificates trong development
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MembershipService.Infrastructure.ExternalServices;
|
||||
|
||||
/// <summary>
|
||||
/// EN: HTTP client for communicating with IAM Service with caching and health check.
|
||||
/// VI: HTTP client để giao tiếp với IAM Service với caching và health check.
|
||||
/// </summary>
|
||||
public class HttpIamServiceClient : IIamServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IamServiceSettings _settings;
|
||||
private readonly ILogger<HttpIamServiceClient> _logger;
|
||||
|
||||
// EN: In-memory cache for user info / VI: Cache in-memory cho user info
|
||||
private readonly ConcurrentDictionary<string, CachedItem<IamUserInfo>> _userCache = new();
|
||||
private readonly ConcurrentDictionary<string, CachedItem<IReadOnlyList<string>>> _rolesCache = new();
|
||||
private readonly ConcurrentDictionary<string, CachedItem<IReadOnlyList<string>>> _permissionsCache = new();
|
||||
|
||||
// EN: Health check cache / VI: Cache health check
|
||||
private CachedItem<IamHealthStatus>? _healthCache;
|
||||
private readonly object _healthLock = new();
|
||||
|
||||
public HttpIamServiceClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<IamServiceSettings> settings,
|
||||
ILogger<HttpIamServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
|
||||
_httpClient.BaseAddress = new Uri(_settings.BaseUrl);
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EN: User Operations / VI: Thao tác User
|
||||
// ============================================
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IamUserInfo?> ValidateUserAsync(
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Check cache first / VI: Kiểm tra cache trước
|
||||
var cacheKey = $"token:{accessToken.GetHashCode()}";
|
||||
if (TryGetFromCache(_userCache, cacheKey, out var cached))
|
||||
{
|
||||
_logger.LogDebug("User info retrieved from cache");
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = CreateRequest(HttpMethod.Get, "/api/v1/users/me", accessToken);
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to validate user token. Status: {StatusCode}", response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var userResponse = await response.Content.ReadFromJsonAsync<UserMeResponse>(cancellationToken);
|
||||
|
||||
if (userResponse?.Data == null)
|
||||
return null;
|
||||
|
||||
var userInfo = new IamUserInfo(
|
||||
userResponse.Data.Id,
|
||||
userResponse.Data.Email,
|
||||
userResponse.Data.DisplayName,
|
||||
true,
|
||||
userResponse.Data.Roles ?? new List<string>(),
|
||||
userResponse.Data.Permissions);
|
||||
|
||||
// EN: Cache the result / VI: Cache kết quả
|
||||
AddToCache(_userCache, cacheKey, userInfo, _settings.CacheDurationSeconds);
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating user with IAM Service");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IamUserInfo?> GetUserByIdAsync(
|
||||
string userId,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Check cache first / VI: Kiểm tra cache trước
|
||||
var cacheKey = $"user:{userId}";
|
||||
if (TryGetFromCache(_userCache, cacheKey, out var cached))
|
||||
{
|
||||
_logger.LogDebug("User {UserId} retrieved from cache", userId);
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = CreateRequest(HttpMethod.Get, $"/api/v1/users/{userId}", accessToken);
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to get user {UserId}. Status: {StatusCode}", userId, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var userResponse = await response.Content.ReadFromJsonAsync<UserResponse>(cancellationToken);
|
||||
|
||||
if (userResponse?.Data == null)
|
||||
return null;
|
||||
|
||||
var userInfo = new IamUserInfo(
|
||||
userResponse.Data.Id,
|
||||
userResponse.Data.Email,
|
||||
userResponse.Data.DisplayName,
|
||||
userResponse.Data.IsActive,
|
||||
userResponse.Data.Roles ?? new List<string>(),
|
||||
userResponse.Data.Permissions);
|
||||
|
||||
// EN: Cache the result / VI: Cache kết quả
|
||||
AddToCache(_userCache, cacheKey, userInfo, _settings.CacheDurationSeconds);
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting user {UserId} from IAM Service", userId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UserExistsAsync(
|
||||
string userId,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await GetUserByIdAsync(userId, accessToken, cancellationToken);
|
||||
return user != null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<string>> GetUserRolesAsync(
|
||||
string userId,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Check cache first / VI: Kiểm tra cache trước
|
||||
var cacheKey = $"roles:{userId}";
|
||||
if (TryGetFromCache(_rolesCache, cacheKey, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = CreateRequest(HttpMethod.Get, $"/api/v1/users/{userId}/roles", accessToken);
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to get roles for user {UserId}. Status: {StatusCode}", userId, response.StatusCode);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var rolesResponse = await response.Content.ReadFromJsonAsync<RolesResponse>(cancellationToken);
|
||||
var roles = rolesResponse?.Data ?? Array.Empty<string>();
|
||||
|
||||
// EN: Cache the result / VI: Cache kết quả
|
||||
AddToCache(_rolesCache, cacheKey, roles, _settings.CacheDurationSeconds);
|
||||
|
||||
return roles;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting roles for user {UserId}", userId);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<string>> GetUserPermissionsAsync(
|
||||
string userId,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Check cache first / VI: Kiểm tra cache trước
|
||||
var cacheKey = $"permissions:{userId}";
|
||||
if (TryGetFromCache(_permissionsCache, cacheKey, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = CreateRequest(HttpMethod.Get, $"/api/v1/users/{userId}/permissions", accessToken);
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to get permissions for user {UserId}. Status: {StatusCode}", userId, response.StatusCode);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var permissionsResponse = await response.Content.ReadFromJsonAsync<PermissionsResponse>(cancellationToken);
|
||||
var permissions = permissionsResponse?.Data ?? Array.Empty<string>();
|
||||
|
||||
// EN: Cache the result / VI: Cache kết quả
|
||||
AddToCache(_permissionsCache, cacheKey, permissions, _settings.CacheDurationSeconds);
|
||||
|
||||
return permissions;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting permissions for user {UserId}", userId);
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> HasPermissionAsync(
|
||||
string userId,
|
||||
string permission,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var permissions = await GetUserPermissionsAsync(userId, accessToken, cancellationToken);
|
||||
return permissions.Contains(permission, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> HasRoleAsync(
|
||||
string userId,
|
||||
string role,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var roles = await GetUserRolesAsync(userId, accessToken, cancellationToken);
|
||||
return roles.Contains(role, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EN: Health Check / VI: Kiểm tra Health
|
||||
// ============================================
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IamHealthStatus> CheckHealthAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Check cache first / VI: Kiểm tra cache trước
|
||||
lock (_healthLock)
|
||||
{
|
||||
if (_healthCache != null && !_healthCache.IsExpired)
|
||||
{
|
||||
_logger.LogDebug("Health status retrieved from cache");
|
||||
return _healthCache.Value;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/health");
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
var healthStatus = new IamHealthStatus(
|
||||
response.IsSuccessStatusCode,
|
||||
response.IsSuccessStatusCode ? "Healthy" : $"Unhealthy ({response.StatusCode})",
|
||||
DateTime.UtcNow);
|
||||
|
||||
// EN: Cache the result / VI: Cache kết quả
|
||||
lock (_healthLock)
|
||||
{
|
||||
_healthCache = new CachedItem<IamHealthStatus>(healthStatus, _settings.HealthCheckCacheDurationSeconds);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("IAM Service health check failed. Status: {StatusCode}", response.StatusCode);
|
||||
}
|
||||
|
||||
return healthStatus;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking IAM Service health");
|
||||
|
||||
var unhealthyStatus = new IamHealthStatus(false, $"Error: {ex.Message}", DateTime.UtcNow);
|
||||
|
||||
// EN: Cache unhealthy status for shorter duration / VI: Cache trạng thái unhealthy với thời gian ngắn hơn
|
||||
lock (_healthLock)
|
||||
{
|
||||
_healthCache = new CachedItem<IamHealthStatus>(unhealthyStatus, 10); // 10 seconds
|
||||
}
|
||||
|
||||
return unhealthyStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var health = await CheckHealthAsync(cancellationToken);
|
||||
return health.IsHealthy;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EN: Cache Management / VI: Quản lý Cache
|
||||
// ============================================
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InvalidateUserCache(string userId)
|
||||
{
|
||||
var keysToRemove = new[]
|
||||
{
|
||||
$"user:{userId}",
|
||||
$"roles:{userId}",
|
||||
$"permissions:{userId}"
|
||||
};
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_userCache.TryRemove(key, out _);
|
||||
_rolesCache.TryRemove(key, out _);
|
||||
_permissionsCache.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cache invalidated for user {UserId}", userId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearCache()
|
||||
{
|
||||
_userCache.Clear();
|
||||
_rolesCache.Clear();
|
||||
_permissionsCache.Clear();
|
||||
|
||||
lock (_healthLock)
|
||||
{
|
||||
_healthCache = null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("All IAM client caches cleared");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EN: Helper Methods / VI: Phương thức hỗ trợ
|
||||
// ============================================
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string path, string accessToken)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, path);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
request.Headers.Add("X-Service-Name", _settings.ServiceName);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static bool TryGetFromCache<T>(
|
||||
ConcurrentDictionary<string, CachedItem<T>> cache,
|
||||
string key,
|
||||
out T value)
|
||||
{
|
||||
if (cache.TryGetValue(key, out var item) && !item.IsExpired)
|
||||
{
|
||||
value = item.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void AddToCache<T>(
|
||||
ConcurrentDictionary<string, CachedItem<T>> cache,
|
||||
string key,
|
||||
T value,
|
||||
int durationSeconds)
|
||||
{
|
||||
cache[key] = new CachedItem<T>(value, durationSeconds);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EN: Internal DTOs / VI: Internal DTOs
|
||||
// ============================================
|
||||
|
||||
private sealed record UserMeResponse(UserData? Data);
|
||||
private sealed record UserResponse(UserFullData? Data);
|
||||
private sealed record RolesResponse(IReadOnlyList<string>? Data);
|
||||
private sealed record PermissionsResponse(IReadOnlyList<string>? Data);
|
||||
|
||||
private sealed record UserData(
|
||||
string Id,
|
||||
string Email,
|
||||
string? DisplayName,
|
||||
List<string>? Roles,
|
||||
List<string>? Permissions);
|
||||
|
||||
private sealed record UserFullData(
|
||||
string Id,
|
||||
string Email,
|
||||
string? DisplayName,
|
||||
bool IsActive,
|
||||
List<string>? Roles,
|
||||
List<string>? Permissions);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Generic cached item with expiration.
|
||||
/// VI: Item cache generic có thời hạn.
|
||||
/// </summary>
|
||||
private sealed class CachedItem<T>
|
||||
{
|
||||
public T Value { get; }
|
||||
private readonly DateTime _expiresAt;
|
||||
|
||||
public CachedItem(T value, int durationSeconds)
|
||||
{
|
||||
Value = value;
|
||||
_expiresAt = DateTime.UtcNow.AddSeconds(durationSeconds);
|
||||
}
|
||||
|
||||
public bool IsExpired => DateTime.UtcNow >= _expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
namespace MembershipService.Infrastructure.ExternalServices;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Configuration for IAM Service communication.
|
||||
/// VI: Cấu hình giao tiếp với IAM Service.
|
||||
/// </summary>
|
||||
public class IamServiceSettings
|
||||
{
|
||||
public const string SectionName = "IamService";
|
||||
|
||||
/// <summary>EN: IAM Service base URL / VI: URL cơ sở của IAM Service</summary>
|
||||
public string BaseUrl { get; set; } = "http://iam-service-net:8080";
|
||||
|
||||
/// <summary>EN: Internal service name header / VI: Header tên service nội bộ</summary>
|
||||
public string ServiceName { get; set; } = "membership-service";
|
||||
|
||||
/// <summary>EN: Request timeout in seconds / VI: Timeout request (giây)</summary>
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>EN: Cache user info duration in seconds / VI: Thời gian cache user info (giây)</summary>
|
||||
public int CacheDurationSeconds { get; set; } = 300; // 5 minutes
|
||||
|
||||
/// <summary>EN: Health check cache duration in seconds / VI: Thời gian cache health check (giây)</summary>
|
||||
public int HealthCheckCacheDurationSeconds { get; set; } = 60; // 1 minute
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: User information from IAM Service.
|
||||
/// VI: Thông tin user từ IAM Service.
|
||||
/// </summary>
|
||||
public record IamUserInfo(
|
||||
string UserId,
|
||||
string Email,
|
||||
string? DisplayName,
|
||||
bool IsActive,
|
||||
IReadOnlyList<string> Roles,
|
||||
IReadOnlyList<string>? Permissions = null);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Role information from IAM Service.
|
||||
/// VI: Thông tin role từ IAM Service.
|
||||
/// </summary>
|
||||
public record IamRoleInfo(
|
||||
string Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
IReadOnlyList<string> Permissions);
|
||||
|
||||
/// <summary>
|
||||
/// EN: IAM Service health status.
|
||||
/// VI: Trạng thái health của IAM Service.
|
||||
/// </summary>
|
||||
public record IamHealthStatus(
|
||||
bool IsHealthy,
|
||||
string Status,
|
||||
DateTime CheckedAt);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Interface for communicating with IAM Service.
|
||||
/// VI: Interface giao tiếp với IAM Service.
|
||||
/// </summary>
|
||||
public interface IIamServiceClient
|
||||
{
|
||||
// ============================================
|
||||
// EN: User Operations / VI: Thao tác User
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// EN: Validate user token and get user info (with caching).
|
||||
/// VI: Xác thực token và lấy thông tin user (có cache).
|
||||
/// </summary>
|
||||
Task<IamUserInfo?> ValidateUserAsync(string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user by ID.
|
||||
/// VI: Lấy user theo ID.
|
||||
/// </summary>
|
||||
Task<IamUserInfo?> GetUserByIdAsync(string userId, string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if user exists.
|
||||
/// VI: Kiểm tra user có tồn tại.
|
||||
/// </summary>
|
||||
Task<bool> UserExistsAsync(string userId, string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user's roles.
|
||||
/// VI: Lấy danh sách roles của user.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> GetUserRolesAsync(string userId, string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user's permissions.
|
||||
/// VI: Lấy danh sách permissions của user.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> GetUserPermissionsAsync(string userId, string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if user has specific permission.
|
||||
/// VI: Kiểm tra user có permission cụ thể.
|
||||
/// </summary>
|
||||
Task<bool> HasPermissionAsync(string userId, string permission, string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if user has specific role.
|
||||
/// VI: Kiểm tra user có role cụ thể.
|
||||
/// </summary>
|
||||
Task<bool> HasRoleAsync(string userId, string role, string accessToken, CancellationToken cancellationToken = default);
|
||||
|
||||
// ============================================
|
||||
// EN: Health Check / VI: Kiểm tra Health
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check IAM Service health (with caching).
|
||||
/// VI: Kiểm tra health của IAM Service (có cache).
|
||||
/// </summary>
|
||||
Task<IamHealthStatus> CheckHealthAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if IAM Service is available.
|
||||
/// VI: Kiểm tra IAM Service có sẵn sàng.
|
||||
/// </summary>
|
||||
Task<bool> IsAvailableAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
// ============================================
|
||||
// EN: Cache Management / VI: Quản lý Cache
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// EN: Invalidate user cache.
|
||||
/// VI: Xóa cache của user.
|
||||
/// </summary>
|
||||
void InvalidateUserCache(string userId);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Clear all caches.
|
||||
/// VI: Xóa toàn bộ cache.
|
||||
/// </summary>
|
||||
void ClearCache();
|
||||
}
|
||||
Reference in New Issue
Block a user