diff --git a/server/AyaNova/Controllers/AttachmentController.cs b/server/AyaNova/Controllers/AttachmentController.cs index 408c42dd..46411f1c 100644 --- a/server/AyaNova/Controllers/AttachmentController.cs +++ b/server/AyaNova/Controllers/AttachmentController.cs @@ -513,61 +513,32 @@ namespace AyaNova.Api.Controllers [HttpGet("download/{id}")] public async Task DownloadAsync([FromRoute] long id, [FromQuery] string t) { - int nFailedAuthDelay = 3000;//should be just long enough to make brute force a hassle but short enough to not annoy people who just mistyped their creds to login - - //NOTE this is the only unauthorized route as it needs to work with wiki url links and relies on the dlkey to work - //copied from Rockfish //https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/ //https://stackoverflow.com/questions/45763149/asp-net-core-jwt-in-uri-query-parameter/45811270#45811270 if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - //NOTE: this is a potentially dangerous route since it's not Authorized so we need to treat it like Auth route and not leak any - //useful information to bad actors and also ensure a delay to avoid brute force or DOS attacks - - if (string.IsNullOrWhiteSpace(t)) - { - await Task.Delay(nFailedAuthDelay);//DOS protection - return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); - } - - - - - //get user by key, if not found then reject - //If user dlkeyexp has not expired then return file - var DownloadUser = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.DlKey == t && z.Active == true); + var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct); if (DownloadUser == null) { - await Task.Delay(nFailedAuthDelay);//DOS protection + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } - //this is necessary because they might have an expired JWT but this would just keep on working without a date check - //the default is the same timespan as the jwt so it's all good - 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)); - } - //Ok, user has a valid download key and it's not expired yet so get the attachment record var dbObject = await ct.FileAttachment.SingleOrDefaultAsync(z => z.Id == id); if (dbObject == null) { - await Task.Delay(nFailedAuthDelay);//fishing protection + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } - //is this allowed? if (!Authorized.HasReadFullRole(DownloadUser.Roles, dbObject.AttachToObjectType)) { - await Task.Delay(nFailedAuthDelay);//DOS protection + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection return StatusCode(403, new ApiNotAuthorizedResponse()); } diff --git a/server/AyaNova/Controllers/AuthController.cs b/server/AyaNova/Controllers/AuthController.cs index e56df79c..fd5d3318 100644 --- a/server/AyaNova/Controllers/AuthController.cs +++ b/server/AyaNova/Controllers/AuthController.cs @@ -71,11 +71,10 @@ namespace AyaNova.Api.Controllers return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason)); } - int nFailedAuthDelay = 3000;//should be just long enough to make brute force a hassle but short enough to not annoy people who just mistyped their creds to login - + #if (DEBUG) - nFailedAuthDelay = 1; + #region TESTING @@ -150,7 +149,7 @@ namespace AyaNova.Api.Controllers if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password)) { //Make a failed pw wait - await Task.Delay(nFailedAuthDelay); + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } @@ -249,7 +248,7 @@ namespace AyaNova.Api.Controllers //No users matched, it's a failed login //Make a failed pw wait - await Task.Delay(nFailedAuthDelay); + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } diff --git a/server/AyaNova/Controllers/BackupController.cs b/server/AyaNova/Controllers/BackupController.cs index 53f51f96..2e931502 100644 --- a/server/AyaNova/Controllers/BackupController.cs +++ b/server/AyaNova/Controllers/BackupController.cs @@ -101,36 +101,25 @@ namespace AyaNova.Api.Controllers [HttpGet("download/{fileName}")] public async Task 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); + var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct); if (DownloadUser == null) { - await Task.Delay(nFailedAuthDelay);//DOS protection + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//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 + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection return StatusCode(403, new ApiNotAuthorizedResponse()); } if (!FileUtil.BackupFileExists(fileName)) { - await Task.Delay(nFailedAuthDelay);//fishing protection + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } string mimetype = fileName.EndsWith("zip") ? "application/zip" : "application/octet-stream"; diff --git a/server/AyaNova/Controllers/ReportController.cs b/server/AyaNova/Controllers/ReportController.cs index 01166891..bd3b4488 100644 --- a/server/AyaNova/Controllers/ReportController.cs +++ b/server/AyaNova/Controllers/ReportController.cs @@ -1,7 +1,6 @@ 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; @@ -12,8 +11,7 @@ using AyaNova.Models; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using AyaNova.Util; - -using PuppeteerSharp; +using Newtonsoft.Json; namespace AyaNova.Api.Controllers { @@ -42,9 +40,6 @@ namespace AyaNova.Api.Controllers } - - - /// /// Create Report /// @@ -184,9 +179,6 @@ namespace AyaNova.Api.Controllers } - //====================================================================================================== - - /// /// Get data from id list in format used by report designer /// @@ -196,37 +188,12 @@ namespace AyaNova.Api.Controllers [HttpPost("data")] public async Task GetReportData([FromBody] ReportDataParameter 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 @@ -277,7 +244,7 @@ namespace AyaNova.Api.Controllers /// - /// Download a report file + /// Download a rendered report /// /// /// download token @@ -286,295 +253,63 @@ namespace AyaNova.Api.Controllers [AllowAnonymous] public async Task 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 (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(nFailedAuthDelay);//fishing protection + await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//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 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")] + /// + /// Download report template + /// + /// Report id + /// download token + /// A single report template as a file [AllowAnonymous] - public async Task ProofOfConcept([FromRoute] string url) + [HttpGet("export/{id}")] + public async Task DownloadTemplate([FromRoute] long id, [FromQuery] string t) { - //https://test.helloayanova.com/api/v8/report/poc - //http://localhost:7575/api/v8/report/poc - - if (!serverState.IsOpen) + if (serverState.IsClosed) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - var httpConnectionFeature = HttpContext.Features.Get(); - var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; - //var localIpAddress = httpConnectionFeature?.LocalIpAddress; + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); - //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 = "'
{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
'"; - var reportTemplate = "'test title " + - "

Test page top

A blue paragraph

{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
" + - "

Some markdown

{{{aymarkdown mdtest}}}
'"; - - //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) + if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null) { - 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" }; + await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection + return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } - else + + var o = await ct.Report.SingleOrDefaultAsync(z => z.Id == id); + //turn into correct format and then send as file + if (o == null) { - log.LogInformation($"IS WINDOWS: Calling browserFetcher download async now:"); - await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); + return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); } - 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(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 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(); - 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 = "'
{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
'"; - var reportTemplate = "'test title " + - "

Test page top

A blue paragraph

{{#with person}}{{firstname}} {{aycaps lastname}}{{/with}}
" + - "

Some markdown

{{{aymarkdown mdtest}}}
'"; - - //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(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"); - - } - + var asText = Newtonsoft.Json.JsonConvert.SerializeObject( + o, + Newtonsoft.Json.Formatting.None, + new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "ReportId" }) }); + var bytes = System.Text.Encoding.UTF8.GetBytes(asText); + var file = new FileContentResult(bytes, "application/octet-stream"); + file.FileDownloadName = Util.FileUtil.StringToSafeFileName(o.Name) + ".json"; + return file; } //----------------------------------------- diff --git a/server/AyaNova/Controllers/TranslationController.cs b/server/AyaNova/Controllers/TranslationController.cs index 9d118049..b08a1c8d 100644 --- a/server/AyaNova/Controllers/TranslationController.cs +++ b/server/AyaNova/Controllers/TranslationController.cs @@ -13,10 +13,7 @@ using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using System; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Linq; -using System.Linq; -using AyaNova.Util; @@ -268,36 +265,16 @@ namespace AyaNova.Api.Controllers if (serverState.IsClosed) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - if (!ModelState.IsValid) - { return BadRequest(new ApiErrorResponse(ModelState)); - } - - int nFailedAuthDelay = 3000; - - if (string.IsNullOrWhiteSpace(t)) + if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null) { - await Task.Delay(nFailedAuthDelay);//DOS protection + await Task.Delay(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);//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)); - } - var o = await ct.Translation.Include(z => z.TranslationItems).SingleOrDefaultAsync(z => z.Id == id); - //turn into correct format and then send as file if (o == null) { @@ -306,7 +283,7 @@ namespace AyaNova.Api.Controllers var asText = Newtonsoft.Json.JsonConvert.SerializeObject( o, Newtonsoft.Json.Formatting.None, - new JsonSerializerSettings { ContractResolver = new ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "TranslationId" }) }); + new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "TranslationId" }) }); var bytes = System.Text.Encoding.UTF8.GetBytes(asText); var file = new FileContentResult(bytes, "application/octet-stream"); file.FileDownloadName = Util.FileUtil.StringToSafeFileName(o.Name) + ".json"; @@ -314,27 +291,6 @@ namespace AyaNova.Api.Controllers } - public class ShouldSerializeContractResolver : DefaultContractResolver - { - private readonly IEnumerable _excludePropertyNames; - - public ShouldSerializeContractResolver(IEnumerable excludePropertyNames) - { - _excludePropertyNames = excludePropertyNames; - } - - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - IList properties = base.CreateProperties(type, memberSerialization); - - // only serializer properties that start with the specified character - properties = - properties.Where(p => !_excludePropertyNames.Any(p2 => p2 == p.PropertyName)).ToList(); - - return properties; - } - } - /// diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index 022b5e49..4ce39841 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -326,7 +326,7 @@ namespace AyaNova.Biz await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); } - +//https://stackoverflow.com/questions/53367966/puppeteer-sharp-still-appear-many-chromium-instance-in-process-task-manager-when log.LogDebug($"Launching headless Chrome now:"); using (var browser = await Puppeteer.LaunchAsync(lo)) using (var page = await browser.NewPageAsync()) diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index 96f790ed..5f2b54a4 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -483,7 +483,7 @@ namespace AyaNova.Biz { //verify chosen customer exists if (!await ct.Customer.AnyAsync(z => z.Id == proposedObj.CustomerId)) - AddError(ApiErrorCode.NOT_FOUND, "CustomerId"); + AddError(ApiErrorCode.NOT_FOUND, "CustomerId"); } } @@ -496,9 +496,9 @@ namespace AyaNova.Biz } else { - //verify chosen HO exists + //verify chosen HO exists if (!await ct.HeadOffice.AnyAsync(z => z.Id == proposedObj.HeadOfficeId)) - AddError(ApiErrorCode.NOT_FOUND, "HeadOfficeId"); + AddError(ApiErrorCode.NOT_FOUND, "HeadOfficeId"); } } @@ -512,8 +512,8 @@ namespace AyaNova.Biz else { //verify that VENDOR SubVendorId exists - if (!await ct.Vendor.AnyAsync(z => z.Id == proposedObj.SubVendorId)) - AddError(ApiErrorCode.NOT_FOUND, "SubVendorId"); + if (!await ct.Vendor.AnyAsync(z => z.Id == proposedObj.SubVendorId)) + AddError(ApiErrorCode.NOT_FOUND, "SubVendorId"); } } @@ -578,6 +578,21 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// // Utilities // + internal static async Task ValidateDownloadTokenAndReturnUserAsync(string dlToken, AyContext ct) + { + if (string.IsNullOrWhiteSpace(dlToken)) + return null; + //get user by key, if not found then reject + var DownloadUser = await ct.User.AsNoTracking().SingleOrDefaultAsync(z => z.DlKey == dlToken && z.Active == true); + if (DownloadUser == null) + return null; + //this is necessary because they might have an expired JWT but this would just keep on working without a date check + //the default is the same timespan as the jwt so it's all good + var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero); + if (DownloadUser.DlKeyExpire < utcNow.DateTime) + return null; + return DownloadUser; + } //replaced by dtUser object instead // internal static object CleanUserForReturn(User o) diff --git a/server/AyaNova/util/JsonUtil.cs b/server/AyaNova/util/JsonUtil.cs index 00c4fc35..996d97da 100644 --- a/server/AyaNova/util/JsonUtil.cs +++ b/server/AyaNova/util/JsonUtil.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; namespace AyaNova.Util { @@ -66,6 +69,24 @@ namespace AyaNova.Util return ret; } + //Contract resolver used for exporting to file translations and report templates + //and ignoring specified propertes + public class ShouldSerializeContractResolver : DefaultContractResolver + { + private readonly IEnumerable _excludePropertyNames; + public ShouldSerializeContractResolver(IEnumerable excludePropertyNames) + { + _excludePropertyNames = excludePropertyNames; + } + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + IList properties = base.CreateProperties(type, memberSerialization); + properties = properties.Where(p => !_excludePropertyNames.Any(p2 => p2 == p.PropertyName)).ToList(); + return properties; + } + } + + }//eoc diff --git a/server/AyaNova/util/ServerBootConfig.cs b/server/AyaNova/util/ServerBootConfig.cs index 1cef69b5..7508f6a1 100644 --- a/server/AyaNova/util/ServerBootConfig.cs +++ b/server/AyaNova/util/ServerBootConfig.cs @@ -11,6 +11,10 @@ namespace AyaNova.Util /// internal static class ServerBootConfig { + //################################################# + //STATIC HARD CODED DEFAULTS NOT SET THROUGH CONFIG + internal const int FAILED_AUTH_DELAY = 3000; + //################################################## //Diagnostic static values used during development, may not be related to config at all, this is just a convenient class to put them in #if (DEBUG)