213 lines
8.4 KiB
C#
213 lines
8.4 KiB
C#
using Gateway.Data;
|
|
using Gateway.Security;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Data.SqlClient;
|
|
using System.Data;
|
|
using System.Text.Json;
|
|
|
|
namespace Gateway.Controllers;
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
[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<ClientDocumentController> _log;
|
|
|
|
public ClientDocumentController(
|
|
SqlService sql,
|
|
ClientContext client,
|
|
IConfiguration config,
|
|
AuthorizationGuard guard,
|
|
ILogger<ClientDocumentController> log)
|
|
{
|
|
_sql = sql;
|
|
_client = client;
|
|
_config = config;
|
|
_guard = guard;
|
|
_log = log;
|
|
}
|
|
|
|
// ── POST /api/documents/list ─────────────────────────────────────────────
|
|
[HttpPost("list")]
|
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<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, error = "No response from database" });
|
|
}
|
|
}
|