This commit is contained in:
2018-06-28 23:41:48 +00:00
commit 515bd37952
256 changed files with 29890 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<GenerateFullPaths>true</GenerateFullPaths>
<Version>8.0.0-alpha</Version>
<FileVersion>8.0.0.0</FileVersion>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<noWarn>1591</noWarn>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="App.Metrics.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="App.Metrics.Reporting.InfluxDB" Version="2.0.0" />
<PackageReference Include="Bogus" Version="22.1.2" />
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.2" />
<PackageReference Include="Enums.NET" Version="2.3.1" />
<PackageReference Include="jose-jwt" Version="2.4.0" />
<PackageReference Include="NLog" Version="4.5.6" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.5.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="2.4.0" />
</ItemGroup>
<Target Name="CopyCustomContent" AfterTargets="AfterBuild">
<Copy SourceFiles=".\resource\de.json;.\resource\en.json;.\resource\es.json;.\resource\fr.json" DestinationFolder="$(OutDir)\resource" />
</Target>
</Project>

View File

@@ -0,0 +1,19 @@
namespace AyaNova.Api.ControllerHelpers
{
public class ApiCreatedResponse
{
public object Result { get; }
public ApiCreatedResponse(object result)
{
Result = result;
}
}//eoc
}//eons

View File

@@ -0,0 +1,99 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Http;
using System;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Microsoft.Extensions.Logging;
using App.Metrics;
using AyaNova.Util;
namespace AyaNova.Api.ControllerHelpers
{
/// <summary>
/// This is essentially an unhandled exception handler
/// </summary>
public class ApiCustomExceptionFilter : IExceptionFilter
{
private readonly ILogger log;
public ApiCustomExceptionFilter(ILoggerFactory logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
this.log = logger.CreateLogger("Server Exception");
}
public void OnException(ExceptionContext context)
{
HttpStatusCode status = HttpStatusCode.InternalServerError;
String message = String.Empty;
#region If need to refine this further and deal with specific types
// var exceptionType = context.Exception.GetType();
// if (exceptionType == typeof(UnauthorizedAccessException))
// {
// message = "Unauthorized Access";
// status = HttpStatusCode.Unauthorized;
// }
// else if (exceptionType == typeof(NotImplementedException))
// {
// message = "A server error occurred.";
// status = HttpStatusCode.NotImplemented;
// }
// // else if (exceptionType == typeof(MyAppException))
// // {
// // message = context.Exception.ToString();
// // status = HttpStatusCode.InternalServerError;
// // }
// else
// {
#endregion
message = context.Exception.Message;
status = HttpStatusCode.InternalServerError;
//}
//No need to log test exceptions to check and filter out
bool loggableError = true;
if (message.StartsWith("Test exception"))
loggableError = false;
//LOG IT
if (loggableError)
log.LogError(context.Exception, "Error");
//Track this exception
IMetrics metrics = (IMetrics)ServiceProviderProvider.Provider.GetService(typeof(IMetrics));
metrics.Measure.Meter.Mark(MetricsRegistry.UnhandledExceptionsMeter,context.Exception.GetType().ToString());
HttpResponse response = context.HttpContext.Response;
response.StatusCode = (int)status;
response.ContentType = "application/json; charset=utf-8";
//This line is critical, without it the response is not proper and fails in various clients (postman, xunit tests with httpclient)
context.ExceptionHandled = true;
//context.Result
response.WriteAsync(JsonConvert.SerializeObject(
new ApiErrorResponse(ApiErrorCode.API_SERVER_ERROR, "Server internal error", "See server log for details"),
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
}
));
}
}//eoc
}//eons

View File

@@ -0,0 +1,29 @@
using Newtonsoft.Json;
using AyaNova.Biz;
namespace AyaNova.Api.ControllerHelpers
{
/// <summary>
/// Detail error for inner part of error response
/// </summary>
public class ApiDetailError
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Code { get; internal set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Message { get; internal set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Target { get; internal set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Error { get; internal set; }
}//eoc
}//eons

View File

@@ -0,0 +1,36 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace AyaNova.Api.ControllerHelpers
{
/// <summary>
///
/// </summary>
public class ApiError
{
public string Code { get; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public List<ApiDetailError> Details { get; internal set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Message { get; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Target { get; }
public ApiError(ApiErrorCode apiCode, string message = null, string target = null)
{
Code = ((int)apiCode).ToString();
Target = target;
Message=message;
}
}//eoc
}//eons

View File

@@ -0,0 +1,37 @@
using System;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using System.Linq;
using System.Collections.Generic;
namespace AyaNova.Api.ControllerHelpers
{
public enum ApiErrorCode : int
{
/*
DON'T FORGET TO UPDATE THE API-ERROR-CODES.MD DOCUMENTATION
AND UPDATE THE ApiErrorCodeStockMessage.cs
*/
API_CLOSED = 2000,
API_OPS_ONLY = 2001,
API_SERVER_ERROR = 2002,
AUTHENTICATION_FAILED = 2003,
NOT_AUTHORIZED = 2004,
CONCURRENCY_CONFLICT=2005,
NOT_FOUND = 2010,
PUT_ID_MISMATCH = 2020,
INVALID_OPERATION = 2030,
VALIDATION_FAILED = 2200,
VALIDATION_REQUIRED = 2201,
VALIDATION_LENGTH_EXCEEDED = 2202,
VALIDATION_INVALID_VALUE = 2203
}
}//eons

View File

@@ -0,0 +1,58 @@
using System;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using System.Linq;
using System.Collections.Generic;
namespace AyaNova.Api.ControllerHelpers
{
internal static class ApiErrorCodeStockMessage
{
internal static string GetMessage(ApiErrorCode code)
{
switch (code)
{
case ApiErrorCode.API_CLOSED:
return "API Closed";
case ApiErrorCode.API_OPS_ONLY:
return "API Closed to non operations routes";
case ApiErrorCode.CONCURRENCY_CONFLICT:
return "Object was changed by another user since retrieval (concurrency token mismatch)";
case ApiErrorCode.NOT_FOUND:
return "Object not found";
case ApiErrorCode.VALIDATION_FAILED:
return "Object did not pass validation";
case ApiErrorCode.VALIDATION_REQUIRED:
return "Required field empty";
case ApiErrorCode.VALIDATION_LENGTH_EXCEEDED:
return "Field too long";
case ApiErrorCode.VALIDATION_INVALID_VALUE:
return "Field is set to a non allowed value";
case ApiErrorCode.AUTHENTICATION_FAILED:
return "Authentication failed";
case ApiErrorCode.PUT_ID_MISMATCH:
return "Update failed: ID mismatch - route ID doesn't match object id";
case ApiErrorCode.INVALID_OPERATION:
return "An attempt was made to perform an invalid operation";
case ApiErrorCode.NOT_AUTHORIZED:
return "User not authorized for this resource operation (insufficient rights)";
default:
return null;
}
}
/*
VALIDATION_FAILED = 2200,
VALIDATION_REQUIRED = 2201,
VALIDATION_LENGTH_EXCEEDED = 2202,
VALIDATION_INVALID_VALUE = 2203
*/
}
}//eons

View File

@@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using AyaNova.Biz;
namespace AyaNova.Api.ControllerHelpers
{
public class ApiErrorResponse
{
[JsonIgnore]
private ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<ApiErrorResponse>();
//Mandatory properties
public ApiError Error { get; }
//Generic error
public ApiErrorResponse(ApiErrorCode apiCode, string target = null, string message = null)
{
//try to get a stock message if nothing specified
if (message == null)
{
message = ApiErrorCodeStockMessage.GetMessage(apiCode);
}
Error = new ApiError(apiCode, message, target);
log.LogDebug("apiCode={0}, target={1}, message={2}", apiCode, target, message);
}
//Bad request error response handling
public ApiErrorResponse(ModelStateDictionary modelState)
{
if (modelState.IsValid)
{
throw new ArgumentException("ModelState must be invalid", nameof(modelState));
}
//Set outer error and then put validation in details
Error = new ApiError(ApiErrorCode.VALIDATION_FAILED, ApiErrorCodeStockMessage.GetMessage(ApiErrorCode.VALIDATION_FAILED));
//https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi/
//Message = "Validation Failed";
Error.Details = new List<ApiDetailError>();
Error.Details.AddRange(modelState.Keys
.SelectMany(key => modelState[key].Errors
.Select(x => new ApiDetailError() { Code = ((int)ApiErrorCode.VALIDATION_FAILED).ToString(), Target = key, Message = x.ErrorMessage, Error=ApiErrorCode.VALIDATION_FAILED.ToString() })));
log.LogDebug("BadRequest - Validation error");
}
//Business rule validation error response
public ApiErrorResponse(List<ValidationError> errors)
{
Error = new ApiError(ApiErrorCode.VALIDATION_FAILED, ApiErrorCodeStockMessage.GetMessage(ApiErrorCode.VALIDATION_FAILED));
Error.Details = new List<ApiDetailError>();
foreach (ValidationError v in errors)
{
Error.Details.Add(new ApiDetailError() { Target = v.Target, Message = v.Message, Error = v.ErrorType.ToString() });
}
log.LogDebug("BadRequest - Validation error");
}
public void AddDetailError(ApiErrorCode apiCode, string target = null, string message = null)
{
if (Error.Details == null)
{
Error.Details = new List<ApiDetailError>();
}
//try to get a stock message if nothing specified
if (message == null)
{
message = ApiErrorCodeStockMessage.GetMessage(apiCode);
}
Error.Details.Add(new ApiDetailError() { Code = ((int)apiCode).ToString(), Target = target, Message = message });
}
}//eoc
}//eons

View File

@@ -0,0 +1,33 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace AyaNova.Api.ControllerHelpers
{
public class ApiNotAuthorizedResponse
{
[JsonIgnore]
private ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<ApiNotAuthorizedResponse>();
//Mandatory properties
public ApiError Error { get; }
//Generic error
public ApiNotAuthorizedResponse()
{
Error = new ApiError(ApiErrorCode.NOT_AUTHORIZED, ApiErrorCodeStockMessage.GetMessage(ApiErrorCode.NOT_AUTHORIZED));
log.LogDebug("ApiErrorCode={0}, message={1}", (int)ApiErrorCode.NOT_AUTHORIZED, Error.Message);
}
}//eoc
}//eons

View File

@@ -0,0 +1,19 @@
namespace AyaNova.Api.ControllerHelpers
{
public class ApiOkResponse
{
public object Result { get; }
public ApiOkResponse(object result)
{
Result = result;
}
}//eoc
}//eons

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace AyaNova.Api.ControllerHelpers
{
public class ApiOkWithPagingResponse<T>
{
public object Result { get; }
public object Paging { get; }
public ApiOkWithPagingResponse(ApiPagedResponse<T> pr)
{
Result = pr.items;
Paging = pr.PageLinks;
}
// public ApiOkWithPagingResponse(object result, AyaNova.Models.PaginationLinkBuilder lb)
// {
// Result = result;
// Paging = lb.PagingData();
// }
}//eoc
}//eons

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using AyaNova.Models;
namespace AyaNova.Api.ControllerHelpers
{
public class ApiPagedResponse<T>
{
public T[] items { get; }
public object PageLinks { get; }
public ApiPagedResponse(T[] returnItems, object pageLinks)
{
items = returnItems;
PageLinks = pageLinks;
}
}//eoc
}//eons

View File

@@ -0,0 +1,186 @@
using System;
using Microsoft.Extensions.Logging;
namespace AyaNova.Api.ControllerHelpers
{
/// <summary>
/// Contains the current status of the server
/// is injected everywhere for routes and others to check
/// </summary>
public class ApiServerState
{
//private ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<ApiServerState>();
public enum ServerState
{
///<summary>Unknown state, used for parsing</summary>
UNKNOWN = 0,
///<summary>No access for anyone API completely locked down</summary>
Closed = 1,
///<summary>Access only to API Operations routes</summary>
OpsOnly = 2,
///<summary>Open for all users (default)</summary>
Open = 3
}
private ServerState _currentState = ServerState.Closed;
private ServerState _priorState = ServerState.Closed;
private string _reason = string.Empty;
private string _priorReason = string.Empty;
private bool SYSTEM_LOCK = false;//really this is a license lock but not called that
public ApiServerState()
{
}
internal void SetSystemLock(string reason)
{
//Lock down the server for license related issue
//Still allows ops routes, treats as if server was set to closed even if they change it to open
//only way to reset it is to fetch a valid license
SetState(ServerState.OpsOnly, reason);
SYSTEM_LOCK = true;
}
//WARNING: if in future this is used for anything other than a license then it will need to see if locked for another reason before unlocking
//recommend putting a code number in the reason then looking to see if it has the matching code
internal void ClearSystemLock()
{
SYSTEM_LOCK = false;
SetState(ServerState.Open, "");
}
/// <summary>
/// Set the server state
/// </summary>
/// <param name="newState"></param>
/// <param name="reason"></param>
public void SetState(ServerState newState, string reason)
{
//No changes allowed during a system lock
if (SYSTEM_LOCK) return;
_reason = reason;//keep the reason even if the state doesn't change
if (newState == _currentState) return;
//Here we will likely need to trigger a notification to users if the state is going to be shutting down or is shut down
_priorState = _currentState;//keep the prior state so it can be resumed easily
_priorReason=_reason;//keep the original reason
_currentState = newState;
}
/// <summary>
/// Get the current state of the server
/// </summary>
/// <returns></returns>
public ServerState GetState()
{
return _currentState;
}
/// <summary>
/// Get the current reason for the state of the server
/// </summary>
/// <returns></returns>
public string Reason
{
get
{
if (_currentState == ServerState.Open)
{
return "";
}
else
{
return $"Server state is: {_currentState.ToString()}, Reason: {_reason}";
}
}
}
public void SetOpsOnly(string reason)
{
//No changes allowed during a system lock
if (SYSTEM_LOCK) return;
SetState(ServerState.OpsOnly, reason);
}
public void SetClosed(string reason)
{
//No changes allowed during a system lock
if (SYSTEM_LOCK) return;
SetState(ServerState.Closed, reason);
}
public void SetOpen()
{
//No changes allowed during a system lock
if (SYSTEM_LOCK) return;
SetState(ServerState.Open, string.Empty);
}
public void ResumePriorState()
{
//No changes allowed during a system lock
if (SYSTEM_LOCK) return;
SetState(_priorState, _priorReason);
}
public bool IsOpsOnly
{
get
{
return _currentState == ServerState.OpsOnly;
}
}
public bool IsOpen
{
get
{
return _currentState == ServerState.Open && !SYSTEM_LOCK;
}
}
public bool IsClosed
{
get
{
return _currentState == ServerState.Closed || SYSTEM_LOCK;
}
}
public bool IsOpenOrOpsOnly
{
get
{
return IsOpen || IsOpsOnly;
}
}
public bool IsSystemLocked
{
get
{
return SYSTEM_LOCK;
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,209 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.Collections.Generic;
using System.Linq;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Util;
using AyaNova.Biz;
namespace AyaNova.Api.ControllerHelpers
{
//Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
//See AttachmentController at bottom of class for example of form that works with this code
/// <summary>
/// Handle processing uplod form with potentially huge files being uploaded (which means can't use simplest built in upload handler method)
/// </summary>
internal static class ApiUploadProcessor
{
/// <summary>
/// Process uploaded attachment file
/// Will be treated as a temporary file for further processing into database
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
internal static async Task<ApiUploadedFilesResult> ProcessAttachmentUpload(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
return await ProcessUpload(httpContext, true);
}
/// <summary>
/// Process uploaded utility file (backup, import etc)
/// Anything that will be stored in the backup folder as is
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
internal static async Task<ApiUploadedFilesResult> ProcessUtilityFileUpload(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
return await ProcessUpload(httpContext, false);
}
/// <summary>
/// handle upload
/// </summary>
/// <param name="httpContext"></param>
/// <param name="processAsAttachment"></param>
/// <returns><see cref="ApiUploadedFilesResult"/> list of files and form field data (if present)</returns>
private static async Task<ApiUploadedFilesResult> ProcessUpload(Microsoft.AspNetCore.Http.HttpContext httpContext, bool processAsAttachment)
{
ApiUploadedFilesResult result = new ApiUploadedFilesResult();
FormOptions _defaultFormOptions = new FormOptions();
// Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(httpContext.Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, httpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
string filePathAndName = string.Empty;
var CleanedUploadFileName = contentDisposition.FileName.Value.Replace("\"", "");
if (processAsAttachment)
{
//get temp file path and temp file name
filePathAndName = FileUtil.NewRandomAttachmentFileName;
}
else
{
//store directly into the backup file folder
//NOTE: all utility files are always stored as lowercase to avoid recognition issues down the road
CleanedUploadFileName = CleanedUploadFileName.ToLowerInvariant();
filePathAndName = FileUtil.GetFullPathForUtilityFile(CleanedUploadFileName);
}
//save to disk
using (var stream = new FileStream(filePathAndName, FileMode.Create))
{
section.Body.CopyTo(stream);
}
result.UploadedFiles.Add(new UploadedFileInfo()
{
InitialUploadedPathName = filePathAndName,
OriginalFileName = CleanedUploadFileName,
MimeType = section.ContentType
});
}
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value
// Do not limit the key name length here because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = String.Empty;
}
formAccumulator.Append(key.Value, value);
if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
{
throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
}
// Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
//Get any extra form fields and return them
result.FormFieldData = formAccumulator.GetResults();
return result;
}
private static Encoding GetEncoding(MultipartSection section)
{
MediaTypeHeaderValue mediaType;
var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// most cases.
if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
{
return Encoding.UTF8;
}
return mediaType.Encoding;
}
/// <summary>
/// Contains result of upload form processor
/// </summary>
public class ApiUploadedFilesResult
{
public Dictionary<string, Microsoft.Extensions.Primitives.StringValues> FormFieldData { get; set; }
public List<UploadedFileInfo> UploadedFiles { get; set; }
public ApiUploadedFilesResult()
{
FormFieldData = new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>();
UploadedFiles = new List<UploadedFileInfo>();
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,121 @@
using EnumsNET;
using System.Collections.Generic;
using AyaNova.Biz;
namespace AyaNova.Api.ControllerHelpers
{
internal static class Authorized
{
/// <summary>
/// User has any ops role limited or full
/// </summary>
/// <param name="HttpContextItems"></param>
/// <param name="CheckRoles"></param>
/// <returns></returns>
internal static bool HasAnyRole(IDictionary<object, object> HttpContextItems, AuthorizationRoles CheckRoles)
{
AuthorizationRoles currentUserRoles = UserRolesFromContext.Roles(HttpContextItems);
if (currentUserRoles.HasAnyFlags(CheckRoles))
return true;
return false;
}
/// <summary>
/// READ / GENERAL ACCESS
/// </summary>
/// <param name="HttpContextItems"></param>
/// <param name="objectType"></param>
/// <returns></returns>
internal static bool IsAuthorizedToRead(IDictionary<object, object> HttpContextItems, AyaType objectType)
{
AuthorizationRoles currentUserRoles = UserRolesFromContext.Roles(HttpContextItems);
//NOTE: this assumes that if you can change you can read
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).Change))
return true;
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).Read))
return true;
return false;
}
/// <summary>
/// CREATE
/// </summary>
/// <param name="HttpContextItems"></param>
/// <param name="objectType"></param>
/// <returns></returns>
internal static bool IsAuthorizedToCreate(IDictionary<object, object> HttpContextItems, AyaType objectType)
{
AuthorizationRoles currentUserRoles = UserRolesFromContext.Roles(HttpContextItems);
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).Change))
return true;
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).EditOwn))
return true;
return false;
}
/// <summary>
/// MODIFY
/// </summary>
/// <param name="HttpContextItems"></param>
/// <param name="objectType"></param>
/// <param name="ownerId"></param>
/// <returns></returns>
internal static bool IsAuthorizedToModify(IDictionary<object, object> HttpContextItems, AyaType objectType, long ownerId = -1)
{
AuthorizationRoles currentUserRoles = UserRolesFromContext.Roles(HttpContextItems);
long currentUserId = UserIdFromContext.Id(HttpContextItems);
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).Change))
return true;
if (ownerId != -1)
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).EditOwn) && ownerId == currentUserId)
return true;
return false;
}
/// <summary>
/// DELETE
/// </summary>
/// <param name="HttpContextItems"></param>
/// <param name="objectType"></param>
/// <param name="ownerId"></param>
/// <returns></returns>
//For now just going to treat as a modify, but for maximum flexibility keeping this as a separate method in case we change our minds in future
internal static bool IsAuthorizedToDelete(IDictionary<object, object> HttpContextItems, AyaType objectType, long ownerId = 1)
{
AuthorizationRoles currentUserRoles = UserRolesFromContext.Roles(HttpContextItems);
long currentUserId = UserIdFromContext.Id(HttpContextItems);
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).Change))
return true;
if (currentUserRoles.HasAnyFlags(BizRoles.GetRoleSet(objectType).EditOwn) && ownerId == currentUserId)
return true;
return false;
}
}
}//eons

View File

