using DMSBLL.ESSDocument; using FastEndpoints; using GB5Shared.DTO.Framework.Enum; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using Newtonsoft.Json; namespace DMSSL.EndPoints.ESSDocument { /// /// Streams a file as a direct download. /// Response: raw binary (NOT wrapped in ResponseStandardDTO) /// Content-Disposition: attachment → browser downloads the file /// /// SECURITY: /// Ownership verified in BLL.GetAndAuthorize() /// Employee can only download their own documents /// public class DownloadDocument : Endpoint { private readonly IESSDocumentBLL _ESSDocumentBLL; public DownloadDocument(IESSDocumentBLL essDocumentBLL) { _ESSDocumentBLL = essDocumentBLL; } public record DownloadDocumentParameters( [property: FromHeader] string Login, [property: QueryParam] int AttachmentId ); public override void Configure() { Get("/ESSDocument/DownloadDocument"); AllowAnonymous(); } public override async Task HandleAsync( DownloadDocumentParameters req, CancellationToken ct) { // ── Step 1: Decode Login header → LoginDTO ──────────────────── LoginDTO? loginDTO; try { loginDTO = JsonConvert.DeserializeObject(req.Login); if (loginDTO is null) throw new Exception("Null LoginDTO"); } catch { await WriteErrorAsync( 401, FrameworkEnumDTO.ResponseStatus.Unauthorized, "Login token is missing or invalid. Please sign in again.", ct); return; } // ── Step 2: Validate AttachmentId ───────────────────────────── if (req.AttachmentId <= 0) { await WriteErrorAsync( 400, FrameworkEnumDTO.ResponseStatus.Unauthorized, // ← use Unauthorized "AttachmentId is required and must be greater than 0.", ct); } try { // ── Step 3: Get file stream from BLL ────────────────────── // BLL.GetAndAuthorize verifies: // - Document exists and STATUS=0 (active) // - Document belongs to logged-in employee var (stream, mimeType, fileName) = await _ESSDocumentBLL .DownloadDocument(req.AttachmentId, loginDTO, ct) .ConfigureAwait(false); // ── Step 4: Write file to HTTP response ─────────────────── HttpContext.Response.StatusCode = 200; HttpContext.Response.ContentType = mimeType; // attachment → browser shows Save dialog HttpContext.Response.Headers.Append( "Content-Disposition", $"attachment; filename=\"{Uri.EscapeDataString(fileName)}\""); // Prevent caching of sensitive documents HttpContext.Response.Headers.Append( "Cache-Control", "no-store, no-cache, must-revalidate"); HttpContext.Response.Headers.Append( "Pragma", "no-cache"); // ── Step 5: Stream file bytes to client ─────────────────── await using (stream) await stream.CopyToAsync( HttpContext.Response.Body, ct) .ConfigureAwait(false); } catch (UnauthorizedAccessException) { await WriteErrorAsync( 403, FrameworkEnumDTO.ResponseStatus.Forbidden, "You do not have permission to access this document.", ct); } catch (FileNotFoundException) { await WriteErrorAsync( 404, FrameworkEnumDTO.ResponseStatus.NotFound, "The requested document was not found or has been deleted.", ct); } catch (Exception ex) { await WriteErrorAsync( 500, FrameworkEnumDTO.ResponseStatus.Forbidden, // ← use Forbidden $"An error occurred while downloading the document: {ex.Message}", ct); } } // ───────────────────────────────────────────────────────────────── // WRITE JSON ERROR RESPONSE // Used when download fails — returns JSON error instead of binary // ───────────────────────────────────────────────────────────────── private Task WriteErrorAsync( int httpStatus, FrameworkEnumDTO.ResponseStatus responseStatus, string message, CancellationToken ct) { var body = new ResponseStandardDTO { Status = responseStatus, Body = message, ErrorBody = message }; HttpContext.Response.StatusCode = httpStatus; HttpContext.Response.ContentType = "application/json"; return HttpContext.Response.WriteAsync( JsonConvert.SerializeObject(body), ct); } } }