This commit is contained in:
2018-09-04 18:38:39 +00:00
parent 1bfb8fcd8a
commit a0296e918d
6 changed files with 543 additions and 24 deletions

View File

@@ -18,16 +18,23 @@ Changes needed to routes??
CODING WORK
+++++++++++
Overall plan for now: anything standing in the way of making the initial client shell UI needs to be done first, everything else can wait
- v7importusers (on hold?)
- Mostly done for now with the exception of client id and headoffice id which await the client and headoffice objects respectively and their importers
*******************************************************
CURRENT DEVELOPMENT OBJECTIVES:
- USER OBJECT
- Check biz rules in v7 for anything that might be missed in RAVEN biz rules
- NEED a document with checklist to go over to ensure that a v7 object ported to RAVEN is "DONE" i.e. not missing any biz rules or properties or something
CLIENT SHELL: anything standing in the way of making the initial client shell UI needs to be done first, everything else can wait
UI Shell is required for driving development processes of backend, once have client can make up initial test forms and then determine best way to write backend biz objects.
Once that is done then can steam ahead on the biz objects but until I have the client I won't know the best way to code them so to avoid re-working shit do it in this order.
********************************************************
IMMEDIATE ITEMS:
================
- USER OBJECT
- User route and controller and biz object
- User routes for create update delete the core User object (no user settings in it) {also see rights in BizRoles.cs as it is not fully fleshed out yet}
- UserOptions object will be used for user configurable settings, not core User stuff to avoid any rights issues or confusion or bypasses
- USER OPTIONS OBJECT
- UserOptions object and routes will be used for user configurable settings, some of it initial shell (timezone, email etc), not core User stuff to avoid any rights issues or confusion or bypasses
- Make user options now even if it only has one setting, I will need it ongoing all the time for a ton of shit.
- Tag groups (modify tags already coded)
@@ -42,6 +49,11 @@ Overall plan for now: anything standing in the way of making the initial client
Ensure all modern best practice security is properly enabled on helloayanova.com so testing is valid
- https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security#Deployment_best_practices
************************************************************************************
FUTURE ITEMS:
=============
CLIENT SHELL
Once I can make the client I need to get into that and make the shell and initial interface with enough stuff to do basic testing initially

View File

@@ -0,0 +1,503 @@
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>
/// 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 UserController : Controller
{
private readonly AyContext ct;
private readonly ILogger<UserController> log;
private readonly ApiServerState serverState;
/// <summary>
/// ctor
/// </summary>
/// <param name="dbcontext"></param>
/// <param name="logger"></param>
/// <param name="apiServerState"></param>
public UserController(AyContext dbcontext, ILogger<UserController> logger, ApiServerState apiServerState)
{
ct = dbcontext;
log = logger;
serverState = apiServerState;
}
/// <summary>
/// Get User
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
/// </summary>
/// <param name="id"></param>
/// <returns>A single User</returns>
[HttpGet("{id}")]
public async Task<IActionResult> GetUser([FromRoute] long id)
{
if (serverState.IsClosed)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!Authorized.IsAuthorizedToRead(HttpContext.Items, AyaType.User))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserBiz biz = new UserBiz(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));
}
//Log
EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.User, AyaEvent.Retrieved), ct);
ct.SaveChanges();
return Ok(new ApiOkResponse(o));
}
/// <summary>
/// Get paged list of Users
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
///
/// </summary>
/// <returns>Paged collection of Users 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.User))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserBiz biz = new UserBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<User> pr = await biz.GetManyAsync(Url, nameof(List), pagingOptions);
return Ok(new ApiOkWithPagingResponse<User>(pr));
}
/// <summary>
/// Get User 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 Users with paging data</returns>
[HttpGet("PickList", Name = nameof(UserPickList))]
public async Task<IActionResult> UserPickList([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.User))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserBiz biz = new UserBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
ApiPagedResponse<NameIdItem> pr = await biz.GetPickListAsync(Url, nameof(UserPickList), pagingOptions, q);
return Ok(new ApiOkWithPagingResponse<NameIdItem>(pr));
}
/// <summary>
/// Put (update) User
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
///
/// </summary>
/// <param name="id"></param>
/// <param name="inObj"></param>
/// <returns></returns>
[HttpPut("{id}")]
public async Task<IActionResult> PutUser([FromRoute] long id, [FromBody] User inObj)
{
if (!serverState.IsOpen)
{
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
var o = await ct.User.SingleOrDefaultAsync(m => m.Id == id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.User, o.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
UserBiz biz = new UserBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Put(o, inObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
//Log
EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.User, AyaEvent.Modified), ct);
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!UserExists(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 = o.ConcurrencyToken }));
}
/// <summary>
/// Patch (update) User
///
/// 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> PatchUser([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument<User> 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
UserBiz biz = new UserBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
var o = await ct.User.SingleOrDefaultAsync(m => m.Id == id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.User, o.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//patch and validate
if (!biz.Patch(o, objectPatch, concurrencyToken))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
try
{
//Log
EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.User, AyaEvent.Modified), ct);
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!UserExists(id))
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
else
{
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken }));
}
/// <summary>
/// Post User
///
/// Required roles:
/// BizAdminFull, InventoryFull, TechFull
/// </summary>
/// <param name="inObj"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostUser([FromBody] User 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.User))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
if (!ModelState.IsValid)
{
return BadRequest(new ApiErrorResponse(ModelState));
}
//Instantiate the business object handler
UserBiz biz = new UserBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
//Create and validate
User o = await biz.CreateAsync(inObj);
if (o == null)
{
//error return
return BadRequest(new ApiErrorResponse(biz.Errors));
}
else
{
//save to get Id
await ct.SaveChangesAsync();
//Log now that we have the Id
EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.User, AyaEvent.Created), ct);
await ct.SaveChangesAsync();
//return success and link
return CreatedAtAction("GetUser", new { id = o.Id }, new ApiCreatedResponse(o));
}
}
/// <summary>
/// Delete User
///
/// Required roles:
/// BizAdminFull, InventoryFull
/// TechFull (owned only)
///
/// </summary>
/// <param name="id"></param>
/// <returns>Ok</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser([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.User.SingleOrDefaultAsync(m => m.Id == id);
if (dbObj == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.User, dbObj.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
//Instantiate the business object handler
UserBiz biz = new UserBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
if (!biz.Delete(dbObj))
{
return BadRequest(new ApiErrorResponse(biz.Errors));
}
//Log
EventLogProcessor.DeleteObject(biz.userId, AyaType.User, dbObj.Id, dbObj.Name, ct);
await ct.SaveChangesAsync();
//Delete children / attached objects
biz.DeleteChildren(dbObj);
return NoContent();
}
private bool UserExists(long id)
{
return ct.User.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.User))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
throw new System.NotSupportedException("Test exception from User 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.User))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
throw new System.ArgumentException("Test exception (ALT) from User controller");
}
/// <summary>
/// Get route that submits a long running operation job for testing
/// </summary>
/// <returns>Nothing</returns>
[HttpGet("TestUserJob")]
public ActionResult TestUserJob()
{
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 = "TestUserJob";
j.JobType = JobType.TestUserJob;
JobsBiz.AddJob(j, ct);
return Accepted(new { JobId = j.GId });//202 accepted
}
//------------
}
}

