using Gateway.Data; using Gateway.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using System.Data; using System.Text.Json; namespace Gateway.Controllers; /// /// Client-facing document endpoints. /// All operations are scoped to the authenticated client — clientId is always /// injected from ClientContext, never trusted from the request body. /// /// POST /api/documents/list - List client's own documents /// POST /api/documents - Upload a document (multipart) /// GET /api/documents/{id}/download - Download (enforces client ownership) /// DELETE /api/documents/{id} - Soft delete (enforces client ownership) /// [ApiController] [Route("api/documents")] public sealed class ClientDocumentController : ControllerBase { private readonly SqlService _sql; private readonly ClientContext _client; private readonly IConfiguration _config; private readonly AuthorizationGuard _guard; private readonly ILogger _log; public ClientDocumentController( SqlService sql, ClientContext client, IConfiguration config, AuthorizationGuard guard, ILogger log) { _sql = sql; _client = client; _config = config; _guard = guard; _log = log; } // ── POST /api/documents/list ───────────────────────────────────────────── [HttpPost("list")] public async Task List([FromBody] JsonElement body, CancellationToken ct) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); try { var rqst = JsonSerializer.Serialize(new { scope = "client", clientId = _client.ClientId // always from session, never from body }); var result = await _sql.ExecProcAsync("dbo.usp_DocumentList", "list", rqst, ct: ct); return Content(result, "application/json"); } catch (Exception ex) { _log.LogError(ex, "Client document list failed"); return StatusCode(500, new { ok = false, error = "Document service error" }); } } // ── POST /api/documents ────────────────────────────────────────────────── [HttpPost] [RequestSizeLimit(52_428_800)] public async Task Upload( IFormFile file, [FromForm] string category, [FromForm] string? description = null, CancellationToken ct = default) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); if (file == null || file.Length == 0) return BadRequest(new { ok = false, error = "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 = "client", docCltId = _client.ClientId // injected from session }); _log.LogInformation("[ClientDocs] Upload {FileName} | Client={ClientId}", file.FileName, _client.ClientId); var result = await ExecUploadAsync(rqst, fileBytes, ct); return Content(result, "application/json"); } catch (Exception ex) { _log.LogError(ex, "Client document upload failed: {FileName}", file?.FileName); return StatusCode(500, new { ok = false, error = "Upload failed" }); } } // ── GET /api/documents/{id}/download ───────────────────────────────────── [HttpGet("{id:long}/download")] public async Task Download(long id, CancellationToken ct = default) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); 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, clientId = _client.ClientId }) }); await using var reader = await cmd.ExecuteReaderAsync(ct); if (!await reader.ReadAsync(ct)) return NotFound(new { ok = false, error = "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) { _log.LogError(ex, "Client document download failed: docId={DocId}", id); return StatusCode(500, new { ok = false, error = "Download failed" }); } } // ── DELETE /api/documents/{id} ─────────────────────────────────────────── [HttpDelete("{id:long}")] public async Task Delete(long id, CancellationToken ct = default) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); try { _log.LogInformation("[ClientDocs] Delete docId={DocId} | Client={ClientId}", id, _client.ClientId); // Pass clientId so the SP enforces ownership before deleting var rqst = JsonSerializer.Serialize(new { docId = id, clientId = _client.ClientId }); var result = await _sql.ExecProcAsync("dbo.usp_Document", "document.delete", rqst, ct: ct); return Content(result, "application/json"); } catch (Exception ex) { _log.LogError(ex, "Client document delete failed: docId={DocId}", id); return StatusCode(500, new { ok = false, error = "Delete failed" }); } } // ─── Upload helper: binary passed separately from JSON rqst ────────────── 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, error = "No response from database" }); } }