using System; 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.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] [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, 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)); } /// /// Duplicate Report /// (Wiki and Attachments are not duplicated) /// /// Source object id /// From route path /// Report [HttpPost("duplicate/{id}")] public async Task DuplicateReport([FromRoute] long id, 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.DuplicateAsync(id); 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)); } /// /// Put (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);//In future may need to return entire object, for now just concurrency token 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/{ayType}")] public async Task GetReportList([FromRoute] AyaType ayType) { 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, ayType)) return StatusCode(403, new ApiNotAuthorizedResponse()); if (!ModelState.IsValid) return BadRequest(new ApiErrorResponse(ModelState)); var o = await biz.GetReportListAsync(ayType); if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); return Ok(ApiOkResponse.Response(o)); } /// /// Get data from id list in format used by report designer /// /// Data required for report /// From route path /// [HttpPost("data")] public async Task GetReportData([FromBody] DataListSelection reportDataParam, 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)); var reportData = await biz.GetReportData(reportDataParam); if (reportData == null) return BadRequest(new ApiErrorResponse(biz.Errors)); else return Ok(ApiOkResponse.Response(reportData)); } /// /// Render Report /// /// report id and object id values for object type specified in report template /// From route path /// downloadable pdf name [HttpPost("render")] public async Task RenderReport([FromBody] RenderReportParameter reportParam, 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/v8/"; try { var result = await biz.RenderReport(reportParam, API_URL); if (string.IsNullOrWhiteSpace(result)) return BadRequest(new ApiErrorResponse(biz.Errors)); else return Ok(ApiOkResponse.Response(result)); } catch (System.Exception ex) { //The Javascript evaluation stack trace can be in the message making it long and internalized, //however the info is useful as it can indicate exactly which function failed etc so sending it all back is best return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message)); } } /// /// 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(15000000)]//currently the largest v7 export for a report template is 828kb, I'm guessing 15mb is more than enough 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); 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