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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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