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; /// /// 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} /// [ApiController] [Route("api/documents")] public sealed class DocumentController : AdminControllerBase { private readonly IConfiguration _config; public DocumentController(SqlService sql, ClientContext client, IConfiguration config, ILogger log) : base(sql, client, log) { _config = config; } // ── POST /api/documents/list ───────────────────────────────────────────── [HttpPost("list")] public async Task 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 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 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 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 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" }); } }