Files
AdPlatform-Server/Management/Controllers/Admin/DocumentController.cs
2026-03-14 13:50:09 -07:00

178 lines
7.1 KiB
C#

using Management.Data;
using Management.Security;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using System.Data;
using System.Text.Json;
namespace Management.Controllers.Admin;
/// <summary>
/// Internal document management — scope='internal' only.
/// Client-scoped documents are managed through the Gateway.
///
/// POST /api/documents/list - List internal documents
/// POST /api/documents - Upload internal document
/// GET /api/documents/{id}/download
/// DELETE /api/documents/{id}
/// </summary>
[ApiController]
[Route("api/documents")]
public sealed class DocumentController : AdminControllerBase
{
private readonly IConfiguration _config;
public DocumentController(SqlService sql, ClientContext client, IConfiguration config, ILogger<DocumentController> log)
: base(sql, client, log)
{
_config = config;
}
// ── POST /api/documents/list ─────────────────────────────────────────────
[HttpPost("list")]
public async Task<IActionResult> List([FromBody] JsonElement body, CancellationToken ct)
{
try
{
// Always internal scope for Management API
var rqst = JsonSerializer.Serialize(new { scope = "internal" });
var result = await Sql.ExecProcAsync("dbo.usp_DocumentList", "list", rqst, ct: ct);
return Content(result, "application/json");
}
catch (Exception ex)
{
Logger.LogError(ex, "Document list failed");
return StatusCode(500, new { ok = false, message = ex.Message });
}
}
// ── POST /api/documents ──────────────────────────────────────────────────
[HttpPost]
[RequestSizeLimit(52_428_800)]
public async Task<IActionResult> Upload(
IFormFile file,
[FromForm] string category,
[FromForm] string? description = null,
CancellationToken ct = default)
{
if (file == null || file.Length == 0)
return BadRequest(new { ok = false, message = "No file provided" });
try
{
byte[] fileBytes;
using (var ms = new MemoryStream())
{
await file.CopyToAsync(ms, ct);
fileBytes = ms.ToArray();
}
var rqst = JsonSerializer.Serialize(new
{
docFileName = file.FileName,
docMimeType = file.ContentType,
docFileSize = file.Length,
docCategory = category,
docDescription = description,
docUploadedBy = Client.Email,
docScope = "internal", // Management API = internal only
docCltId = (string?)null
});
var result = await ExecUploadAsync(rqst, fileBytes, ct);
return Content(result, "application/json");
}
catch (Exception ex)
{
Logger.LogError(ex, "Document upload failed: {FileName}", file?.FileName);
return StatusCode(500, new { ok = false, message = ex.Message });
}
}
// ── GET /api/documents/{id}/download ─────────────────────────────────────
[HttpGet("{id:long}/download")]
public async Task<IActionResult> Download(long id, CancellationToken ct = default)
{
try
{
var cs = _config.GetConnectionString("Sql")
?? throw new InvalidOperationException("Missing ConnectionStrings:Sql");
await using var conn = new SqlConnection(cs);
await conn.OpenAsync(ct);
await using var cmd = new SqlCommand("dbo.usp_Document", conn)
{
CommandType = CommandType.StoredProcedure,
CommandTimeout = 60
};
cmd.Parameters.Add(new SqlParameter("@action", SqlDbType.VarChar, 50) { Value = "document.download" });
cmd.Parameters.Add(new SqlParameter("@rqst", SqlDbType.NVarChar, -1) { Value = JsonSerializer.Serialize(new { docId = id }) });
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct))
return NotFound(new { ok = false, message = "Document not found" });
var fileName = reader.GetString(reader.GetOrdinal("docFileName"));
var mimeType = reader.GetString(reader.GetOrdinal("docMimeType"));
var content = (byte[])reader["docContent"];
return File(content, mimeType, fileName);
}
catch (Exception ex)
{
Logger.LogError(ex, "Document download failed: docId={DocId}", id);
return StatusCode(500, new { ok = false, message = ex.Message });
}
}
// ── DELETE /api/documents/{id} ───────────────────────────────────────────
[HttpDelete("{id:long}")]
public async Task<IActionResult> Delete(long id, CancellationToken ct = default)
{
try
{
Logger.LogInformation("[Documents] Delete docId={DocId} by {User}", id, Client.Email);
var rqst = JsonSerializer.Serialize(new { docId = id });
var result = await Sql.ExecProcAsync("dbo.usp_Document", "document.delete", rqst, ct: ct);
return Content(result, "application/json");
}
catch (Exception ex)
{
Logger.LogError(ex, "Document delete failed: docId={DocId}", id);
return StatusCode(500, new { ok = false, message = ex.Message });
}
}
// ─── Upload helper: binary @filecontent passed separately ────────────────
private async Task<string> ExecUploadAsync(string rqst, byte[] fileContent, CancellationToken ct)
{
var cs = _config.GetConnectionString("Sql")
?? throw new InvalidOperationException("Missing ConnectionStrings:Sql");
await using var conn = new SqlConnection(cs);
await conn.OpenAsync(ct);
await using var cmd = new SqlCommand("dbo.usp_Document", conn)
{
CommandType = CommandType.StoredProcedure,
CommandTimeout = 60
};
cmd.Parameters.Add(new SqlParameter("@action", SqlDbType.VarChar, 50) { Value = "document.upload" });
cmd.Parameters.Add(new SqlParameter("@rqst", SqlDbType.NVarChar, -1) { Value = rqst });
cmd.Parameters.Add(new SqlParameter("@filecontent", SqlDbType.VarBinary, -1) { Value = fileContent });
var pResp = new SqlParameter("@resp", SqlDbType.NVarChar, -1)
{
Direction = ParameterDirection.Output
};
cmd.Parameters.Add(pResp);
await cmd.ExecuteNonQueryAsync(ct);
return pResp.Value as string
?? JsonSerializer.Serialize(new { ok = false, message = "No response from database" });
}
}