@@ -0,0 +1,50 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace AyaNova.Api.ControllerHelpers
{
//FROM DOCS HERE:
//https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
//https://github.com/aspnet/Docs/tree/74a44669d5e7039e2d4d2cb3f8b0c4ed742d1124/aspnetcore/mvc/models/file-uploads/sample/FileUploadSample
/// <summary>
///
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
/// <summary>
///
/// </summary>
/// <param name="context"></param>
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
}
var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace AyaNova.Api.ControllerHelpers
{
/// <summary>
///
/// </summary>
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec says 70 characters is a reasonable limit.
/// <summary>
///
/// </summary>
/// <param name="contentType"></param>
/// <param name="lengthLimit"></param>
/// <returns></returns>
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
/// <summary>
///
/// </summary>
/// <param name="contentType"></param>
/// <returns></returns>
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>
///
/// </summary>
/// <param name="contentDisposition"></param>
/// <returns></returns>
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
/// <summary>
///
/// </summary>
/// <param name="contentDisposition"></param>
/// <returns></returns>
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace AyaNova.Api.ControllerHelpers
{
public class PaginationLinkBuilder
{ //adapted from //https://www.jerriepelser.com/blog/paging-in-aspnet-webapi-pagination-links/
public Uri FirstPage { get; private set; }
public Uri LastPage { get; private set; }
public Uri NextPage { get; private set; }
public Uri PreviousPage { get; private set; }
public PagingOptions PagingOptions { get; }
public long TotalRecordCount { get; }
public PaginationLinkBuilder(IUrlHelper urlHelper, string routeName, object routeValues, PagingOptions pagingOptions, long totalRecordCount)
{
PagingOptions = pagingOptions;
TotalRecordCount = totalRecordCount;
// Determine total number of pages
var pageCount = totalRecordCount > 0
? (int)Math.Ceiling(totalRecordCount / (double)pagingOptions.Limit)
: 0;
// Create page links
FirstPage = new Uri(urlHelper.Link(routeName, new RouteValueDictionary(routeValues)
{
{"pageNo", 1},
{"pageSize", pagingOptions.Limit}
}));
LastPage = new Uri(urlHelper.Link(routeName, new RouteValueDictionary(routeValues)
{
{"pageNo", pageCount},
{"pageSize", pagingOptions.Limit}
}));
if (pagingOptions.Offset > 1)
{
PreviousPage = new Uri(urlHelper.Link(routeName, new RouteValueDictionary(routeValues)
{
{"pageNo", pagingOptions.Offset - 1},
{"pageSize", pagingOptions.Limit}
}));
}
if (pagingOptions.Offset < pageCount)
{
NextPage = new Uri(urlHelper.Link(routeName, new RouteValueDictionary(routeValues)
{
{"pageNo", pagingOptions.Offset + 1},
{"pageSize", pagingOptions.Limit}
}));
}
}
/// <summary>
/// Return paging data suitable for API return
/// </summary>
/// <returns></returns>
public Object PagingLinksObject()
{
return new
{
Count = TotalRecordCount,
Offset = PagingOptions.Offset,
Limit = PagingOptions.Limit,
First = FirstPage,
Previous = PreviousPage,
Next = NextPage,
Last = LastPage
};
}
}
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace AyaNova.Api.ControllerHelpers
{
public sealed class PagingOptions
{
public const int MaxPageSize = 100;
public const int DefaultOffset = 0;
public const int DefaultLimit = 25;
[FromQuery]
[Range(0, int.MaxValue)]
public int? Offset { get; set; }
[FromQuery]
[Range(1, MaxPageSize, ErrorMessage = "Limit must be greater than 0 and less than 100.")]
public int? Limit { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using EnumsNET;
using System.Collections.Generic;
namespace AyaNova.Api.ControllerHelpers
{
internal static class UserIdFromContext
{
internal static long Id(IDictionary<object, object> HttpContextItems)
{
long? l = (long?)HttpContextItems["AY_USER_ID"];
if (l==null)
return 0L;
return (long)l;
}
}
}//eons

View File

@@ -0,0 +1,20 @@
using EnumsNET;
using System.Collections.Generic;
namespace AyaNova.Api.ControllerHelpers
{
internal static class UserNameFromContext
{
internal static string Name(IDictionary<object, object> HttpContextItems)
{
string s = (string)HttpContextItems["AY_USERNAME"];
if (string.IsNullOrWhiteSpace(s))
return "UNKNOWN USER NAME";
return s;
}
}
}//eons

View File

@@ -0,0 +1,18 @@
using EnumsNET;
using System.Collections.Generic;
using AyaNova.Biz;
namespace AyaNova.Api.ControllerHelpers
{
internal static class UserRolesFromContext
{
internal static AuthorizationRoles Roles(IDictionary<object, object> HttpContextItems)
{
return (AuthorizationRoles)HttpContextItems["AY_ROLES"];
}
}
}//eons

View File

@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using AyaNova.Util;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Meta controller class
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/")]
public class ApiMetaController : Controller
{
private readonly ILogger<ApiMetaController> _log;
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
public ApiMetaController(ILogger<ApiMetaController> logger)
{
_log = logger;
}
/// <summary>
/// AyaNova API documentation and manual
/// </summary>
/// <returns></returns>
[HttpGet]
public ContentResult Index()
{
var resp = $@"<html lang=""en"">
<head>
<meta charset=""utf-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1, shrink-to-fit=no"">
<title>AyaNova server</title>
</head>
<body >
<div style=""text-align: center;"">
<div style=""display: inline-block;text-align:left;"">
<h1>{AyaNovaVersion.FullNameAndVersion}</h1>
<a href=""/docs"">AyaNova manual</a><br/><br/>
<a href=""/api-docs"">API explorer</a><br/><br/>
<a href=""mailto:support@ayanova.com"">Email AyaNova support</a><br/><br/>
<h4>{LocaleBiz.GetDefaultLocalizedText("HelpLicense").Result}</h4>
<pre>{AyaNova.Core.License.LicenseInfo}</pre>
<h4>Schema version</h4>
<pre>{AySchema.currentSchema.ToString()}</pre>
<h4>Server time</h4>
<pre>{DateUtil.ServerDateTimeString(System.DateTime.UtcNow)}</pre>
<pre>{TimeZoneInfo.Local.Id}</pre>
<h4>Server logs</h4>
<pre>{ServerBootConfig.AYANOVA_LOG_PATH}</pre>
</div>
</div>
</body>
</html>";
return new ContentResult
{
ContentType = "text/html",
StatusCode = 200,
Content = resp
};
}
}
}

View File

@@ -0,0 +1,452 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.Collections.Generic;
using System.Linq;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Util;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
//FROM DOCS HERE:
//https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
//https://github.com/aspnet/Docs/tree/74a44669d5e7039e2d4d2cb3f8b0c4ed742d1124/aspnetcore/mvc/models/file-uploads/sample/FileUploadSample
/// <summary>
/// Attachment controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class AttachmentController : Controller
{
private readonly AyContext ct;
private readonly ILogger<AttachmentController> log;
private readonly ApiServerState serverState;
//private static readonly FormOptions _defaultFormOptions = new FormOptions();
/// <summary>
///
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public AttachmentController(AyContext dbcontext, ILogger<AttachmentController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
//TODO: Centralize this code somewhere else, it's going to be needed for backup as well
//consider the 1 hour is this legit depending on client
/// <summary>
/// Get download token
/// A download token is good for 1 hour from issue
/// </summary>
/// <returns>Current download token for user</returns>
[HttpGet("DownloadToken")]
public async Task<IActionResult> GetDownloadToken()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
long lUserId = UserIdFromContext.Id(HttpContext.Items);
var u = await ct.User.FirstOrDefaultAsync(a => a.Id == lUserId);
if (u == null)
return NotFound();
else
{
//Generate a download token and store it with the user account
//users who are authenticated can get their token via download route
Guid g = Guid.NewGuid();
string dlkey = Convert.ToBase64String(g.ToByteArray());
dlkey = dlkey.Replace("=", "");
dlkey = dlkey.Replace("+", "");
//get expiry date for download token
var exp = new DateTimeOffset(DateTime.Now.AddHours(1).ToUniversalTime(), TimeSpan.Zero);
u.DlKey = dlkey;
u.DlKeyExpire = exp.DateTime;
ct.User.Update(u);
try
{
await ct.SaveChangesAsync();//triggering concurrency exception here
}
catch (Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException)
{
log.LogInformation("Auth retry dlkey");
};
return Ok(new ApiOkResponse(new { dlkey = u.DlKey, expires = u.DlKeyExpire }));
}
}
/// <summary>
/// Upload attachment file
///
/// Required roles: Same roles as object that file is being attached to
///
/// </summary>
/// <returns>NameValue list of filenames and attachment id's</returns>
[HttpPost]
[DisableFormValueModelBinding]
[RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267
public async Task<IActionResult> Upload()
{
//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(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
var returnList = new List<NameIdItem>();
try
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", $"Expected a multipart request, but got {Request.ContentType}"));
}
var uploadFormData = await ApiUploadProcessor.ProcessAttachmentUpload(HttpContext);
bool badRequest = false;
string AttachToObjectType = string.Empty;
string AttachToObjectId = string.Empty;
string errorMessage = string.Empty;
if (!uploadFormData.FormFieldData.ContainsKey("AttachToObjectType") || !uploadFormData.FormFieldData.ContainsKey("AttachToObjectId"))
{
badRequest = true;
errorMessage = "AttachToObjectType and / or AttachToObjectId are missing and are required";
}
if (!badRequest)
{
AttachToObjectType = uploadFormData.FormFieldData["AttachToObjectType"].ToString();
AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
if (string.IsNullOrWhiteSpace(AttachToObjectType) || string.IsNullOrWhiteSpace(AttachToObjectId))
{
badRequest = true;
errorMessage = "AttachToObjectType and / or AttachToObjectId are empty and are required";
}
}
//Get type and id object from post paramters
AyaTypeId attachToObject = null;
if (!badRequest)
{
attachToObject = new AyaTypeId(AttachToObjectType, AttachToObjectId);
if (attachToObject.IsEmpty)
{
badRequest = true;
errorMessage = "AttachToObjectType and / or AttachToObjectId are not valid and are required";
}
}
//Is it an attachable type of object?
if (!badRequest)
{
if (!attachToObject.IsAttachable)
{
badRequest = true;
errorMessage = attachToObject.ObjectType.ToString() + " - AttachToObjectType does not support attachments";
}
}
//does attach to object exist?
if (!badRequest)
{
//check if object exists
long attachToObjectOwnerId = attachToObject.OwnerId(ct);
if (attachToObjectOwnerId == -1)
{
badRequest = true;
errorMessage = "Invalid attach object";
}
else
{
// User needs modify rights to the object type in question
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, attachToObject.ObjectType, attachToObjectOwnerId))
{
//delete temp files
DeleteTempFileUploadDueToBadRequest(uploadFormData);
return StatusCode(401, new ApiNotAuthorizedResponse());
}
}
}
if (badRequest)
{
//delete temp files
DeleteTempFileUploadDueToBadRequest(uploadFormData);
//return bad request
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, errorMessage));
}
//We have our files and a confirmed AyObject, ready to attach and save permanently
if (uploadFormData.UploadedFiles.Count > 0)
{
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
{
var v = FileUtil.storeFileAttachment(a.InitialUploadedPathName, a.MimeType, a.OriginalFileName, UserIdFromContext.Id(HttpContext.Items), attachToObject, ct);
returnList.Add(new NameIdItem()
{
Name = v.DisplayFileName,
Id = v.Id
});
}
}
}
catch (InvalidDataException ex)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", ex.Message));
}
//Return the list of attachment ids and filenames
return Ok(new ApiOkResponse(returnList));
}
/// <summary>
/// Utility to delete files that were uploaded but couldn't be stored for some reason, called by Attach route
/// </summary>
/// <param name="uploadFormData"></param>
private static void DeleteTempFileUploadDueToBadRequest(ApiUploadProcessor.ApiUploadedFilesResult uploadFormData)
{
if (uploadFormData.UploadedFiles.Count > 0)
{
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
{
System.IO.File.Delete(a.InitialUploadedPathName);
}
}
}
/// <summary>
/// Delete Attachment
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAttachment([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, dbObj.AttachToObjectType, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//do the delete
//this handles removing the file if there are no refs left and also the db record for the attachment
FileUtil.deleteFileAttachment(dbObj, ct);
return NoContent();
}
/// <summary>
/// Download a file attachment
/// </summary>
/// <param name="id"></param>
/// <param name="dlkey"></param>
/// <returns></returns>
[HttpGet("download/{id}")]
public async Task<IActionResult> Download([FromRoute] long id, [FromQuery] string dlkey)
{
//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(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (string.IsNullOrWhiteSpace(dlkey))
{
return NotFound();
}
//get user by key, if not found then reject
//If user dlkeyexp has not expired then return file
var dlkeyUser = await ct.User.SingleOrDefaultAsync(m => m.DlKey == dlkey);
if (dlkeyUser == null)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
}
//Make sure the token provided is for the current user
long lAuthenticatedUserId = UserIdFromContext.Id(HttpContext.Items);
if (lAuthenticatedUserId != dlkeyUser.Id)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token not valid"));
}
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
if (dlkeyUser.DlKeyExpire < utcNow.DateTime)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_AUTHORIZED, "dlkey", "Download token has expired"));
}
//Ok, user has a valid download key and it's not expired yet so get the attachment record
var dbObj = await ct.FileAttachment.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
//is this allowed?
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, dbObj.AttachToObjectType))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//they are allowed, let's send the file
string mimetype = dbObj.ContentType;
var filePath = FileUtil.GetPermanentAttachmentFilePath(dbObj.StoredFileName);
if (!System.IO.File.Exists(filePath))
{
//TODO: this should trigger some kind of notification to the ops people
//and a red light on the dashboard
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, null, $"Physical file {dbObj.StoredFileName} not found despite attachment record, this file is missing"));
}
return PhysicalFile(filePath, mimetype, dbObj.DisplayFileName);
}
////////////////////////////////////////////////////////////////////////////////////
}//eoc
}//eons
#region sample html form to work with this
/*
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#upload").click(function (evt) {
var fileUpload = $("#files").get(0);
var files = fileUpload.files;
var data = new FormData();
for (var i = 0; i < files.length; i++) {
data.append(files[i].name, files[i]);
}
//attachment test
data.append('AttachToObjectType','2');//object 2 is widget
data.append('AttachToObjectId','200');//there should normally always be a widget with id 1
$.ajax({
type: "POST",
url: "http://localhost:7575/api/v8.0/Attachment",
headers: {
Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTIxNTY5ODE5IiwiZXhwIjoiMTUyNDE2MTgxOSIsImlzcyI6IkF5YU5vdmEiLCJpZCI6IjEifQ.4PzkzZNYK5mJkbfCHGQ2N2248atAvvcDgApoz65oIC0"
},
contentType: false,
processData: false,
data: data,
success: function (message) {
alert("upload successful!");
console.log(message);
},
error: function (error) {
console.log(error);
alert("There was an error uploading files!");
}
});
});
});
</script>
</head>
<body>
<form method="post" enctype="multipart/form-data">
<input type="file" id="files" name="files" multiple />
<!-- <input type="file" accept=".zip,application/zip" id="files" name="files" multiple /> -->
<input type="button" id="upload" value="Upload file(s)" />
</form>
</body>
</html>
*/
#endregion

View File

@@ -0,0 +1,162 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using System.Linq;
using System;
using System.Threading.Tasks;
using App.Metrics;
//required to inject configuration in constructor
using Microsoft.Extensions.Configuration;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Authentication controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class AuthController : Controller
{
private readonly AyContext ct;
private readonly ILogger<AuthController> log;
private readonly IConfiguration _configuration;
private readonly ApiServerState serverState;
private readonly IMetrics metrics;
/// <summary>
/// ctor
/// </summary>
/// <param name="context"></param>
/// <param name="logger"></param>
/// <param name="configuration"></param>
/// <param name="apiServerState"></param>
/// <param name="Metrics"></param>
public AuthController(AyContext context, ILogger<AuthController> logger, IConfiguration configuration, ApiServerState apiServerState, IMetrics Metrics)//these two are injected, see startup.cs
{
ct = context;
log = logger;
_configuration = configuration;
serverState = apiServerState;
metrics = Metrics;
}
//AUTHENTICATE CREDS
//RETURN JWT
/// <summary>
/// Post credentials to receive a JSON web token
/// </summary>
/// <remarks>
/// This route is used to authenticate to the AyaNova API.
/// Once you have a token you need to include it in all requests that require authentication like this:
/// <code>Authorization: Bearer [TOKEN]</code>
/// Note the space between Bearer and the token. Also, do not include the square brackets
/// </remarks>
/// <param name="creds"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostCreds([FromBody] AuthController.CredentialsParam creds) //if was a json body then //public JsonResult PostCreds([FromBody] string login, [FromBody] string password)
{
//a bit different as ops users can still login if the state is opsonly
//so the only real barrier here would be a completely closed api
if (!serverState.IsOpenOrOpsOnly)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
int nFailedAuthDelay = 10000;
#if (DEBUG)
nFailedAuthDelay = 1;
#endif
if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
{
metrics.Measure.Meter.Mark(MetricsRegistry.FailedLoginMeter);
//Make a failed pw wait
await Task.Delay(nFailedAuthDelay);
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
//Multiple users are allowed the same password and login
//Salt will differentiate them so get all users that match login, then try to match pw
var users = await ct.User.AsNoTracking().Where(m => m.Login == creds.Login).ToListAsync();
foreach (User u in users)
{
string hashed = Hasher.hash(u.Salt, creds.Password);
if (hashed == u.Password)
{
//Restrict auth due to server state?
//If we're here the server state is not closed, but it might be ops only
//If the server is ops only then this user needs to be ops or else they are not allowed in
if (serverState.IsOpsOnly &&
!u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminFull) &&
!u.Roles.HasFlag(Biz.AuthorizationRoles.OpsAdminLimited))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//build the key (JWT set in startup.cs)
byte[] secretKey = System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET);
//create a new datetime offset of now in utc time
var iat = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);//timespan zero means zero time off utc / specifying this is a UTC datetime
var exp = new DateTimeOffset(DateTime.Now.AddDays(30).ToUniversalTime(), TimeSpan.Zero);
var payload = new Dictionary<string, object>()
{
{ "iat", iat.ToUnixTimeSeconds().ToString() },
{ "exp", exp.ToUnixTimeSeconds().ToString() },//in payload exp must be in unix epoch time per standard
{ "iss", "AyaNova" },
{ "id", u.Id.ToString() }
};
//NOTE: probably don't need Jose.JWT as am using Microsoft jwt stuff to validate routes so it should also be able to
//issue tokens as well, but it looked cmplex and this works so unless need to remove in future keeping it.
string token = Jose.JWT.Encode(payload, secretKey, Jose.JwsAlgorithm.HS256);
log.LogInformation($"User number \"{u.Id}\" logged in from \"{Util.StringUtil.MaskIPAddress(HttpContext.Connection.RemoteIpAddress.ToString())}\" ok");
metrics.Measure.Meter.Mark(MetricsRegistry.SuccessfulLoginMeter);
return Ok(new ApiOkResponse(new
{
ok = 1,
issued = iat,
expires = exp,
token = token,
id = u.Id
}));
}
}
//No users matched, it's a failed login
//Make a failed pw wait
metrics.Measure.Meter.Mark(MetricsRegistry.FailedLoginMeter);
await Task.Delay(nFailedAuthDelay);
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
}
//------------------------------------------------------
public class CredentialsParam
{
[System.ComponentModel.DataAnnotations.Required]
public string Login { get; set; }
[System.ComponentModel.DataAnnotations.Required]
public string Password { get; set; }
}
}//eoc
}//eons

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// AyaType list controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class AyaTypeController : Controller
{
private readonly AyContext ct;
private readonly ILogger<AyaTypeController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public AyaTypeController(AyContext dbcontext, ILogger<AyaTypeController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get name value list of AyaNova business object types
///
/// Required roles: Any
/// </summary>
/// <returns>List</returns>
[HttpGet]
public ActionResult GetAyaTypes()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
List<NameIdItem> l = new List<NameIdItem>();
var values = Enum.GetValues(typeof(AyaType));
foreach (AyaType t in values)
{
string name=t.ToString();
if(t.HasAttribute(typeof(AttachableAttribute))){
name+=" [Attachable]";
}
if(t.HasAttribute(typeof(TaggableAttribute))){
name+=" [Taggable]";
}
l.Add(new NameIdItem() { Name = name, Id = (long)t });
}
return Ok(new ApiOkResponse(l));
}
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Util;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.Collections.Generic;
using System.Linq;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//************************************************************************************************************** */
//JUNE 19th 2018 LARGE FILE UPLOAD POSSIBLY NEW INFO HERE:
//http://www.talkingdotnet.com/how-to-increase-file-upload-size-asp-net-core/
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AyaNova.Api.Controllers
{
//FROM DOCS HERE:
//https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
//https://github.com/aspnet/Docs/tree/74a44669d5e7039e2d4d2cb3f8b0c4ed742d1124/aspnetcore/mvc/models/file-uploads/sample/FileUploadSample
/// <summary>
/// Backup and restore controller for uploading or downloading backup files
/// and triggering a restore from backup
///
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class BackupController : Controller
{
private readonly AyContext ct;
private readonly ILogger<BackupController> log;
private readonly ApiServerState serverState;
/// <summary>
///
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public BackupController(AyContext dbcontext, ILogger<BackupController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/*
TODO:
A backup archive consists of a similar format to the v7 data dumper utility, json file per object in subdirectories corresponding to object type all in a zip archive
Route to trigger restore from selected file
Route to force immediate backup
Backup code in biz objects "IBackup" interface (which in turn is probably going to implement an IExport interface as it serves dual purpose
of exporting data, or maybe that's special purpose custom objects for exporting like csv etc since there is likely a graph of data involved)
- object is exported / backed up to json
Restore code in biz objects "IRestore" interface
- object(s) imported via restore and data given to them or file or whatever (See discource project)
*/
//TODO: Copy the code from ImportAyaNova7Controller upload method instead of this old crap
// /// <summary>
// /// Upload AyaNova backup files
// /// **Files of the same name will overwrite without warning**
// /// Maximum 10gb
// /// </summary>
// /// <returns></returns>
// [HttpPost("Upload")]
// [DisableFormValueModelBinding]
// [RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267
// public async Task<IActionResult> Upload()
// {
// //Adapted from the example found here: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads#uploading-large-files-with-streaming
// try
// {
// if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
// {
// return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", $"Expected a multipart request, but got {Request.ContentType}"));
// }
// // Used to accumulate all the form url encoded key value pairs in the
// // request.
// var formAccumulator = new KeyValueAccumulator();
// //string targetFilePath = null;
// var boundary = MultipartRequestHelper.GetBoundary(
// MediaTypeHeaderValue.Parse(Request.ContentType),
// _defaultFormOptions.MultipartBoundaryLengthLimit);
// var reader = new MultipartReader(boundary, HttpContext.Request.Body);
// var section = await reader.ReadNextSectionAsync();
// while (section != null)
// {
// ContentDispositionHeaderValue contentDisposition;
// var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
// if (hasContentDispositionHeader)
// {
// if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
// {
// //Save file
// //as it's just a backup file there is no db involvement at all
// FileUtil.storeBackupFile(section.Body, contentDisposition.FileName.Value.Replace("\"",""));
// }
// else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
// {
// // Content-Disposition: form-data; name="key"
// //
// // value
// // Do not limit the key name length here because the
// // multipart headers length limit is already in effect.
// var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
// var encoding = GetEncoding(section);
// using (var streamReader = new StreamReader(
// section.Body,
// encoding,
// detectEncodingFromByteOrderMarks: true,
// bufferSize: 1024,
// leaveOpen: true))
// {
// // The value length limit is enforced by MultipartBodyLengthLimit
// var value = await streamReader.ReadToEndAsync();
// if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
// {
// value = String.Empty;
// }
// formAccumulator.Append(key.Value, value);
// if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
// {
// throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
// }
// }
// }
// }
// // Drains any remaining section body that has not been consumed and
// // reads the headers for the next section.
// section = await reader.ReadNextSectionAsync();
// }
// }
// catch (InvalidDataException ex)
// {
// return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", ex.Message));
// }
// return Ok();
// }
// private static Encoding GetEncoding(MultipartSection section)
// {
// MediaTypeHeaderValue mediaType;
// var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// // UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// // most cases.
// if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
// {
// return Encoding.UTF8;
// }
// return mediaType.Encoding;
// }
}//eoc
}//eons

View File

@@ -0,0 +1,283 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Util;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Import AyaNova 7 data controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class ImportAyaNova7Controller : Controller
{
private readonly AyContext ct;
private readonly ILogger<ImportAyaNova7Controller> log;
private readonly ApiServerState serverState;
/// <summary>
///
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public ImportAyaNova7Controller(AyContext dbcontext, ILogger<ImportAyaNova7Controller> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Upload AyaNova 7 import file
///
/// Required roles: OpsAdminFull
///
/// </summary>
/// <returns>NameValue list of filenames and id's</returns>
[HttpPost]
[DisableFormValueModelBinding]
[RequestSizeLimit(10737418241)]//10737418240 = 10gb https://github.com/aspnet/Announcements/issues/267
public async Task<IActionResult> Upload()
{
//Open or opsOnly and user is opsadminfull
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.AyaNova7Import))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
var returnList = new List<String>();
try
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", $"Expected a multipart request, but got {Request.ContentType}"));
}
var uploadFormData = await ApiUploadProcessor.ProcessUtilityFileUpload(HttpContext);
bool badRequest = false;
string errorMessage = string.Empty;
//are these the right files?
if (uploadFormData.UploadedFiles.Count > 0)
{
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
{
//should look like this: ayanova.data.dump.2018-04-2--12-30-57.zip
string lwr = a.OriginalFileName.ToLowerInvariant();
if (!(lwr.StartsWith("ayanova.data.dump") && lwr.EndsWith(".zip")))
{
badRequest = true;
errorMessage = $"File uploaded \"{lwr}\" does not appear to be an AyaNova 7 data dump file. The name should start with \"ayanova.data.dump\" have a date in the middle and end with \".zip\". Upload process is terminated without saving.";
}
}
}
if (badRequest)
{
//delete temp files
if (uploadFormData.UploadedFiles.Count > 0)
{
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
{
System.IO.File.Delete(a.InitialUploadedPathName);
}
}
//return bad request
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, errorMessage));
}
//We have our files and a confirmed AyObject, ready to attach and save permanently
if (uploadFormData.UploadedFiles.Count > 0)
{
foreach (UploadedFileInfo a in uploadFormData.UploadedFiles)
{
returnList.Add(a.OriginalFileName);
}
}
}
catch (InvalidDataException ex)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "FileUploadAttempt", ex.Message));
}
//Return the list of attachment ids and filenames
return Ok(new ApiOkResponse(returnList));
}
/// <summary>
/// Delete import file
///
/// Required roles: OpsAdminFull
/// </summary>
/// <param name="filename"></param>
/// <returns>Ok</returns>
[HttpDelete("{filename}")]
public ActionResult Delete([FromRoute] string filename)
{
//Open or opsOnly and user is opsadminfull
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.AyaNova7Import))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//do the delete
//this handles removing the file if there are no efs left and also the db record for the attachment
FileUtil.DeleteUtilityFile(filename);
return NoContent();
}
/// <summary>
/// Get AyaNova 7 data dump uploaded files list
///
/// Required roles: OpsAdminFull
///
/// This list cannot be filtered or queried
///
/// </summary>
/// <returns>List of uploaded data dump files</returns>
[HttpGet]
public ActionResult List()
{
//Open or opsOnly and user is opsadminfull
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.AyaNova7Import))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//dump file name example: ayanova.data.dump.XXX.zip
List<string> l = FileUtil.UtilityFileList("ayanova.data.dump.*.zip");
return Ok(new ApiOkResponse(l));
}
/// <summary>
/// Start import of previously uploaded import file
///
/// Required roles: OpsAdminFull
///
/// </summary>
/// <param name="filename"></param>
/// <returns>Ok</returns>
[HttpPost("startImport/{filename}")]
public ActionResult StartImport([FromRoute] string filename)
{
//Open or opsOnly and user is opsadminfull
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//UPDATE: I think it should be ok so commenting this out for now pending something coming up in testing
// //TODO: I decided not to allow trial to import v7 data.
// //This was a snap decision, I didn't think about it much other than
// //I'm concerned right now as of April 17 2018 during development that
// //a trial user will import their old AyaNova data and then ... well somehow continue to use it I guess,
// //maybe it's a non-issue as a trial will only work so long anyway
// #if (!DEBUG)
// if (AyaNova.Core.License.LicenseIsTrial)
// {
// return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Current license is a trial license key. Only a licensed database can be used with import."));
// }
// #endif
//Create, in that they are creating new data in AyaNova
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.AyaNova7Import))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//does the file even exist?
if (!FileUtil.UtilityFileExists(filename))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, "filename", "File not found, ensure the name via the GET route endpoint list of previously uploaded import files"));
}
//Create the job here
dynamic jobInfo = new JObject();
jobInfo.ImportFileName = filename;
OpsJob j = new OpsJob();
j.Name = $"Import AyaNova7 data (import file \"{filename}\"";
j.JobType = JobType.ImportV7Data;
j.OwnerId = UserIdFromContext.Id(HttpContext.Items);
j.JobInfo = jobInfo.ToString();
JobsBiz.AddJob(j, ct);
return Accepted(new { JobId = j.GId });//202 accepted
}
////////////////////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,134 @@
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.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Tag controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class JobOperationsController : Controller
{
private readonly AyContext ct;
private readonly ILogger<JobOperationsController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public JobOperationsController(AyContext dbcontext, ILogger<JobOperationsController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get Operations jobs list
///
/// Required roles: OpsAdminFull, OpsAdminLimited, BizAdminFull, BizAdminLimited
///
/// This list cannot be filtered or queried as there are typically not many jobs
///
/// </summary>
/// <returns>List of operations jobs</returns>
[HttpGet]
public async Task<IActionResult> List()
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.JobOperations))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
JobOperationsBiz biz = new JobOperationsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
List<JobOperationsFetchInfo> l = await biz.GetJobListAsync();
return Ok(new ApiOkResponse(l));
}
/// <summary>
/// Get Operations log for a job
///
/// Required roles: OpsAdminFull, OpsAdminLimited, BizAdminFull, BizAdminLimited
///
/// This list cannot be filtered or queried as there are typically not many jobs
///
/// </summary>
/// <param name="gid"></param>
/// <returns>A tag</returns>
[HttpGet("logs/{gid}")]
public async Task<IActionResult> GetLogs([FromRoute] Guid gid)
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.JobOperations))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
JobOperationsBiz biz = new JobOperationsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
List<JobOperationsLogInfoItem> l = await biz.GetJobLogListAsync(gid);
return Ok(new ApiOkResponse(l));
}
//------------
}//eoc
}//eons

