From 27fd1c5db23b34456161cc71ac198bbbaf6086a0 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Sun, 17 May 2020 14:24:26 +0000 Subject: [PATCH] --- server/AyaNova/Controllers/UserController.cs | 119 ++++---- server/AyaNova/biz/UserBiz.cs | 274 +++++++++++++++---- server/AyaNova/biz/WidgetBiz.cs | 4 +- 3 files changed, 300 insertions(+), 97 deletions(-) diff --git a/server/AyaNova/Controllers/UserController.cs b/server/AyaNova/Controllers/UserController.cs index 9ceca15f..da6e67f0 100644 --- a/server/AyaNova/Controllers/UserController.cs +++ b/server/AyaNova/Controllers/UserController.cs @@ -83,65 +83,92 @@ namespace AyaNova.Api.Controllers } - /// /// Put (update) User /// (Login and / or Password are not changed if set to null / omitted) - /// - /// - /// + /// + /// /// - [HttpPut("{id}")] - public async Task PutUser([FromRoute] long id, [FromBody] User inObj) + [HttpPut] + public async Task PutUser([FromBody] User updatedObject) { - if (!serverState.IsOpen) + if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - if (!ModelState.IsValid) - { return BadRequest(new ApiErrorResponse(ModelState)); - } - - var o = await ct.User.SingleOrDefaultAsync(m => m.Id == id); - + UserBiz biz = UserBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + var o = await biz.PutAsync(updatedObject);//In future may need to return entire object, for now just concurrency token if (o == null) { - return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); - } - - //Instantiate the business object handler - UserBiz biz = UserBiz.GetBiz(ct, HttpContext); - - if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) - { - return StatusCode(403, new ApiNotAuthorizedResponse()); - } - - - - try - { - if (!await biz.PutAsync(o, inObj)) + if (biz.Errors.Exists(m => m.Code == ApiErrorCode.CONCURRENCY_CONFLICT)) + return StatusCode(409, new ApiErrorResponse(biz.Errors)); + else return BadRequest(new ApiErrorResponse(biz.Errors)); } - 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(ApiOkResponse.Response(new { Concurrency = o.Concurrency }, true)); + return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency }, true)); ; } + // /// + // /// Put (update) User + // /// (Login and / or Password are not changed if set to null / omitted) + // /// + // /// + // /// + // /// + // [HttpPut("{id}")] + // public async Task PutUser([FromRoute] long id, [FromBody] User inObj) + // { + // if (!serverState.IsOpen) + // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, 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)); + // } + + // //Instantiate the business object handler + // UserBiz biz = UserBiz.GetBiz(ct, HttpContext); + + // if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + // { + // return StatusCode(403, new ApiNotAuthorizedResponse()); + // } + + + + // try + // { + // if (!await biz.PutAsync(o, inObj)) + // return BadRequest(new ApiErrorResponse(biz.Errors)); + // } + // 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(ApiOkResponse.Response(new { Concurrency = o.Concurrency }, true)); + // } + + /// /// Create User @@ -152,7 +179,7 @@ namespace AyaNova.Api.Controllers [HttpPost] public async Task PostUser([FromBody] User inObj, ApiVersion apiVersion) { - if (!serverState.IsOpen) + if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); //Instantiate the business object handler @@ -200,7 +227,7 @@ namespace AyaNova.Api.Controllers [HttpDelete("{id}")] public async Task DeleteUser([FromRoute] long id) { - if (!serverState.IsOpen) + if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); if (!ModelState.IsValid) diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index 49d2d9eb..0ef5ec31 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -1,11 +1,13 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; - using EnumsNET; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; +using System; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; namespace AyaNova.Biz { @@ -43,7 +45,12 @@ namespace AyaNova.Biz return new UserBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); } - + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.User.AnyAsync(e => e.Id == id); + } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE @@ -114,14 +121,16 @@ namespace AyaNova.Biz /// GET //Get one - internal async Task GetAsync(long fetchId) + internal async Task GetAsync(long Id, bool logTheGetEvent = true) { + //This is simple so nothing more here, but often will be copying to a different output object or some other ops - var dbFullUser = await ct.User.SingleOrDefaultAsync(m => m.Id == fetchId); + var dbFullUser = await ct.User.SingleOrDefaultAsync(m => m.Id == Id); if (dbFullUser != null) { //Log - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, fetchId, BizType, AyaEvent.Retrieved), ct); + if (logTheGetEvent) + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, Id, BizType, AyaEvent.Retrieved), ct); dtUser retUser = new dtUser(); @@ -136,65 +145,137 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // - - //put - internal async Task PutAsync(User dbObj, User inObj) + internal async Task PutAsync(User putObject) { - //Get a snapshot of the original db value object before changes + User dbObject = await ct.User.SingleOrDefaultAsync(m => m.Id == putObject.Id); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND, "id"); + return null; + } User SnapshotOfOriginalDBObj = new User(); - CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj); - - //Update the db object with the PUT object values - CopyObject.Copy(inObj, dbObj, "Id, Salt, CurrentAuthToken, DlKey, DlKeyExpire"); - dbObj.Tags = TagBiz.NormalizeTags(dbObj.Tags); - dbObj.CustomFields = JsonUtil.CompactJson(dbObj.CustomFields); + CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); + CopyObject.Copy(putObject, dbObject, "Id, Salt, CurrentAuthToken, DlKey, DlKeyExpire"); + dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); + dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); //NOTE: It's valid to call this without intending to change login or password (null values) //Is the user updating the password? - if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotOfOriginalDBObj.Password != inObj.Password) + if (!string.IsNullOrWhiteSpace(putObject.Password) && SnapshotOfOriginalDBObj.Password != putObject.Password) { //YES password is being updated: - dbObj.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, inObj.Password); + dbObject.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, putObject.Password); } else { //No, use the snapshot password value - dbObj.Password = SnapshotOfOriginalDBObj.Password; - dbObj.Salt = SnapshotOfOriginalDBObj.Salt; + dbObject.Password = SnapshotOfOriginalDBObj.Password; + dbObject.Salt = SnapshotOfOriginalDBObj.Salt; } //Updating login? - if (!string.IsNullOrWhiteSpace(inObj.Login)) + if (!string.IsNullOrWhiteSpace(putObject.Login)) { //YES Login is being updated: - dbObj.Login = inObj.Login; + dbObject.Login = putObject.Login; } else { //No, use the original value - dbObj.Login = SnapshotOfOriginalDBObj.Login; + dbObject.Login = SnapshotOfOriginalDBObj.Login; } - //Set "original" value of concurrency token to input token - //this will allow EF to check it out - ct.Entry(dbObj).OriginalValues["Concurrency"] = inObj.Concurrency; - - await ValidateAsync(dbObj, SnapshotOfOriginalDBObj); - if (HasErrors) - return false; - await ct.SaveChangesAsync(); - - //Log modification and save context - await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObj.Id, BizType, AyaEvent.Modified), ct); - - await SearchIndexAsync(dbObj, false); - - await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObj.Tags, SnapshotOfOriginalDBObj.Tags); - - return true; + ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; + await ValidateAsync(dbObject, SnapshotOfOriginalDBObj); + if (HasErrors) return null; + try + { + await ct.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await ExistsAsync(putObject.Id)) + AddError(ApiErrorCode.NOT_FOUND); + else + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); + await SearchIndexAsync(dbObject, false); + await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); + return dbObject; } - - //put + + + + + // //put + // internal async Task PutAsync(User putObject) + // { + // User dbObject = await ct.User.SingleOrDefaultAsync(m => m.Id == putObject.Id); + // if (dbObject == null) + // { + // AddError(ApiErrorCode.NOT_FOUND, "id"); + // return null; + // } + + // //Get a snapshot of the original db value object before changes + // User SnapshotOfOriginalDBObj = new User(); + // CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); + + // //Update the db object with the PUT object values + // CopyObject.Copy(putObject, dbObject, "Id, Salt, CurrentAuthToken, DlKey, DlKeyExpire"); + // dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); + // dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); + + // //NOTE: It's valid to call this without intending to change login or password (null values) + // //Is the user updating the password? + // if (!string.IsNullOrWhiteSpace(putObject.Password) && SnapshotOfOriginalDBObj.Password != putObject.Password) + // { + // //YES password is being updated: + // dbObject.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, putObject.Password); + // } + // else + // { + // //No, use the snapshot password value + // dbObject.Password = SnapshotOfOriginalDBObj.Password; + // dbObject.Salt = SnapshotOfOriginalDBObj.Salt; + // } + // //Updating login? + // if (!string.IsNullOrWhiteSpace(putObject.Login)) + // { + // //YES Login is being updated: + // dbObject.Login = putObject.Login; + // } + // else + // { + // //No, use the original value + // dbObject.Login = SnapshotOfOriginalDBObj.Login; + // } + + + // //Set "original" value of concurrency token to input token + // //this will allow EF to check it out + // ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; + + // await ValidateAsync(dbObject, SnapshotOfOriginalDBObj); + // if (HasErrors) + // return false; + // await ct.SaveChangesAsync(); + + // //Log modification and save context + // await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); + + // await SearchIndexAsync(dbObject, false); + + // await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); + + // return true; + // } + + ///////////////////////////////////////////// + //PASSWORD + // internal async Task ChangePasswordAsync(long userId, string newPassword) { User dbObj = await ct.User.FirstOrDefaultAsync(m => m.Id == userId); @@ -485,14 +566,11 @@ namespace AyaNova.Biz // public async Task HandleJobAsync(OpsJob job) { - //just to hide compiler warning for now - await Task.CompletedTask; - //Hand off the particular job to the corresponding processing code - //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so - //basically any error condition during job processing should throw up an exception if it can't be handled switch (job.JobType) { - + case JobType.BulkCoreBizObjectOperation: + await ProcessBulkJobAsync(job); + break; default: throw new System.ArgumentOutOfRangeException($"UserBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); } @@ -500,7 +578,107 @@ namespace AyaNova.Biz } - //Other job handlers here... + + + private async Task ProcessBulkJobAsync(OpsJob job) + { + await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running, ct); + await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} started...", ct); + + List idList = new List(); + long ProcessedObjectCount = 0; + JObject jobData = JObject.Parse(job.JobInfo); + + + //TAGS? + string ToTag = null; + string Tag = null; + if (jobData.ContainsKey("tag")) + Tag = (string)jobData["tag"]; + + if (jobData.ContainsKey("toTag")) + ToTag = (string)jobData["toTag"]; + + if (jobData.ContainsKey("idList")) + { + idList = ((JArray)jobData["idList"]).ToObject>(); + // jobData["idList"].Value>(); + } + else + { + //we need to fetch a list of them all because ALL items was selected + //If they wanted to filter (say by Active=true) they can do that from a grid list mass op + idList = await ct.User.Select(m => m.Id).ToListAsync(); + } + + + bool SaveIt = false; + //Iterate the list fetching each in turn and sending to the processor to handle + foreach (long id in idList) + { + try + { + SaveIt = false; + //errors would pile up here if not cleared on each iteration + ClearErrors(); + + //Fetch + var o = await GetAsync(id, false); + //call out processor for each item in turn + switch (job.SubType) + { + case JobSubType.TagAddAny: + case JobSubType.TagAdd: + if (!o.Tags.Contains(Tag)) + { + o.Tags.Add(Tag); + SaveIt = true; + } + break; + case JobSubType.TagRemoveAny: + case JobSubType.TagRemove: + SaveIt = o.Tags.Remove(Tag); + break; + case JobSubType.TagReplaceAny: + case JobSubType.TagReplace: + int index = o.Tags.IndexOf(Tag); + if (index != -1) + { + o.Tags[index] = ToTag; + SaveIt = true; + } + break; + default: + throw new System.ArgumentOutOfRangeException($"ProcessBulkJob -> Invalid job Subtype{job.SubType}"); + } + + if (SaveIt) + { + User u = new User(); + CopyObject.Copy(o, u, "Id, Salt, CurrentAuthToken, DlKey, DlKeyExpire"); + u = await PutAsync(u); + if (u == null) + await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}: {GetErrorsAsString()}", ct); + else + ProcessedObjectCount++; + } + else + { + + ProcessedObjectCount++; + } + + } + catch (Exception ex) + { + await JobsBiz.LogJobAsync(job.GId, $"Error processing item {id}", ct); + await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex), ct); + } + } + await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} processed {ProcessedObjectCount} of {idList.Count}", ct); + await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed, ct); + } + ///////////////////////////////////////////////////////////////////// diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs index ea594f24..c63ba008 100644 --- a/server/AyaNova/biz/WidgetBiz.cs +++ b/server/AyaNova/biz/WidgetBiz.cs @@ -324,9 +324,7 @@ namespace AyaNova.Biz private async Task ProcessBulkJobAsync(OpsJob job) - { - - //Simulate a long running job here + { await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running, ct); await JobsBiz.LogJobAsync(job.GId, $"Bulk job {job.SubType} started...", ct);