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