/// <summary> /// Resends activation link for a user account /// </summary> /// <param name="dbCtx"></param> /// <param name="emailSender"></param> /// <param name="emailAccount"></param> /// <param name="emailTemplate"></param> /// <param name="password"></param> /// <returns></returns> public async Task ResendActivationLinkAsync(DbContext dbCtx, IEmailSender emailSender, IEmailAccount emailAccount = null, IEmailTemplate emailTemplate = null, string password = null) { if (Uuid == default(Guid)) { throw new InvalidOperationException("You cannot resend an activation link - this user has not yet been created..."); } var newActivationDetails = await Auth.GetNewAccountActivationDetailsAsync(Uuid); //generate new email confirmation token var emailConfirmationToken = Auth.MergeIdWithToken( Uuid, newActivationDetails.newAccountActivationToken ); //send out the email if (emailTemplate != null && emailAccount != null) { emailSender.Send( emailAccount, emailTemplate.Prepare(new Dictionary <string, object> { { "VerificationKey", emailConfirmationToken }, { "InitialPassword", newActivationDetails.newPass } }), Email ); } }
/// <summary> /// Creates a new user account in both MembershipReboot database and in the MapHive meta database; /// sends out a confirmation email if email account and template are provided /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="TAccount"></typeparam> /// <param name="userAccountService"></param> /// <param name="dbCtx"></param> /// <param name="emailAccount"></param> /// <param name="emailTemplate"></param> /// <returns></returns> protected internal virtual async Task <T> CreateAsync <T, TAccount>(DbContext dbCtx, UserAccountService <TAccount> userAccountService, IEmailAccount emailAccount = null, IEmailTemplate emailTemplate = null) where T : MapHiveUserBase where TAccount : RelationalUserAccount { T output; //need to validate the model first await ValidateAsync(dbCtx); //make sure the email is ALWAYS lower case Email = Email.ToLower(); //check if the email is already used or not; throw validation feedback exception if so //Note - could do it in the mh meta, but both dbs must be in sync anyway var emailInUse = userAccountService.GetByEmail(Email) != null; if (emailInUse) { throw Validation.Utils.GenerateValidationFailedException(nameof(Email), ValidationErrors.EmailInUse); } //user account exists in two places - mbr and mh databases. Therefore need to handle them both properly wrapped into transactions DbContext mbrDbCtx = GetMembershipRebootDbCtx(userAccountService); System.Data.Common.DbTransaction mbrTrans = null; System.Data.Common.DbTransaction mhTrans = null; //since this method wraps the op on 2 dbs into transactions, it must handle connections manually and take care of closing it aftwerwards //it is therefore required to clone contexts with independent conns so the base contexts can be reused var clonedMhDbCtx = dbCtx.Clone(contextOwnsConnection: false); var clonedMbrDbCtx = mbrDbCtx.Clone(false); try { //open the connections as otherwise will not be able to begin transaction await clonedMbrDbCtx.Database.Connection.OpenAsync(); await clonedMhDbCtx.Database.Connection.OpenAsync(); //begin the transaction and set the transaction object back on the db context so it uses it mbrTrans = clonedMbrDbCtx.Database.Connection.BeginTransaction(); clonedMbrDbCtx.Database.UseTransaction(mbrTrans); mhTrans = clonedMhDbCtx.Database.Connection.BeginTransaction(); clonedMhDbCtx.Database.UseTransaction(mhTrans); //first create a membership reboot account //wire up evt too, to intercept what mbr is trying to say... AccountCreatedEvent <TAccount> e = null; userAccountService.Configuration.AddEventHandler(new MembershipRebootEventHandlers.AccountCreatedEventHandler <TAccount>( (evt) => { e = evt; })); var newMbrAccount = userAccountService.CreateAccount(this.Email, Cartomatic.Utils.Crypto.Generator.GenerateRandomString(10), this.Email); //so can next pass some data to the mh meta user object this.Uuid = newMbrAccount.ID; //mbr work done, so can create the user within the mh metadata db output = await base.CreateAsync <T>(clonedMhDbCtx); //looks like we're good to go, so can commit mbrTrans.Commit(); mhTrans.Commit(); var opFeedback = new Dictionary <string, object> { { nameof(e.InitialPassword), e.InitialPassword }, { nameof(e.VerificationKey), e.VerificationKey } }; //if email related objects have been provided, send the account created email if (emailAccount != null && emailTemplate != null) { EmailSender.Send( emailAccount, emailTemplate.Prepare(opFeedback), Email ); } //finally the user created event UserCreated?.Invoke( this, new Events.OpFeedbackEventArgs { OperationFeedback = opFeedback } ); } catch (Exception ex) { mbrTrans?.Rollback(); mhTrans?.Rollback(); throw Validation.Utils.GenerateValidationFailedException(ex); } finally { //try to close the connections as they were opened manually and therefore may not have been closed! clonedMhDbCtx.Database.Connection.CloseConnection(dispose: true); clonedMbrDbCtx.Database.Connection.CloseConnection(dispose: true); mbrTrans?.Dispose(); mhTrans?.Dispose(); } return(output); }
/// <summary> /// Creates a new user account in both Identity database and in the MapHive meta database; /// sends out a confirmation email if email account and template are provided /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbCtx"></param> /// <param name="emailSender"></param> /// <param name="emailAccount"></param> /// <param name="emailTemplate"></param> /// <returns></returns> protected internal virtual async Task <T> CreateAsync <T>(DbContext dbCtx, IEmailSender emailSender, IEmailAccount emailAccount, IEmailTemplate emailTemplate) where T : Base { T output; //need to validate the model first await ValidateAsync(dbCtx); //make sure the email is ALWAYS lower case Email = Email.ToLower(); //grab user manager var userManager = MapHive.Core.Identity.UserManagerUtils.GetUserManager(); //check if the email is already used or not; throw validation feedback exception if so //Note - could do it in the mh meta, but both dbs must be in sync anyway var emailInUse = await userManager.FindByEmailAsync(Email) != null; if (emailInUse) { throw Validation.Utils.GenerateValidationFailedException(nameof(Email), ValidationErrors.EmailInUse); } try { var rndPass = Cartomatic.Utils.Crypto.Generator.GenerateRandomString(10); var idUser = new MapHiveIdentityUser { Id = Guid.NewGuid(), UserName = Email.ToLower(), Email = Email.ToLower() }; var result = await userManager.CreateAsync(idUser, rndPass); //so can next pass some data to the mh meta user object this.Uuid = idUser.Id; //identity work done, so can create the user within the mh metadata db output = await base.CreateAsync <T>(dbCtx); var opFeedback = new Dictionary <string, object> { { "InitialPassword", rndPass }, { "VerificationKey", Auth.MergeIdWithToken( idUser.Id, await userManager.GenerateEmailConfirmationTokenAsync(idUser) ) } }; //if email related objects have been provided, send the account created email if (emailAccount != null && emailTemplate != null) { emailSender.Send( emailAccount, emailTemplate.Prepare(opFeedback), Email ); } //finally the user created event UserCreated?.Invoke( this, new Events.OpFeedbackEventArgs { OperationFeedback = opFeedback } ); } catch (Exception ex) { throw Validation.Utils.GenerateValidationFailedException(ex); } return(output); }
/// <summary> /// Creates a user acount, sends out email, modifies pass if a custom pass is provided; /// this is a simple wrapper over the standard user.CreateAsync that adds an option to provide a specific password /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dbCtx"></param> /// <param name="user"></param> /// <param name="emailSender"></param> /// <param name="emailAccount"></param> /// <param name="emailTemplate"></param> /// <param name="password"></param> /// <returns></returns> public static async Task <CreateUserAccountOutput> CreateUserAccountAsync(DbContext dbCtx, MapHiveUser user, IEmailSender emailSender, IEmailAccount emailAccount = null, IEmailTemplate emailTemplate = null, string password = null) { var output = new CreateUserAccountOutput { User = user }; //see if a user has already been created. if so do not attempt to create it; if (user.Uuid != default(Guid)) { return(output); } //need to grab an initial pass to change it if a pass has been provided var initialPass = string.Empty; //wire up an evt listener, so can react to user created evt and send a confirmation email user.UserCreated += (sender, args) => { initialPass = (string)args.OperationFeedback["InitialPassword"]; //output, so can use it in the m2m tests output.InitialPassword = initialPass; output.VerificationKey = Auth.MergeIdWithToken( user.Uuid, (string)args.OperationFeedback["VerificationKey"] ); //prepare email if present emailTemplate?.Prepare(args.OperationFeedback); if (emailTemplate != null && emailAccount != null) { emailSender.Send( emailAccount, emailTemplate, user.Email ); } }; //create user without auto email send here - it's customised and sent via evt handler above var createdUser = await user.CreateAsync(dbCtx); //once user has been created adjust his pass if provided if (!string.IsNullOrEmpty(password)) { //grab user manager var userManager = MapHive.Core.Identity.UserManagerUtils.GetUserManager(); var idUser = await userManager.FindByIdAsync(user.Uuid.ToString()); await userManager.ChangePasswordAsync(idUser, initialPass, password); } return(output); }
/// <summary> /// Resends an activation link for a user; expects a user_created email template and a valid user identifier /// </summary> /// <param name="context"></param> /// <param name="userId"></param> /// <param name="ea"></param> /// <param name="emailTpl"></param> /// <returns></returns> public static async Task ResendActivationLink(DbContext context, Guid userId, IEmailAccount ea = null, IEmailTemplate emailTpl = null) { //Note: MBR seems to not like resending an activation link with a new password... It simply does not generate a new one but rather regenerates the verification key //because of that need to fake an account creation with some random email and then grab some auto generated data out of it // //basically a new account is created in order to get a new token and a new pass and verification key with its creation date //such user account is then destroyed but the necessary details are set on the account that we try to resend a link for. if (userId == default(Guid)) { throw new InvalidOperationException("You cannot resend an activation link - this user has not yet been created..."); } var mbrCtx = new CustomDbContext("MapHiveMbr"); //get the mbr user object var mbrUser = await mbrCtx.Users.FirstOrDefaultAsync(u => u.ID == userId); if (mbrUser == null) { throw new InvalidOperationException("User does not exist in MBR."); } //need user account service to properly create a MBR user var userAccountService = CustomUserAccountService.GetInstance("MapHiveMbr"); //wire up an evt listener - this is the way mbr talks AccountCreatedEvent <CustomUserAccount> e = null; userAccountService.Configuration.AddEventHandler( new MembershipRebootEventHandlers.AccountCreatedEventHandler <CustomUserAccount>(evt => e = evt)); //rrnd email - after all need to avoid scenarios when two folks try the same resend activation procedure at once var rndEmail = $"{DateTime.Now.Ticks}@somedomain.com"; //finally a new rnd user, so we can get a properly recreated verification key and a new pass... var newMbrAccount = userAccountService.CreateAccount(rndEmail, Cartomatic.Utils.Crypto.Generator.GenerateRandomString(10), rndEmail); //update the account in question with //mbrUser.VerificationKey = newMbrAccount.VerificationKey; //mbrUser.VerificationPurpose = newMbrAccount.VerificationPurpose; //mbrUser.HashedPassword = newMbrAccount.HashedPassword; //mbrUser.VerificationKeySent = newMbrAccount.VerificationKeySent //because the properties are read only, we need to do some crazy hocus-pocus again //note: looks like the type returned via mbrCtx.Users.FirstOrDefaultAsync is somewhat more dynamic and does not //map properly. therefore need to use a 'barebone' object instance var obj = new CustomUserAccount(); //Warning - this sql is postgresql specific! var updateSql = $@"UPDATE {mbrCtx.GetTableSchema(obj)}.""{mbrCtx.GetTableName(obj)}"" SET ""{mbrCtx.GetTableColumnName(obj, nameof(mbrUser.VerificationKey))}"" = '{newMbrAccount.VerificationKey}', ""{mbrCtx.GetTableColumnName(obj, nameof(mbrUser.VerificationPurpose))}"" = {(int)newMbrAccount.VerificationPurpose}, ""{mbrCtx.GetTableColumnName(obj, nameof(mbrUser.HashedPassword))}"" = '{newMbrAccount.HashedPassword}', ""{mbrCtx.GetTableColumnName(obj, nameof(mbrUser.VerificationKeySent))}"" = '{newMbrAccount.VerificationKeySent}' WHERE ""{mbrCtx.GetTableColumnName(obj, nameof(mbrUser.ID))}"" = '{mbrUser.ID}';"; //get rid of the new account mbrCtx.Users.Remove(await mbrCtx.Users.FirstAsync(u => u.ID == newMbrAccount.ID)); //and save da mess await mbrCtx.SaveChangesAsync(); await mbrCtx.Database.ExecuteSqlCommandAsync(updateSql); //send out the email if (emailTpl != null && ea != null) { MapHive.Server.Core.Email.EmailSender.Send( ea, emailTpl.Prepare(new Dictionary <string, object> { { nameof(e.VerificationKey), e.VerificationKey }, { nameof(e.InitialPassword), e.InitialPassword } }), mbrUser.Email ); } }