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 log; private readonly ApiServerState serverState; /// /// ctor /// /// /// /// public ReportController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState) { ct = dbcontext; log = logger; serverState = apiServerState; } /// /// Create Report /// /// /// From route path /// [HttpPost] public async Task 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)); } /// /// Get Report /// /// /// Report [HttpGet("{id}")] public async Task 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)); } /// /// Update Report /// /// /// [HttpPut] public async Task 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 })); ; } /// /// Delete Report /// /// /// NoContent [HttpDelete("{id}")] public async Task 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(); } /// /// Get Report list for object /// /// Type of object /// Name / id report list of allowed reports for role of requester [HttpGet("list/{aType}")] public async Task 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)); } /// /// Get a limited amount of sample data from id list in format used by report designer /// /// Data required for report /// From route path /// [HttpPost("data")] public async Task 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")); } } /// /// Start Render Report job /// /// report id and object id values for object type specified in report template /// From route path /// Job Id [HttpPost("render-job")] public async Task 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(); 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 }); } /// /// Attempt cancel render job /// /// /// nothing [HttpPost("request-cancel/{gid}")] public async Task 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(); } /// /// Download a rendered report /// /// /// download token /// [HttpGet("download/{fileName}")] [AllowAnonymous] public async Task 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"); } /// /// Download report template /// /// Report id /// download token /// A single report template as a file [AllowAnonymous] [HttpGet("export/{id}")] public async Task 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; } /// /// Upload Reprot template export file /// Max 15mb total /// /// Accepted [Authorize] [HttpPost("upload")] [DisableFormValueModelBinding] [RequestSizeLimit(AyaNova.Util.ServerBootConfig.MAX_REPORT_TEMPLATE_UPLOAD_BYTES)] public async Task 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 FileData = new List(); 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>(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