using System.IO; using FastEndpoints; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.FastEndPoint; using GB5Shared.Storage; using TMSBLL.Certificate; using TMSDAL.DTO.CertRenewalAlert; using static GB5Shared.GB5Constant.Constant; namespace TMSSL.EndPoints.Certificate { // Authenticated download — separate from the public VerifyCertificate endpoint, which never // returns PDF bytes. // // Reads the file via IAttachmentStorageResolver (S3 or Network, whichever the tenant is // configured for) using TATTACHMENT.CMSID/ATTACHMENTOPTION — NOT IFileUploadBLL, which only // reads from local disk via a separate FileStorage:PhysicalFilePath config and has no idea // the newer attachment pipeline (CertificatePdfPipeline/AttachmentUploadService) may have // stored the file in S3 or under a different base path. // // IMPORTANT: All validation must complete BEFORE SendStreamAsync is called. // Once SendStreamAsync writes response headers, no JSON/HTML error body can follow — // the FE would receive a mixed stream and save it as a blank/corrupt PDF. public class DownloadCertificate : BaseEndpoint> { private readonly ITrainingCertificateBLL _TrainingCertificateBLL; private readonly IAttachmentStorageResolver _StorageResolver; public DownloadCertificate(ITrainingCertificateBLL trainingCertificateBLL, IAttachmentStorageResolver storageResolver) { _TrainingCertificateBLL = trainingCertificateBLL; _StorageResolver = storageResolver; } public override void Configure() { Get("/TrainingCertificate/DownloadCertificate"); AllowAnonymous(); } public record Parameters( [property: QueryParam] int TrainingCertificateId, [property: FromHeader] string Login ); protected override string? GetCacheKey(Parameters req, LoginDTO login) => null; protected override async Task> ExecuteAsync(Parameters req, LoginDTO login, CancellationToken ct) { // ── Step 1: resolve AttachmentId ───────────────────────────────── int attachmentId; try { attachmentId = await _TrainingCertificateBLL .GetAttachmentIdByCertificate(req.TrainingCertificateId, login, ct) .ConfigureAwait(false); } catch (Exception ex) { HttpContext.Response.StatusCode = 500; HttpContext.Response.ContentType = "application/json"; await HttpContext.Response.WriteAsync( $"{{\"error\":\"Error resolving certificate: {ex.Message}\"}}", ct); await HttpContext.Response.CompleteAsync(); return null!; } if (attachmentId <= 0) { HttpContext.Response.StatusCode = 404; HttpContext.Response.ContentType = "application/json"; await HttpContext.Response.WriteAsync( "{\"error\":\"No signed PDF available for this certificate yet. Call RegenerateCertificate to generate it first.\"}", ct); await HttpContext.Response.CompleteAsync(); return null!; } // ── Step 2: resolve storage location + fetch bytes ──────────────── AttachmentStorageInfoDTO? storageInfo; try { storageInfo = await _TrainingCertificateBLL .GetAttachmentStorageInfo(attachmentId, login, ct) .ConfigureAwait(false); } catch (Exception ex) { HttpContext.Response.StatusCode = 500; HttpContext.Response.ContentType = "application/json"; await HttpContext.Response.WriteAsync( $"{{\"error\":\"Error resolving certificate file location: {ex.Message}\"}}", ct); await HttpContext.Response.CompleteAsync(); return null!; } if (storageInfo is null || string.IsNullOrEmpty(storageInfo.CmsId)) { HttpContext.Response.StatusCode = 404; HttpContext.Response.ContentType = "application/json"; await HttpContext.Response.WriteAsync( "{\"error\":\"Certificate file not found in document store.\"}", ct); await HttpContext.Response.CompleteAsync(); return null!; } byte[] bytes; try { var storage = await _StorageResolver.ResolveAsync(login, storageInfo.AttachmentOption, ct).ConfigureAwait(false); using var sourceStream = await storage.GetStreamAsync(storageInfo.CmsId, ct).ConfigureAwait(false); using var buffer = new MemoryStream(); await sourceStream.CopyToAsync(buffer, ct).ConfigureAwait(false); bytes = buffer.ToArray(); } catch (Exception ex) { HttpContext.Response.StatusCode = 500; HttpContext.Response.ContentType = "application/json"; await HttpContext.Response.WriteAsync( $"{{\"error\":\"Error fetching certificate file: {ex.Message}\"}}", ct); await HttpContext.Response.CompleteAsync(); return null!; } // ── Step 3: stream PDF — no more error returns after this point ── string fileName = string.IsNullOrEmpty(storageInfo.DisplayFileName) ? $"Certificate_{req.TrainingCertificateId}.pdf" : storageInfo.DisplayFileName; string contentType = string.IsNullOrEmpty(storageInfo.MimeType) ? "application/pdf" : storageInfo.MimeType; using var ms = new MemoryStream(bytes); await HttpContext.Response.SendStreamAsync(ms, fileName, ms.Length, contentType, cancellation: ct); return null!; } } }