diff --git a/server/AyaNova/Controllers/UserOptionsController.cs b/server/AyaNova/Controllers/UserOptionsController.cs
index 8b879bfa..9a75194f 100644
--- a/server/AyaNova/Controllers/UserOptionsController.cs
+++ b/server/AyaNova/Controllers/UserOptionsController.cs
@@ -15,7 +15,7 @@ using AyaNova.Biz;
namespace AyaNova.Api.Controllers
{
-
+
///
/// UserOptions
///
@@ -44,15 +44,13 @@ namespace AyaNova.Api.Controllers
}
-
-
///
/// Get full UserOptions object
///
/// Required roles:
- /// BizAdminFull, InventoryFull, BizAdminLimited, InventoryLimited, TechFull, TechLimited, Accounting
+ /// BizAdminFull, BizAdminLimited or be users own record
///
- ///
+ /// UserId
/// A single UserOptions
[HttpGet("{id}")]
public async Task GetUserOptions([FromRoute] long id)
@@ -62,7 +60,10 @@ namespace AyaNova.Api.Controllers
return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
}
- if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
+ var UserId = UserIdFromContext.Id(HttpContext.Items);
+
+ //Different than normal here: a user is *always* allowed to retrieve their own user options object
+ if (id != UserId && !Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
@@ -73,101 +74,29 @@ namespace AyaNova.Api.Controllers
}
//Instantiate the business object handler
- UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+ UserOptionsBiz biz = new UserOptionsBiz(ct, UserId, 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.UserOptions, AyaEvent.Retrieved), ct);
- ct.SaveChanges();
+ }
+
return Ok(new ApiOkResponse(o));
}
- ///
- /// Get paged list of UserOptionss
- ///
- /// Required roles: Any
- ///
- ///
- /// Paged collection of UserOptionss with paging data
- [HttpGet("ListUserOptionss", Name = nameof(ListUserOptionss))]//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 ListUserOptionss([FromQuery] PagingOptions pagingOptions)
- {
- if (serverState.IsClosed)
- {
- return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
- }
-
- if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
- {
- return StatusCode(401, new ApiNotAuthorizedResponse());
- }
-
- if (!ModelState.IsValid)
- {
- return BadRequest(new ApiErrorResponse(ModelState));
- }
-
- //Instantiate the business object handler
- UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
-
- ApiPagedResponse pr = await biz.GetManyAsync(Url, nameof(ListUserOptionss), pagingOptions);
- return Ok(new ApiOkWithPagingResponse(pr));
- }
-
-
-
- ///
- /// Get UserOptions pick list
- ///
- /// Required roles: Any
- ///
- /// 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
- ///
- /// Paged id/name collection of UserOptionss with paging data
- [HttpGet("PickList", Name = nameof(UserOptionsPickList))]
- public async Task UserOptionsPickList([FromQuery] string q, [FromQuery] PagingOptions pagingOptions)
- {
- 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
- UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
-
- ApiPagedResponse pr = await biz.GetPickListAsync(Url, nameof(UserOptionsPickList), pagingOptions, q);
- return Ok(new ApiOkWithPagingResponse(pr));
- }
-
-
+
///
/// Put (update) UserOptions
///
/// Required roles:
- /// BizAdminFull, InventoryFull
- /// TechFull (owned only)
+ /// BizAdminFull or be users own record
///
///
- ///
+ /// User id
///
///
[HttpPut("{id}")]
@@ -182,15 +111,16 @@ namespace AyaNova.Api.Controllers
{
return BadRequest(new ApiErrorResponse(ModelState));
}
+ var UserId = UserIdFromContext.Id(HttpContext.Items);
- var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id);
+ var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
- if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
+ if (id != UserId && !Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
@@ -217,15 +147,11 @@ namespace AyaNova.Api.Controllers
}
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
+ //exists but was changed by another user
return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
}
}
-
-
return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken }));
}
@@ -235,17 +161,15 @@ namespace AyaNova.Api.Controllers
/// Patch (update) UserOptions
///
/// Required roles:
- /// BizAdminFull, InventoryFull
- /// TechFull (owned only)
+ /// BizAdminFull or be users own record
///
- ///
+ /// UserId
///
///
///
[HttpPatch("{id}/{concurrencyToken}")]
public async Task PatchUserOptions([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument objectPatch)
- {
- //https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/
+ {
if (!serverState.IsOpen)
{
@@ -257,18 +181,19 @@ namespace AyaNova.Api.Controllers
return BadRequest(new ApiErrorResponse(ModelState));
}
+ var UserId = UserIdFromContext.Id(HttpContext.Items);
+
//Instantiate the business object handler
- UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+ UserOptionsBiz biz = new UserOptionsBiz(ct, UserId, UserRolesFromContext.Roles(HttpContext.Items));
-
- var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == id);
+ var o = await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == id);
if (o == null)
{
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
}
- if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
+ if (id != UserId && !Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.UserOptions, o.OwnerId))
{
return StatusCode(401, new ApiNotAuthorizedResponse());
}
@@ -297,197 +222,18 @@ namespace AyaNova.Api.Controllers
}
}
-
-
-
return Ok(new ApiOkResponse(new { ConcurrencyToken = o.ConcurrencyToken }));
}
- ///
- /// Post UserOptions
- ///
- /// Required roles:
- /// BizAdminFull, InventoryFull, TechFull
- ///
- ///
- ///
- [HttpPost]
- public async Task PostUserOptions([FromBody] UserOptions 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.UserOptions))
- {
- return StatusCode(401, new ApiNotAuthorizedResponse());
- }
-
- if (!ModelState.IsValid)
- {
- return BadRequest(new ApiErrorResponse(ModelState));
- }
-
- //Instantiate the business object handler
- UserOptionsBiz biz = new UserOptionsBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
-
- //Create and validate
- UserOptions 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.UserOptions, AyaEvent.Created), ct);
- await ct.SaveChangesAsync();
-
- //return success and link
- return CreatedAtAction("GetUserOptions", new { id = o.Id }, new ApiCreatedResponse(o));
- }
- }
-
-
-
- ///
- /// Delete UserOptions
- ///
- /// Required roles:
- /// BizAdminFull, InventoryFull
- /// TechFull (owned only)
- ///
- ///
- ///
- /// Ok
- [HttpDelete("{id}")]
- public async Task DeleteUserOptions([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.UserOptions.SingleOrDefaultAsync(m => m.Id == id);
- if (dbObj == null)
- {
- return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
- }
-
- if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.UserOptions, dbObj.OwnerId))
- {
- return StatusCode(401, new ApiNotAuthorizedResponse());
- }
-
- //Instantiate the business object handler
- UserOptionsBiz biz = new UserOptionsBiz(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.UserOptions, dbObj.Id, dbObj.Name, ct);
-
- await ct.SaveChangesAsync();
-
- //Delete children / attached objects
- biz.DeleteChildren(dbObj);
-
-
- return NoContent();
- }
-
-
-
private bool UserOptionsExists(long id)
{
- return ct.UserOptions.Any(e => e.Id == id);
+ //NOTE: checks by UserId, NOT by Id as in most other objects
+ return ct.UserOptions.Any(e => e.UserId == id);
}
- ///
- /// Get route that triggers exception for testing
- ///
- /// Nothing, triggers exception
- [HttpGet("exception")]
- public ActionResult GetException()
- {
- if (!serverState.IsOpen)
- {
- return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
- }
-
- if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
- {
- return StatusCode(401, new ApiNotAuthorizedResponse());
- }
-
- throw new System.NotSupportedException("Test exception from UserOptions controller");
- }
-
- ///
- /// Get route that triggers an alternate type of exception for testing
- ///
- /// Nothing, triggers exception
- [HttpGet("altexception")]
- public ActionResult GetAltException()
- {
- if (!serverState.IsOpen)
- {
- return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
- }
-
- if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.UserOptions))
- {
- return StatusCode(401, new ApiNotAuthorizedResponse());
- }
-
- throw new System.ArgumentException("Test exception (ALT) from UserOptions controller");
- }
-
-
- ///
- /// Get route that submits a simulated long running operation job for testing
- ///
- /// Nothing
- [HttpGet("TestUserOptionsJob")]
- public ActionResult TestUserOptionsJob()
- {
- 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 = "TestUserOptionsJob";
- j.JobType = JobType.TestUserOptionsJob;
- JobsBiz.AddJob(j, ct);
- return Accepted(new { JobId = j.GId });//202 accepted
- }
-
//------------
diff --git a/server/AyaNova/biz/AyaType.cs b/server/AyaNova/biz/AyaType.cs
index 5bebd5e3..722b37d0 100644
--- a/server/AyaNova/biz/AyaType.cs
+++ b/server/AyaNova/biz/AyaType.cs
@@ -25,7 +25,8 @@ namespace AyaNova.Biz
AyaNova7Import = 10,
TrialSeeder = 11,
Metrics = 12,
- Locale = 13
+ Locale = 13,
+ UserOptions=14
}
diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs
index 704e079f..56b833a9 100644
--- a/server/AyaNova/biz/BizRoles.cs
+++ b/server/AyaNova/biz/BizRoles.cs
@@ -23,7 +23,7 @@ namespace AyaNova.Biz
//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. Change is automatically checked for so only add different roles from change
- //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
+ //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
@@ -38,6 +38,19 @@ namespace AyaNova.Biz
ReadFullRecord = AuthorizationRoles.BizAdminLimited
});
+ ////////////////////////////////////////////////////////////
+ //USEROPTIONS
+ //(Identical to User, though route also allows own record access full changes)
+ //
+ roles.Add(AyaType.UserOptions, new BizRoleSet()
+ {
+ Change = AuthorizationRoles.BizAdminFull,
+ EditOwn = AuthorizationRoles.NoRole,//no one can make a user but a bizadminfull
+ ReadFullRecord = AuthorizationRoles.BizAdminLimited
+ });
+
+
+
////////////////////////////////////////////////////////////
//WIDGET
//
diff --git a/server/AyaNova/biz/UserOptionsBiz.cs b/server/AyaNova/biz/UserOptionsBiz.cs
index f909eca8..d0d46883 100644
--- a/server/AyaNova/biz/UserOptionsBiz.cs
+++ b/server/AyaNova/biz/UserOptionsBiz.cs
@@ -35,8 +35,9 @@ namespace AyaNova.Biz
//Get one
internal async Task GetAsync(long fetchId)
{
+ //NOTE: get by UserId as there is a 1:1 relationship, not by useroptions id
//This is simple so nothing more here, but often will be copying to a different output object or some other ops
- return await ct.UserOptions.SingleOrDefaultAsync(m => m.Id == fetchId);
+ return await ct.UserOptions.SingleOrDefaultAsync(m => m.UserId == fetchId);
}
@@ -49,7 +50,7 @@ namespace AyaNova.Biz
{
//Replace the db object with the PUT object
- CopyObject.Copy(inObj, dbObj, "Id");
+ CopyObject.Copy(inObj, dbObj, "Id, UserId, OwnerId");
//Set "original" value of concurrency token to input token
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
@@ -65,7 +66,7 @@ namespace AyaNova.Biz
internal bool Patch(UserOptions dbObj, JsonPatchDocument objectPatch, uint concurrencyToken)
{
//Validate Patch is allowed
- if (!ValidateJsonPatch.Validate(this, objectPatch)) return false;
+ if (!ValidateJsonPatch.Validate(this, objectPatch, "UserId")) return false;
//Do the patching
objectPatch.ApplyTo(dbObj);
diff --git a/test/raven-integration/User/UserOptionsRu.cs b/test/raven-integration/User/UserOptionsRu.cs
new file mode 100644
index 00000000..aa4e5a33
--- /dev/null
+++ b/test/raven-integration/User/UserOptionsRu.cs
@@ -0,0 +1,131 @@
+using System;
+using Xunit;
+using Newtonsoft.Json.Linq;
+using FluentAssertions;
+
+namespace raven_integration
+{
+
+ public class UserOptionsRu
+ {
+
+ ///
+ /// Test all CRUD routes for a UserOptions object
+ ///
+ [Fact]
+ public async void RU()
+ {
+
+ //CREATE a user
+ dynamic D1 = new JObject();
+ D1.name = Util.Uniquify("Test UserOptions User");
+ D1.ownerId = 1L;
+ D1.active = true;
+ D1.login = Util.Uniquify("LOGIN");
+ D1.password = Util.Uniquify("PASSWORD");
+ D1.roles = 0;//norole
+ D1.localeId = 1;//random locale
+ D1.userType = 3;//non scheduleable
+
+ ApiResponse R = await Util.PostAsync("User", await Util.GetTokenAsync("manager", "l3tm3in"), D1.ToString());
+ Util.ValidateDataReturnResponseOk(R);
+ long UserId = R.ObjectResponse["result"]["id"].Value();
+
+ //Now there should be a user options available for this user
+
+
+ //RETRIEVE companion USEROPTIONS object
+
+ //Get it
+ R = await Util.GetAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
+ Util.ValidateDataReturnResponseOk(R);
+ //ensure the default value is set
+ R.ObjectResponse["result"]["uiColor"].Value().Should().Be(0);
+ uint concurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value();
+
+ //UPDATE
+ //PUT
+ dynamic D2 = new JObject();
+ D2.emailaddress = "testuseroptions@helloayanova.com";
+ D2.TimeZoneOffset = -7.5M;//Decimal value
+ D2.UiColor = -2097216;//Int value (no suffix for int literals)
+ D2.concurrencyToken = concurrencyToken;
+ ApiResponse PUTTestResponse = await Util.PutAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D2.ToString());
+ Util.ValidateHTTPStatusCode(PUTTestResponse, 200);
+
+ //VALIDATE
+ R = await Util.GetAsync("UserOptions/" + UserId.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
+ Util.ValidateDataReturnResponseOk(R);
+ //ensure the default value is set
+ R.ObjectResponse["result"]["emailAddress"].Value().Should().Be(D2.emailaddress.ToString());
+ R.ObjectResponse["result"]["timeZoneOffset"].Value().Should().Be((decimal)D2.TimeZoneOffset);
+ R.ObjectResponse["result"]["uiColor"].Value().Should().Be((int)D2.UiColor);
+ concurrencyToken = R.ObjectResponse["result"]["concurrencyToken"].Value();
+
+ // //update w2id
+ // D2.name = Util.Uniquify("UPDATED VIA PUT SECOND TEST User");
+ // D2.OwnerId = 1;
+ // D2.concurrencyToken = R2.ObjectResponse["result"]["concurrencyToken"].Value();
+ // ApiResponse PUTTestResponse = await Util.PutAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), D2.ToString());
+ // Util.ValidateHTTPStatusCode(PUTTestResponse, 200);
+
+ // //check PUT worked
+ // ApiResponse checkPUTWorked = await Util.GetAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
+ // Util.ValidateNoErrorInResponse(checkPUTWorked);
+ // checkPUTWorked.ObjectResponse["result"]["name"].Value().Should().Be(D2.name.ToString());
+ // uint concurrencyToken = PUTTestResponse.ObjectResponse["result"]["concurrencyToken"].Value();
+
+ // //PATCH
+ // var newName = Util.Uniquify("UPDATED VIA PATCH SECOND TEST User");
+ // string patchJson = "[{\"value\": \"" + newName + "\",\"path\": \"/name\",\"op\": \"replace\"}]";
+ // ApiResponse PATCHTestResponse = await Util.PatchAsync("User/" + d2Id.ToString() + "/" + concurrencyToken.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"), patchJson);
+ // Util.ValidateHTTPStatusCode(PATCHTestResponse, 200);
+
+ // //check PATCH worked
+ // ApiResponse checkPATCHWorked = await Util.GetAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
+ // Util.ValidateNoErrorInResponse(checkPATCHWorked);
+ // checkPATCHWorked.ObjectResponse["result"]["name"].Value().Should().Be(newName);
+
+ // //DELETE
+ // ApiResponse DELETETestResponse = await Util.DeleteAsync("User/" + d2Id.ToString(), await Util.GetTokenAsync("manager", "l3tm3in"));
+ // Util.ValidateHTTPStatusCode(DELETETestResponse, 204);
+ }
+
+
+ ///
+ /// Test not found
+ ///
+ [Fact]
+ public async void GetNonExistentItemShouldError()
+ {
+ //Get non existant
+ //Should return status code 404, api error code 2010
+ ApiResponse R = await Util.GetAsync("UserOptions/999999", await Util.GetTokenAsync("manager", "l3tm3in"));
+ Util.ValidateResponseNotFound(R);
+ }
+
+ ///
+ /// Test bad modelstate
+ ///
+ [Fact]
+ public async void GetBadModelStateShouldError()
+ {
+ //Get non existant
+ //Should return status code 400, api error code 2200 and a first target in details of "id"
+ ApiResponse R = await Util.GetAsync("UserOptions/2q2", await Util.GetTokenAsync("manager", "l3tm3in"));
+ Util.ValidateBadModelStateResponse(R, "id");
+ }
+
+
+
+
+
+
+
+
+
+
+ //==================================================
+
+ }//eoc
+}//eons