Files
raven/server/AyaNova/Controllers/ReportController.cs
2020-09-03 18:57:23 +00:00

579 lines
27 KiB
C#

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using System.IO;
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 PuppeteerSharp;
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<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, 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>
/// Duplicate Report
/// (Wiki and Attachments are not duplicated)
/// </summary>
/// <param name="id">Source object id</param>
/// <param name="apiVersion">From route path</param>
/// <returns>Report</returns>
[HttpPost("duplicate/{id}")]
public async Task<IActionResult> 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));
}
/// <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>
/// Put (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);//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 })); ;
}
/// <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="ayType">Type of object</param>
/// <returns>Name / id report list of allowed reports for role of requester</returns>
[HttpGet("list/{ayType}")]
public async Task<IActionResult> 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));
}
//======================================================================================================
/// <summary>
/// Get data from id list in format used by report designer
/// </summary>
/// <param name="reportDataParam">Data required for report</param>
/// <param name="apiVersion">From route path</param>
/// <returns></returns>
[HttpPost("data")]
public async Task<IActionResult> GetReportData([FromBody] ObjectReportDataParameter reportDataParam, ApiVersion apiVersion)
{
/*{
public AyaType ObjectType { get; set; }
public long[] SelectedRowIds { get; set; }
public string DataListKey { get; set; }
public string ListView { get; set; }//optional, if null or empty will use default list view built into DataList
}*/
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));
// Newtonsoft.Json.Linq.JArray reportData = null;
// if (reportDataParam.SelectedRowIds.Length > 0)
// {
// //pre-selected id values
// reportData = await biz.GetReportData(reportDataParam.ObjectType, reportDataParam.SelectedRowIds);
// }
// else
// {
// //get from datalist values
// //get the list of id's from the data list view
// var rowIds = await AyaNova.DataList.DataListFetcher.GetIdListResponseAsync(reportDataParam.DataListKey, reportDataParam.ListView, ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items), log);
// //now get the report data
// reportData = await biz.GetReportData(reportDataParam.ObjectType, rowIds);
// }
var reportData = await biz.GetReportData(reportDataParam);
if (reportData == null)
return BadRequest(new ApiErrorResponse(biz.Errors));
else
return Ok(ApiOkResponse.Response(reportData));
}
/// <summary>
/// Render Report
/// </summary>
/// <param name="reportParam">report id and object id values for object type specified in report template</param>
/// <param name="apiVersion">From route path</param>
/// <returns>downloadable pdf name</returns>
[HttpPost("render")]
public async Task<IActionResult> 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<IHttpConnectionFeature>();
var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/";
var result = await biz.RenderReport(reportParam, API_URL);
if (string.IsNullOrWhiteSpace(result))
return BadRequest(new ApiErrorResponse(biz.Errors));
else
return Ok(ApiOkResponse.Response(result));
}
/// <summary>
/// Download a report file
/// </summary>
/// <param name="fileName"></param>
/// <param name="t">download token</param>
/// <returns></returns>
[HttpGet("download/{fileName}")]
public async Task<IActionResult> DownloadAsync([FromRoute] string fileName, [FromQuery] string t)
{
int nFailedAuthDelay = 3000;
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
if (string.IsNullOrWhiteSpace(t))
{
await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
var DownloadUser = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.DlKey == t && z.Active == true);
if (DownloadUser == null)
{
await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
if (DownloadUser.DlKeyExpire < utcNow.DateTime)
{
await Task.Delay(nFailedAuthDelay);//DOS protection
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
// if (!Authorized.HasModifyRole(DownloadUser.Roles, AyaType.Backup))//not technically modify but treating as such as a backup is very sensitive data
// {
// await Task.Delay(nFailedAuthDelay);//DOS protection
// return StatusCode(403, new ApiNotAuthorizedResponse());
// }
if (!FileUtil.TemporaryFileExists(fileName))
{
await Task.Delay(nFailedAuthDelay);//fishing protection
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
var FilePath = FileUtil.GetFullPathForTemporaryFile(fileName);
// await EventLogProcessor.LogEventToDatabaseAsync(new Event(DownloadUser.Id, 0, AyaType.NoType, AyaEvent.UtilityFileDownload, fileName), ct);
return PhysicalFile(FilePath, "application/pdf");
}
// [HttpGet("render-test")]
// [AllowAnonymous]
// public async Task<IActionResult> GetTestReport([FromRoute] string test)
// {
// if (!serverState.IsOpen)
// return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
// string outputFile = FileUtil.NewRandomTempFilesFolderFileName;
// switch (test)
// {
// case "chrome-reddit-to-pdf":
// //first test, just render a web page to pdf and return it
// //return PhysicalFile(filePath, mimetype, dbObject.DisplayFileName);
// outputFile += ".pdf";
// //http://www.puppeteersharp.com/api/index.html
// //https://github.com/hardkoded/puppeteer-sharp
// await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
// var browser = await Puppeteer.LaunchAsync(new LaunchOptions
// {
// Headless = true
// });
// var page = await browser.NewPageAsync();
// await page.GoToAsync("https://github.com/hardkoded/puppeteer-sharp");
// await page.PdfAsync(outputFile);
// return PhysicalFile(outputFile, "application/pdf");
// }
// return NotFound(test);
// }
//---------------------------------------------------------------------
[HttpGet("poc")]
[AllowAnonymous]
public async Task<IActionResult> ProofOfConcept([FromRoute] string url)
{
//https://test.helloayanova.com/api/v8/report/poc
//http://localhost:7575/api/v8/report/poc
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
var httpConnectionFeature = HttpContext.Features.Get<IHttpConnectionFeature>();
var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/";
//var localIpAddress = httpConnectionFeature?.LocalIpAddress;
//todo: validate files are present somehow?
var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt");
if (!Directory.Exists(ReportJSFolderPath))
throw new System.Exception($"E1012: \"reportjs\" folder not found where expected: \"{ReportJSFolderPath}\", installation damaged?");
//sample CSS
var reportCSS = @"
@page {
margin: 1cm;
}
@page :first {
margin: 2cm;
}
.ay-red {
color:red;
}
.ay-blue {
color:blue;
}
";
//sample template
//var reportTemplate = "'<div>{{#with person}}<span class=\"ay-red\">{{firstname}}</span> {{aycaps lastname}}{{/with}}</div>'";
var reportTemplate = "'<!DOCTYPE html><html><head><title>test title</title></head><body> <img src=\"{{aylogo.medium}}\" />" +
"<h1>Test page top</h1><p class=\"ay-blue\">A blue paragraph</p><div>{{#with person}}<span class=\"ay-red\">{{firstname}}</span> {{aycaps lastname}}{{/with}}</div>" +
"<br/><br/><h4>Some markdown</h4><div>{{{aymarkdown mdtest}}}</div></body></html>'";
//data object
var aylogo = $"{{small:'{API_URL}logo/small',medium:'{API_URL}logo/medium',large:'{API_URL}logo/large'}}";
var reportData = "{ person: { firstname: 'Tyler', lastname: 'Mott' },aylogo:[AYLOGO], mdtest:'| CODE | MEANING |\\n| ----- | ------------------------------ |\\n| E1000 | Could not connect to the database specified in the [connection string](ops-config-db.md). |\\n| E1050 | XXXXXXXX |\\n| E1012 | Missing resource folder. AyaNova was started from the wrong location or was not installed properly. |\\n' }";
reportData = reportData.Replace("[AYLOGO]", aylogo);
log.LogInformation($"setting Chrome launchoptions for os:");
var lo = new LaunchOptions { Headless = true };
bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
if (!isWindows)
{
log.LogInformation($"IS NOT WINDOWS: setting executable path for chrome");
lo.ExecutablePath = "/usr/bin/chromium-browser";//this is the default path for docker based alpine dist, but maybe not others, need to make a config setting likely
lo.Args = new string[] { "--no-sandbox" };
}
else
{
log.LogInformation($"IS WINDOWS: Calling browserFetcher download async now:");
await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
}
log.LogInformation($"Calling browser.launchAsync now:");
using (var browser = await Puppeteer.LaunchAsync(lo))
using (var page = await browser.NewPageAsync())
{
log.LogInformation($"In using for page ops adding scripts and report now:");
//Add handlebars JS for compiling and presenting
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") });
//add marked for markdown processing
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") });
//test add helpers
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") });
//execute to add to handlebars
await page.EvaluateExpressionAsync("ayRegisterHelpers();");
//compile and run handlebars template
var compileScript = $"Handlebars.compile({reportTemplate})({reportData});";
var resultHTML = await page.EvaluateExpressionAsync<string>(compileScript);
//render report as HTML
await page.SetContentAsync(resultHTML);
//add style (after page or it won't work)
await page.AddStyleTagAsync(new AddTagOptions { Content = reportCSS });
//useful for debugging purposes only
//var pagecontent = await page.GetContentAsync();
//render to pdf and return
var pdfBuffer = await page.PdfDataAsync();
log.LogInformation($"returning results now:");
return new FileContentResult(pdfBuffer, "application/pdf");
}
}
[HttpPost("post-poc")]
public async Task<IActionResult> PostProofOfConcept([FromBody] NameItem nameItem)
{
//https://test.helloayanova.com/api/v8/report/poc
//http://localhost:7575/api/v8/report/poc
if (!serverState.IsOpen)
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
var httpConnectionFeature = HttpContext.Features.Get<IHttpConnectionFeature>();
var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/";
//var localIpAddress = httpConnectionFeature?.LocalIpAddress;
//todo: validate files are present somehow?
var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt");
if (!Directory.Exists(ReportJSFolderPath))
throw new System.Exception($"E1012: \"reportjs\" folder not found where expected: \"{ReportJSFolderPath}\", installation damaged?");
//sample CSS
var reportCSS = @"
@page {
margin: 1cm;
}
@page :first {
margin: 2cm;
}
.ay-red {
color:red;
}
.ay-blue {
color:blue;
}
";
//sample template
//var reportTemplate = "'<div>{{#with person}}<span class=\"ay-red\">{{firstname}}</span> {{aycaps lastname}}{{/with}}</div>'";
var reportTemplate = "'<!DOCTYPE html><html><head><title>test title</title></head><body> <img src=\"{{aylogo.medium}}\" />" +
"<h1>Test page top</h1><p class=\"ay-blue\">A blue paragraph</p><div>{{#with person}}<span class=\"ay-red\">{{firstname}}</span> {{aycaps lastname}}{{/with}}</div>" +
"<br/><br/><h4>Some markdown</h4><div>{{{aymarkdown mdtest}}}</div></body></html>'";
//data object
var aylogo = $"{{small:'{API_URL}logo/small',medium:'{API_URL}logo/medium',large:'{API_URL}logo/large'}}";
var reportData = "{ person: { firstname: 'Thatcher', lastname: '[NAME]' },aylogo:[AYLOGO], mdtest:'| CODE | MEANING |\\n| ----- | ------------------------------ |\\n| E1000 | Could not connect to the database specified in the [connection string](ops-config-db.md). |\\n| E1050 | XXXXXXXX |\\n| E1012 | Missing resource folder. AyaNova was started from the wrong location or was not installed properly. |\\n' }";
reportData = reportData.Replace("[AYLOGO]", aylogo);
reportData = reportData.Replace("[NAME]", nameItem.Name);
log.LogInformation($"setting Chrome launchoptions for os:");
var lo = new LaunchOptions { Headless = true };
bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
if (!isWindows)
{
log.LogInformation($"IS NOT WINDOWS: setting executable path for chrome");
lo.ExecutablePath = "/usr/bin/chromium-browser";//this is the default path for docker based alpine dist, but maybe not others, need to make a config setting likely
lo.Args = new string[] { "--no-sandbox" };
}
else
{
log.LogInformation($"IS WINDOWS: Calling browserFetcher download async now:");
await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
}
log.LogInformation($"Calling browser.launchAsync now:");
using (var browser = await Puppeteer.LaunchAsync(lo))
using (var page = await browser.NewPageAsync())
{
log.LogInformation($"In using for page ops adding scripts and report now:");
//Add handlebars JS for compiling and presenting
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") });
//add marked for markdown processing
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") });
//test add helpers
await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") });
//execute to add to handlebars
await page.EvaluateExpressionAsync("ayRegisterHelpers();");
//compile and run handlebars template
var compileScript = $"Handlebars.compile({reportTemplate})({reportData});";
var resultHTML = await page.EvaluateExpressionAsync<string>(compileScript);
//render report as HTML
await page.SetContentAsync(resultHTML);
//add style (after page or it won't work)
await page.AddStyleTagAsync(new AddTagOptions { Content = reportCSS });
string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf";
string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName);
//render to pdf and return
await page.PdfAsync(outputFullPath);
log.LogDebug($"returning results now:");
log.LogInformation($"returning results now:");
return Ok(ApiOkResponse.Response(outputFileName));
// return new FileContentResult(pdfBuffer, "application/pdf");
}
}
//-----------------------------------------
//------------
/*
NOTES/TODO during testing
Need job to automatically erase any temp files older than 5 minutes (or whatever)
*/
}//eoc
}//eons