View File

@@ -0,0 +1,197 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// License route
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class LicenseController : Controller
{
private readonly AyContext ct;
private readonly ILogger<LicenseController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public LicenseController(AyContext dbcontext, ILogger<LicenseController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get License info
///
/// Required roles:
/// AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminFull |
/// AuthorizationRoles.BizAdminLimited | AuthorizationRoles.OpsAdminLimited
/// </summary>
/// <returns>Information about the currently installed license in AyaNova</returns>
[HttpGet()]
public ActionResult GetLicenseInfo()
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.License))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
var ret = AyaNova.Core.License.LicenseInfoAsJson;
return Ok(new ApiOkResponse(ret));
}
/// <summary>
/// Fetch license
///
/// Posting to this route causes AyaNova to attempt to refresh it's license
/// from the AyaNova license server
///
/// Required roles:
/// AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminFull
/// </summary>
/// <returns>On success returns information about the currently installed license in AyaNova</returns>
[HttpPost]
public ActionResult FetchLicense()
{
//Open or opsOnly and user is opsadminfull
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.License))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
try
{
AyaNova.Core.License.Fetch(serverState, ct, log);
}
catch (Exception ex)
{
Exception rootex = ex;
while (rootex.InnerException != null)
{
rootex = rootex.InnerException;
}
if (rootex.Message.Contains("E1020"))
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "LICENSE_KEY", rootex.Message));
}
else
{
throw ex;
}
}
var ret = AyaNova.Core.License.LicenseInfoAsJson;
return Ok(new ApiOkResponse(ret));
}
/// <summary>
/// Request trial license
///
/// Posting to this route causes AyaNova to request a trial license key from the AyaNova license server
/// Database must be empty and unlicensed or trial license
///
/// Required roles:
/// [OpsFull, BizAdminFull]
///
/// </summary>
/// <param name="requestData"></param>
/// <returns>HTTP 204 No Content result code on success or fail code with explanation</returns>
[HttpPost("trial")]
public ActionResult RequestTrial([FromBody] dtoTrialRequestData requestData)
{
//Open or opsOnly and user is opsadminfull
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.License))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
if (!AyaNova.Util.DbUtil.DBIsEmpty(ct, log))
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Only an empty AyaNova database can request a trial key. Erase the database to proceed with a new trial."));
}
if (!AyaNova.Core.License.ActiveKey.IsEmpty && !AyaNova.Core.License.ActiveKey.TrialLicense)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "There is an active registered license. Only an unlicensed or trial license database can request a trial key."));
}
//Send the request to RockFish here (or at least start the job to do it in which case return Accepted instead of no content and update comment above)
var ret = Core.License.RequestTrial(requestData.EmailAddress, requestData.RegisteredTo, log);
return Ok(new ApiOkResponse(ret));
}
//------------------------------------------------------
public class dtoTrialRequestData
{
[System.ComponentModel.DataAnnotations.Required]
public string RegisteredTo { get; set; }
[System.ComponentModel.DataAnnotations.Required, System.ComponentModel.DataAnnotations.EmailAddress]
public string EmailAddress { get; set; }
}
}//eoc
}//eons

View File

@@ -0,0 +1,384 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
//DOCUMENTATING THE API
//https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/recommended-tags-for-documentation-comments
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments
/// <summary>
/// Localized text controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class LocaleController : Controller
{
private readonly AyContext ct;
private readonly ILogger<LocaleController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public LocaleController(AyContext dbcontext, ILogger<LocaleController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get Locale all values
///
/// Required roles: Any
/// </summary>
/// <param name="id"></param>
/// <returns>A single Locale and it's values</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetLocale([FromRoute] long id)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var o = await biz.GetAsync(id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Get Locale pick list
/// Required roles: Any
///
/// </summary>
/// <returns>Picklist in alphabetical order of all locales</returns>
[HttpGet("PickList")]
public async Task<IActionResult> LocalePickList()
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var l = await biz.GetPickListAsync();
return Ok(new ApiOkResponse(l));
}
/// <summary>
/// Get subset of locale values
/// Required roles: Any
///
/// </summary>
/// <param name="inObj">LocaleSubsetParam object defining the locale Id and a list of keys required</param>
/// <returns>A key value array of localized text values</returns>
[HttpPost("SubSet")]
public async Task<IActionResult> SubSet([FromBody] LocaleSubsetParam inObj)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var l = await biz.GetSubset(inObj);
return Ok(new ApiOkResponse(l));
}
/// <summary>
/// Duplicates an existing locale with a new name
///
/// Required roles: OpsAdminFull | BizAdminFull
///
/// </summary>
/// <param name="inObj">NameIdItem object containing source locale Id and new name</param>
/// <returns>Error response or newly created locale</returns>
[HttpPost("Duplicate")]
public async Task<IActionResult> Duplicate([FromBody] NameIdItem inObj)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var o = await biz.DuplicateAsync(inObj);
if (o == null)
{
//error return
return BadRequest(new ApiErrorResponse(biz.Errors));
}
else
{
//save and success return
await ct.SaveChangesAsync();
return CreatedAtAction("GetLocale", new { id = o.Id }, new ApiCreatedResponse(o));
}
}
/// <summary>
/// Put (UpdateLocaleItemDisplayText)
///
/// Required roles: OpsAdminFull | BizAdminFull
///
/// Update a single key with new display text
///
/// </summary>
/// <param name="inObj">NewText/Id/Concurrency token object. NewText is new display text, Id is LocaleItem Id, concurrency token is required</param>
/// <returns></returns>
[HttpPut("UpdateLocaleItemDisplayText")]
public async Task<IActionResult> PutLocalItemDisplyaText([FromBody] NewTextIdConcurrencyTokenItem inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var oFromDb = await ct.LocaleItem.SingleOrDefaultAsync(m => m.Id == inObj.Id);
if (oFromDb == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
//Now fetch locale for rights and to ensure not stock
var oDbParent = await ct.Locale.SingleOrDefaultAsync(x => x.Id == oFromDb.LocaleId);
if (oDbParent == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.Locale, oDbParent.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.PutLocaleItemDisplayText(oFromDb, inObj, oDbParent))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!biz.LocaleItemExists(inObj.Id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
//exists but was changed by another user
//I considered returning new and old record, but where would it end?
//Better to let the client decide what to do than to send extra data that is not required
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
}
/// <summary>
/// Put (UpdateLocaleName)
///
/// Required roles: OpsAdminFull | BizAdminFull
///
/// Update a locale to change the name (non-stock locales only)
///
/// </summary>
/// <param name="inObj">NewText/Id/Concurrency token object. NewText is new locale name, Id is Locale Id, concurrency token is required</param>
/// <returns></returns>
[HttpPut("UpdateLocaleName")]
public async Task<IActionResult> PutLocaleName([FromBody] NewTextIdConcurrencyTokenItem inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var oFromDb = await ct.Locale.SingleOrDefaultAsync(m => m.Id == inObj.Id);
if (oFromDb == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.Locale, oFromDb.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.PutLocaleName(oFromDb, inObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!biz.LocaleExists(inObj.Id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
//exists but was changed by another user
//I considered returning new and old record, but where would it end?
//Better to let the client decide what to do than to send extra data that is not required
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
}
/// <summary>
/// Delete Locale
///
/// Required roles:
/// BizAdminFull, InventoryFull
///
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteLocale([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Fetch locale and it's children
//(fetch here so can return proper REST responses on failing basic validity)
var dbObj = ct.Locale.Include(x => x.LocaleItems).SingleOrDefault(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.Locale, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
LocaleBiz biz = new LocaleBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Delete(dbObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
await ct.SaveChangesAsync();
//Delete children / attached objects
// biz.DeleteChildren(dbObj);
return NoContent();
}
// private bool LocaleExists(long id)
// {
// return ct.Locale.Any(e => e.Id == id);
// }
//------------
public class LocaleSubsetParam
{
[System.ComponentModel.DataAnnotations.Required]
public long LocaleId { get; set; }
[System.ComponentModel.DataAnnotations.Required]
public List<string> Keys { get; set; }
}
}
}

View File

@@ -0,0 +1,177 @@
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Log files controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
//[Produces("application/json")]
[Authorize]
public class LogFilesController : Controller
{
private readonly AyContext ct;
private readonly ILogger<LogFilesController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public LogFilesController(AyContext dbcontext, ILogger<LogFilesController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get server log
///
/// Required roles:
/// OpsAdminFull | OpsAdminLimited
/// </summary>
/// <param name="logname"></param>
/// <returns>A single log file in plain text</returns>
[HttpGet("{logname}")]
public ActionResult GetLog([FromRoute] string logname)
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.LogFile))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//stream the file contents into a json object and return
//build the full path from the log file name and defined path
var logFilePath = System.IO.Path.Combine(ServerBootConfig.AYANOVA_LOG_PATH, logname);
//does file exist?
if (!System.IO.File.Exists(logFilePath))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
// //read it and stream it back in a json object
// Newtonsoft.Json.Linq.JObject o = Newtonsoft.Json.Linq.JObject.FromObject(new
// {
// log = new
// {
// name = logname,
// log = System.IO.File.ReadAllText(logFilePath)
// }
// });
// return Ok(new ApiOkResponse(o));
return Content(System.IO.File.ReadAllText(logFilePath));
}
/// <summary>
/// Get list of operations logs
///
/// Required roles:
/// OpsAdminFull | OpsAdminLimited
///
/// </summary>
/// <returns></returns>
[HttpGet()]
public ActionResult ListLogs()
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.LogFile))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Iterate all log files and build return
var files = System.IO.Directory.GetFiles(ServerBootConfig.AYANOVA_LOG_PATH, "log-ayanova*.txt");
Newtonsoft.Json.Linq.JObject o = Newtonsoft.Json.Linq.JObject.FromObject(new
{
logs =
from f in files
orderby f
select new
{
logName = System.IO.Path.GetFileName(f)
}
});
// Newtonsoft.Json.Linq.JObject o = Newtonsoft.Json.Linq.JObject.FromObject(new
// {
// logs = new
// {
// licensedTo = ActiveKey.RegisteredTo,
// registeredEmail = ActiveKey.FetchEmail,
// trial = ActiveKey.Trial,
// keySerial = ActiveKey.Id,
// keySource = ActiveKey.Source,
// created = ActiveKey.Created.ToString(),
// features =
// from f in files
// orderby f
// select new
// {
// logName = f
// }
// }
// });
return Ok(new ApiOkResponse(o));
}
//------------
}
}

View File

@@ -0,0 +1,137 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using App.Metrics;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Log files controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Authorize]
public class MetricsController : Controller
{
private readonly AyContext ct;
private readonly ILogger<LogFilesController> log;
private readonly ApiServerState serverState;
private readonly IMetrics metrics;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
/// <param name="Metrics"></param>
public MetricsController(AyContext dbcontext, ILogger<LogFilesController> logger, ApiServerState apiServerState, IMetrics Metrics)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
metrics = Metrics;
}
/// <summary>
/// Get metrics as text document
///
/// Required roles:
/// OpsAdminFull | OpsAdminLimited
/// </summary>
/// <returns>Snapshot of metrics</returns>
[HttpGet("TextSnapShot")]
public async Task<IActionResult> GetMetrics()
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Metrics))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
string sResult = await GetTheMetrics("plain");
return Content(sResult);
}
/// <summary>
/// Get metrics as json object
///
/// Required roles:
/// OpsAdminFull | OpsAdminLimited
/// </summary>
/// <returns>Snapshot of metrics</returns>
[HttpGet("JsonSnapShot")]
public async Task<IActionResult> GetJsonMetrics()
{
//Open or opsOnly and user is opsadminfull or opsadminlimited
if (!serverState.IsOpenOrOpsOnly || (serverState.IsOpsOnly && !Authorized.HasAnyRole(HttpContext.Items, AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited)))
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Metrics))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
string sResult = await GetTheMetrics("json");
//return Ok(new ApiOkResponse(new { metrics = sResult }));
// /THIS IS NOT RETURNING VALID PARSEABLE JSON, FIX IT
//IDEAS:
//try parsing the result first then return it
//
JObject json = JObject.Parse(sResult);
return Ok(new ApiOkResponse(json));
}
/// <summary>
/// Get the metrics snapshot
/// </summary>
/// <param name="format">Either "json" for json format or "plain" for plaintext format</param>
/// <returns></returns>
private async Task<string> GetTheMetrics(string format)
{
var snapshot = metrics.Snapshot.Get();
var formatters = ((IMetricsRoot)metrics).OutputMetricsFormatters;
string sResult = $"ERROR GETTING METRICS IN {format} FORMAT";
foreach (var formatter in formatters)
{
if (formatter.MediaType.Format == format)
{
using (var stream = new MemoryStream())
{
await formatter.WriteAsync(stream, snapshot);
sResult = System.Text.Encoding.UTF8.GetString(stream.ToArray());
}
}
}
return sResult;
}
//------------
}
}

View File

@@ -0,0 +1,120 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Server state controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class ServerStateController : Controller
{
private readonly ILogger<ServerStateController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public ServerStateController(ILogger<ServerStateController> logger, ApiServerState apiServerState)
{
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get server state
///
/// Required roles:
/// [NONE / authentication not required]
/// </summary>
/// <returns>Current server state (Closed, OpsOnly, Open)</returns>
[HttpGet]
public ActionResult Get()
{
return Ok(new ApiOkResponse(new ServerStateModel() { ServerState = serverState.GetState().ToString(), Reason = serverState.Reason }));
}
/// <summary>
/// Set server state
///
/// Required roles:
/// [OpsFull, BizAdminFull]
///
/// Valid parameters:
/// One of "Closed", "OpsOnly" or "Open"
///
/// </summary>
/// <param name="state">{"NewState":"Closed"}</param>
/// <returns>NoContent 204</returns>
[HttpPost]
[Authorize]
public ActionResult PostServerState([FromBody] ServerStateModel state)
{
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.ServerState))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (serverState.IsSystemLocked)//no state change allowed when system locked, must correct the problem first
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
ApiServerState.ServerState desiredState;
if (!Enum.TryParse<ApiServerState.ServerState>(state.ServerState, true, out desiredState))
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, "Invalid state - must be one of \"Closing\", \"Closed\", \"OpsOnly\" or \"Open\""));
}
log.LogInformation($"ServerState change request by user {UserNameFromContext.Name(HttpContext.Items)} from current state of \"{serverState.GetState().ToString()}\" to \"{desiredState.ToString()}\"");
serverState.SetState(desiredState, state.Reason);
return NoContent();
}
/// <summary>
/// Parameter object
/// </summary>
public class ServerStateModel
{
/// <summary>
/// One of "Closed", "OpsOnly" or "Open"
/// </summary>
/// <returns></returns>
[Required]
public string ServerState { get; set; }
/// <summary>
/// Reason for server state
/// </summary>
/// <returns></returns>
public string Reason { get; set; }
}
//------------
}
}

View File

@@ -0,0 +1,366 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// Tag controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class TagController : Controller
{
private readonly AyContext ct;
private readonly ILogger<TagController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public TagController(AyContext dbcontext, ILogger<TagController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get Tag
///
/// Required roles:
/// AnyOne
/// </summary>
/// <param name="id"></param>
/// <returns>A tag</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetTag([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Tag))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
TagBiz biz = new TagBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var o = await biz.GetAsync(id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Get Tag pick list
///
/// Required roles: AnyRole
///
/// This endpoint queries the Name property of tags for
/// items that **START WITH** the characters submitted in the
/// "q" parameter
///
/// Unlike most other picklists, wildcard characters if found in the query will be escaped and be considered part of the search string
/// Query is case insensitive as all tags are lowercase
///
/// Empty queries will return all tags
///
/// </summary>
/// <returns>Paged id/name collection of tags with paging data</returns>
[HttpGet("PickList", Name = nameof(PickList))]
public async Task<IActionResult> PickList([FromQuery] string q, [FromQuery] PagingOptions pagingOptions)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Tag))//Note: anyone can read a tag, but that might change in future so keeping this code in
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
TagBiz biz = new TagBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<NameIdItem> pr = await biz.GetPickListAsync(Url, nameof(PickList), pagingOptions, q);
return Ok(new ApiOkWithPagingResponse<NameIdItem>(pr));
}
/// <summary>
/// Post TAG
///
/// Required roles:
/// BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
/// </summary>
/// <param name="inObj">String name of tag</param>
/// <returns><see cref="Tag"/> object</returns>
[HttpPost]
public async Task<IActionResult> PostTag([FromBody] NameItem inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.Tag))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
TagBiz biz = new TagBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
//Create and validate
Tag o = await biz.CreateAsync(inObj.Name);
if (o == null)
{
//error return
return BadRequest(new ApiErrorResponse(biz.Errors));
}
else
{
//save and success return
await ct.SaveChangesAsync();
return CreatedAtAction("GetTag", new { id = o.Id }, new ApiCreatedResponse(o));
}
}
/// <summary>
/// Put (update) Tag
///
/// Required roles:
/// BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
///
/// </summary>
/// <param name="id"></param>
/// <param name="oIn"></param>
/// <returns></returns>
[HttpPut("{id}")]
public async Task<IActionResult> PutTag([FromRoute] long id, [FromBody] Tag oIn)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var oFromDb = await ct.Tag.SingleOrDefaultAsync(m => m.Id == id);
if (oFromDb == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.Tag, oFromDb.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
TagBiz biz = new TagBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Put(oFromDb, oIn))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TagExists(id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
//exists but was changed by another user
//I considered returning new and old record, but where would it end?
//Better to let the client decide what to do than to send extra data that is not required
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
}
/// <summary>
/// Patch (update) Tag
/// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
/// </summary>
/// <param name="id"></param>
/// <param name="concurrencyToken"></param>
/// <param name="objectPatch"></param>
/// <returns></returns>
[HttpPatch("{id}/{concurrencyToken}")]
public async Task<IActionResult> PatchTag([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument<Tag> objectPatch)
{
//https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
TagBiz biz = new TagBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var oFromDb = await ct.Tag.SingleOrDefaultAsync(m => m.Id == id);
if (oFromDb == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.Tag, oFromDb.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//patch and validate
if (!biz.Patch(oFromDb, objectPatch, concurrencyToken))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TagExists(id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
}
/// <summary>
/// Delete Tag
/// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTag([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var dbObj = await ct.Tag.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.Tag, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
TagBiz biz = new TagBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Delete(dbObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
await ct.SaveChangesAsync();
return NoContent();
}
private bool TagExists(long id)
{
return ct.Tag.Any(e => e.Id == id);
}
//------------
}
}

View File

@@ -0,0 +1,258 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
/// <summary>
/// TagMap controller
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class TagMapController : Controller
{
private readonly AyContext ct;
private readonly ILogger<TagMapController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public TagMapController(AyContext dbcontext, ILogger<TagMapController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get TagMap object
///
/// Required roles: Same roles as tagged object
/// </summary>
/// <param name="id"></param>
/// <returns>A TagMap</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetTagMap([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.TagMap))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
TagMapBiz biz = new TagMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var o = await biz.GetAsync(id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
//Check rights to parent tagged object
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, o.TagToObjectType))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Post TagMap - Map a tag to an object / Id
///
/// Required roles: Same roles as tagged object
/// </summary>
/// <param name="inObj">TagMapInfo</param>
/// <returns><see cref="TagMap"/> object</returns>
[HttpPost]
public async Task<IActionResult> PostTagMap([FromBody] TagMapInfo inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.TagMap))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Rights to parent taggable object?
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, inObj.TagToObjectType))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
TagMapBiz biz = new TagMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
//Create and validate
TagMap o = await biz.CreateAsync(inObj);
if (o == null)
{
//error return
return BadRequest(new ApiErrorResponse(biz.Errors));
}
else
{
//save and success return
await ct.SaveChangesAsync();
return CreatedAtAction("GetTagMap", new { id = o.Id }, new ApiCreatedResponse(o));
}
}
/// <summary>
/// Delete TagMap
/// Required roles: Same roles as tagged object
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTagMap([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var dbObj = await ct.TagMap.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.TagMap, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Rights to parent tagged object?
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, dbObj.TagToObjectType, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
TagMapBiz biz = new TagMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Delete(dbObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
await ct.SaveChangesAsync();
return NoContent();
}
/// <summary>
/// Get Tag pick list
///
/// Required roles: Follows parent (tagged object) roles
///
/// </summary>
/// <returns>Name / Id collection of tags on object</returns>
[HttpGet("TagsOnObject")]
public async Task<IActionResult> TagsOnObjectList([FromBody] TypeAndIdInfo inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Tag))//Note: anyone can read a tag, but that might change in future so keeping this code in
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Check rights to parent tagged object
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, inObj.ObjectType))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
TagMapBiz biz = new TagMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var l = await biz.GetTagsOnObjectListAsync(new AyaTypeId(inObj.ObjectType, inObj.ObjectId));
return Ok(new ApiOkResponse(l));
}
private bool TagMapExists(long id)
{
return ct.TagMap.Any(e => e.Id == id);
}
//------------
}//eoc
}

