diff --git a/server/AyaNova/Controllers/UserController.cs b/server/AyaNova/Controllers/UserController.cs index f2d3f9e6..e56b7e73 100644 --- a/server/AyaNova/Controllers/UserController.cs +++ b/server/AyaNova/Controllers/UserController.cs @@ -376,6 +376,31 @@ namespace AyaNova.Api.Controllers return Ok(ApiOkResponse.Response(u.UserType != UserType.Customer && u.UserType != UserType.HeadOffice)); } + /// + /// Generate new random credentials for User + /// and email them to the user + /// + /// + /// User id + /// From route path + /// NoContent + [HttpPost("generate-creds-email/{id}")] + public async Task GenerateCredsAndEmailUser([FromRoute] long id, ApiVersion apiVersion) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + UserBiz biz = UserBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + bool successfulOperation=await biz.GenerateCredsAndEmailUser(id); + if (successfulOperation == false) + return BadRequest(new ApiErrorResponse(biz.Errors)); + else + return NoContent(); + } + //------------ }//eoc diff --git a/server/AyaNova/biz/NotifyEventProcessor.cs b/server/AyaNova/biz/NotifyEventProcessor.cs index 4aa7f093..7f1bd03e 100644 --- a/server/AyaNova/biz/NotifyEventProcessor.cs +++ b/server/AyaNova/biz/NotifyEventProcessor.cs @@ -125,6 +125,8 @@ namespace AyaNova.Biz }//eom + + //This is told about an event and then determines if there are any subscriptions related to that event and proceses them accordingly //todo: this should take some kind of general event type like the AyaEvent types (i.e. which CRUD operation is in effect if relevant) //and also a biz object before and after or just before if not a change and also a AyaType diff --git a/server/AyaNova/biz/NotifyEventType.cs b/server/AyaNova/biz/NotifyEventType.cs index 2dafccf4..09da113b 100644 --- a/server/AyaNova/biz/NotifyEventType.cs +++ b/server/AyaNova/biz/NotifyEventType.cs @@ -41,7 +41,8 @@ namespace AyaNova.Biz QuoteStatusAge = 29,//* Quote object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set WorkorderFinished = 30, //*Service work order is set to any status that is flagged as a "Finished" type of status. Customer & User WorkorderCreatedForCustomer = 31, //*Service work order is created for Customer, only applies to that customer user notify sub for that customer, customer id is in conditional ID value for subscription - WorkorderFinishedFollowUp = 32 //* Service workorder closed status follow up again after this many TIMESPAN + WorkorderFinishedFollowUp = 32, //* Service workorder closed status follow up again after this many TIMESPAN + SendUserCredentials = 33 // Internal System use only: When user generates new credentials and sends them this is the notification type for that see UserBiz GenerateCredsAndEmailUser //NEW ITEMS REQUIRE translation KEYS diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index 2f45e120..4c9c175b 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -310,6 +310,64 @@ namespace AyaNova.Biz } + ///////////////////////////////////////////// + // GENERATE AND EMAIL CREDS + // + internal async Task GenerateCredsAndEmailUser(long userId) + { + User dbObject = await ct.User.Include(o => o.UserOptions).FirstOrDefaultAsync(z => z.Id == userId); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND); + return false; + } + if (string.IsNullOrWhiteSpace(dbObject.UserOptions.EmailAddress)) + { + AddError(ApiErrorCode.VALIDATION_REQUIRED, "EmailAddress"); + return false; + } + var ServerUrl = ServerGlobalOpsSettingsCache.Notify.AyaNovaServerURL; + if (string.IsNullOrWhiteSpace(ServerUrl)) + { + await NotifyEventProcessor.AddOpsProblemEvent("User::GenerateCredsAndEmailUser - The OPS Notification setting is empty for AyaNova Server URL. This prevents Notification system from linking events to openable objects."); + AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServerUrl", "Error: no server url configured in notification settings. Can't direct user to server for login. Set server URL and try again."); + return false; + } + + + var newPassword = Hasher.GetRandomAlphanumericString(32); + var newLogin = Hasher.GetRandomAlphanumericString(32); + dbObject.Password = Hasher.hash(dbObject.Salt, newPassword); + dbObject.Login = newLogin; + await ct.SaveChangesAsync(); + + //send message + ServerUrl = ServerUrl.Trim().TrimEnd('/'); + + //Translations + List TransKeysRequired = new List(); + TransKeysRequired.Add("UserLogin"); + TransKeysRequired.Add("UserPassword"); + TransKeysRequired.Add("NewCredsMessageBody"); + TransKeysRequired.Add("NewCredsMessageTitle"); + long EffectiveTranslationId = dbObject.UserOptions.TranslationId; + if (EffectiveTranslationId == 0) EffectiveTranslationId = ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID; + var TransDict = await TranslationBiz.GetSubsetStaticAsync(TransKeysRequired, EffectiveTranslationId); + var Title = TransDict["NewCredsMessageTitle"]; + var NewCredsMessage = TransDict["NewCredsMessageBody"]; + var Creds = $"{TransDict["UserLogin"]}:\n{newLogin}\n{TransDict["UserPassword"]}:\n{newPassword}\n"; + + IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer; + + await m.SendEmailAsync(dbObject.UserOptions.EmailAddress, Title, $"{NewCredsMessage}{Creds}{ServerUrl}/home-user-settings", ServerGlobalOpsSettingsCache.Notify); + + //Log modification and save context + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified, "GeneratedNewCredentialsAndEmailedToUser"), ct); + return true; + } + + + private async Task SearchIndexAsync(User obj, bool isNew) { //SEARCH INDEXING @@ -592,7 +650,7 @@ namespace AyaNova.Biz return DownloadUser; } - + //////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/server/AyaNova/util/Hasher.cs b/server/AyaNova/util/Hasher.cs index ab35bb13..3514c8d2 100644 --- a/server/AyaNova/util/Hasher.cs +++ b/server/AyaNova/util/Hasher.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using Microsoft.AspNetCore.Cryptography.KeyDerivation; namespace AyaNova.Util @@ -32,6 +34,39 @@ namespace AyaNova.Util return Convert.ToBase64String(salt); } + + + public static string GetRandomAlphanumericString(int length) + { + const string alphanumericCharacters = "0123456789abcdefghijkmnopqrstuvwxyz"; + return GetRandomString(length, alphanumericCharacters); + } + + public static string GetRandomString(int length, IEnumerable characterSet) + { + if (length < 0) + throw new ArgumentException("length must not be negative", "length"); + if (length > int.MaxValue / 8) // 250 million chars ought to be enough for anybody + throw new ArgumentException("length is too big", "length"); + if (characterSet == null) + throw new ArgumentNullException("characterSet"); + var characterArray = characterSet.Distinct().ToArray(); + if (characterArray.Length == 0) + throw new ArgumentException("characterSet must not be empty", "characterSet"); + + var bytes = new byte[length * 8]; + new RNGCryptoServiceProvider().GetBytes(bytes); + var result = new char[length]; + for (int i = 0; i < length; i++) + { + ulong value = BitConverter.ToUInt64(bytes, i * 8); + result[i] = characterArray[value % (uint)characterArray.Length]; + } + return new string(result); + } + + + }//eoc }//eons \ No newline at end of file