This commit is contained in:
42
server/AyaNova/AyaNova.csproj
Normal file
42
server/AyaNova/AyaNova.csproj
Normal 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>
|
||||
19
server/AyaNova/ControllerHelpers/ApiCreatedResponse.cs
Normal file
19
server/AyaNova/ControllerHelpers/ApiCreatedResponse.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
namespace AyaNova.Api.ControllerHelpers
|
||||
{
|
||||
|
||||
|
||||
|
||||
public class ApiCreatedResponse
|
||||
{
|
||||
|
||||
public object Result { get; }
|
||||
|
||||
public ApiCreatedResponse(object result)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
}//eoc
|
||||
|
||||
|
||||
}//eons
|
||||
99
server/AyaNova/ControllerHelpers/ApiCustomExceptionFilter.cs
Normal file
99
server/AyaNova/ControllerHelpers/ApiCustomExceptionFilter.cs
Normal 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
|
||||
29
server/AyaNova/ControllerHelpers/ApiDetailError.cs
Normal file
29
server/AyaNova/ControllerHelpers/ApiDetailError.cs
Normal 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
|
||||
36
server/AyaNova/ControllerHelpers/ApiError.cs
Normal file
36
server/AyaNova/ControllerHelpers/ApiError.cs
Normal 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
|
||||
37
server/AyaNova/ControllerHelpers/ApiErrorCode.cs
Normal file
37
server/AyaNova/ControllerHelpers/ApiErrorCode.cs
Normal 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
|
||||
58
server/AyaNova/ControllerHelpers/ApiErrorCodeStockMessage.cs
Normal file
58
server/AyaNova/ControllerHelpers/ApiErrorCodeStockMessage.cs
Normal 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
|
||||
104
server/AyaNova/ControllerHelpers/ApiErrorResponse.cs
Normal file
104
server/AyaNova/ControllerHelpers/ApiErrorResponse.cs
Normal 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
|
||||
33
server/AyaNova/ControllerHelpers/ApiNotAuthorizedResponse.cs
Normal file
33
server/AyaNova/ControllerHelpers/ApiNotAuthorizedResponse.cs
Normal 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
|
||||
19
server/AyaNova/ControllerHelpers/ApiOkResponse.cs
Normal file
19
server/AyaNova/ControllerHelpers/ApiOkResponse.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
namespace AyaNova.Api.ControllerHelpers
|
||||
{
|
||||
|
||||
|
||||
|
||||
public class ApiOkResponse
|
||||
{
|
||||
|
||||
public object Result { get; }
|
||||
|
||||
public ApiOkResponse(object result)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
}//eoc
|
||||
|
||||
|
||||
}//eons
|
||||
30
server/AyaNova/ControllerHelpers/ApiOkWithPagingResponse.cs
Normal file
30
server/AyaNova/ControllerHelpers/ApiOkWithPagingResponse.cs
Normal 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
|
||||
24
server/AyaNova/ControllerHelpers/ApiPagedResponse.cs
Normal file
24
server/AyaNova/ControllerHelpers/ApiPagedResponse.cs
Normal 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
|
||||
186
server/AyaNova/ControllerHelpers/ApiServerState.cs
Normal file
186
server/AyaNova/ControllerHelpers/ApiServerState.cs
Normal 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
|
||||
209
server/AyaNova/ControllerHelpers/ApiUploadProcessor.cs
Normal file
209
server/AyaNova/ControllerHelpers/ApiUploadProcessor.cs
Normal 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
|
||||
121
server/AyaNova/ControllerHelpers/Authorized.cs
Normal file
121
server/AyaNova/ControllerHelpers/Authorized.cs
Normal 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
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
77
server/AyaNova/ControllerHelpers/MultipartRequestHelper.cs
Normal file
77
server/AyaNova/ControllerHelpers/MultipartRequestHelper.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
89
server/AyaNova/ControllerHelpers/PaginationLinkBuilder.cs
Normal file
89
server/AyaNova/ControllerHelpers/PaginationLinkBuilder.cs
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
23
server/AyaNova/ControllerHelpers/PagingOptions.cs
Normal file
23
server/AyaNova/ControllerHelpers/PagingOptions.cs
Normal 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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
21
server/AyaNova/ControllerHelpers/UserIdFromContext.cs
Normal file
21
server/AyaNova/ControllerHelpers/UserIdFromContext.cs
Normal 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
|
||||
20
server/AyaNova/ControllerHelpers/UserNameFromContext.cs
Normal file
20
server/AyaNova/ControllerHelpers/UserNameFromContext.cs
Normal 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
|
||||
18
server/AyaNova/ControllerHelpers/UserRolesFromContext.cs
Normal file
18
server/AyaNova/ControllerHelpers/UserRolesFromContext.cs
Normal 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
|
||||
76
server/AyaNova/Controllers/ApiRootController.cs
Normal file
76
server/AyaNova/Controllers/ApiRootController.cs
Normal 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
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
452
server/AyaNova/Controllers/AttachmentController.cs
Normal file
452
server/AyaNova/Controllers/AttachmentController.cs
Normal 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
|
||||
162
server/AyaNova/Controllers/AuthController.cs
Normal file
162
server/AyaNova/Controllers/AuthController.cs
Normal 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
|
||||
83
server/AyaNova/Controllers/AyaTypeController.cs
Normal file
83
server/AyaNova/Controllers/AyaTypeController.cs
Normal 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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
203
server/AyaNova/Controllers/BackupController.cs
Normal file
203
server/AyaNova/Controllers/BackupController.cs
Normal 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
|
||||
283
server/AyaNova/Controllers/ImportAyaNova7Controller.cs
Normal file
283
server/AyaNova/Controllers/ImportAyaNova7Controller.cs
Normal 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
|
||||
|
||||
134
server/AyaNova/Controllers/JobOperationsController.cs
Normal file
134
server/AyaNova/Controllers/JobOperationsController.cs
Normal 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
|
||||
197
server/AyaNova/Controllers/LicenseController.cs
Normal file
197
server/AyaNova/Controllers/LicenseController.cs
Normal 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
|
||||
384
server/AyaNova/Controllers/LocaleController.cs
Normal file
384
server/AyaNova/Controllers/LocaleController.cs
Normal 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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
177
server/AyaNova/Controllers/LogFilesController.cs
Normal file
177
server/AyaNova/Controllers/LogFilesController.cs
Normal 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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//------------
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
137
server/AyaNova/Controllers/MetricsController.cs
Normal file
137
server/AyaNova/Controllers/MetricsController.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
//------------
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
120
server/AyaNova/Controllers/ServerStateController.cs
Normal file
120
server/AyaNova/Controllers/ServerStateController.cs
Normal 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; }
|
||||
}
|
||||
|
||||
//------------
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
366
server/AyaNova/Controllers/TagController.cs
Normal file
366
server/AyaNova/Controllers/TagController.cs
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//------------
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
258
server/AyaNova/Controllers/TagMapController.cs
Normal file
258
server/AyaNova/Controllers/TagMapController.cs
Normal 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
|
||||
}
|
||||
124
server/AyaNova/Controllers/TrialController.cs
Normal file
124
server/AyaNova/Controllers/TrialController.cs
Normal 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
|
||||
482
server/AyaNova/Controllers/WidgetController.cs
Normal file
482
server/AyaNova/Controllers/WidgetController.cs
Normal 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
246
server/AyaNova/Program.cs
Normal 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
439
server/AyaNova/Startup.cs
Normal 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
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
server/AyaNova/SwaggerDefaultValues.cs
Normal file
47
server/AyaNova/SwaggerDefaultValues.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
server/AyaNova/appsettings.Development.json
Normal file
10
server/AyaNova/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"System": "Trace",
|
||||
"Microsoft": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
server/AyaNova/appsettings.json
Normal file
17
server/AyaNova/appsettings.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"Debug": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace"
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
15
server/AyaNova/biz/AttachableAttribute.cs
Normal file
15
server/AyaNova/biz/AttachableAttribute.cs
Normal 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
|
||||
56
server/AyaNova/biz/AuthorizationRoles.cs
Normal file
56
server/AyaNova/biz/AuthorizationRoles.cs
Normal 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
|
||||
|
||||
48
server/AyaNova/biz/AyaObjectOwnerId.cs
Normal file
48
server/AyaNova/biz/AyaObjectOwnerId.cs
Normal 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
|
||||
156
server/AyaNova/biz/AyaType.cs
Normal file
156
server/AyaNova/biz/AyaType.cs
Normal 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
|
||||
*/
|
||||
103
server/AyaNova/biz/AyaTypeId.cs
Normal file
103
server/AyaNova/biz/AyaTypeId.cs
Normal 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
|
||||
|
||||
83
server/AyaNova/biz/BizObject.cs
Normal file
83
server/AyaNova/biz/BizObject.cs
Normal 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
|
||||
55
server/AyaNova/biz/BizObjectFactory.cs
Normal file
55
server/AyaNova/biz/BizObjectFactory.cs
Normal 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
|
||||
|
||||
15
server/AyaNova/biz/BizRoleSet.cs
Normal file
15
server/AyaNova/biz/BizRoleSet.cs
Normal 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
|
||||
163
server/AyaNova/biz/BizRoles.cs
Normal file
163
server/AyaNova/biz/BizRoles.cs
Normal 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
|
||||
|
||||
61
server/AyaNova/biz/IBizObject.cs
Normal file
61
server/AyaNova/biz/IBizObject.cs
Normal 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);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
server/AyaNova/biz/IImportAyaNova7Object.cs
Normal file
28
server/AyaNova/biz/IImportAyaNova7Object.cs
Normal 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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
22
server/AyaNova/biz/IJobObject.cs
Normal file
22
server/AyaNova/biz/IJobObject.cs
Normal 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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
163
server/AyaNova/biz/ImportAyaNova7Biz.cs
Normal file
163
server/AyaNova/biz/ImportAyaNova7Biz.cs
Normal 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
|
||||
|
||||
109
server/AyaNova/biz/JobOperationsBiz.cs
Normal file
109
server/AyaNova/biz/JobOperationsBiz.cs
Normal 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
|
||||
|
||||
17
server/AyaNova/biz/JobStatus.cs
Normal file
17
server/AyaNova/biz/JobStatus.cs
Normal 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
|
||||
20
server/AyaNova/biz/JobType.cs
Normal file
20
server/AyaNova/biz/JobType.cs
Normal 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
|
||||
382
server/AyaNova/biz/JobsBiz.cs
Normal file
382
server/AyaNova/biz/JobsBiz.cs
Normal 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
|
||||
|
||||
456
server/AyaNova/biz/LocaleBiz.cs
Normal file
456
server/AyaNova/biz/LocaleBiz.cs
Normal 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
|
||||
|
||||
105
server/AyaNova/biz/PrimeData.cs
Normal file
105
server/AyaNova/biz/PrimeData.cs
Normal 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
|
||||
275
server/AyaNova/biz/TagBiz.cs
Normal file
275
server/AyaNova/biz/TagBiz.cs
Normal 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
|
||||
|
||||
180
server/AyaNova/biz/TagMapBiz.cs
Normal file
180
server/AyaNova/biz/TagMapBiz.cs
Normal 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
|
||||
|
||||
15
server/AyaNova/biz/TaggableAttribute.cs
Normal file
15
server/AyaNova/biz/TaggableAttribute.cs
Normal 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
|
||||
96
server/AyaNova/biz/TrialBiz.cs
Normal file
96
server/AyaNova/biz/TrialBiz.cs
Normal 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
|
||||
|
||||
12
server/AyaNova/biz/ValidationError.cs
Normal file
12
server/AyaNova/biz/ValidationError.cs
Normal 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
|
||||
22
server/AyaNova/biz/ValidationErrorType.cs
Normal file
22
server/AyaNova/biz/ValidationErrorType.cs
Normal 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
|
||||
|
||||
|
||||
|
||||
314
server/AyaNova/biz/WidgetBiz.cs
Normal file
314
server/AyaNova/biz/WidgetBiz.cs
Normal 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
|
||||
|
||||
72
server/AyaNova/generator/BackgroundService.cs
Normal file
72
server/AyaNova/generator/BackgroundService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
54
server/AyaNova/generator/CoreJobMetricsReport.cs
Normal file
54
server/AyaNova/generator/CoreJobMetricsReport.cs
Normal 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
|
||||
|
||||
132
server/AyaNova/generator/CoreJobMetricsSnapshot.cs
Normal file
132
server/AyaNova/generator/CoreJobMetricsSnapshot.cs
Normal 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
|
||||
|
||||
127
server/AyaNova/generator/CoreJobSweeper.cs
Normal file
127
server/AyaNova/generator/CoreJobSweeper.cs
Normal 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
|
||||
|
||||
124
server/AyaNova/generator/Generate.cs
Normal file
124
server/AyaNova/generator/Generate.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
91
server/AyaNova/models/AyContext.cs
Normal file
91
server/AyaNova/models/AyContext.cs
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
//-----------
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
37
server/AyaNova/models/FileAttachment.cs
Normal file
37
server/AyaNova/models/FileAttachment.cs
Normal 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; }
|
||||
20
server/AyaNova/models/License.cs
Normal file
20
server/AyaNova/models/License.cs
Normal 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; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
52
server/AyaNova/models/Locale.cs
Normal file
52
server/AyaNova/models/Locale.cs
Normal 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
|
||||
28
server/AyaNova/models/LocaleItem.cs
Normal file
28
server/AyaNova/models/LocaleItem.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
60
server/AyaNova/models/OpsJob.cs
Normal file
60
server/AyaNova/models/OpsJob.cs
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
server/AyaNova/models/OpsJobLog.cs
Normal file
35
server/AyaNova/models/OpsJobLog.cs
Normal 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
30
server/AyaNova/models/Tag.cs
Normal file
30
server/AyaNova/models/Tag.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
33
server/AyaNova/models/TagMap.cs
Normal file
33
server/AyaNova/models/TagMap.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
32
server/AyaNova/models/User.cs
Normal file
32
server/AyaNova/models/User.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
server/AyaNova/models/Widget.cs
Normal file
35
server/AyaNova/models/Widget.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
20
server/AyaNova/models/dto/ImportV7MapItem.cs
Normal file
20
server/AyaNova/models/dto/ImportV7MapItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
server/AyaNova/models/dto/JobOperationsFetchInfo.cs
Normal file
40
server/AyaNova/models/dto/JobOperationsFetchInfo.cs
Normal 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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
26
server/AyaNova/models/dto/JobOperationsLogInfoItem.cs
Normal file
26
server/AyaNova/models/dto/JobOperationsLogInfoItem.cs
Normal 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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
server/AyaNova/models/dto/NameIdActiveItem.cs
Normal file
12
server/AyaNova/models/dto/NameIdActiveItem.cs
Normal 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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
10
server/AyaNova/models/dto/NameIdItem.cs
Normal file
10
server/AyaNova/models/dto/NameIdItem.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace AyaNova.Models
|
||||
{
|
||||
|
||||
public partial class NameIdItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
12
server/AyaNova/models/dto/NameItem.cs
Normal file
12
server/AyaNova/models/dto/NameItem.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
11
server/AyaNova/models/dto/NewTextIdConcurrencyTokenItem.cs
Normal file
11
server/AyaNova/models/dto/NewTextIdConcurrencyTokenItem.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
14
server/AyaNova/models/dto/TagMapInfo.cs
Normal file
14
server/AyaNova/models/dto/TagMapInfo.cs
Normal 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; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
13
server/AyaNova/models/dto/TypeAndIdInfo.cs
Normal file
13
server/AyaNova/models/dto/TypeAndIdInfo.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using AyaNova.Biz;
|
||||
|
||||
namespace AyaNova.Models
|
||||
{
|
||||
|
||||
public class TypeAndIdInfo
|
||||
{
|
||||
public long ObjectId { get; set; }
|
||||
public AyaType ObjectType { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
14
server/AyaNova/models/dto/UploadedFileInfo.cs
Normal file
14
server/AyaNova/models/dto/UploadedFileInfo.cs
Normal 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; }
|
||||
}
|
||||
|
||||
}
|
||||
1415
server/AyaNova/resource/de.json
Normal file
1415
server/AyaNova/resource/de.json
Normal file
File diff suppressed because it is too large
Load Diff
1415
server/AyaNova/resource/en.json
Normal file
1415
server/AyaNova/resource/en.json
Normal file
File diff suppressed because it is too large
Load Diff
1415
server/AyaNova/resource/es.json
Normal file
1415
server/AyaNova/resource/es.json
Normal file
File diff suppressed because it is too large
Load Diff
1415
server/AyaNova/resource/fr.json
Normal file
1415
server/AyaNova/resource/fr.json
Normal file
File diff suppressed because it is too large
Load Diff
16
server/AyaNova/util/ApplicationLogging.cs
Normal file
16
server/AyaNova/util/ApplicationLogging.cs
Normal 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
Reference in New Issue
Block a user