using FrameworkDAL.CustomCode.DiagnosticsSession; using FrameworkDAL.DTO.DiagnosticsSession; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using System.Text.Json; namespace FrameworkBLL.DiagnosticsSession; public class DiagnosticsSessionBLL : IDiagnosticsSessionBLL { private readonly IDiagnosticsSessionDAL _dal; private readonly IValidation _validation; private readonly ILogger _logger; // Max session duration: 4 hours. Prevents indefinitely open debug sessions in prod. private static readonly TimeSpan MaxSessionDuration = TimeSpan.FromHours(4); public DiagnosticsSessionBLL( IDiagnosticsSessionDAL dal, IValidation validation, ILogger logger) { _dal = dal; _validation = validation; _logger = logger; } public async Task GetActiveSessionsAsync(LoginDTO login, CancellationToken ct) { try { var sessions = await _dal.GetActiveSessionsAsync(login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(sessions); } catch (Exception ex) { _logger.LogError(ex, "GetActiveSessions failed for TenantId {TenantId}", login.ClientId); throw; } } public async Task CreateSessionAsync( DiagnosticsSessionDTO dto, LoginDTO login, CancellationToken ct) { try { await _validation.Range(1, 4, dto.SessionScope, nameof(dto.SessionScope)); await _validation.Range(1, 4, dto.DiagnosticLevel, nameof(dto.DiagnosticLevel)); // Enforce max 4-hour window var maxExpiry = DateTime.UtcNow.Add(MaxSessionDuration); if (dto.ExpiresAt > maxExpiry) dto.ExpiresAt = maxExpiry; if (dto.ExpiresAt <= DateTime.UtcNow) dto.ExpiresAt = DateTime.UtcNow.AddHours(1); var sessionId = await _dal.SaveSessionAsync(dto, login, ct).ConfigureAwait(false); _logger.LogInformation( "DiagnosticSession {SessionId} created — scope={Scope} level={Level} expires={Expiry} by user {UserId}", sessionId, dto.SessionScope, dto.DiagnosticLevel, dto.ExpiresAt, login.UserId); return $"{SuccessResponse.SaveSuccessMessage} {sessionId}"; } catch (Exception ex) { _logger.LogError(ex, "CreateSession failed for TenantId {TenantId}", login.ClientId); throw; } } public async Task CloseSessionAsync(int sessionId, LoginDTO login, CancellationToken ct) { try { await _dal.CloseSessionAsync(sessionId, login, ct).ConfigureAwait(false); _logger.LogInformation( "DiagnosticSession {SessionId} closed by user {UserId}", sessionId, login.UserId); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { _logger.LogError(ex, "CloseSession {SessionId} failed", sessionId); throw; } } public async Task IssueTokenAsync( SupportTokenDTO dto, LoginDTO login, CancellationToken ct) { try { await _validation.NotEmpty(dto.IssuedTo, nameof(dto.IssuedTo)); // Generate token: SUP-YYYYMMDD-XXXX (uppercase alphanumeric suffix) dto.Token = GenerateSupportToken(); // Max token validity: 8 hours var maxExpiry = DateTime.UtcNow.AddHours(8); if (dto.ExpiresAt > maxExpiry || dto.ExpiresAt <= DateTime.UtcNow) dto.ExpiresAt = DateTime.UtcNow.AddHours(4); dto.TenantId = login.ClientId; var tokenId = await _dal.SaveTokenAsync(dto, login, ct).ConfigureAwait(false); _logger.LogInformation( "SupportToken {Token} issued to {IssuedTo} by user {UserId} expires {Expiry}", dto.Token, dto.IssuedTo, login.UserId, dto.ExpiresAt); return JsonSerializer.Serialize(new { tokenId, token = dto.Token, expiresAt = dto.ExpiresAt }); } catch (Exception ex) { _logger.LogError(ex, "IssueToken failed for TenantId {TenantId}", login.ClientId); throw; } } public async Task ValidateTokenAsync( string token, string clientIp, LoginDTO login, CancellationToken ct) { try { await _validation.NotEmpty(token, nameof(token)); var tokenDto = await _dal.GetTokenAsync(token, login, ct).ConfigureAwait(false); if (tokenDto is null) return JsonSerializer.Serialize(new { valid = false, reason = "Token not found or expired" }); await _dal.MarkTokenUsedAsync(token, clientIp, login, ct).ConfigureAwait(false); _logger.LogInformation( "SupportToken {TokenPrefix}… validated from IP {Ip}", token.Length > 8 ? token[..8] : "short", clientIp); return JsonSerializer.Serialize(new { valid = true, capabilities = tokenDto.Capabilities, expiresAt = tokenDto.ExpiresAt, }); } catch (Exception ex) { _logger.LogError(ex, "ValidateToken failed for token prefix {TokenPrefix}", token.Length > 8 ? token[..8] : "short"); throw; } } public async Task RevokeTokenAsync(int tokenId, LoginDTO login, CancellationToken ct) { try { await _dal.RevokeTokenAsync(tokenId, login, ct).ConfigureAwait(false); _logger.LogInformation("SupportToken {TokenId} revoked by user {UserId}", tokenId, login.UserId); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { _logger.LogError(ex, "RevokeToken {TokenId} failed", tokenId); throw; } } public async Task> GetMatchingSessionsAsync( int tenantId, int userId, string? sessionId, LoginDTO login, CancellationToken ct) { var all = await _dal.GetActiveSessionsAsync(login, ct).ConfigureAwait(false); return all.Where(s => s.SessionScope == 1 || // Global (s.SessionScope == 2 && s.ScopeId == tenantId.ToString()) || // Tenant (s.SessionScope == 3 && s.ScopeId == userId.ToString()) || // User (s.SessionScope == 4 && s.ScopeId == sessionId) // Session ).ToList(); } private static string GenerateSupportToken() { var date = DateTime.UtcNow.ToString("yyyyMMdd"); var suffix = Guid.NewGuid().ToString("N")[..4].ToUpperInvariant(); return $"SUP-{date}-{suffix}"; } }