View File

@@ -50,7 +50,7 @@ namespace AyaNova.Api.Controllers
/// <summary>
/// Get widget
/// Get full widget object
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
@@ -96,8 +96,7 @@ namespace AyaNova.Api.Controllers
/// <summary>
/// Get paged list of widgets
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
/// Required roles: Any role
///
/// </summary>
/// <returns>Paged collection of widgets with paging data</returns>
@@ -132,8 +131,7 @@ namespace AyaNova.Api.Controllers
/// <summary>
/// Get widget pick list
///
/// Required roles:
/// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
/// Required roles: Any
///
/// This list supports querying the Name property
/// include a "q" parameter for string to search for
@@ -475,7 +473,7 @@ namespace AyaNova.Api.Controllers
/// <summary>
/// Get route that submits a long running operation job for testing
/// Get route that submits a simulated long running operation job for testing
/// </summary>
/// <returns>Nothing</returns>
[HttpGet("TestWidgetJob")]

View File

@@ -18,18 +18,23 @@ namespace AyaNova.Biz
//Add all object roles here
//NOTE: do not need to add change roles to read roles, Authorized.cs takes care of that automatically
//by assuming if you can change you can read
//HOW THIS WORKS / WHATS EXPECTED
//CHANGE = CREATE, RETRIEVE, UPDATE, DELETE - Full rights
//EDITOWN = special subset of CHANGE: You can create and if it's one you created then you have rights to edit it or delete, but you can't edit ones others have created
//READ = You can read *all* the fields of the record, but can't modify it.
//PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly
//DELETE = There is no specific delete right for now though it's checked for by routes in Authorized.cs in case we want to add it in future as a separate right from create.
#region All roles initialization
////////////////////////////////////////////////////////////
//USER
//
//TODO: flesh this out more when user routes are made
//These rights only apply to the core User object itself
//any settings that are user configurable should go under a UserOptions object instead
//
roles.Add(AyaType.User, new BizRoleSet()
{
Change = AuthorizationRoles.BizAdminFull,
EditOwn = AuthorizationRoles.NoRole,//Only biz admin has full rights to edit a user?? Maybe minor changes are allowed or not stored as a User sub field for user configurable things
EditOwn = AuthorizationRoles.NoRole,//no one can make a user but a bizadminfull
Read = AuthorizationRoles.BizAdminFull | AuthorizationRoles.BizAdminLimited
});
@@ -40,8 +45,7 @@ namespace AyaNova.Biz
{
Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.InventoryFull,
EditOwn = AuthorizationRoles.TechFull,
Read = AuthorizationRoles.BizAdminLimited | AuthorizationRoles.InventoryLimited |
AuthorizationRoles.TechFull | AuthorizationRoles.TechLimited | AuthorizationRoles.AccountingFull
Read = AuthorizationRoles.AnyRole
});
////////////////////////////////////////////////////////////
@@ -143,7 +147,6 @@ namespace AyaNova.Biz
////////////////////////////////////////////////////////////////////
#endregion all roles init

View File

@@ -144,8 +144,6 @@ namespace AyaNova.Biz
//----------------
JobsBiz.LogJob(job.GId, "ImportAyaNova7 finished", ct);
JobsBiz.UpdateJobStatus(job.GId, JobStatus.Completed, ct);

View File

@@ -260,6 +260,9 @@ namespace AyaNova.Biz
BrokenRules.Assert("HeadOfficeIDInvalid", "Error.Object.RequiredFieldEmpty,O.HeadOffice", "HeadOfficeID",
(mUserType == UserTypes.HeadOffice && mHeadOfficeID == Guid.Empty));
ACTIVE: need to check user count against license when re-activating a user
need to check open workorders and any other critical items when de-activating a user
*/
if (!inObj.UserType.IsValid())
@@ -314,7 +317,9 @@ namespace AyaNova.Biz
AddError(ValidationErrorType.InvalidValue, "Roles");
}
//Name must be less than 255 characters
if (inObj.EmployeeNumber.Length > 255)
AddError(ValidationErrorType.LengthExceeded, "EmployeeNumber", "255 max");