Files
raven/server/AyaNova/Controllers/ReportController.cs

425 lines
20 KiB
C#

using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AyaNova.Api.Controllers
{
[ApiController]
[Asp.Versioning.ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/report")]
[Produces("application/json")]
[Authorize]
public class ReportController : ControllerBase
{
private readonly AyContext ct;
private readonly ILogger<ReportController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public ReportController(AyContext dbcontext, ILogger<ReportController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Create Report
/// </summary>
/// <param name="newObject"></param>
/// <param name="apiVersion">From route path</param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostReport([FromBody] Report newObject, Asp.Versioning.ApiVersion apiVersion)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasCreateRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
Report o = await biz.CreateAsync(newObject);
if (o == null)
return BadRequest(new ApiErrorResponse(biz.Errors));
else
return CreatedAtAction(nameof(ReportController.GetReport), new { id = o.Id, version = apiVersion.ToString() }, new ApiCreatedResponse(o));
}
/// <summary>
/// Get Report
/// </summary>
/// <param name="id"></param>
/// <returns>Report</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetReport([FromRoute] long id)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var o = await biz.GetAsync(id);
if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
return Ok(ApiOkResponse.Response(o));
}
/// <summary>
/// Update Report
/// </summary>
/// <param name="updatedObject"></param>
/// <returns></returns>
[HttpPut]
public async Task<IActionResult> PutReport([FromBody] Report updatedObject)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
var o = await biz.PutAsync(updatedObject);
if (o == null)
{
if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT))
return StatusCode(409, new ApiErrorResponse(biz.Errors));
else
return BadRequest(new ApiErrorResponse(biz.Errors));
}
return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency })); ;
}
/// <summary>
/// Delete Report
/// </summary>
/// <param name="id"></param>
/// <returns>NoContent</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteReport([FromRoute] long id)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!await biz.DeleteAsync(id))
return BadRequest(new ApiErrorResponse(biz.Errors));
return NoContent();
}
/// <summary>
/// Get Report list for object
/// </summary>
/// <param name="aType">Type of object</param>
/// <returns>Name / id report list of allowed reports for role of requester</returns>
[HttpGet("list/{aType}")]
public async Task<IActionResult> GetReportList([FromRoute] AyaType aType)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
//extra check if they have rights to the type of object in question, this nips it in the bud before they even get to the fetch data stage later
if (!Authorized.HasReadFullRole(HttpContext.Items, aType))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var o = await biz.GetReportListAsync(aType);
if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
return Ok(ApiOkResponse.Response(o));
}
/// <summary>
/// Get a limited amount of sample data from id list in format used by report designer
/// </summary>
/// <param name="selectedRequest">Data required for report</param>
/// <param name="apiVersion">From route path</param>
/// <returns></returns>
[HttpPost("data")]
public async Task<IActionResult> GetReportData([FromBody] DataListSelectedRequest selectedRequest, Asp.Versioning.ApiVersion apiVersion)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
//cap the data returned
selectedRequest.ReportDesignerSample = true;
JArray reportData;
try
{
reportData = await biz.GetReportDataForReportDesigner(selectedRequest);
if (reportData == null)
return BadRequest(new ApiErrorResponse(biz.Errors));
else
return Ok(ApiOkResponse.Response(reportData));
}
catch (ReportRenderTimeOutException)
{
log.LogInformation($"GetReportData timeout data list key: {selectedRequest.DataListKey}, record count:{selectedRequest.SelectedRowIds.LongLength}, user:{UserNameFromContext.Name(HttpContext.Items)} ");
//note: this route is called by the report designer to get a limited subset of records so we should never see this error but including it for completeness
//report designer should show this as a general error
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "timeout - select fewer records"));
}
}
/// <summary>
/// Start Render Report job
/// </summary>
/// <param name="reportRequest">report id and object id values for object type specified in report template</param>
/// <param name="apiVersion">From route path</param>
/// <returns>Job Id</returns>
[HttpPost("render-job")]
public async Task<IActionResult> RequestRenderReport([FromBody] DataListReportRequest reportRequest, Asp.Versioning.ApiVersion apiVersion)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
var httpConnectionFeature = HttpContext.Features.Get<IHttpConnectionFeature>();
var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/{AyaNovaVersion.CurrentApiVersion}/";
var result = await biz.RequestRenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL, UserNameFromContext.Name(HttpContext.Items));
if (result == null)
return BadRequest(new ApiErrorResponse(biz.Errors));
else
return Accepted(new { JobId = result });
}
/// <summary>
/// Attempt cancel render job
/// </summary>
/// <param name="gid"></param>
/// <returns>nothing</returns>
[HttpPost("request-cancel/{gid}")]
public async Task<IActionResult> RequestCancelJob([FromRoute] Guid gid)
{
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType))
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
log.LogDebug($"request-cancel called for report rendering job id {gid}");
await biz.CancelJob(gid);
return NoContent();
}
/// <summary>
/// Download a rendered report
/// </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));
if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
{
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
if (!FileUtil.TemporaryFileExists(fileName))
{
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
var FilePath = FileUtil.GetFullPathForTemporaryFile(fileName);
return PhysicalFile(FilePath, "application/pdf");
}
/// <summary>
/// Download report template
/// </summary>
/// <param name="id">Report id</param>
/// <param name="t">download token</param>
/// <returns>A single report template as a file</returns>
[AllowAnonymous]
[HttpGet("export/{id}")]
public async Task<IActionResult> DownloadTemplate([FromRoute] long id, [FromQuery] string t)
{
if (serverState.IsClosed)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
{
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
var o = await ct.Report.SingleOrDefaultAsync(z => z.Id == id);
//turn into correct format and then send as file
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
var asText = Newtonsoft.Json.JsonConvert.SerializeObject(
o,
Newtonsoft.Json.Formatting.None,
new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id" }) });
var bytes = System.Text.Encoding.UTF8.GetBytes(asText);
var file = new FileContentResult(bytes, "application/octet-stream");
file.FileDownloadName = Util.FileUtil.StringToSafeFileName(o.Name) + ".ayrt";
return file;
}
/// <summary>
/// Upload Reprot template export file
/// Max 15mb total
/// </summary>
/// <returns>Accepted</returns>
[Authorize]
[HttpPost("upload")]
[DisableFormValueModelBinding]
[RequestSizeLimit(AyaNova.Util.ServerBootConfig.MAX_REPORT_TEMPLATE_UPLOAD_BYTES)]
public async Task<IActionResult> UploadAsync()
{
//Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
// AyaTypeId attachToObject = null;
ApiUploadProcessor.ApiUploadedFilesResult uploadFormData = null;
try
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, $"Expected a multipart request, but got {Request.ContentType}"));
//Save uploads to disk under temporary file names until we decide how to handle them
// uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext);
//Save uploads to disk under temporary file names until we decide how to handle them
uploadFormData = await ApiUploadProcessor.ProcessUploadAsync(HttpContext);
if (!string.IsNullOrWhiteSpace(uploadFormData.Error))
{
//delete temp files
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
//file too large is most likely issue so in that case return this localized properly
if (uploadFormData.Error.Contains("413"))
{
var TransId = UserTranslationIdFromContext.Id(HttpContext.Items);
return BadRequest(new ApiErrorResponse(
ApiErrorCode.VALIDATION_LENGTH_EXCEEDED,
null,
String.Format(await TranslationBiz.GetTranslationStaticAsync("AyaFileFileTooLarge", TransId, ct), AyaNova.Util.FileUtil.GetBytesReadable(AyaNova.Util.ServerBootConfig.MAX_REPORT_TEMPLATE_UPLOAD_BYTES))));
}
else//not too big, something else
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, uploadFormData.Error));
}
List<UploadFileData> FileData = new List<UploadFileData>();
if (!uploadFormData.FormFieldData.ContainsKey("FileData"))//only filedata is required
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Missing required FormFieldData value: FileData"));
}
//fileData in JSON stringify format
FileData = Newtonsoft.Json.JsonConvert.DeserializeObject<List<UploadFileData>>(uploadFormData.FormFieldData["FileData"].ToString());
//Instantiate the business object handler
ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext);
//We have our files now can parse and insert into db
if (uploadFormData.UploadedFiles.Count > 0)
{
//deserialize each file and import
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
{
JObject o = JObject.Parse(System.IO.File.ReadAllText(a.InitialUploadedPathName));
if (!await biz.ImportAsync(o))
{
//delete all the files temporarily uploaded and return bad request
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
return BadRequest(new ApiErrorResponse(biz.Errors));
}
}
}
}
catch (System.IO.InvalidDataException ex)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message));
}
finally
{
//delete all the files temporarily uploaded and return bad request
ApiUploadProcessor.DeleteTempUploadFile(uploadFormData);
}
//Return the list of attachment ids and filenames
return Accepted();
}
// private static void DeleteTempUploadFile(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData)
// {
// if (uploadFormData.UploadedFiles.Count > 0)
// {
// foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
// {
// System.IO.File.Delete(a.InitialUploadedPathName);
// }
// }
// }
//-----------------------------------------
}//eoc
}//eons