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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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