View File

@@ -0,0 +1,124 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using Newtonsoft.Json.Linq;
namespace AyaNova.Api.Controllers
{
/// <summary>
///Test controller class used during development
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class TrialController : Controller
{
private readonly AyContext ct;
private readonly ILogger<WidgetController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public TrialController(AyContext dbcontext, ILogger<WidgetController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Seed a trial database with sample data.
///
/// You can control the size and scope of the seeded data with the passed in size value
/// "Small" - a small one man shop dataset
/// "Medium" - Local service company with multiple employees and departments dataset
/// "Large" - Large corporate multi regional dataset
/// </summary>
/// <param name="size">Valid values are "Small", "Medium", "Large"</param>
/// <returns></returns>
[HttpPost("seed/{size}")]
public ActionResult SeedTrialDatabase([FromRoute] string size)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
if (!AyaNova.Core.License.ActiveKey.TrialLicense)
{
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, "Current license is not a trial license key. Only a trial can be seeded."));
}
Seeder.SeedLevel seedLevel = Seeder.SeedLevel.SmallOneManShopTrialDataSet;
switch (size.ToLowerInvariant())
{
case "small":
seedLevel = Seeder.SeedLevel.SmallOneManShopTrialDataSet;
break;
case "medium":
seedLevel = Seeder.SeedLevel.MediumLocalServiceCompanyTrialDataSet;
break;
case "large":
seedLevel = Seeder.SeedLevel.LargeCorporateMultiRegionalTrialDataSet;
break;
default:
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, "size", "Valid values are \"small\", \"medium\", \"large\""));
}
//Create the job here
JObject o = JObject.FromObject(new
{
seedLevel = seedLevel
// channel = new
// {
// title = "Star Wars",
// link = "http://www.starwars.com",
// description = "Star Wars blog.",
// item =
// from p in posts
// orderby p.Title
// select new
// {
// title = p.Title,
// description = p.Description,
// link = p.Link,
// category = p.Categories
// }
// }
});
OpsJob j = new OpsJob();
j.Name = $"Seed test data (size={size})";
j.JobType = JobType.SeedTestData;
j.Exclusive=true;//don't run other jobs, this will erase the db
j.JobInfo = o.ToString();
JobsBiz.AddJob(j, ct);
return Accepted(new { JobId = j.GId });//202 accepted
}
//------------
}//eoc
}//eons

View File

@@ -0,0 +1,482 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
//DOCUMENTATING THE API
//https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/recommended-tags-for-documentation-comments
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments
/// <summary>
/// Sample controller class used during development for testing purposes
/// </summary>
[ApiVersion("8.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Authorize]
public class WidgetController : Controller
{
private readonly AyContext ct;
private readonly ILogger<WidgetController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public WidgetController(AyContext dbcontext, ILogger<WidgetController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get widget
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
/// </summary>
/// <param name="id"></param>
/// <returns>A single widget</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetWidget([FromRoute] long id)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Widget))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var o = await biz.GetAsync(id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Get paged list of widgets
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
///
/// </summary>
/// <returns>Paged collection of widgets with paging data</returns>
[HttpGet("List", Name = nameof(List))]//We MUST have a "Name" defined or we can't get the link for the pagination, non paged urls don't need a name
public async Task<IActionResult> List([FromQuery] PagingOptions pagingOptions)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Widget))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<Widget> pr = await biz.GetManyAsync(Url, nameof(List), pagingOptions);
return Ok(new ApiOkWithPagingResponse<Widget>(pr));
}
/// <summary>
/// Get widget pick list
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
///
/// This list supports querying the Name property
/// include a "q" parameter for string to search for
/// use % for wildcards.
///
/// e.g. q=%Jones%
///
/// Query is case insensitive
/// </summary>
/// <returns>Paged id/name collection of widgets with paging data</returns>
[HttpGet("PickList", Name = nameof(WidgetPickList))]
public async Task<IActionResult> WidgetPickList([FromQuery] string q, [FromQuery] PagingOptions pagingOptions)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Widget))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<NameIdItem> pr = await biz.GetPickListAsync(Url, nameof(WidgetPickList), pagingOptions, q);
return Ok(new ApiOkWithPagingResponse<NameIdItem>(pr));
}
/// <summary>
/// Put (update) widget
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
///
/// </summary>
/// <param name="id"></param>
/// <param name="inObj"></param>
/// <returns></returns>
[HttpPut("{id}")]
public async Task<IActionResult> PutWidget([FromRoute] long id, [FromBody] Widget inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var oFromDb = await ct.Widget.SingleOrDefaultAsync(m => m.Id == id);
if (oFromDb == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.Widget, oFromDb.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Put(oFromDb, inObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!WidgetExists(id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
//exists but was changed by another user
//I considered returning new and old record, but where would it end?
//Better to let the client decide what to do than to send extra data that is not required
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
}
/// <summary>
/// Patch (update) widget
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
/// </summary>
/// <param name="id"></param>
/// <param name="concurrencyToken"></param>
/// <param name="objectPatch"></param>
/// <returns></returns>
[HttpPatch("{id}/{concurrencyToken}")]
public async Task<IActionResult> PatchWidget([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument<Widget> objectPatch)
{
//https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var oFromDb = await ct.Widget.SingleOrDefaultAsync(m => m.Id == id);
if (oFromDb == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.Widget, oFromDb.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//patch and validate
if (!biz.Patch(oFromDb, objectPatch, concurrencyToken))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!WidgetExists(id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
}
/// <summary>
/// Post widget
///
/// Required roles:
/// BizAdminFull, InventoryFull, TechFull
/// </summary>
/// <param name="inObj"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostWidget([FromBody] Widget inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
//If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner
if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.Widget))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
//Create and validate
Widget o = await biz.CreateAsync(inObj);
if (o == null)
{
//error return
return BadRequest(new ApiErrorResponse(biz.Errors));
}
else
{
//save and success return
await ct.SaveChangesAsync();
return CreatedAtAction("GetWidget", new { id = o.Id }, new ApiCreatedResponse(o));
}
}
/// <summary>
/// Delete widget
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
///
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteWidget([FromRoute] long id)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var dbObj = await ct.Widget.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.Widget, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
WidgetBiz biz = new WidgetBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Delete(dbObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
await ct.SaveChangesAsync();
//Delete children / attached objects
biz.DeleteChildren(dbObj);
return NoContent();
}
private bool WidgetExists(long id)
{
return ct.Widget.Any(e => e.Id == id);
}
/// <summary>
/// Get route that triggers exception for testing
/// </summary>
/// <returns>Nothing, triggers exception</returns>
[HttpGet("exception")]
public ActionResult GetException()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Widget))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
throw new System.NotSupportedException("Test exception from widget controller");
}
/// <summary>
/// Get route that triggers an alternate type of exception for testing
/// </summary>
/// <returns>Nothing, triggers exception</returns>
[HttpGet("altexception")]
public ActionResult GetAltException()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.Widget))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
throw new System.ArgumentException("Test exception (ALT) from widget controller");
}
/// <summary>
/// Get route that submits a long running operation job for testing
/// </summary>
/// <returns>Nothing</returns>
[HttpGet("TestWidgetJob")]
public ActionResult TestWidgetJob()
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.JobOperations))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Create the job here
OpsJob j = new OpsJob();
j.Name = "TestWidgetJob";
j.JobType = JobType.TestWidgetJob;
JobsBiz.AddJob(j, ct);
return Accepted(new { JobId = j.GId });//202 accepted
}
//------------
}
}

246
server/AyaNova/Program.cs Normal file
View File

@@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NLog.Web;
using NLog.Targets;
using NLog.Config;
using App.Metrics;
using App.Metrics.AspNetCore;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
namespace AyaNova
{
public class Program
{
public static void Main(string[] args)
{
//Get config
var config = new ConfigurationBuilder().AddEnvironmentVariables().AddCommandLine(args).Build();
ServerBootConfig.SetConfiguration(config);
#region Initialize Logging
//default log level
NLog.LogLevel NLogLevel = NLog.LogLevel.Info;
bool logLevelIsInfoOrHigher = true;
switch (ServerBootConfig.AYANOVA_LOG_LEVEL.ToLowerInvariant())
{
case "fatal":
NLogLevel = NLog.LogLevel.Fatal;
break;
case "error":
NLogLevel = NLog.LogLevel.Error;
break;
case "warn":
NLogLevel = NLog.LogLevel.Warn;
break;
case "info":
NLogLevel = NLog.LogLevel.Info;
break;
case "debug":
NLogLevel = NLog.LogLevel.Debug;
logLevelIsInfoOrHigher = false;
break;
case "trace":
NLogLevel = NLog.LogLevel.Trace;
logLevelIsInfoOrHigher = false;
break;
default:
NLogLevel = NLog.LogLevel.Info;
break;
}
// Step 1. Create configuration object
var logConfig = new LoggingConfiguration();
// Step 2. Create targets and add them to the configuration
var fileTarget = new FileTarget();
logConfig.AddTarget("file", fileTarget);
//console target for really serious errors only
var consoleTarget = new ConsoleTarget();
logConfig.AddTarget("console", consoleTarget);
var nullTarget = new NLog.Targets.NullTarget();
logConfig.AddTarget("blackhole", nullTarget);
// Step 3. Set target properties
fileTarget.FileName = Path.Combine(ServerBootConfig.AYANOVA_LOG_PATH, "log-ayanova.txt");
fileTarget.Layout = "${longdate}|${uppercase:${level}}|${logger}|${message:exceptionSeparator==>:withException=true}";
fileTarget.ArchiveFileName = Path.Combine(ServerBootConfig.AYANOVA_LOG_PATH, "log-ayanova-{#}.txt");
fileTarget.ArchiveEvery = FileArchivePeriod.Wednesday;
fileTarget.MaxArchiveFiles = 3;
// Step 4. Define rules
//filter out all Microsoft INFO level logs as they are too much
var logRuleFilterOutMicrosoft = new LoggingRule("Microsoft.*", NLog.LogLevel.Trace, NLog.LogLevel.Info, nullTarget);
logRuleFilterOutMicrosoft.Final = true;
//filter out all Microsoft EF CORE concurrency exceptions, it's a nuisance unless debugging or something
//This is what I have to filter because it's the top exception: Microsoft.EntityFrameworkCore.Update
//But this is what I'm actually trying to filter: Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException
//however there doesn't appear to be a way to filter out based on content so...
var logRuleFilterOutMicrosoftEfCoreConcurrencyExceptions = new LoggingRule("Microsoft.EntityFrameworkCore.Update", NLog.LogLevel.Trace, NLog.LogLevel.Error, nullTarget);
logRuleFilterOutMicrosoftEfCoreConcurrencyExceptions.Final = true;
//Log all other regular items at selected level
var logRuleAyaNovaItems = new LoggingRule("*", NLogLevel, fileTarget);
//Log error or above to console
var logRuleForConsole = new LoggingRule("*", NLog.LogLevel.Error, consoleTarget);
//add console serious error only log rule
logConfig.LoggingRules.Add(logRuleForConsole);
//only log microsoft stuff it log is debug level or lower
if (logLevelIsInfoOrHigher)
{
//filter OUT microsoft stuff
logConfig.LoggingRules.Add(logRuleFilterOutMicrosoft);
logConfig.LoggingRules.Add(logRuleFilterOutMicrosoftEfCoreConcurrencyExceptions);
}
logConfig.LoggingRules.Add(logRuleAyaNovaItems);
//Turn on internal logging:
if (ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG)
{
NLog.Common.InternalLogger.LogFile = "log-ayanova-logger.txt";
NLog.Common.InternalLogger.LogLevel = NLog.LogLevel.Debug;
}
// NLog: setup the logger first to catch all errors
var logger = NLogBuilder.ConfigureNLog(logConfig).GetCurrentClassLogger();
//This is the first log entry
logger.Info("AYANOVA SERVER BOOTING (log level: \"{0}\")", ServerBootConfig.AYANOVA_LOG_LEVEL);
logger.Info(AyaNovaVersion.FullNameAndVersion);
logger.Debug("BOOT: Log path is \"{0}\" ", ServerBootConfig.AYANOVA_LOG_PATH);
if (ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG)
{
logger.Warn("BOOT: AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG is enabled! Disable as soon as no longer required.");
}
//Log environmental settings
logger.Info("BOOT: OS - {0}", Environment.OSVersion.ToString());
logger.Debug("BOOT: Machine - {0}", Environment.MachineName);
logger.Debug("BOOT: User - {0}", Environment.UserName);
logger.Debug("BOOT: .Net Version - {0}", Environment.Version.ToString());
logger.Debug("BOOT: CPU count - {0}", Environment.ProcessorCount);
logger.Debug("BOOT: Default language - \"{0}\"", ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE);
#endregion
//Ensure we are in the correct folder
string startFolder = Directory.GetCurrentDirectory();
var wwwRootFolder = Path.Combine(startFolder, "wwwroot");
//Test for web root path
//If user starts AyaNova from folder that is not the contentRoot then
//AyaNova won't be able to serve static files
if (!Directory.Exists(wwwRootFolder))
{
var err = string.Format("BOOT: E1010 - AyaNova was not started in the correct folder. AyaNova must be started from the folder that contains the \"wwwroot\" folder but was started instead from this folder: \"{0}\" which does not contain the wwwroot folder.", startFolder);
logger.Fatal(err);
throw new System.ApplicationException(err);
}
try
{
BuildWebHost(args, logger).Run();
}
catch (Exception e)
{
logger.Fatal(e, "BOOT: E1090 - AyaNova server can't start due to unexpected exception during initialization");
throw;
}
}
public static IWebHost BuildWebHost(string[] args, NLog.Logger logger)
{
logger.Debug("BOOT: building web host");
var configuration = new ConfigurationBuilder().AddCommandLine(args).Build();
return WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseSetting("detailedErrors", "true")
.UseUrls(ServerBootConfig.AYANOVA_USE_URLS)//default port and urls, set first can be overridden by any later setting here
.UseConfiguration(configuration)//support command line override of port (dotnet run urls=http://*:8081)
.UseIISIntegration()//support IIS integration just in case, it appears here to override prior settings if necessary (port)
.ConfigureMetricsWithDefaults(builder =>
{
if (ServerBootConfig.AYANOVA_METRICS_USE_INFLUXDB)
{
builder.Report.ToInfluxDb(
options =>
{
options.InfluxDb.BaseUri = new Uri(ServerBootConfig.AYANOVA_METRICS_INFLUXDB_BASEURL);
options.InfluxDb.Database = ServerBootConfig.AYANOVA_METRICS_INFLUXDB_DBNAME;
if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_METRICS_INFLUXDB_CONSISTENCY))
{
options.InfluxDb.Consistenency = ServerBootConfig.AYANOVA_METRICS_INFLUXDB_CONSISTENCY;
}
options.InfluxDb.UserName = ServerBootConfig.AYANOVA_METRICS_INFLUXDB_USERNAME;
options.InfluxDb.Password = ServerBootConfig.AYANOVA_METRICS_INFLUXDB_PASSWORD;
if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_METRICS_INFLUXDB_RETENSION_POLICY))
{
options.InfluxDb.RetensionPolicy = ServerBootConfig.AYANOVA_METRICS_INFLUXDB_RETENSION_POLICY;
}
options.InfluxDb.CreateDataBaseIfNotExists = true;
options.HttpPolicy.BackoffPeriod = TimeSpan.FromSeconds(30);
options.HttpPolicy.FailuresBeforeBackoff = 5;
options.HttpPolicy.Timeout = TimeSpan.FromSeconds(10);
//options.MetricsOutputFormatter = new App.Metrics.Formatters.Json.MetricsJsonOutputFormatter();
//options.Filter = filter;
options.FlushInterval = TimeSpan.FromSeconds(20);
});
}
})
.UseMetricsEndpoints(opt =>
{
opt.EnvironmentInfoEndpointEnabled = false;
opt.MetricsEndpointEnabled = false;
opt.MetricsTextEndpointEnabled = false;
})
.UseMetrics()
.UseStartup<Startup>()
.ConfigureLogging((context, logging) =>
{
// clear all previously registered providers
//https://stackoverflow.com/a/46336988/8939
logging.ClearProviders();
})
.UseNLog() // NLog: setup NLog for Dependency injection
.Build();
}
}//eoc
}//eons

439
server/AyaNova/Startup.cs Normal file
View File

