Files
raven/server/AyaNova/Controllers/ExportController.cs
John Cardinal cce07f3e6b case 4727 - attached-by override for admins only, export download by its owner only
OverrideUserId on attachment upload is used only from SuperUser or a user
with change rights on User, otherwise ignored. Export files carry the
user's id in their name and export/download serves only the key user's
own export files. F25 (4727 step 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:39:50 -07:00

163 lines
6.9 KiB
C#

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using System;
namespace AyaNova.Api.Controllers
{
[ApiController]
[Asp.Versioning.ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/export")]
[Produces("application/json")]
[Authorize]
public class ExportController : ControllerBase
{
private readonly AyContext ct;
private readonly ILogger<ExportController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public ExportController(AyContext dbcontext, ILogger<ExportController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Export to file
/// </summary>
/// <param name="selectedRequest"></param>
/// <returns>downloadable export file name</returns>
[HttpPost("render")]
public async Task<IActionResult> RenderExport([FromBody] DataListSelectedRequest selectedRequest)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
if (selectedRequest == null)
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_REQUIRED, null, "DataListSelectedRequest is required"));
if (!Authorized.HasReadFullRole(HttpContext.Items, selectedRequest.AType))
return StatusCode(403, new ApiNotAuthorizedResponse());
var UserId = UserIdFromContext.Id(HttpContext.Items);
var UserRoles = UserRolesFromContext.Roles(HttpContext.Items);
var UserTranslationId = UserTranslationIdFromContext.Id(HttpContext.Items);
//Rehydrate id list if necessary
if (selectedRequest.SelectedRowIds.Length == 0)
selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(
selectedRequest,
ct,
UserRoles,
log,
UserId,
UserTranslationId);
log.LogDebug($"Instantiating biz object handler for {selectedRequest.AType}");
var biz = BizObjectFactory.GetBizObject(selectedRequest.AType, ct, UserId, UserRoles, UserTranslationId);
log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items");
//case 4727 (F25): the name carries the user's id, so only that user can download it
string baseFileName = FileUtil.StringToSafeFileName($"{selectedRequest.AType.ToString().ToLowerInvariant()}-u{UserId}-{FileUtil.GetSafeDateFileName()}");
string outputSourceFileName = baseFileName + ".json";
string outputSourceFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputSourceFileName);
log.LogDebug($"Calling render export data to file {outputSourceFullPath}");
try
{
using (StreamWriter file = System.IO.File.CreateText(outputSourceFullPath))
using (JsonTextWriter writer = new JsonTextWriter(file))
{
var dat = await ((IExportAbleObject)biz).GetExportData(selectedRequest, Guid.Empty);//todo: jobify
dat.WriteTo(writer);
}
log.LogDebug($"Completed, returning results");
return Ok(ApiOkResponse.Response(outputSourceFileName));
}
catch (ReportRenderTimeOutException)
{
log.LogInformation($"RenderExport timeout data list key: {selectedRequest.DataListKey}, record count:{selectedRequest.SelectedRowIds.LongLength}, user:{UserNameFromContext.Name(HttpContext.Items)} ");
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "timeout - select fewer records"));
}
}
public static IList<dynamic> ToDynamicList(JArray data)
{
var dynamicData = new List<dynamic>();
var expConverter = new Newtonsoft.Json.Converters.ExpandoObjectConverter();
foreach (var dataItem in data)
{
dynamic obj = JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(dataItem.ToString(), expConverter);
dynamicData.Add(obj);
}
return dynamicData;
}
/// <summary>
/// Download a rendered Export
/// </summary>
/// <param name="fileName"></param>
/// <param name="t">download token</param>
/// <returns></returns>
[HttpGet("download/{fileName}")]
[AllowAnonymous]
public async Task<IActionResult> DownloadAsync([FromRoute] string fileName, [FromQuery] string t)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct);
if (DownloadUser == null)
{
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
//case 4727 (F25): only the caller's own export file (see the name in render), no other temporary file
if (fileName == null || !fileName.Contains($"-u{DownloadUser.Id}-") || !fileName.EndsWith(".json") || !FileUtil.TemporaryFileExists(fileName))
{
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
var FilePath = FileUtil.GetFullPathForTemporaryFile(fileName);
//including the file name triggers save automatically "attachment" rather than viewing it "inline"
return PhysicalFile(FilePath, "application/json", fileName);
}
//-----------------------------------------
}//eoc
}//eons