@@ -0,0 +1,439 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Hosting;
using AyaNova.Models;
using AyaNova.Util;
using AyaNova.Generator;
using AyaNova.Biz;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
using System.IO;
using System.Reflection;
using System.Linq;
using System;
namespace AyaNova
{
public class Startup
{
/////////////////////////////////////////////////////////////
//
public Startup(ILogger<Startup> logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
{
_log = logger;
_hostingEnvironment = hostingEnvironment;
AyaNova.Util.ApplicationLogging.LoggerFactory = logFactory;
//this must be set here
ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH=hostingEnvironment.ContentRootPath;
}
private readonly ILogger<Startup> _log;
private string _connectionString = "";
private readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment _hostingEnvironment;
////////////////////////////////////////////////////////////
// This method gets called by the runtime. Use this method to add services to the container.
//
public void ConfigureServices(IServiceCollection services)
{
_log.LogDebug("BOOT: initializing services...");
//Server state service for shutting people out of api
_log.LogDebug("BOOT: init ApiServerState service");
services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState());
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
_log.LogDebug("BOOT: init ApiExplorer service");
services.AddMvcCore().AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV");
_log.LogDebug("BOOT: ensuring user and backup folders exist and are separate locations...");
FileUtil.EnsureUserAndUtilityFoldersExistAndAreNotIdentical(_hostingEnvironment.ContentRootPath);
#region DATABASE
_connectionString = ServerBootConfig.AYANOVA_DB_CONNECTION;
//Check DB server exists and can be connected to
_log.LogDebug("BOOT: Testing database server connection...");
//parse the connection string properly
DbUtil.ParseConnectionString(_log, _connectionString);
//Test for server
//Will retry 10 times every 3 seconds for a total of 30 seconds
if (!DbUtil.DatabaseServerExists(_log, "BOOT: waiting for db server"))
{
var err = $"BOOT: E1000 - AyaNova can't connect to the database server after trying for 30 seconds (connection string is:\"{DbUtil.DisplayableConnectionString}\")";
_log.LogCritical(err);
throw new System.ApplicationException(err);
}
//ensure database is ready and present
DbUtil.EnsureDatabaseExists(_log);
bool LOG_SENSITIVE_DATA = false;
#if (DEBUG)
//LOG_SENSITIVE_DATA = true;
#endif
_log.LogDebug("BOOT: init EF service");
services.AddEntityFrameworkNpgsql().AddDbContext<AyContext>(
options => options.UseNpgsql(_connectionString,
opt => opt.EnableRetryOnFailure())//http://www.npgsql.org/efcore/misc.html?q=execution%20strategy#execution-strategy
.ConfigureWarnings(warnings => //https://livebook.manning.com/#!/book/entity-framework-core-in-action/chapter-12/v-10/85
warnings.Throw( //Throw an exception on client eval, not necessarily an error but a smell
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.QueryClientEvaluationWarning))
.EnableSensitiveDataLogging(LOG_SENSITIVE_DATA)
);
#endregion
_log.LogDebug("BOOT: init ApiVersioning service");
// services.AddApiVersioning(o => o.ReportApiVersions = true);
services
.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0");
options.ReportApiVersions = true;
});
_log.LogDebug("BOOT: init MVC service");
_log.LogDebug("BOOT: init Metrics service");
services.AddMvc(config =>
{
//was this but needed logging, not certain about the new way of adding so keeping this in case it all goes sideways in testing
//config.Filters.Add(typeof(AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter));
config.Filters.Add(new AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter(AyaNova.Util.ApplicationLogging.LoggerFactory));
}).AddMetrics();
#region Swagger
//https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studio-code
//https://swagger.io/
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore
_log.LogDebug("BOOT: init API explorer service");
services.AddSwaggerGen(
c =>
{
// resolve the IApiVersionDescriptionProvider service
// note: that we have to build a temporary service provider here because one has not been created yet
var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
// add a custom operation filter which sets default values
c.OperationFilter<SwaggerDefaultValues>();
// integrate xml comments
c.IncludeXmlComments(XmlCommentsFilePath);
//this is required to allow authentication when testing secure routes via swagger UI
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT Authorization header using the Bearer scheme. Get your token by logging in via the Auth route then enter it here with the \"Bearer \" prefix. Example: \"Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
c.AddSecurityRequirement(new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>
{
{ "Bearer", new string[] { } }
});
});
#endregion
#region JWT AUTHENTICATION
//get the key if specified
var secretKey = ServerBootConfig.AYANOVA_JWT_SECRET;
//If no key specified make a unique one based on license ID which is unique to each customer or trialler
if (string.IsNullOrWhiteSpace(secretKey))
{
// This ensures a key can't work on another AyaNova installation
if (AyaNova.Core.License.ActiveKey.TrialLicense)
secretKey = AyaNova.Core.License.ActiveKey.Id + "5G*QQJ8#bQ7$Xr_@sXfHq4";
else
secretKey = AyaNova.Core.License.ActiveKey.RegisteredTo + "5G*QQJ8#bQ7$Xr_@sXfHq4";
}
//If secretKey is less than 32 characters, pad it
if (secretKey.Length < 32)
{
secretKey = secretKey.PadRight(32, '-');
}
ServerBootConfig.AYANOVA_JWT_SECRET = secretKey;
var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET));
_log.LogDebug("BOOT: init Authorization service");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
// options.AutomaticAuthenticate = true;
// options.AutomaticChallenge = true;
options.TokenValidationParameters = new TokenValidationParameters
{
// Token signature will be verified using a private key.
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = "AyaNova",
ValidateAudience = false,
//ValidAudience = "http://localhost:7575/"
// Token will only be valid if not expired yet, with 5 minutes clock skew.
ValidateLifetime = true,
RequireExpirationTime = true,
ClockSkew = new TimeSpan(0, 5, 0),
};
});
#endregion
_log.LogDebug("BOOT: init Generator service");
services.AddSingleton<IHostedService, GeneratorService>();
}
////////////////////////////////////////////////////////////
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
//
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env,
AyContext dbContext, IApiVersionDescriptionProvider provider, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider)
{
_log.LogDebug("BOOT: configuring request pipeline...");
//Store a reference to the dependency injection service for static classes
ServiceProviderProvider.Provider = app.ApplicationServices;
//Enable ability to handle reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
});
#region SWAGGER
_log.LogDebug("BOOT: pipeline - api explorer");
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
app.UseSwaggerUI(c =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
//clean up the swagger explorer UI page and remove the branding
//via our own css
//NOTE: this broke when updated to v2.x of swagger and it can be fixed according to docs:
//https://github.com/domaindrivendev/Swashbuckle.AspNetCore#inject-custom-css
// c.InjectStylesheet("/api/sw.css");
c.DefaultModelsExpandDepth(-1);
c.DocumentTitle = "AyaNova API explorer";
c.RoutePrefix = "api-docs";
});
#endregion swagger
#region STATIC FILES
_log.LogDebug("BOOT: pipeline - static files");
app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = context =>
{
if (context.File.Name == "default.htm")
{
context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
context.Context.Response.Headers.Add("Expires", "-1");
}
}
});
#endregion
#region AUTH / ROLES
_log.LogDebug("BOOT: pipeline - authentication");
//Use authentication middleware
app.UseAuthentication();
//Custom middleware to get user roles and put them into the request so
//they can be authorized in routes
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
context.Request.HttpContext.Items["AY_ROLES"] = 0;
}
else
{
//Get user ID from claims
long userId = Convert.ToInt64(context.User.FindFirst(c => c.Type == "id").Value);
//Get the database context
var ct = context.RequestServices.GetService<AyContext>();
//get the user record
var u = ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, Id = m.Id }).First();
context.Request.HttpContext.Items["AY_ROLES"] = u.roles;
context.Request.HttpContext.Items["AY_USERNAME"] = u.name;
context.Request.HttpContext.Items["AY_USER_ID"] = u.Id;
}
await next.Invoke();
});
#endregion
//USE MVC
_log.LogDebug("BOOT: pipeline - MVC");
app.UseMvc();
if (ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE)
{
_log.LogWarning("BOOT: AYANOVA_PERMANENTLY_ERASE_DATABASE is true, dropping and recreating database");
Util.DbUtil.DropAndRecreateDb(_log);
AySchema.CheckAndUpdate(dbContext, _log);
}
//Check schema
_log.LogDebug("BOOT: db schema check");
AySchema.CheckAndUpdate(dbContext, _log);
//Check database integrity
_log.LogDebug("BOOT: db integrity check");
DbUtil.CheckFingerPrint(AySchema.EXPECTED_COLUMN_COUNT, AySchema.EXPECTED_INDEX_COUNT, _log);
//Initialize license
AyaNova.Core.License.Initialize(apiServerState, dbContext, _log);
//Ensure locales are present, not missing any keys and that there is a server default locale that exists
LocaleBiz lb = new LocaleBiz(dbContext, 1, AuthorizationRoles.OpsAdminFull);
lb.ValidateLocales();
#if (DEBUG)
// Util.DbUtil.DropAndRecreateDb(_log);
// AySchema.CheckAndUpdate(dbContext, _log);
// lb.ValidateLocales();
// AyaNova.Core.License.Initialize(apiServerState, dbContext, _log);
// AyaNova.Core.License.Fetch(apiServerState, dbContext, _log);
// Util.Seeder.SeedDatabase(dbContext, Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet);
#endif
//Open up the server for visitors
apiServerState.SetOpen();
//final startup log
_log.LogInformation("BOOT: COMPLETED - SERVER IS NOW OPEN");
}
#region Swagger and API Versioning utilities
static string XmlCommentsFilePath
{
get
{
//Obsolete, used new method: https://developers.de/blogs/holger_vetter/archive/2017/06/30/swagger-includexmlcomments-platformservices-obsolete-replacement.aspx
//var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var basePath = AppContext.BaseDirectory;
var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
return Path.Combine(basePath, fileName);
}
}
static Info CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new Info()
{
Title = $"AyaNova API {description.ApiVersion}",
Version = description.ApiVersion.ToString()
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
namespace AyaNova
{
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
/// <summary>
/// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter.
/// </summary>
/// <remarks>This <see cref="IOperationFilter"/> is only required due to bugs in the <see cref="SwaggerGenerator"/>.
/// Once they are fixed and published, this class can be removed.</remarks>
public class SwaggerDefaultValues : IOperationFilter
{
/// <summary>
/// Applies the filter to the specified operation using the given context.
/// </summary>
/// <param name="operation">The operation to apply the filter to.</param>
/// <param name="context">The current operation filter context.</param>
public void Apply( Operation operation, OperationFilterContext context )
{
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
foreach ( var parameter in operation.Parameters.OfType<NonBodyParameter>() )
{
var description = context.ApiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name );
var routeInfo = description.RouteInfo;
if ( parameter.Description == null )
{
parameter.Description = description.ModelMetadata?.Description;
}
if ( routeInfo == null )
{
continue;
}
if ( parameter.Default == null )
{
parameter.Default = routeInfo.DefaultValue;
}
parameter.Required |= !routeInfo.IsOptional;
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Trace",
"System": "Trace",
"Microsoft": "Trace"
}
}
}

View File

@@ -0,0 +1,17 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Trace"
}
},
"Console": {
"LogLevel": {
"Default": "Trace"
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace AyaNova.Biz
{
/// <summary>
/// Marker attribute indicating that an object supports attachments
/// Used in <see cref="AyaType"/>
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class AttachableAttribute : Attribute
{
//No code required, it's just a marker
//https://docs.microsoft.com/en-us/dotnet/standard/attributes/writing-custom-attributes
}
}//eons

View File

@@ -0,0 +1,56 @@
using System;
namespace AyaNova.Biz
{
/// <summary>
/// Authorization roles
/// </summary>
[Flags]
public enum AuthorizationRoles : int
{
//https://stackoverflow.com/questions/8447/what-does-the-flags-enum-attribute-mean-in-c
//MAX 32!!! or will overflow int and needs to be turned into a long
//Must be a power of two: https://en.wikipedia.org/wiki/Power_of_two
///<summary>No role set</summary>
NoRole = 0,
///<summary>BizAdminLimited</summary>
BizAdminLimited = 1,
///<summary>BizAdminFull</summary>
BizAdminFull = 2,
///<summary>DispatchLimited</summary>
DispatchLimited = 4,
///<summary>DispatchFull</summary>
DispatchFull = 8,
///<summary>InventoryLimited</summary>
InventoryLimited = 16,
///<summary>InventoryFull</summary>
InventoryFull = 32,
///<summary>AccountingFull</summary>
AccountingFull = 64,//No limited role, not sure if there is a need
///<summary>TechLimited</summary>
TechLimited = 128,
///<summary>TechFull</summary>
TechFull = 256,
///<summary>SubContractorLimited</summary>
SubContractorLimited = 512,
///<summary>SubContractorFull</summary>
SubContractorFull = 1024,
///<summary>ClientLimited</summary>
ClientLimited = 2048,
///<summary>ClientFull</summary>
ClientFull = 4096,
///<summary>OpsAdminLimited</summary>
OpsAdminLimited = 8192,
///<summary>OpsAdminFull</summary>
OpsAdminFull = 16384,
///<summary>Anyone of any role</summary>
AnyRole = BizAdminLimited | BizAdminFull | DispatchLimited | DispatchFull | InventoryLimited |
InventoryFull | AccountingFull | TechLimited | TechFull | SubContractorLimited |
SubContractorFull | ClientLimited | ClientFull | OpsAdminLimited | OpsAdminFull
}//end SecurityLevelTypes
}//end namespace GZTW.AyaNova.BLL

View File

@@ -0,0 +1,48 @@
using AyaNova.Models;
using AyaNova.Biz;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Reflection;
using System;
namespace AyaNova.Biz
{
/// <summary>
/// Returns owner Id if the object exists or 0 if exists but there is no owner ID property or -1 if the object doesn't exist
/// </summary>
internal static class AyaObjectOwnerId
{
internal static long Get(AyaTypeId o, AyContext ct)
{
if (o.IsEmpty) return -1;
//Get the type of the model of AyaObject
Type t = Type.GetType("AyaNova.Models." + o.ObjectType.ToString());
//Run a find query on the db context based on the model's type
object record = ct.Find(t, o.ObjectId);
if (record == null)
{
return -1;
}
PropertyInfo ownerIdPropertyInfo = record.GetType().GetProperty("OwnerId");
if (ownerIdPropertyInfo == null)
return 0;//object exists and it doesn't have an ownerID property
long ret = (long)ownerIdPropertyInfo.GetValue(record, null);
return ret;
}
}
}//eons

View File

@@ -0,0 +1,156 @@
namespace AyaNova.Biz
{
/// <summary>
/// All AyaNova types and their attributes indicating what features that type will support (tagging, attachments etc)
/// </summary>
public enum AyaType : int
{
NotValid = 0,
Global = 1,
[Attachable, Taggable]
Widget = 2,
[Attachable, Taggable]
User = 3,
ServerState = 4,
License = 5,
LogFile = 6,
Tag = 7,
TagMap = 8,
JobOperations = 9,
AyaNova7Import = 10,
TrialSeeder = 11,
Metrics = 12,
Locale = 13
}
}//eons
/*
///////////////////////////////////////////////////////////
// RootObjectTypes.cs
// Implementation of Class RootObjectTypes
// CSLA type: enumeration
// Created on: 07-Jun-2004 8:41:36 AM
// Object design: Joyce Class Incomplete
///////////////////////////////////////////////////////////
using System.ComponentModel;
namespace GZTW.AyaNova.BLL
{
/// <summary>
/// RootObject types.
/// Note that some items here are not strictly root
/// objects, but are included because they need to be identified
/// for other purposes such as indexed keywords etc.
/// </summary>
public enum RootObjectTypes : int {
[Description("LT:O.Nothing")] Nothing = 0,
[Description("LT:O.Global")] Global = 1,
[Description("LT:O.Region")] Region = 2,
[Description("LT:O.Client")] Client = 3,
[Description("LT:O.Vendor")] Vendor = 4,
[Description("LT:O.HeadOffice")] HeadOffice = 5,
[Description("LT:O.RentalUnit")] RentalUnit = 6,
[Description("LT:O.Unit")] Unit = 7,
[Description("LT:O.UnitModel")] UnitModel = 8,
[Description("LT:O.Workorder")] Workorder = 9,
[Description("LT:O.WorkorderItem")] WorkorderItem = 10,
[Description("LT:O.UserSkillAssigned")] UserSkillAssigned = 11,
[Description("LT:O.UserCertificationAssigned")] UserCertificationAssigned = 12,
[Description("LT:O.User")] User = 13,
[Description("LT:O.Part")] Part = 14,
[Description("LT:O.LoanItem")] LoanItem = 15,
[Description("LT:O.DispatchZone")] DispatchZone = 16,
[Description("LT:O.Rate")] Rate = 17,
[Description("LT:O.Contract")] Contract = 18,
[Description("LT:O.Project")] Project = 19,
[Description("LT:O.PurchaseOrder")] PurchaseOrder = 20,
[Description("LT:O.ClientGroup")] ClientGroup = 21,
[Description("LT:O.WorkorderCategory")] WorkorderCategory = 22,
[Description("LT:O.WorkorderItemScheduledUser")] WorkorderItemScheduledUser = 23,
[Description("LT:O.WorkorderItemOutsideService")] WorkorderItemOutsideService = 24,
[Description("LT:O.WorkorderItemPart")] WorkorderItemPart = 25,
[Description("LT:O.WorkorderItemLabor")] WorkorderItemLabor = 26,
[Description("LT:O.WorkorderItemTravel")] WorkorderItemTravel = 27,
[Description("LT:O.WorkorderItemMiscExpense")] WorkorderItemMiscExpense = 28,
[Description("LT:O.WorkorderItemPartRequest")] WorkorderItemPartRequest = 29,
[Description("LT:O.WorkorderItemLoan")] WorkorderItemLoan = 30,
[Description("LT:O.ClientNote")] ClientNote = 31,
[Description("LT:O.ServiceBank")] ServiceBank = 32,
[Description("LT:O.WorkorderQuote")] WorkorderQuote = 33,
[Description("LT:O.WorkorderService")] WorkorderService = 34,
[Description("LT:O.AssignedDoc")] AssignedDocument = 35,
[Description("LT:O.PartWarehouse")] PartWarehouse = 36,
[Description("LT:O.UnitMeterReading")] UnitMeterReading = 37,
[Description("LT:O.UnitModelCategory")] UnitModelCategory = 38,
[Description("LT:O.Locale")] Locale = 39,
[Description("LT:O.SearchResult")] SearchResult = 40,
[Description("LT:O.WorkorderItemType")] WorkorderItemType = 41,
[Description("LT:O.UnitServiceType")] UnitServiceType = 42,
[Description("LT:O.PartAssembly")] PartAssembly = 43,
[Description("LT:O.AyaFile")] AyaFile = 44,//case 73
[Description("LT:O.Contact")] Contact = 45,
[Description("LT:O.ContactPhone")] ContactPhone = 46,
[Description("LT:O.WorkorderPreventiveMaintenance")] WorkorderPreventiveMaintenance = 47,
[Description("LT:O.TaskGroup")] TaskGroup = 48,
[Description("LT:O.ScheduleMarker")] ScheduleMarker = 49,
[Description("LT:O.ClientServiceRequest")] ClientServiceRequest = 50,
[Description("LT:O.ScheduleableUserGroup")] ScheduleableUserGroup = 51,
[Description("LT:O.Task")] Task = 52,
[Description("LT:O.Memo")] Memo = 53,
[Description("LT:O.PartCategory")] PartCategory=54,
[Description("LT:O.UnitOfMeasure")] UnitOfMeasure=55,
[Description("LT:O.TaxCode")] TaxCode=56,
[Description("LT:O.PartSerial")] PartSerial = 57,
[Description("LT:O.PartInventoryAdjustment")] PartInventoryAdjustment = 58,
[Description("LT:O.PartInventoryAdjustmentItem")] PartInventoryAdjustmentItem = 59,
[Description("LT:O.Priority")] Priority=60,
[Description("LT:O.UserSkill")] UserSkill=61,
[Description("LT:O.WorkorderStatus")] WorkorderStatus=62,
[Description("LT:O.UserCertification")] UserCertification=63,
[Description("LT:O.ClientNoteType")] ClientNoteType=64,
[Description("LT:O.SecurityGroup")] SecurityGroup=65,
[Description("LT:O.PurchaseOrderReceiptItem")] PurchaseOrderReceiptItem=66,
[Description("LT:O.PartByWarehouseInventory")] PartByWarehouseInventory=67,
[Description("LT:O.Report")] Report=68,
[Description("LT:O.WorkorderQuoteTemplate")]
WorkorderQuoteTemplate = 69,
[Description("LT:O.WorkorderServiceTemplate")]
WorkorderServiceTemplate = 70,
[Description("LT:O.WorkorderPreventiveMaintenanceTemplate")]
WorkorderPreventiveMaintenanceTemplate = 71,
[Description("LT:O.WikiPage")]//case 73
WikiPage = 72,
[Description("LT:O.GridFilter")]//case 941
GridFilter = 73,
[Description("LT:O.NotifySubscription")]//case 941
NotifySubscription = 74,
[Description("LT:O.PurchaseOrderReceipt")]//case 941
PurchaseOrderReceipt = 75,
[Description("LT:O.Notification")]//case 1172
Notification = 76,
[Description("LT:UI.Go.Schedule")]//case 812
Schedule = 77,
[Description("LT:O.WorkorderItemTask")]//case 1317
WorkorderItemTask = 78,
[Description("LT:O.WorkorderItemUnit")]//case 1317
WorkorderItemUnit = 79,
[Description("LT:ScheduleMarker.Label.FollowUp")]//case 1975
FollowUp = 80
}//end RootObjectTypes
}//end namespace GZTW.AyaNova.BLL
*/

View File

@@ -0,0 +1,103 @@
using System;
using AyaNova.Models;
namespace AyaNova.Biz
{
public class AyaTypeId : System.Object
{
private long _id;
private AyaType _ayaType;
public long ObjectId
{
get
{
return _id;
}
}
public AyaType ObjectType
{
get
{
return _ayaType;
}
}
public int ObjectTypeAsInt
{
get
{
return (int)_ayaType;
}
}
public AyaTypeId(AyaType ObjectType, long Id)
{
_id = Id;
_ayaType = ObjectType;
}
public AyaTypeId(string sObjectTypeNumeral, string sId)
{
_id = long.Parse(sId);
int nType = int.Parse(sObjectTypeNumeral);
if (!AyaTypeExists(nType))
_ayaType = AyaType.NotValid;
else
_ayaType = (AyaType)Enum.Parse(typeof(AyaType), sObjectTypeNumeral);
}
public bool IsEmpty
{
get
{
return (_ayaType == AyaType.NotValid) || (_id == 0);
}
}
/// <summary>
/// Get the ownerId for the object in question
/// </summary>
/// <param name="ct">db context</param>
/// <returns>0 if object doesn't have an owner Id, the owner Id or -1 if the object doesn't exist in the db</returns>
public long OwnerId(AyContext ct)
{
return AyaObjectOwnerId.Get(this, ct);
}
/// <summary>
/// Check if the numeric or name type value is an actual enum value
/// </summary>
/// <param name="enumNumber"></param>
/// <returns></returns>
public bool AyaTypeExists(int enumNumber)
{
return Enum.IsDefined(typeof(AyaType), enumNumber);
}
//Custom attribute checking
/// <summary>
/// Is object attachable
/// </summary>
/// <returns></returns>
public bool IsAttachable
{
get
{
return this.ObjectType.HasAttribute(typeof(AttachableAttribute));
}
}
}
}//eons

View File

@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using AyaNova.Biz;
namespace AyaNova.Biz
{
/// <summary>
/// Business object base class
/// </summary>
internal abstract class BizObject : IBizObject
{
public BizObject()
{
}
#region Roles
public AuthorizationRoles CurrentUserRoles { get; set; }
#endregion roles
#region Error handling
private readonly List<ValidationError> _errors = new List<ValidationError>();
public List<ValidationError> Errors => _errors;
public bool HasErrors => _errors.Any();
// public void AddError(string errorMessage, string field="")
// {
// _errors.Add(new ValidationError() { Message = errorMessage, Target = field });
// }
public void AddvalidationError(ValidationError validationError)
{
_errors.Add(validationError);
}
public bool PropertyHasErrors(string propertyName)
{
if (_errors.Count == 0) return false;
var v = _errors.FirstOrDefault(m => m.Target == propertyName);
return (v != null);
}
public void AddError(ValidationErrorType errorType, string propertyName = null, string errorMessage = null)
{
_errors.Add(new ValidationError() { ErrorType = errorType, Message = errorMessage, Target = propertyName });
}
public string GetErrorsAsString()
{
if (!HasErrors) return string.Empty;
StringBuilder sb = new StringBuilder();
sb.AppendLine("Validation errors:");
foreach (ValidationError e in _errors)
{
var msg=e.Message;
if(string.IsNullOrWhiteSpace(msg)){
msg=e.ErrorType.ToString();
}
sb.AppendLine($"Target: {e.Target} error: {msg}");
}
return sb.ToString();
}
#endregion error handling
}//eoc
}//eons

View File

@@ -0,0 +1,55 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
internal static class BizObjectFactory
{
//Returns the biz object class that corresponds to the type presented
internal static BizObject GetBizObject(AyaType aytype, AyContext dbcontext, long userId = 1, AuthorizationRoles roles = AuthorizationRoles.AnyRole)
{
switch (aytype)
{
case AyaType.Widget:
return new WidgetBiz(dbcontext, userId, roles);
case AyaType.Tag:
return new TagBiz(dbcontext, userId, roles);
case AyaType.TagMap:
return new TagMapBiz(dbcontext, userId, roles);
case AyaType.JobOperations:
return new JobOperationsBiz(dbcontext, userId, roles);
case AyaType.AyaNova7Import:
return new ImportAyaNova7Biz(dbcontext, userId, roles);
case AyaType.TrialSeeder:
return new TrialBiz(dbcontext, userId, roles);
case AyaType.Locale:
return new LocaleBiz(dbcontext, userId, roles);
default:
throw new System.NotSupportedException($"AyaNova.BLL.BizObjectFactory::GetBizObject type {aytype.ToString()} is not supported");
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,15 @@
namespace AyaNova.Biz
{
/// <summary>
/// This is a set of roles to be stored in the central BizRoles with a key for each object type
/// </summary>
public class BizRoleSet
{
public AuthorizationRoles Change { get; set; }
public AuthorizationRoles EditOwn { get; set; }
public AuthorizationRoles Read { get; set; }
}//eoc
}//eons

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
namespace AyaNova.Biz
{
/// <summary>
/// roles of all business objects
/// </summary>
internal static class BizRoles
{
static Dictionary<AyaType, BizRoleSet> roles = new Dictionary<AyaType, BizRoleSet>();
static BizRoles()
{
//Add all object roles here
//NOTE: do not need to add change roles to read roles, Authorized.cs takes care of that automatically
//by assuming if you can change you can read
#region All roles initialization
////////////////////////////////////////////////////////////
//WIDGET
//
roles.Add(AyaType.Widget, new BizRoleSet()
{
Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.InventoryFull,
EditOwn = AuthorizationRoles.TechFull,
Read = AuthorizationRoles.BizAdminLimited | AuthorizationRoles.InventoryLimited |
AuthorizationRoles.TechFull | AuthorizationRoles.TechLimited | AuthorizationRoles.AccountingFull
});
////////////////////////////////////////////////////////////
//SERVERSTATE
//
roles.Add(AyaType.ServerState, new BizRoleSet()
{
Change = AuthorizationRoles.OpsAdminFull,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
//LICENSE
//
roles.Add(AyaType.License, new BizRoleSet()
{
Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminFull,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.BizAdminLimited | AuthorizationRoles.OpsAdminLimited
});
////////////////////////////////////////////////////////////
//LOGFILE
//
roles.Add(AyaType.LogFile, new BizRoleSet()
{
Change = AuthorizationRoles.NoRole,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited
});
////////////////////////////////////////////////////////////
//TAG
//Full roles can make new tags and can edit or delete existing tags
roles.Add(AyaType.Tag, new BizRoleSet()
{
Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull | AuthorizationRoles.TechFull | AuthorizationRoles.AccountingFull,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
//TAGMAP
//Any roles can tag objects and remove tags as per their rights to the taggable object type in question
roles.Add(AyaType.TagMap, new BizRoleSet()
{
Change = AuthorizationRoles.AnyRole,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
//OPERATIONS
//Only opsfull can change operations
//ops and biz admin can view operations
roles.Add(AyaType.JobOperations, new BizRoleSet()
{
Change = AuthorizationRoles.OpsAdminFull,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.OpsAdminLimited | AuthorizationRoles.BizAdminFull | AuthorizationRoles.BizAdminLimited
});
////////////////////////////////////////////////////////////
//AyaNova7Import
//Only opsfull can change operations
//opsfull can view operations
roles.Add(AyaType.AyaNova7Import, new BizRoleSet()
{
Change = AuthorizationRoles.OpsAdminFull,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.OpsAdminFull
});
////////////////////////////////////////////////////////////
//METRICS
//
roles.Add(AyaType.Metrics, new BizRoleSet()
{
Change = AuthorizationRoles.NoRole,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.OpsAdminFull | AuthorizationRoles.OpsAdminLimited
});
////////////////////////////////////////////////////////////
//LOCALE
//
roles.Add(AyaType.Locale, new BizRoleSet()
{
Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminFull,
EditOwn = AuthorizationRoles.NoRole,
Read = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////////////
#endregion all roles init
}//end of constructor
/// <summary>
/// Get roleset for biz object
/// </summary>
/// <param name="forType"></param>
/// <returns></returns>
internal static BizRoleSet GetRoleSet(AyaType forType)
{
if (roles.ContainsKey(forType))
{
return roles[forType];
}
else
{
return null;
}
}
}//end of class
}//eons

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
/// <summary>
///
/// </summary>
internal interface IBizObject
{
//validate via validation attributes
//https://stackoverflow.com/questions/36330981/is-the-validationresult-class-suitable-when-validating-the-state-of-an-object
/// <summary>
/// Roles of current user
/// </summary>
/// <returns></returns>
AuthorizationRoles CurrentUserRoles { get; set; }
/// <summary>
/// Contains list of errors
/// </summary>
List<ValidationError> Errors { get; }
/// <summary>
/// Is true if there are errors
/// </summary>
bool HasErrors { get; }
/// <summary>
/// Is true if the field specified exists in the list
/// </summary>
bool PropertyHasErrors(string propertyName);
/// <summary>
///
/// </summary>
/// <param name="errorType"></param>
/// <param name="errorMessage"></param>
/// <param name="propertyName"></param>
void AddError(ValidationErrorType errorType, string propertyName=null, string errorMessage=null);
/// <summary>
///
/// </summary>
/// <param name="validationError"></param>
void AddvalidationError(ValidationError validationError);
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
namespace AyaNova.Biz
{
/// <summary>
/// Interface for biz objects that support importing AyaNova 7 data
/// </summary>
internal interface IImportAyaNova7Object
{
/// <summary>
/// Import from the JSON data provided
/// </summary>
/// <param name="v7ImportData">Json object containing source record</param>
/// <param name="importMap">A collection that can be used to match import records to new records, NOT persistent between imports</param>
/// <param name="JobId">JobId for logging or controlling jobs from within processor</param>
/// <returns>True if imported, False if not imported due to invalid or other error (logged in job log)</returns>
Task<bool> ImportV7Async(JObject v7ImportData, List<ImportAyaNova7MapItem> importMap, Guid JobId);
}
}

View File

@@ -0,0 +1,22 @@
using AyaNova.Models;
namespace AyaNova.Biz
{
/// <summary>
/// Interface for biz objects that support jobs / long running operations
/// </summary>
internal interface IJobObject
{
/// <summary>
/// Start and process an operation
/// NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so
/// basically any error condition during job processing should throw up an exception if it can't be handled
/// </summary>
/// <param name="job"></param>
System.Threading.Tasks.Task HandleJobAsync(OpsJob job);
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
internal class ImportAyaNova7Biz : BizObject, IJobObject
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
internal ImportAyaNova7Biz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
//Hand off the particular job to the corresponding processing code
//NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so
//basically any error condition during job processing should throw up an exception if it can't be handled
//There might be future other job types so doing it like this for all biz job handlers for now
switch (job.JobType)
{
case JobType.ImportV7Data:
await ProcessImportV7JobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"ImportAyaNovaBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
/// <summary>
/// /// Handle the test job
/// </summary>
/// <param name="job"></param>
private async Task ProcessImportV7JobAsync(OpsJob job)
{
//NOTE: If this code throws an exception the caller will automatically set the job to failed and log the exeption so
//basically any error condition during job processing should throw up an exception if it can't be handled
List<ImportAyaNova7MapItem> importMap = new List<ImportAyaNova7MapItem>();
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct);
JobsBiz.LogJob(job.GId, $"ImportAyaNova7 starting", ct);
//Get the import filename from the jsondata
JObject jobData = JObject.Parse(job.JobInfo);
var importFileName = jobData["ImportFileName"].Value<string>();
if (string.IsNullOrWhiteSpace(importFileName))
{
throw new System.ArgumentNullException("ImportAyaNova7 job failed due to no import filename being specified");
}
if (!FileUtil.UtilityFileExists(importFileName))
{
throw new System.ArgumentNullException("ImportAyaNova7 job failed due to import file specified not existing");
}
//get the contents of the archive
List<string> zipEntries = FileUtil.ZipGetUtilityFileEntries(importFileName);
//Iterate through the import items in the preferred order, checking for corresponding entries in the zip file
//In turn try to instantiate the type and id job that can handle that import, attempt to case or see if implements IImportAyaNova7Object
//, if null / not supported for import then skip and log as not currently supported
//Pass off the JSON data from the import file into the import job item by item
//IMPORT REGIONS AS TAGS
await DoImport("GZTW.AyaNova.BLL.Region", AyaType.Tag, job.GId, importMap, importFileName, zipEntries);
//IMPORT LOCALES
await DoImport("GZTW.AyaNova.BLL.Locale", AyaType.Locale, job.GId, importMap, importFileName, zipEntries);
JobsBiz.LogJob(job.GId, "ImportAyaNova7 finished", ct);
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);
}
/// <summary>
/// This method does the actual import by
/// - Fetching the list of entries in the zip archive that match the passed in startsWtih (folder name in zip archive)
/// - Instantiating the corresponding new biz object type to handle the import
/// - Passing the json parsed to the biz object one at a time to do the import
/// </summary>
/// <param name="entryStartsWith"></param>
/// <param name="importerType"></param>
/// <param name="jobId"></param>
/// <param name="importMap"></param>
/// <param name="importFileName"></param>
/// <param name="zipEntries"></param>
/// <returns></returns>
private async Task DoImport(string entryStartsWith, AyaType importerType, Guid jobId, List<ImportAyaNova7MapItem> importMap, string importFileName, List<string> zipEntries)
{
var zipObjectList = zipEntries.Where(m => m.StartsWith(entryStartsWith)).ToList();
long importCount = 0;
long notImportCount = 0;
if (zipObjectList.Count > 0)
{
JobsBiz.LogJob(jobId, $"Starting import of {entryStartsWith} objects", ct);
var jList = FileUtil.ZipGetUtilityArchiveEntriesAsJsonObjects(zipObjectList, importFileName);
IImportAyaNova7Object o = (IImportAyaNova7Object)BizObjectFactory.GetBizObject(importerType, ct);
foreach (JObject j in jList)
{
bool bImportSucceeded = false;
//some new types can import multiple old types and it might matter which is which to the importer
//so tag it with the original type
j.Add("V7_TYPE", JToken.FromObject(entryStartsWith));
bImportSucceeded = await o.ImportV7Async(j, importMap, jobId);
if (bImportSucceeded)
importCount++;
else
notImportCount++;
}
if (importCount > 0)
{
JobsBiz.LogJob(jobId, $"Successfully imported {importCount.ToString()} of {zipObjectList.Count.ToString()} {entryStartsWith} objects", ct);
}
if (notImportCount > 0)
{
JobsBiz.LogJob(jobId, $"Did not import {notImportCount.ToString()} of {zipObjectList.Count.ToString()} {entryStartsWith} objects", ct);
}
}
}
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,109 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
internal class JobOperationsBiz : BizObject
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
internal JobOperationsBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
#region CONTROLLER ROUTES SUPPORT
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get list of jobs
internal async Task<List<JobOperationsFetchInfo>> GetJobListAsync()
{
List<JobOperationsFetchInfo> ret = new List<JobOperationsFetchInfo>();
var jobitems = await ct.OpsJob
.OrderBy(m => m.Created)
.ToListAsync();
foreach (OpsJob i in jobitems)
{
//fetch the most recent log time for each job
var mostRecentLogItem = await ct.OpsJobLog
.Where(c => c.JobId == i.GId)
.OrderByDescending(t => t.Created)
.FirstOrDefaultAsync();
JobOperationsFetchInfo o = new JobOperationsFetchInfo();
if (mostRecentLogItem != null)
o.LastAction = mostRecentLogItem.Created;
else
o.LastAction = i.Created;
o.Created = i.Created;
o.GId = i.GId;
o.JobStatus = i.JobStatus.ToString();
o.Name = i.Name;
ret.Add(o);
}
return ret;
}
//Get list of logs for job
internal async Task<List<JobOperationsLogInfoItem>> GetJobLogListAsync(Guid jobId)
{
List<JobOperationsLogInfoItem> ret = new List<JobOperationsLogInfoItem>();
var l = await ct.OpsJobLog
.Where(c => c.JobId == jobId)
.OrderBy(m => m.Created)
.ToListAsync();
foreach (OpsJobLog i in l)
{
JobOperationsLogInfoItem o = new JobOperationsLogInfoItem();
o.Created = i.Created;
o.StatusText = i.StatusText;
ret.Add(o);
}
return ret;
}
#endregion controller routes
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,17 @@
namespace AyaNova.Biz
{
/// <summary>
/// Job status for opsjobs
/// </summary>
public enum JobStatus : int
{
Sleeping = 1,
Running = 2,
Completed = 3,
Failed = 4
}
}//eons

View File

@@ -0,0 +1,20 @@
namespace AyaNova.Biz
{
/// <summary>
/// All AyaNova Job types, used by OpsJob and biz objects for long running applications
/// </summary>
public enum JobType : int
{
NotSet = 0,
TestWidgetJob = 1,//test job for unit testing
CoreJobSweeper = 2,
ImportV7Data = 3,
SeedTestData = 4,
}
}//eons

View File

@@ -0,0 +1,382 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using AyaNova.Util;
namespace AyaNova.Biz
{
internal static class JobsBiz
{
private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("JobsBiz");
#region JOB OPS
/// <summary>
/// Get a non tracked list of jobs for an object
/// </summary>
/// <param name="ayObj"></param>
/// <param name="ct"></param>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetJobsForObjectAsync(AyaTypeId ayObj, AyContext ct)
{
return await ct.OpsJob
.AsNoTracking()
.Where(c => c.ObjectId == ayObj.ObjectId && c.ObjectType == ayObj.ObjectType)
.OrderBy(m => m.Created)
.ToListAsync();
}
/// <summary>
/// Get a non tracked list of jobs that are ready to process and exclusive only
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetReadyJobsExclusiveOnlyAsync(AyContext ct)
{
return await GetReadyJobsAsync(true, ct);
}
/// <summary>
/// Get a non tracked list of jobs that are ready to process and exclusive only
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetReadyJobsNotExlusiveOnlyAsync(AyContext ct)
{
return await GetReadyJobsAsync(false, ct);
}
/// <summary>
/// Get a non tracked list of jobs filtered by exclusivity
/// </summary>
/// <returns></returns>
private static async Task<List<OpsJob>> GetReadyJobsAsync(bool exclusiveOnly, AyContext ct)
{
var ret = await ct.OpsJob
.AsNoTracking()
.Where(c => c.StartAfter < System.DateTime.UtcNow && c.Exclusive == exclusiveOnly && c.JobStatus == JobStatus.Sleeping)
.OrderBy(m => m.Created)
.ToListAsync();
return ret;
}
/// <summary>
/// Get a non tracked list of all jobs that are not completed
/// could be running or sleeping
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetAllSleepingOrRunningJobsAsync(AyContext ct)
{
var ret = await ct.OpsJob
.AsNoTracking()
.Where(c => c.JobStatus == JobStatus.Sleeping || c.JobStatus == JobStatus.Running)
.OrderBy(m => m.Created)
.ToListAsync();
return ret;
}
/// <summary>
/// Get a non tracked list of all jobs for a JobType
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetAllJobsForJobTypeAsync(AyContext ct, JobType jobType)
{
var ret = await ct.OpsJob
.AsNoTracking()
.Where(c => c.JobType == jobType)
.OrderBy(m => m.Created)
.ToListAsync();
return ret;
}
/// <summary>
/// Get a non tracked list of all jobs that are status running but have no last activity for XX HOURS
/// </summary>
/// <returns></returns>
internal static async Task<List<OpsJob>> GetPotentiallyDeadRunningJobsAsync(AyContext ct)
{
var ret = await ct.OpsJob
.AsNoTracking()
.Where(c => c.JobStatus == JobStatus.Sleeping || c.JobStatus == JobStatus.Running)
.OrderBy(m => m.Created)
.ToListAsync();
return ret;
}
/// <summary>
/// Get a count of all jobs for a JobStatus
/// </summary>
/// <returns></returns>
internal static async Task<long> GetCountForJobStatusAsync(AyContext ct, JobStatus jobStatus)
{
var ret = await ct.OpsJob
.Where(c => c.JobStatus == jobStatus)
.LongCountAsync();
return ret;
}
/// <summary>
/// Add a new job to the database
/// </summary>
/// <param name="newJob"></param>
/// <param name="ct"></param>
/// <returns></returns>
internal static OpsJob AddJob(OpsJob newJob, AyContext ct)
{
ct.OpsJob.Add(newJob);
ct.SaveChanges();
return newJob;
}
/// <summary>
/// Remove any jobs or logs for the object in question
/// </summary>
/// <param name="ayObj"></param>
/// <param name="ct"></param>
internal static async Task DeleteJobsForObjectAsync(AyaTypeId ayObj, AyContext ct)
{
//Get a list of all jobid's for the object passed in
List<OpsJob> jobsForObject = GetJobsForObjectAsync(ayObj, ct).Result;
//short circuit
if (jobsForObject.Count == 0)
return;
using (var transaction = ct.Database.BeginTransaction())
{
try
{
foreach (OpsJob jobToBeDeleted in jobsForObject)
{
await removeJobAndLogsAsync(ct, jobToBeDeleted.GId);
}
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception ex)
{
throw ex;
}
}
}
/// <summary>
/// Remove job and logs for that job
/// </summary>
/// <param name="jobId"></param>
/// <param name="ct"></param>
internal static async Task DeleteJobAndLogAsync(Guid jobId, AyContext ct)
{
using (var transaction = ct.Database.BeginTransaction())
{
try
{
await removeJobAndLogsAsync(ct, jobId);
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception ex)
{
throw ex;
}
}
}
/// <summary>
/// REmove the job and it's logs
/// </summary>
/// <param name="ct"></param>
/// <param name="jobIdToBeDeleted"></param>
private static async Task removeJobAndLogsAsync(AyContext ct, Guid jobIdToBeDeleted)
{
//delete logs
await ct.Database.ExecuteSqlCommandAsync("delete from aopsjoblog where jobid = {0}", new object[] { jobIdToBeDeleted });
//delete the job
await ct.Database.ExecuteSqlCommandAsync("delete from aopsjob where gid = {0}", new object[] { jobIdToBeDeleted });
}
/// <summary>
/// Make a log entry for a job
/// </summary>
/// <param name="jobId"></param>
/// <param name="statusText"></param>
/// <param name="ct"></param>
internal static OpsJobLog LogJob(Guid jobId, string statusText, AyContext ct)
{
if (string.IsNullOrWhiteSpace(statusText))
statusText = "No status provided";
OpsJobLog newObj = new OpsJobLog();
newObj.JobId = jobId;
newObj.StatusText = statusText;
ct.OpsJobLog.Add(newObj);
ct.SaveChanges();
return newObj;
}
/// <summary>
/// Update the status of a job
/// </summary>
/// <param name="jobId"></param>
/// <param name="newStatus"></param>
/// <param name="ct"></param>
internal static OpsJob UpdateJobStatus(Guid jobId, JobStatus newStatus, AyContext ct)
{
var oFromDb = ct.OpsJob.SingleOrDefault(m => m.GId == jobId);
if (oFromDb == null) return null;
oFromDb.JobStatus = newStatus;
ct.SaveChanges();
return oFromDb;
}
#endregion Job ops
#region PROCESSOR
/// <summary>
/// Process all jobs (stock jobs and those found in operations table)
/// </summary>
/// <returns></returns>
internal static async Task ProcessJobsAsync(AyContext ct)
{
//Flush metrics report before anything else happens
log.LogTrace("Flushing metrics to reporters");
await CoreJobMetricsReport.DoJobAsync();
//BIZOBJECT DYNAMIC JOBS
//get a list of exclusive jobs that are due to happen
//Call into each item in turn
List<OpsJob> exclusiveJobs = await GetReadyJobsExclusiveOnlyAsync(ct);
foreach (OpsJob j in exclusiveJobs)
{
try
{
await ProcessJobAsync(j, ct);
}
catch (Exception ex)
{
log.LogError(ex, $"ProcessJobs::Exclusive -> job {j.Name} failed with exception");
LogJob(j.GId, "Job failed with errors:", ct);
LogJob(j.GId, ExceptionUtil.ExtractAllExceptionMessages(ex), ct);
UpdateJobStatus(j.GId, JobStatus.Failed, ct);
}
}
//Get a list of non-exlusive jobs that are due
//TODO: Parallelize / background this block
//http://www.dotnetcurry.com/dotnet/1360/concurrent-programming-dotnet-core
//var backgroundTask = Task.Run(() => DoComplexCalculation(42));
//also have to deal with db object etc, I guess they'd have to instantiate themselves to avoid disposed object being used error
//This area may turn out to need a re-write in future, but I think it might only involve this block and ProcessJobAsync
//the actual individual objects that are responsible for jobs will likely not need a signature rewrite or anything (I hope)
//For now I'm hoping that no job will be so slow that it can hold up all the other jobs indefinitely.
List<OpsJob> sharedJobs = await GetReadyJobsNotExlusiveOnlyAsync(ct);
foreach (OpsJob j in sharedJobs)
{
try
{
await ProcessJobAsync(j, ct);
}
catch (Exception ex)
{
log.LogError(ex, $"ProcessJobs::Shared -> job {j.Name} failed with exception");
LogJob(j.GId, "Job failed with errors:", ct);
LogJob(j.GId, ExceptionUtil.ExtractAllExceptionMessages(ex), ct);
UpdateJobStatus(j.GId, JobStatus.Failed, ct);
}
}
//STOCK JOBS
//Sweep jobs table
await CoreJobSweeper.DoSweepAsync(ct);
//Health check / metrics
await CoreJobMetricsSnapshot.DoJobAsync(ct);
//License check??
//Notifications
}
/// <summary>
/// Process a job by calling into it's biz object
/// </summary>
/// <param name="job"></param>
/// <param name="ct"></param>
/// <returns></returns>
internal static async Task ProcessJobAsync(OpsJob job, AyContext ct)
{
log.LogDebug($"ProcessJobAsync -> Processing job {job.Name} (type {job.JobType.ToString()})");
IJobObject o = null;
switch (job.JobType)
{
case JobType.TestWidgetJob:
o = (IJobObject)BizObjectFactory.GetBizObject(AyaType.Widget, ct);
break;
case JobType.ImportV7Data:
o = (IJobObject)BizObjectFactory.GetBizObject(AyaType.AyaNova7Import, ct);
break;
case JobType.SeedTestData:
o = (IJobObject)BizObjectFactory.GetBizObject(AyaType.TrialSeeder, ct);
break;
default:
throw new System.NotSupportedException($"ProcessJobAsync type {job.JobType.ToString()} is not supported");
}
await o.HandleJobAsync(job);
log.LogDebug($"ProcessJobAsync -> Job completed {job.Name} (type {job.JobType.ToString()})");
}
#endregion process jobs
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,456 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
namespace AyaNova.Biz
{
internal class LocaleBiz : BizObject, IImportAyaNova7Object
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
internal LocaleBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DUPLICATE - only way to create a new locale
//
internal async Task<Locale> DuplicateAsync(NameIdItem inObj)
{
//make sure sourceid exists
if (!LocaleExists(inObj.Id))
AddError(ValidationErrorType.InvalidValue, "Id", "Source locale id does not exist");
//Ensure name is unique and not too long and not empty
Validate(inObj.Name, true);
if (HasErrors)
return null;
//fetch the existing locale for duplication
var SourceLocale = await ct.Locale.Include(x => x.LocaleItems).SingleOrDefaultAsync(m => m.Id == inObj.Id);
//replicate the source to a new dest and save
Locale NewLocale = new Locale();
NewLocale.Name = inObj.Name;
NewLocale.OwnerId = this.userId;
NewLocale.Stock = false;
foreach (LocaleItem i in SourceLocale.LocaleItems)
{
NewLocale.LocaleItems.Add(new LocaleItem() { Key = i.Key, Display = i.Display });
}
//Add it to the context so the controller can save it
ct.Locale.Add(NewLocale);
return NewLocale;
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get entire locale
internal async Task<Locale> GetAsync(long fetchId)
{
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.Locale.Include(x => x.LocaleItems).SingleOrDefaultAsync(m => m.Id == fetchId);
}
//get picklist (simple non-paged)
internal async Task<List<NameIdItem>> GetPickListAsync()
{
List<NameIdItem> l = new List<NameIdItem>();
l = await ct.Locale
.OrderBy(m => m.Name)
.Select(m => new NameIdItem()
{
Id = m.Id,
Name = m.Name
}).ToListAsync();
return l;
}
//Get the keys for a list of keys provided
internal async Task<List<KeyValuePair<string, string>>> GetSubset(AyaNova.Api.Controllers.LocaleController.LocaleSubsetParam param)
{
var ret = await ct.LocaleItem.Where(x => x.LocaleId == param.LocaleId && param.Keys.Contains(x.Key)).ToDictionaryAsync(x => x.Key, x => x.Display);
return ret.ToList();
}
/// <summary>
/// Get the value of the key provided in the default locale chosen
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
internal static async Task<string> GetDefaultLocalizedText(string key)
{
if (string.IsNullOrWhiteSpace(key))
return "ERROR: GetDefaultLocalizedText NO KEY VALUE SPECIFIED";
AyContext ct = ServiceProviderProvider.DBContext;
return await ct.LocaleItem.Where(m => m.LocaleId == ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID && m.Key == key).Select(m => m.Display).FirstOrDefaultAsync();
}
//Get all stock keys that are valid (used for import)
internal static List<string> GetKeyList()
{
AyContext ct = ServiceProviderProvider.DBContext;
return ct.LocaleItem.Where(m => m.LocaleId == 1).OrderBy(m => m.Key).Select(m => m.Key).ToList();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal bool PutLocaleItemDisplayText(LocaleItem dbObj, NewTextIdConcurrencyTokenItem inObj, Locale dbParent)
{
if (dbParent.Stock == true)
{
AddError(ValidationErrorType.InvalidOperation, "object", "LocaleItem is from a Stock locale and cannot be modified");
return false;
}
//Replace the db object with the PUT object
//CopyObject.Copy(inObj, dbObj, "Id");
dbObj.Display = inObj.NewText;
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
//Only thing to validate is if it has data at all in it
if (string.IsNullOrWhiteSpace(inObj.NewText))
AddError(ValidationErrorType.RequiredPropertyEmpty, "Display (NewText)");
if (HasErrors)
return false;
return true;
}
internal bool PutLocaleName(Locale dbObj, NewTextIdConcurrencyTokenItem inObj)
{
if (dbObj.Stock == true)
{
AddError(ValidationErrorType.InvalidOperation, "object", "Locale is a Stock locale and cannot be modified");
return false;
}
dbObj.Name = inObj.NewText;
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
Validate(dbObj.Name, false);
if (HasErrors)
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(Locale dbObj)
{
//Determine if the object can be deleted, do the deletion tentatively
ValidateCanDelete(dbObj);
if (HasErrors)
return false;
ct.Locale.Remove(dbObj);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(string inObjName, bool isNew)
{
//run validation and biz rules
//Name required
if (string.IsNullOrWhiteSpace(inObjName))
AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
//Name must be less than 255 characters
if (inObjName.Length > 255)
AddError(ValidationErrorType.LengthExceeded, "Name", "255 char max");
//Name must be unique
if (ct.Locale.Where(m => m.Name == inObjName).FirstOrDefault() != null)
AddError(ValidationErrorType.NotUnique, "Name");
return;
}
//Can delete?
private void ValidateCanDelete(Locale inObj)
{
//Decided to short circuit these; if there is one issue then return immediately (fail fast rule)
//Ensure it's not a stock locale
if (inObj.Stock == true)
{
AddError(ValidationErrorType.InvalidOperation, "object", "Locale is a Stock locale and cannot be deleted");
return;
}
if (inObj.Id == ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID)
{
AddError(ValidationErrorType.InvalidOperation, "object", "Locale is set as the default server locale (AYANOVA_DEFAULT_LANGUAGE_ID) and can not be deleted");
return;
}
//See if any users exist with this locale selected in which case it's not deleteable
if (ct.User.Any(e => e.LocaleId == inObj.Id))
{
AddError(ValidationErrorType.ReferentialIntegrity, "object", "Can't be deleted in use by one or more Users");
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UTILITIES
//
public long LocaleNameToId(string localeName)
{
var v = ct.Locale.Where(c => c.Name == localeName).Select(x => x.Id);
if (v.Count() < 1) return 0;
return v.First();
}
public bool LocaleExists(string localeName)
{
return LocaleNameToId(localeName) != 0;
}
public bool LocaleExists(long id)
{
return ct.Locale.Any(e => e.Id == id);
}
public bool LocaleItemExists(long id)
{
return ct.LocaleItem.Any(e => e.Id == id);
}
/// <summary>
/// Used by import, translate the old v7 locale key name into the new shorter version
/// </summary>
/// <param name="oldKey"></param>
/// <returns></returns>
public string Translatev7LocaleKey(string oldKey)
{
string s = oldKey.Replace(".Label.", ".", StringComparison.InvariantCultureIgnoreCase);
if (s.StartsWith("O.", StringComparison.InvariantCultureIgnoreCase))
s = s.Replace("O.", "", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace(".ToolBar.", ".", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace(".Go.", ".", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace(".Command.", ".", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace(".Error.", ".", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace(".Object.", ".", StringComparison.InvariantCultureIgnoreCase);
if (s.StartsWith("UI.", StringComparison.InvariantCultureIgnoreCase))
s = s.Replace("UI.", "", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace(".", "", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace("AddressAddress", "Address", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace("ContactPhoneContactPhone", "ContactPhone", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace("ContactPhonePhone", "ContactPhone", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace("PurchaseOrderPurchaseOrder", "PurchaseOrder", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace("WorkorderItemMiscExpenseExpense", "WorkorderItemMiscExpense", StringComparison.InvariantCultureIgnoreCase);
s = s.Replace("WorkorderItemTravelTravel", "WorkorderItemTravel", StringComparison.InvariantCultureIgnoreCase);
return s;
}
//ocaleBiz::ImportV7 - old Key "Locale.Label.UI.DestLocale" translates to new Key "LocaleDestLocale" which is not valid!
//LocaleUIDestLocale
/// <summary>
/// Ensure stock locales and setup defaults
/// Called by boot preflight check code AFTER it has already ensured the locale is a two letter code if stock one was chosen
/// </summary>
public void ValidateLocales()
{
//Ensure default locales are present and that there is a server default locale that exists
if (!LocaleExists("en"))
{
throw new System.Exception($"E1015: stock locale English (en) not found in database!");
}
if (!LocaleExists("es"))
{
throw new System.Exception($"E1015: stock locale Spanish (es) not found in database!");
}
if (!LocaleExists("de"))
{
throw new System.Exception($"E1015: stock locale German (de) not found in database!");
}
if (!LocaleExists("fr"))
{
throw new System.Exception($"E1015: stock locale French (fr) not found in database!");
}
//Ensure chosen default locale exists
switch (ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE)
{
case "en":
case "es":
case "de":
case "fr":
break;
default:
if (!LocaleExists(ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE))
{
throw new System.Exception($"E1015: stock locale French (fr) not found in database!");
}
break;
}
//Put the default locale ID number into the ServerBootConfig for later use
ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID = LocaleNameToId(ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE);
}
/////////////////////////////////////////////////////////////////////
/// IMPORT v7 implementation
public async Task<bool> ImportV7Async(JObject j, List<ImportAyaNova7MapItem> importMap, Guid jobId)
{
//some types need to import from more than one source hence the seemingly redundant switch statement for futureproofing
switch (j["V7_TYPE"].Value<string>())
{
case "GZTW.AyaNova.BLL.Locale":
{
//Get source locale name from filename using regex
var SourceLocaleFileName = j["V7_SOURCE_FILE_NAME"].Value<string>();
Regex RxExtractLocaleName = new Regex(@"locale\.(.*)\.json");
var v = RxExtractLocaleName.Match(SourceLocaleFileName);
var SourceLocaleName = v.Groups[1].ToString();
//ENsure doesn't already exist
if (LocaleExists(SourceLocaleName))
{
//If there are any validation errors, log in joblog and move on
JobsBiz.LogJob(jobId, $"LocaleBiz::ImportV7Async -> - Locale \"{SourceLocaleName}\" already exists in database, can not import over an existing locale", ct);
return false;
}
//keys to skip importing
List<string> SkipKeys = new List<string>();
SkipKeys.Add("UI.Label.CurrentUserName");
SkipKeys.Add("V7_SOURCE_FILE_NAME");
SkipKeys.Add("V7_TYPE");
List<string> ValidKeys = GetKeyList();
Dictionary<string, string> NewLocaleDict = new Dictionary<string, string>();
foreach (var Pair in j.Children())
{
var V7Value = Pair.First.Value<string>();
var V7KeyName = ((JProperty)Pair).Name;
if (!SkipKeys.Contains(V7KeyName))
{
var RavenKeyName = Translatev7LocaleKey(V7KeyName);
if (!ValidKeys.Contains(RavenKeyName))
{
throw new System.ArgumentOutOfRangeException($"LocaleBiz::ImportV7 - old Key \"{V7KeyName}\" translates to new Key \"{RavenKeyName}\" which is not valid!");
}
if (!NewLocaleDict.ContainsKey(RavenKeyName))
{
NewLocaleDict.Add(RavenKeyName, V7Value);
}
else
{
//Use the shortest V7Value string in the case of dupes
if (NewLocaleDict[RavenKeyName].Length > V7Value.Length)
{
NewLocaleDict[RavenKeyName] = V7Value;
}
}
}
}
//Validate it's the correct number of keys expected
if (NewLocaleDict.Count != ValidKeys.Count)
{
throw new System.ArgumentOutOfRangeException($"LocaleBiz::ImportV7 - Import locale \"{SourceLocaleName}\" has an unexpected number of keys: {NewLocaleDict.Count}, expected {ValidKeys.Count} ");
}
//have file name, have all localized text
Locale l = new Locale();
l.Name = SourceLocaleName;
l.OwnerId = 1;
l.Stock = false;
foreach (KeyValuePair<string, string> K in NewLocaleDict)
{
l.LocaleItems.Add(new LocaleItem() { Key = K.Key, Display = K.Value });
}
ct.Locale.Add(l);
ct.SaveChanges();
}
break;
}
//just to hide compiler warning for now
await Task.CompletedTask;
//this is the equivalent of returning void for a Task signature with nothing to return
return true;
}
/////////////////////////////////////////////////////////////////////
}//eoc
//
}//eons

View File

@@ -0,0 +1,105 @@
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
using AyaNova.Util;
using AyaNova.Models;
namespace AyaNova.Biz
{
public static class PrimeData
{
// private readonly AyContext ct;
// private readonly ILogger<PrimeData> log;
// public PrimeData(AyContext dbcontext, ILogger<PrimeData> logger)
// {
// ct = dbcontext;
// log = logger;
// }
/// <summary>
/// Prime the database
/// </summary>
public static void PrimeManagerAccount(AyContext ct)
{
//get a db and logger
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("PrimeData");
User u = new User();
u.Name = "AyaNova Administrator";
u.Salt = Hasher.GenerateSalt();
u.Login = "manager";
u.Password = Hasher.hash(u.Salt, "l3tm3in");
u.Roles = AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull;
u.OwnerId = 1;
u.LocaleId=ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID;//Ensure primeLocales is called first
ct.User.Add(u);
ct.SaveChanges();
}
/// <summary>
/// Prime the locales
/// This may be called before there are any users on a fresh db boot
/// </summary>
public static void PrimeLocales(AyContext ct)
{
//Read in each stock locale from a text file and then create them in the DB
var ResourceFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource");
if (!Directory.Exists(ResourceFolderPath))
{
throw new System.Exception($"E1012: \"resource\" folder not found where expected: \"{ResourceFolderPath}\", installation damaged?");
}
ImportLocale(ct, ResourceFolderPath, "en");
ImportLocale(ct, ResourceFolderPath, "es");
ImportLocale(ct, ResourceFolderPath, "fr");
ImportLocale(ct, ResourceFolderPath, "de");
//Ensure locales are present, not missing any keys and that there is a server default locale that exists
LocaleBiz lb = new LocaleBiz(ct, 1, AuthorizationRoles.OpsAdminFull);
lb.ValidateLocales();
}
private static void ImportLocale(AyContext ct, string resourceFolderPath, string localeCode)
{
var LocalePath = Path.Combine(resourceFolderPath, $"{localeCode}.json");
if (!File.Exists(LocalePath))
{
throw new System.Exception($"E1013: stock locale file \"{localeCode}\" not found where expected: \"{LocalePath}\", installation damaged?");
}
JObject o = JObject.Parse(File.ReadAllText(LocalePath));
Locale l = new Locale();
l.Name = localeCode;
l.OwnerId = 1;
l.Stock = true;
foreach (JToken t in o.Children())
{
var key = t.Path;
var display = t.First.Value<string>();
l.LocaleItems.Add(new LocaleItem() { Key = key, Display = display });//, Locale = l
}
ct.Locale.Add(l);
ct.SaveChanges();
}
}//eoc
}//eons

View File

@@ -0,0 +1,275 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace AyaNova.Biz
{
internal class TagBiz : BizObject, IImportAyaNova7Object
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
internal TagBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
/*
TODO: add methods here to deal with various tag operations in the db
*/
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<Tag> CreateAsync(string inObj)
{
//Must be lowercase per rules
//This may be naive when we get international customers but for now supporting utf-8 and it appears it's safe to do this with unicode
inObj = inObj.ToLowerInvariant();
//No spaces in tags, replace with dashes
inObj = inObj.Replace(" ", "-");
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with Tag
Tag outObj = new Tag()
{
Name = inObj,
OwnerId = userId,
Created = System.DateTime.UtcNow
};
await ct.Tag.AddAsync(outObj);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<Tag> GetAsync(long fetchId)
{
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.Tag.SingleOrDefaultAsync(m => m.Id == fetchId);
}
//get picklist (paged)
//Unlike most picklists, this one only checks for starts with and wildcards are not supported / treated as part of tag name
internal async Task<ApiPagedResponse<NameIdItem>> GetPickListAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions, string q)
{
pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
NameIdItem[] items;
int totalRecordCount = 0;
if (!string.IsNullOrWhiteSpace(q))
{
//tags are allow saved this way so search this way too
q = q.ToLowerInvariant();
items = await ct.Tag
//There is some debate on this for efficiency
//I chose this method because I think it escapes potential wildcards in the string automatically
//and I don't want people using wildcards with this, only starts with is supported
//https://stackoverflow.com/questions/45708715/entity-framework-ef-functions-like-vs-string-contains
.Where(m => m.Name.StartsWith(q))
// .Where(m => EF.Functions.ILike(m.Name, q))
.OrderBy(m => m.Name)
.Skip(pagingOptions.Offset.Value)
.Take(pagingOptions.Limit.Value)
.Select(m => new NameIdItem()
{
Id = m.Id,
Name = m.Name
}).ToArrayAsync();
totalRecordCount = await ct.Tag.Where(m => m.Name.StartsWith(q)).CountAsync();
}
else
{
items = await ct.Tag
.OrderBy(m => m.Name)
.Skip(pagingOptions.Offset.Value)
.Take(pagingOptions.Limit.Value)
.Select(m => new NameIdItem()
{
Id = m.Id,
Name = m.Name
}).ToArrayAsync();
totalRecordCount = await ct.Tag.CountAsync();
}
var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(items, pageLinks);
return pr;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal bool Put(Tag dbObj, Tag inObj)
{
//Replace the db object with the PUT object
CopyObject.Copy(inObj, dbObj, "Id");
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
//Must be lowercase per rules
//This may be naive when we get international customers but for now supporting utf-8 and it appears it's safe to do this with unicode
dbObj.Name = dbObj.Name.ToLowerInvariant();
Validate(dbObj.Name, false);
if (HasErrors)
return false;
return true;
}
//patch
internal bool Patch(Tag dbObj, JsonPatchDocument<Tag> objectPatch, uint concurrencyToken)
{
//Do the patching
objectPatch.ApplyTo(dbObj);
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
dbObj.Name = dbObj.Name.ToLowerInvariant();
Validate(dbObj.Name, false);
if (HasErrors)
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(Tag dbObj)
{
//Determine if the object can be deleted, do the deletion tentatively
ValidateCanDelete(dbObj);
if (HasErrors)
return false;
ct.Tag.Remove(dbObj);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(string inObj, bool isNew)
{
//run validation and biz rules
//Name required
if (string.IsNullOrWhiteSpace(inObj))
AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
//Name must be less than 35 characters
if (inObj.Length > 35)
AddError(ValidationErrorType.LengthExceeded, "Name", "35 char max");
//Name must be unique
if (ct.Tag.Where(m => m.Name == inObj).FirstOrDefault() != null)
AddError(ValidationErrorType.NotUnique, "Name");
return;
}
//Can delete?
private void ValidateCanDelete(Tag inObj)
{
//whatever needs to be check to delete this object
//See if any tagmaps exist with this tag in which case it's not deleteable
if (ct.TagMap.Any(e => e.TagId == inObj.Id))
{
AddError(ValidationErrorType.ReferentialIntegrity, "object", "Can't be deleted while has relations");
}
}
/////////////////////////////////////////////////////////////////////
/// IMPORT v7 implementation
public async Task<bool> ImportV7Async(JObject j, List<ImportAyaNova7MapItem> importMap, Guid jobId)
{
switch (j["V7_TYPE"].Value<string>())
{
case "GZTW.AyaNova.BLL.Region":
{
var name = j["Name"].Value<string>();
var oldId = new Guid(j["ID"].Value<string>());
//In RAVEN tags can only be 35 characters
name = StringUtil.MaxLength(name, 35);
Tag o = await CreateAsync(name);
if (HasErrors)
{
//If there are any validation errors, log in joblog and move on
JobsBiz.LogJob(jobId, $"TagBiz::ImportV7Async -> import object \"{name}\" source id {oldId.ToString()} failed validation and was not imported: {GetErrorsAsString()} ", ct);
return false;
}
else
{
await ct.SaveChangesAsync();
var mapItem = new ImportAyaNova7MapItem(oldId, AyaType.Tag, o.Id);
}
}
break;
}
//this is the equivalent of returning void for a Task signature with nothing to return
return true;
}
/////////////////////////////////////////////////////////////////////
}//eoc
//
}//eons

View File

@@ -0,0 +1,180 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
using System.Collections.Generic;
namespace AyaNova.Biz
{
internal class TagMapBiz : BizObject
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
internal TagMapBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
/*
TODO: add methods here to deal with various tag operations in the db
*/
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<TagMap> CreateAsync(TagMapInfo inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with TagMap
TagMap outObj = new TagMap()
{
TagId = inObj.TagId,
TagToObjectId = inObj.TagToObjectId,
TagToObjectType = inObj.TagToObjectType,
OwnerId = userId
};
await ct.TagMap.AddAsync(outObj);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<TagMap> GetAsync(long fetchId)
{
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.TagMap.SingleOrDefaultAsync(m => m.Id == fetchId);
}
internal async Task<List<NameIdItem>> GetTagsOnObjectListAsync(AyaTypeId tid)
{
/*
NOTES: This will be a bit of a "hot" path as every fetch of any main object will involve this
for now, just make it work and later can improve performance
Also is sort going to be adequate, it's supposed to be based on invariant culture
*/
List<NameIdItem> l = new List<NameIdItem>();
//Get the list of tags on the object
var tagmapsOnObject = await ct.TagMap
.Where(m => m.TagToObjectId == tid.ObjectId && m.TagToObjectType == tid.ObjectType)
.Select(m => m.TagId)
.ToListAsync();
foreach (long tagId in tagmapsOnObject)
{
var tagFromDb = await ct.Tag.SingleOrDefaultAsync(m => m.Id == tagId);
if (tagFromDb != null)
{
l.Add(new NameIdItem() { Id = tagFromDb.Id, Name = tagFromDb.Name });
}
}
//Return the list sorted alphabetically
//Note if this is commonly required then maybe make a helper / extension for it
return (l.OrderBy(o => o.Name).ToList());
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(TagMap dbObj)
{
//Determine if the object can be deleted, do the deletion tentatively
ValidateCanDelete(dbObj);
if (HasErrors)
return false;
ct.TagMap.Remove(dbObj);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE ALL TAGMAPS FOR OBJECT
//
static internal bool DeleteAllForObject(AyaTypeId parentObj, AyContext ct)
{
//Be careful in future, if you put ToString at the end of each object in the string interpolation
//npgsql driver will assume it's a string and put quotes around it triggering an error that a string can't be compared to an int
ct.Database.ExecuteSqlCommand($"delete from atagmap where tagtoobjectid={parentObj.ObjectId} and tagtoobjecttype={parentObj.ObjectTypeAsInt}");
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(TagMapInfo inObj, bool isNew)
{
//run validation and biz rules
// //Name required
// if (string.IsNullOrWhiteSpace(inObj))
// AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
// //Name must be less than 35 characters
// if (inObj.Length > 35)
// AddError(ValidationErrorType.LengthExceeded, "Name", "35 char max");
return;
}
//Can delete?
private void ValidateCanDelete(TagMap inObj)
{
//whatever needs to be check to delete this object
//See if any tagmaps exist with this tag in which case it's not deleteable
// if (ct.TagMap.Any(e => e.TagMapId == inObj.Id))
// {
// AddError(ValidationErrorType.ReferentialIntegrity, "object", "Can't be deleted while has relations");
// }
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,15 @@
using System;
namespace AyaNova.Biz
{
/// <summary>
/// Marker attribute indicating that an object supports tagging
/// Used in <see cref="AyaType"/>
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class TaggableAttribute : Attribute
{
//No code required, it's just a marker
//https://docs.microsoft.com/en-us/dotnet/standard/attributes/writing-custom-attributes
}
}//eons

View File

@@ -0,0 +1,96 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
/// <summary>
/// Handle data seeding and other trial ops
/// </summary>
internal class TrialBiz : BizObject, IJobObject
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
// private readonly ApiServerState serverState;
internal TrialBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
//serverState = apiServerState;, ApiServerState apiServerState
}
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
//Hand off the particular job to the corresponding processing code
//NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so
//basically any error condition during job processing should throw up an exception if it can't be handled
//There might be future other job types so doing it like this for all biz job handlers for now
switch (job.JobType)
{
case JobType.SeedTestData:
await ProcessSeedTestData(job);
// ProcessSeedTestData(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"TrialBiz.HandleJobAsync -> Invalid job type{job.JobType.ToString()}");
}
}
/// <summary>
/// Handle the job
/// </summary>
/// <param name="job"></param>
private async Task ProcessSeedTestData(OpsJob job)
{//
//NOTE: If this code throws an exception the caller will automatically set the job to failed and log the exeption so
//basically any error condition during job processing should throw up an exception if it can't be handled
//FOR NOW NOT ASYNC so faking it at end of this method
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct);
JobsBiz.LogJob(job.GId, $"Starting...", ct);
//Get the import filename from the jsondata
JObject jobData = JObject.Parse(job.JobInfo);
var seedLevel = (Seeder.SeedLevel)jobData["seedLevel"].Value<int>();
Seeder.SeedDatabase(ct, seedLevel);
JobsBiz.LogJob(job.GId, "Finished.", ct);
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);
await Task.CompletedTask;
}
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,12 @@
namespace AyaNova.Biz
{
public class ValidationError
{
public ValidationErrorType ErrorType { get; set; }
public string Target { get; set; }
public string Message { get; set; }
}//eoc
}//eons

View File

@@ -0,0 +1,22 @@
namespace AyaNova.Biz
{
public enum ValidationErrorType
{
RequiredPropertyEmpty = 1,
LengthExceeded = 2,
NotUnique = 3,
StartDateMustComeBeforeEndDate = 4,
InvalidValue = 5,
ReferentialIntegrity = 6,
InvalidOperation = 7
//!! NOTE - UPDATE api-validation-error-codes.md documentation when adding items
}
}//eons

View File

@@ -0,0 +1,314 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
internal class WidgetBiz : BizObject, IJobObject
{
private readonly AyContext ct;
private readonly long userId;
private readonly AuthorizationRoles userRoles;
internal WidgetBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
userId = currentUserId;
userRoles = UserRoles;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
internal async Task<Widget> CreateAsync(Widget inObj)
{
Validate(inObj, true);
if (HasErrors)
return null;
else
{
//do stuff with widget
Widget outObj = inObj;
outObj.OwnerId = userId;
//SearchHelper(break down text fields, save to db)
//TagHelper(collection of tags??)
await ct.Widget.AddAsync(outObj);
return outObj;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
/// GET
//Get one
internal async Task<Widget> GetAsync(long fetchId)
{
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
return await ct.Widget.SingleOrDefaultAsync(m => m.Id == fetchId);
}
//get many (paged)
internal async Task<ApiPagedResponse<Widget>> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions)
{
pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
var items = await ct.Widget
.OrderBy(m => m.Id)
.Skip(pagingOptions.Offset.Value)
.Take(pagingOptions.Limit.Value)
.ToArrayAsync();
var totalRecordCount = await ct.Widget.CountAsync();
var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
ApiPagedResponse<Widget> pr = new ApiPagedResponse<Widget>(items, pageLinks);
return pr;
}
//get picklist (paged)
internal async Task<ApiPagedResponse<NameIdItem>> GetPickListAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions, string q)
{
pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
NameIdItem[] items;
int totalRecordCount = 0;
if (!string.IsNullOrWhiteSpace(q))
{
items = await ct.Widget
.Where(m => EF.Functions.ILike(m.Name, q))
.OrderBy(m => m.Name)
.Skip(pagingOptions.Offset.Value)
.Take(pagingOptions.Limit.Value)
.Select(m => new NameIdItem()
{
Id = m.Id,
Name = m.Name
}).ToArrayAsync();
totalRecordCount = await ct.Widget.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync();
}
else
{
items = await ct.Widget
.OrderBy(m => m.Name)
.Skip(pagingOptions.Offset.Value)
.Take(pagingOptions.Limit.Value)
.Select(m => new NameIdItem()
{
Id = m.Id,
Name = m.Name
}).ToArrayAsync();
totalRecordCount = await ct.Widget.CountAsync();
}
var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
ApiPagedResponse<NameIdItem> pr = new ApiPagedResponse<NameIdItem>(items, pageLinks);
return pr;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
//put
internal bool Put(Widget dbObj, Widget inObj)
{
//Replace the db object with the PUT object
CopyObject.Copy(inObj, dbObj, "Id");
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
Validate(dbObj, false);
if (HasErrors)
return false;
return true;
}
//patch
internal bool Patch(Widget dbObj, JsonPatchDocument<Widget> objectPatch, uint concurrencyToken)
{
//Do the patching
objectPatch.ApplyTo(dbObj);
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
Validate(dbObj, false);
if (HasErrors)
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal bool Delete(Widget dbObj)
{
//Determine if the object can be deleted, do the deletion tentatively
//Probably also in here deal with tags and associated search text etc
ValidateCanDelete(dbObj);
if (HasErrors)
return false;
ct.Widget.Remove(dbObj);
return true;
}
/// <summary>
/// Delete child objects like tags and attachments and etc
/// </summary>
/// <param name="dbObj"></param>
internal void DeleteChildren(Widget dbObj)
{
//TAGS
TagMapBiz.DeleteAllForObject(new AyaTypeId(AyaType.Widget, dbObj.Id), ct);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private void Validate(Widget inObj, bool isNew)
{
//run validation and biz rules
if (isNew)
{
//NEW widgets must be active
if (inObj.Active == null || ((bool)inObj.Active) == false)
{
AddError(ValidationErrorType.InvalidValue, "Active", "New widget must be active");
}
}
//OwnerId required
if (!isNew)
{
if (inObj.OwnerId == 0)
AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId");
}
//Name required
if (string.IsNullOrWhiteSpace(inObj.Name))
AddError(ValidationErrorType.RequiredPropertyEmpty, "Name");
//Name must be less than 255 characters
if (inObj.Name.Length > 255)
AddError(ValidationErrorType.LengthExceeded, "Name", "255 max");
//If name is otherwise OK, check that name is unique
if (!PropertyHasErrors("Name"))
{
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
if (ct.Widget.Any(m => m.Name == inObj.Name && m.Id != inObj.Id))
{
AddError(ValidationErrorType.NotUnique, "Name");
}
}
//Start date AND end date must both be null or both contain values
if (inObj.StartDate == null && inObj.EndDate != null)
AddError(ValidationErrorType.RequiredPropertyEmpty, "StartDate");
if (inObj.StartDate != null && inObj.EndDate == null)
AddError(ValidationErrorType.RequiredPropertyEmpty, "EndDate");
//Start date before end date
if (inObj.StartDate != null && inObj.EndDate != null)
if (inObj.StartDate > inObj.EndDate)
AddError(ValidationErrorType.StartDateMustComeBeforeEndDate, "StartDate");
//Enum is valid value
if (!inObj.Roles.IsValid())
{
AddError(ValidationErrorType.InvalidValue, "Roles");
}
return;
}
//Can delete?
private void ValidateCanDelete(Widget inObj)
{
//whatever needs to be check to delete this object
}
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
//Hand off the particular job to the corresponding processing code
//NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so
//basically any error condition during job processing should throw up an exception if it can't be handled
switch (job.JobType)
{
case JobType.TestWidgetJob:
await ProcessTestJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"WidgetBiz.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
/// <summary>
/// /// Handle the test job
/// </summary>
/// <param name="job"></param>
private async Task ProcessTestJobAsync(OpsJob job)
{
var sleepTime = 30 * 1000;
//Simulate a long running job here
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Running, ct);
JobsBiz.LogJob(job.GId, $"WidgetBiz::ProcessTestJob started, sleeping for {sleepTime} seconds...", ct);
//Uncomment this to test if the job prevents other routes from running
//result is NO it doesn't prevent other requests, so we are a-ok for now
await Task.Delay(sleepTime);
JobsBiz.LogJob(job.GId, "WidgetBiz::ProcessTestJob done sleeping setting job to finished", ct);
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);
}
//Other job handlers here...
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,72 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace AyaNova.Generator
{
//This is a temporary class until .net 2.1 is released
//https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice
// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts =
new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it,
// this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using App.Metrics;
using AyaNova.Util;
using System.Linq;
namespace AyaNova.Biz
{
/// <summary>
/// called by Generator to flush metrics to reporter
///
/// </summary>
internal static class CoreJobMetricsReport
{
private static TimeSpan DO_EVERY_INTERVAL = new TimeSpan(0, 0, 20);//FLUSH EVERY 20 SECONDS
private static DateTime lastReportFlushDone = DateTime.MinValue;
////////////////////////////////////////////////////////////////////////////////////////////////
// DoAsync
//
public static async Task DoJobAsync()
{
//https://www.app-metrics.io/
IMetrics metrics = (IMetrics)ServiceProviderProvider.Provider.GetService(typeof(IMetrics));
//No more quickly than doeveryinterval
if (!DateUtil.IsAfterDuration(lastReportFlushDone, DO_EVERY_INTERVAL))
return;
//RUN ALL REPORTS - FLUSH STATS
var mr = (IMetricsRoot)metrics;
Task.WaitAll(mr.ReportRunner.RunAllAsync().ToArray());
lastReportFlushDone = DateTime.UtcNow;
//just to hide compiler warning for now
await Task.CompletedTask;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using App.Metrics;
using AyaNova.Util;
using AyaNova.Models;
namespace AyaNova.Biz
{
/// <summary>
/// called by Generator to gather server metrics and check on things
/// See MetricsRegistry for defined metrics
///
/// </summary>
internal static class CoreJobMetricsSnapshot
{
private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobMetricsSnapshot");
#if (DEBUG)
private static TimeSpan DO_EVERY_INTERVAL = new TimeSpan(0, 1, 0);//DEBUG do a check every 60 seconds
#else
private static TimeSpan DO_EVERY_INTERVAL = new TimeSpan(0, 15, 0);//RELEASE do a check every 15 minutes
#endif
private static DateTime lastServerCheckDone = DateTime.MinValue;
private static DateTime lastRecordCountCheck = DateTime.MinValue;
private static DateTime lastFileCountCheck = DateTime.MinValue;
////////////////////////////////////////////////////////////////////////////////////////////////
// DoAsync
//
public static async Task DoJobAsync(AyContext ct)
{
//https://www.app-metrics.io/
IMetrics metrics = (IMetrics)ServiceProviderProvider.Provider.GetService(typeof(IMetrics));
//This will get triggered roughly every minute (10 seconds in debug), but we don't want to healthcheck that frequently
if (!DateUtil.IsAfterDuration(lastServerCheckDone, DO_EVERY_INTERVAL))
return;
log.LogTrace("Starting metrics snapshot");
//Gather core metrics here
var process = Process.GetCurrentProcess();
//PHYSICAL MEMORY
metrics.Measure.Gauge.SetValue(MetricsRegistry.PhysicalMemoryGauge, process.WorkingSet64);
//PRIVATE BYTES
metrics.Measure.Gauge.SetValue(MetricsRegistry.PrivateBytesGauge, process.PrivateMemorySize64);
//RECORDS IN TABLE
//Only do this once per hour
if (DateUtil.IsAfterDuration(lastRecordCountCheck, 1))
{
lastRecordCountCheck = DateTime.UtcNow;
log.LogTrace("Counting table records");
//Get a count of important tables in db
List<string> allTableNames = DbUtil.GetAllTablenames();
//Skip some tables as they are internal and / or only ever have one record
List<string> skipTableNames = new List<string>();
skipTableNames.Add("alicense");
skipTableNames.Add("aschemaversion");
foreach (string table in allTableNames)
{
if (!skipTableNames.Contains(table))
{
var tags = new MetricTags("TableTagKey", table);
metrics.Measure.Gauge.SetValue(MetricsRegistry.DBRecordsGauge, tags, DbUtil.CountOfRecords(table));
}
}
}
//JOB COUNTS (DEAD, RUNNING, COMPLETED, SLEEPING)
foreach (JobStatus stat in Enum.GetValues(typeof(JobStatus)))
{
var jobtag = new MetricTags("JobStatus", stat.ToString());
metrics.Measure.Gauge.SetValue(MetricsRegistry.JobsGauge, jobtag, await JobsBiz.GetCountForJobStatusAsync(ct, stat));
}
//FILES ON DISK
//Only do this once per hour
if (DateUtil.IsAfterDuration(lastFileCountCheck, 1))
{
lastFileCountCheck = DateTime.UtcNow;
log.LogTrace("Files on disk information");
var UtilFilesInfo = FileUtil.GetUtilityFolderSizeInfo();
var UserFilesInfo = FileUtil.GetAttachmentFolderSizeInfo();
var mtag = new MetricTags("File type", "Business object files");
metrics.Measure.Gauge.SetValue(MetricsRegistry.FileCountGauge, mtag, UserFilesInfo.FileCountWithChildren);
metrics.Measure.Gauge.SetValue(MetricsRegistry.FileSizeGauge, mtag, UserFilesInfo.SizeWithChildren);
mtag = new MetricTags("File type", "OPS files");
metrics.Measure.Gauge.SetValue(MetricsRegistry.FileCountGauge, mtag, UtilFilesInfo.FileCountWithChildren);
metrics.Measure.Gauge.SetValue(MetricsRegistry.FileSizeGauge, mtag, UtilFilesInfo.SizeWithChildren);
}
lastServerCheckDone = DateTime.UtcNow;
//just to hide compiler warning for now
await Task.CompletedTask;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,127 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.Extensions.Logging;
using EnumsNET;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
using AyaNova.Models;
namespace AyaNova.Biz
{
/// <summary>
/// JobSweeper - called by Generator to clean out old jobs that are completed and their logs
///
/// </summary>
internal static class CoreJobSweeper
{
private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobSweeper");
private static DateTime lastSweep = DateTime.MinValue;
private static TimeSpan SWEEP_EVERY_INTERVAL = new TimeSpan(0, 30, 0);
private static TimeSpan SUCCEEDED_JOBS_DELETE_AFTER_THIS_TIMESPAN = new TimeSpan(24, 0, 0);//24 hours
private static TimeSpan FAILED_JOBS_DELETE_AFTER_THIS_TIMESPAN = new TimeSpan(14, 0, 0, 0);//14 days (gives people time to notice and look into it)
private static TimeSpan RUNNING_JOBS_BECOME_FAILED_AFTER_THIS_TIMESPAN = new TimeSpan(24, 0, 0);//24 hours (time running jobs are allowed to sit in "running" state before considered failed)
////////////////////////////////////////////////////////////////////////////////////////////////
// DoSweep
//
public static async Task DoSweepAsync(AyContext ct)
{
//This will get triggered roughly every minute, but we don't want to sweep that frequently
if (DateTime.UtcNow - lastSweep < SWEEP_EVERY_INTERVAL)
return;
log.LogTrace("Sweep starting");
//SWEEP SUCCESSFUL JOBS
//calculate cutoff to delete
DateTime dtDeleteCutoff = DateTime.UtcNow - SUCCEEDED_JOBS_DELETE_AFTER_THIS_TIMESPAN;
await sweepAsync(ct, dtDeleteCutoff, JobStatus.Completed);
//SWEEP FAILED JOBS
//calculate cutoff to delete
dtDeleteCutoff = DateTime.UtcNow - FAILED_JOBS_DELETE_AFTER_THIS_TIMESPAN;
await sweepAsync(ct, dtDeleteCutoff, JobStatus.Failed);
//KILL STUCK JOBS
//calculate cutoff to delete
DateTime dtRunningDeadline = DateTime.UtcNow - RUNNING_JOBS_BECOME_FAILED_AFTER_THIS_TIMESPAN;
await killStuckJobsAsync(ct, dtRunningDeadline);
lastSweep = DateTime.UtcNow;
}
private static async Task sweepAsync(AyContext ct, DateTime dtDeleteCutoff, JobStatus jobStatus)
{
//Get the deleteable succeeded jobs list
var jobs = await ct.OpsJob
.AsNoTracking()
.Where(c => c.Created < dtDeleteCutoff && c.JobStatus == jobStatus)
.OrderBy(m => m.Created)
.ToListAsync();
log.LogTrace($"SweepAsync processing: cutoff={dtDeleteCutoff.ToString()}, for {jobs.Count.ToString()} jobs of status {jobStatus.ToString()}");
foreach (OpsJob j in jobs)
{
try
{
await JobsBiz.DeleteJobAndLogAsync(j.GId, ct);
}
catch (Exception ex)
{
log.LogError(ex, "sweepAsync exception calling JobsBiz.DeleteJobAndLogAsync");
//for now just throw it but this needs to be removed when logging added and better handling
throw (ex);
}
}
}
/// <summary>
/// Kill jobs that have been stuck in "running" state for too long
/// </summary>
/// <param name="ct"></param>
/// <param name="dtRunningDeadline"></param>
/// <returns></returns>
private static async Task killStuckJobsAsync(AyContext ct, DateTime dtRunningDeadline)
{
//Get the deleteable succeeded jobs list
var jobs = await ct.OpsJob
.AsNoTracking()
.Where(c => c.Created < dtRunningDeadline && c.JobStatus == JobStatus.Running)
.OrderBy(m => m.Created)
.ToListAsync();
log.LogTrace($"killStuckJobsAsync processing: cutoff={dtRunningDeadline.ToString()}, for {jobs.Count.ToString()} jobs of status {JobStatus.Running.ToString()}");
foreach (OpsJob j in jobs)
{
//OPSMETRIC
JobsBiz.LogJob(j.GId, "Job took too long to run - setting to failed", ct);
log.LogError($"Job found job stuck in running status and set to failed: deadline={dtRunningDeadline.ToString()}, jobId={j.GId.ToString()}, jobname={j.Name}, jobtype={j.JobType.ToString()}, jobObjectType={j.ObjectType.ToString()}, jobObjectId={j.ObjectId.ToString()}");
JobsBiz.UpdateJobStatus(j.GId, JobStatus.Failed, ct);
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons

View File

@@ -0,0 +1,124 @@
using System.Threading;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
namespace AyaNova.Generator
{
//Implemented from a example here
//https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice
/*
TODO: Generator tasks that should happen:
- Periodically erase any temp files written to userfiles root (attachments temp files) that are older than a day
- These files should be normally erased within seconds after uploading and processing into their permanent folder but shit will go wrong
*/
public class GeneratorService : BackgroundService
{
private readonly ILogger<GeneratorService> log;
// private readonly AyContext ct;
// private readonly ApiServerState serverState;
private readonly IServiceProvider provider;
#if(DEBUG)
private const int GENERATE_SECONDS = 10;
#else
private const int GENERATE_SECONDS = 60;
#endif
// public GeneratorService(ILogger<GeneratorService> logger, AyContext dbcontext, ApiServerState apiServerState)
// {
// ct = dbcontext;
// log = logger;
// serverState = apiServerState;
// }
public GeneratorService(ILogger<GeneratorService> logger, IServiceProvider serviceProvider)
{
// ct = dbcontext;
provider = serviceProvider;
log = logger;
// serverState = apiServerState;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//don't immediately run the generator stuff on boot
bool justStarted = false;
log.LogDebug($"GeneratorService is starting.");
stoppingToken.Register(() =>
log.LogDebug($" GeneratorService background task is stopping."));
while (!stoppingToken.IsCancellationRequested)
{
if (!justStarted)
{
log.LogDebug($"GeneratorService task doing background work.");
using (IServiceScope scope = provider.CreateScope())
{
AyContext ct = scope.ServiceProvider.GetRequiredService<AyContext>();
ApiServerState serverState = scope.ServiceProvider.GetRequiredService<ApiServerState>();
if (!serverState.IsOpen)
{
log.LogDebug($"GeneratorService: ServerState is closed returning without processing jobs, will try again next iteration");
}
//=================================================================
try
{
await JobsBiz.ProcessJobsAsync(ct);
}
catch (Exception ex)
{
log.LogError("Generate::ProcessJobs resulted in exception error ", ex);
}
//=================================================================
}
}
await Task.Delay((GENERATE_SECONDS * 1000), stoppingToken);
justStarted = false;
}
log.LogDebug($"GeneratorService background task is stopping.");
}
//originally but kept getting compiler error
// public override async Task StopAsync(CancellationToken stoppingToken)
// {
// log.LogDebug($"GeneratorService StopAsync triggered.");
// // Run your graceful clean-up actions
// }
public override Task StopAsync(CancellationToken stoppingToken)
{
log.LogDebug($"GeneratorService StopAsync triggered.");
return Task.FromResult(0);
// Run your graceful clean-up actions
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using AyaNova.Util;
using Microsoft.Extensions.Logging;
namespace AyaNova.Models
{
public partial class AyContext : DbContext
{
public virtual DbSet<User> User { get; set; }
public virtual DbSet<License> License { get; set; }
public virtual DbSet<Widget> Widget { get; set; }
public virtual DbSet<FileAttachment> FileAttachment { get; set; }
public virtual DbSet<Tag> Tag { get; set; }
public virtual DbSet<TagMap> TagMap { get; set; }
public virtual DbSet<OpsJob> OpsJob { get; set; }
public virtual DbSet<OpsJobLog> OpsJobLog { get; set; }
public virtual DbSet<Locale> Locale { get; set; }
public virtual DbSet<LocaleItem> LocaleItem { get; set; }
//Note: had to add this constructor to work with the code in startup.cs that gets the connection string from the appsettings.json file
//and commented out the above on configuring
public AyContext(DbContextOptions<AyContext> options) : base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql/
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
// Replace table names
entity.Relational().TableName = "a" + entity.Relational().TableName.ToLowerInvariant();
// Replace column names
foreach (var property in entity.GetProperties())
{
//Any object that has a concurrencytoken field
//set it up to work properly with PostgreSQL
if (property.Name == "ConcurrencyToken")
{
property.Relational().ColumnName = "xmin";
property.Relational().ColumnType = "xid";
property.ValueGenerated = ValueGenerated.OnAddOrUpdate;
property.IsConcurrencyToken = true;
}
else
property.Relational().ColumnName = property.Name.ToLowerInvariant();
}
foreach (var key in entity.GetKeys())
{
key.Relational().Name = key.Relational().Name.ToLowerInvariant();
}
foreach (var key in entity.GetForeignKeys())
{
key.Relational().Name = key.Relational().Name.ToLowerInvariant();
}
foreach (var index in entity.GetIndexes())
{
index.Relational().Name = index.Relational().Name.ToLowerInvariant();
}
}
//Indexes must be specified through fluent api unfortunately
modelBuilder.Entity<FileAttachment>().HasIndex(p => p.StoredFileName);
//Relationships
modelBuilder.Entity<Locale>()
.HasMany(c => c.LocaleItems)
.WithOne(e => e.Locale)
.IsRequired();//default delete behaviour is cascade when set to isrequired
//-----------
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
public partial class FileAttachment
{
public long Id { get; set; }
public uint ConcurrencyToken { get; set; }
public DateTime Created { get; set; }//time it was uploaded not original file creation time, we don't have that
[Required]
public long OwnerId { get; set; }
//-----------------------------------------
[Required]
public long AttachToObjectId { get; set; }
[Required]
public AyaType AttachToObjectType { get; set; }//int
[Required]
public string StoredFileName { get; set; }
[Required]
public string DisplayFileName { get; set; }
[Required]
public string ContentType { get; set; }//mime type
public string Notes { get; set; }
public FileAttachment()
{
Created = System.DateTime.UtcNow;
}
}
}
//"AttachToObjectType and / or AttachToObjectId public AuthorizationRoles Roles { get; set; }

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
namespace AyaNova.Models
{
public partial class License
{
public long Id { get; set; }
public string Key { get; set; }
public Guid DbId { get; set; }
public int LastFetchStatus { get; set; }
public string LastFetchMessage { get; set; }
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace AyaNova.Models
{
// [JsonObject(IsReference = true)]
public partial class Locale
{
public long Id { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
[Required]
public string Name { get; set; }
public bool? Stock { get; set; }
public DateTime Created { get; set; }
public Locale()
{
Created = System.DateTime.UtcNow;
}
//Relationship
//was this but..
// public ICollection<LocaleItem> LocaleItems { get; set; }
//Not perhaps so useful here but this is a good way to lazy initialize collections which
//is more efficient when there are many child collections (workorder) and means no need to null check the collection
//https://stackoverflow.com/a/20773057/8939
private ICollection<LocaleItem> _localeItem;
public virtual ICollection<LocaleItem> LocaleItems
{
get
{
return this._localeItem ?? (this._localeItem = new HashSet<LocaleItem>());
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace AyaNova.Models
{
public partial class LocaleItem
{
public long Id { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public string Key { get; set; }
[Required]
public string Display { get; set; }
public long LocaleId { get; set; }
//Relation
[JsonIgnore]
public Locale Locale { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
/// <summary>
/// Operations job
/// </summary>
public partial class OpsJob
{
[Key]
public Guid GId { get; set; }
[Required]
public DateTime Created { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
[Required]
public string Name { get; set; }
//------------------------
[Required]
public bool Exclusive { get; set; }//true lock api and don't run other jobs until completed / false=run any time with other jobs async
public DateTime StartAfter { get; set; }
[Required]
public JobType JobType { get; set; }
public long ObjectId { get; set; }
public AyaType ObjectType { get; set; }
[Required]
public JobStatus JobStatus { get; set; }
/// <summary>
/// Json string of any required extra info for job
/// </summary>
public string JobInfo { get; set; }//json as string of any required extra info for job
public OpsJob(){
GId=new Guid();
Created=DateTime.UtcNow;
OwnerId=1;
Name="new job";
Exclusive=false;
StartAfter=Created;
JobType=JobType.NotSet;
ObjectId=0;
ObjectType=AyaType.NotValid;
JobStatus=JobStatus.Sleeping;
JobInfo=null;
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
/// <summary>
/// Operations job log
/// </summary>
public partial class OpsJobLog
{
[Key]
public Guid GId { get; set; }
[Required]
public Guid JobId { get; set; }
[Required]
public DateTime Created { get; set; }
[Required]
public string StatusText { get; set; }
public OpsJobLog(){
GId=new Guid();
Created=DateTime.UtcNow;
StatusText="default / not set";
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
public partial class Tag
{
public long Id { get; set; }
public DateTime Created { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
[Required]
public string Name { get; set; }//max 35 characters ascii set
public Tag()
{
Created = System.DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
public partial class TagMap
{
public long Id { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
public DateTime Created { get; set; }
[Required]
public long TagId { get; set; }
[Required]
public long TagToObjectId { get; set; }
[Required]
public AyaType TagToObjectType { get; set; }
public TagMap()
{
Created = System.DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
public partial class User
{
public long Id { get; set; }
public DateTime Created { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
public string Name { get; set; }
public string Login { get; set; }
public string Password { get; set; }
public string Salt { get; set; }
public AuthorizationRoles Roles { get; set; }
public string DlKey { get; set; }
public DateTime? DlKeyExpire { get; set; }
public long LocaleId { get; set; }
public User()
{
Created = System.DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using AyaNova.Biz;
using System.ComponentModel.DataAnnotations;
namespace AyaNova.Models
{
//Test class for development
public partial class Widget
{
public long Id { get; set; }
public uint ConcurrencyToken { get; set; }
[Required]
public long OwnerId { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public decimal? DollarAmount { get; set; }
public bool? Active { get; set; }
public AuthorizationRoles Roles { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public Widget()
{
Created = System.DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using AyaNova.Biz;
namespace AyaNova.Models
{
/// <summary>
/// Provides mapping during import
/// </summary>
public partial class ImportAyaNova7MapItem
{
public AyaTypeId NewObject { get; set; }
public Guid V7ObjectId { get; set; }
public ImportAyaNova7MapItem(Guid v7Guid, AyaType ayaType, long newId){
NewObject=new AyaTypeId(ayaType,newId);
V7ObjectId=v7Guid;
}
}
}

View File

@@ -0,0 +1,40 @@
using AyaNova.Biz;
using System;
namespace AyaNova.Models
{
/// <summary>
/// Job info fetch data
/// </summary>
public class JobOperationsFetchInfo
{
/// <summary>
/// Identity of the job
/// </summary>
/// <returns> id value of job, can be used to fetch logs</returns>
public Guid GId { get; set; }
/// <summary>
/// Date job was submitted
/// </summary>
/// <returns>UTC date/time</returns>
public DateTime Created { get; set; }
/// <summary>
/// Date of the most recent operation for this job
/// </summary>
/// <returns>UTC date/time</returns>
public DateTime LastAction { get; set; }
/// <summary>
/// Descriptive name of the job
/// </summary>
/// <returns>string</returns>
public string Name { get; set; }
/// <summary>
/// Status of the job
/// </summary>
/// <returns>Job status as string</returns>
public string JobStatus { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
using AyaNova.Biz;
using System;
namespace AyaNova.Models
{
/// <summary>
/// Job log item
/// </summary>
public class JobOperationsLogInfoItem
{
/// <summary>
/// Date of log entry
/// </summary>
/// <returns>UTC date/time</returns>
public DateTime Created { get; set; }
/// <summary>
/// Log text
/// </summary>
/// <returns>string</returns>
public string StatusText { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
namespace AyaNova.Models
{
public partial class NameIdActiveItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool? Active { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace AyaNova.Models
{
public partial class NameIdItem
{
public long Id { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
namespace AyaNova.Models
{
/// <summary>
/// Dto object for name only parameters in routes
/// </summary>
public partial class NameItem
{
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace AyaNova.Models
{
public partial class NewTextIdConcurrencyTokenItem
{
public long Id { get; set; }
public string NewText { get; set; }
public uint ConcurrencyToken { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using AyaNova.Biz;
namespace AyaNova.Models
{
public class TagMapInfo
{
public long TagId { get; set; }
public long TagToObjectId { get; set; }
public AyaType TagToObjectType { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using AyaNova.Biz;
namespace AyaNova.Models
{
public class TypeAndIdInfo
{
public long ObjectId { get; set; }
public AyaType ObjectType { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace AyaNova.Models
{
/// <summary>
/// Uploaded file info in it's temporary state
/// </summary>
public class UploadedFileInfo
{
public string InitialUploadedPathName { get; set; }
public string OriginalFileName { get; set; }
public string MimeType { get; set; }
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.Logging;
namespace AyaNova.Util
{
/// <summary>
/// Shared logger
/// </summary>
internal static class ApplicationLogging
{
internal static ILoggerFactory LoggerFactory { get; set; }// = new LoggerFactory();
internal static ILogger CreateLogger<T>() => LoggerFactory.CreateLogger<T>();
internal static ILogger CreateLogger(string categoryName) => LoggerFactory.CreateLogger(categoryName);
}
}

Some files were not shown because too many files have changed in this diff Show More