feature/reset_password_be kohteeseen BE_dev 3 vuotta sitten
| @@ -0,0 +1,387 @@ | |||
| using System.Net.Mail; | |||
| using System.Net; | |||
| using Diligent.WebAPI.Contracts.Models; | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| /// <summary> | |||
| /// Provieds an API for sending emails in both sync & async fashion, as well as sending emails with delays. | |||
| /// </summary> | |||
| public class Emailer : IEmailer | |||
| { | |||
| private readonly AuthorizationSettings _settings; | |||
| public Emailer() | |||
| { | |||
| _settings = new AuthorizationSettings | |||
| { | |||
| SmtpServer = "smtp.mailtrap.io", | |||
| SmtpPort = 2525, | |||
| SmtpUseSSL = true, | |||
| SmtpUsername = "460e3c49f02e37", | |||
| SmtpPassword = "66443869eaad55", | |||
| SmtpFrom = "noreply@hrcenter.net", | |||
| SmtpFromName = "HRCenter Team" | |||
| }; | |||
| } | |||
| /// <summary> | |||
| /// Sends an email asynchronously and inserts a new <see cref="DiligEmail"/> record to the underlying database. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="cc"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <see cref="ArgumentNullException"/> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| public async Task<bool> SendEmailAndWriteToDbAsync(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null) | |||
| { | |||
| var emailResult = await SendEmailAsync(to, subject, body, isHtml, cc); | |||
| var email = CreateEmail(to, subject, body, isHtml); | |||
| if (emailResult) | |||
| { | |||
| email.SentTime = DateTime.UtcNow; | |||
| } | |||
| //var dbResult = await WriteEmailToDbAsync(email); | |||
| return emailResult; // && dbResult > 0; | |||
| } | |||
| public async Task<bool> SendEmailAndWriteToDbAsync(string to, string subject, string body, bool isHtml = false, List<string> cc = null) | |||
| { | |||
| return await SendEmailAndWriteToDbAsync(new List<string> { to }, subject, body, isHtml, cc); | |||
| } | |||
| /// <summary> | |||
| /// Sends an email synchronously and inserts a new <see cref="DiligEmail"/> record to the underlying database. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="cc"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <see cref="ArgumentNullException"/> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| public bool SendEmailAndWriteToDb(List<string> to, string subject, string body, bool isHtml = false, | |||
| List<string> cc = null) | |||
| { | |||
| var emailResult = SendEmail(to, subject, body, isHtml, cc); | |||
| var email = CreateEmail(to, subject, body, isHtml); | |||
| if (emailResult) | |||
| { | |||
| email.SentTime = DateTime.UtcNow; | |||
| } | |||
| //var dbResult = WriteEmailToDb(email); | |||
| return emailResult; // && dbResult > 0; | |||
| } | |||
| public bool SendEmailAndWriteToDb(string to, string subject, string body, bool isHtml = false, | |||
| List<string> cc = null) | |||
| { | |||
| return SendEmailAndWriteToDb(new List<string> { to }, subject, body, isHtml, cc); | |||
| } | |||
| /// <summary> | |||
| /// Sends an email synchronously. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="cc"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <see cref="ArgumentNullException"/> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| public bool SendEmail(List<string> to, string subject, string body, bool isHtml = false, | |||
| List<string> cc = null) | |||
| { | |||
| try | |||
| { | |||
| using (var smtp = GetSmtpClient()) | |||
| { | |||
| var message = GetMailMessage(to, subject, body, isHtml, cc); | |||
| smtp.Send(message); | |||
| return true; | |||
| } | |||
| } | |||
| catch (ArgumentException) | |||
| { | |||
| throw; | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| throw new Exception("Failed to send email message.", e); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Sends an email asynchronously. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="cc"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <see cref="ArgumentNullException"/> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| public async Task<bool> SendEmailAsync(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null) | |||
| { | |||
| try | |||
| { | |||
| using (var smtp = GetSmtpClient()) | |||
| { | |||
| var message = GetMailMessage(to, subject, body, isHtml, cc); | |||
| await smtp.SendMailAsync(message); | |||
| return true; | |||
| } | |||
| } | |||
| catch (ArgumentException) | |||
| { | |||
| throw; | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| throw new Exception("Failed to send email message.", e); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Creates a <see cref="SmtpClient"/> object and configures it. | |||
| /// </summary> | |||
| /// <returns></returns> | |||
| public SmtpClient GetSmtpClient() | |||
| { | |||
| var smtp = new SmtpClient(_settings.SmtpServer) { Timeout = 1000000 }; | |||
| if (!string.IsNullOrWhiteSpace(_settings.SmtpUsername)) | |||
| { | |||
| smtp.UseDefaultCredentials = false; | |||
| smtp.Credentials = new NetworkCredential( | |||
| _settings.SmtpUsername, | |||
| _settings.SmtpPassword); | |||
| smtp.EnableSsl = _settings.SmtpUseSSL; | |||
| smtp.Port = _settings.SmtpPort; | |||
| } | |||
| return smtp; | |||
| } | |||
| /// <summary> | |||
| /// Creates a new <see cref="MailMessage"/> from the specified arguments. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="cc"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <returns></returns> | |||
| public MailMessage GetMailMessage(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null) | |||
| { | |||
| var message = new MailMessage | |||
| { | |||
| Sender = new MailAddress(_settings.SmtpFrom, _settings.SmtpFromName), | |||
| From = new MailAddress(_settings.SmtpFrom), | |||
| Subject = subject, | |||
| Body = body, | |||
| IsBodyHtml = isHtml | |||
| }; | |||
| if (to.Any()) | |||
| { | |||
| message.To.Add(string.Join(",", to.Where(email => !string.IsNullOrWhiteSpace(email)))); | |||
| } | |||
| else | |||
| { | |||
| throw new ArgumentException("The list of recipient emails can not be empty"); | |||
| } | |||
| if (cc != null && cc.Any()) | |||
| { | |||
| message.CC.Add(string.Join(",", cc.Where(email => !string.IsNullOrWhiteSpace(email)))); | |||
| } | |||
| return message; | |||
| } | |||
| /// <summary> | |||
| /// Sends an email aysnchronously. If the "dont send before" argument is specified, then the email will be sent with a delay - after the specified time. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="dontSendBefore"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| public async Task<bool> SendEmailWithDelayAsync(List<string> to, string subject, string body, bool isHtml = false, DateTime? dontSendBefore = null) | |||
| { | |||
| try | |||
| { | |||
| var email = CreateEmail(to, subject, body, isHtml, dontSendBefore); | |||
| //var result = await WriteEmailToDbAsync(email); | |||
| return true; | |||
| } | |||
| catch (ArgumentException) | |||
| { | |||
| throw; | |||
| } | |||
| catch (Exception e) | |||
| { | |||
| throw new Exception("Error while attempting to send an email with delay.", e); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Creates a <see cref="DiligEmail"/> object with specified arguments. | |||
| /// </summary> | |||
| /// <param name="to"></param> | |||
| /// <param name="subject"></param> | |||
| /// <param name="body"></param> | |||
| /// <param name="isHtml"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <param name="dontSendBefore"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <returns></returns> | |||
| public DiligEmail CreateEmail(List<string> to, string subject, string body, bool isHtml = false, | |||
| DateTime? dontSendBefore = null) | |||
| { | |||
| if (!to.Any()) | |||
| { | |||
| throw new ArgumentException("The list of recipient emails can not be empty"); | |||
| } | |||
| var email = new DiligEmail | |||
| { | |||
| To = to.Aggregate((previous, next) => previous + ";" + next), | |||
| Subject = subject, | |||
| Body = body, | |||
| IsHtml = isHtml, | |||
| DontSendBefore = dontSendBefore, | |||
| CreateTime = DateTime.UtcNow | |||
| }; | |||
| return email; | |||
| } | |||
| /// <summary> | |||
| /// Fills the specified <see cref="DiligEmail"/> object with the specified <see cref="EmailAttachment"/> list. | |||
| /// </summary> | |||
| /// <param name="email"></param> | |||
| /// <param name="attachments"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <returns></returns> | |||
| //public DiligEmail FillEmailAttachments(DiligEmail email) | |||
| //{ | |||
| // if (email == null) | |||
| // { | |||
| // throw new ArgumentNullException(nameof(email), "Email can not be null"); | |||
| // } | |||
| // if (attachments != null && attachments.Any()) | |||
| // { | |||
| // attachments.ForEach(attachment => | |||
| // { | |||
| // email.DiligEmailAttachments.Add(new DiligEmailAttachment | |||
| // { | |||
| // FileName = attachment.FileName, | |||
| // SourceFileName = attachment.SourceFileName, | |||
| // Type = attachment.Type, | |||
| // Disposition = attachment.Disposition | |||
| // }); | |||
| // }); | |||
| // } | |||
| // return email; | |||
| //} | |||
| /// <summary> | |||
| /// Writes the specified <see cref="DiligEmail"/> object to the underlying database. | |||
| /// </summary> | |||
| /// <param name="email"></param> | |||
| /// <exception cref="ArgumentException"></exception> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| //public async Task<int> WriteEmailToDbAsync(DiligEmail email) | |||
| //{ | |||
| // try | |||
| // { | |||
| // if (email == null) | |||
| // { | |||
| // throw new ArgumentNullException(nameof(email), "Email can not be null"); | |||
| // } | |||
| // _entities.DiligEmails.Add(email); | |||
| // var result = await _entities.SaveChangesAsync(); | |||
| // return result; | |||
| // } | |||
| // catch (Exception e) | |||
| // { | |||
| // throw new Exception("Failed to write entry into the database", e); | |||
| // } | |||
| //} | |||
| /// <summary> | |||
| /// Writes the specified <see cref="DiligEmail"/> object to the underlying database. | |||
| /// </summary> | |||
| /// <param name="email"></param> | |||
| /// <exception cref="ArgumentNullException"></exception> | |||
| /// <exception cref="Exception"></exception> | |||
| /// <returns></returns> | |||
| //public int WriteEmailToDb(DiligEmail email) | |||
| //{ | |||
| // try | |||
| // { | |||
| // if (email == null) | |||
| // { | |||
| // throw new ArgumentNullException(nameof(email), "Email can not be null"); | |||
| // } | |||
| // _entities.DiligEmails.Add(email); | |||
| // var result = _entities.SaveChanges(); | |||
| // return result; | |||
| // } | |||
| // catch (ArgumentException) | |||
| // { | |||
| // throw; | |||
| // } | |||
| // catch (Exception e) | |||
| // { | |||
| // throw new Exception("Failed to write entry into the database", e); | |||
| // } | |||
| //} | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| using Diligent.WebAPI.Contracts.Models; | |||
| using System.Net.Mail; | |||
| namespace Diligent.WebAPI.Business.Services.Interfaces | |||
| { | |||
| public interface IEmailer | |||
| { | |||
| DiligEmail CreateEmail(List<string> to, string subject, string body, bool isHtml = false, DateTime? dontSendBefore = null); | |||
| MailMessage GetMailMessage(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| SmtpClient GetSmtpClient(); | |||
| bool SendEmail(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| bool SendEmailAndWriteToDb(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| bool SendEmailAndWriteToDb(string to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| Task<bool> SendEmailAndWriteToDbAsync(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| Task<bool> SendEmailAndWriteToDbAsync(string to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| Task<bool> SendEmailAsync(List<string> to, string subject, string body, bool isHtml = false, List<string> cc = null); | |||
| Task<bool> SendEmailWithDelayAsync(List<string> to, string subject, string body, bool isHtml = false, DateTime? dontSendBefore = null); | |||
| } | |||
| } | |||
| @@ -18,5 +18,9 @@ | |||
| Task UpdateRefreshToken(RefreshToken refreshToken); | |||
| Task<ServiceResponseDTO<string>> DeleteRefreshToken(int userId); | |||
| Task<ServiceResponseDTO<object>> GetEmailConfirmationUrlAsync(string email); | |||
| Task<ServiceResponseDTO<object>> PasswordResetAsync(string email, string code, string password); | |||
| } | |||
| } | |||
| @@ -1,5 +1,12 @@ | |||
| using Newtonsoft.Json; | |||
| using Diligent.WebAPI.Business.Services.Interfaces; | |||
| using Diligent.WebAPI.Data.Entities; | |||
| using Microsoft.AspNetCore.Mvc; | |||
| using Microsoft.AspNetCore.WebUtilities; | |||
| using Microsoft.Extensions.Logging; | |||
| using Newtonsoft.Json; | |||
| using System; | |||
| using System.Net; | |||
| using System.Security.Policy; | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| @@ -10,13 +17,17 @@ namespace Diligent.WebAPI.Business.Services | |||
| private readonly UserManager<User> _userManager; | |||
| private readonly IMapper _mapper; | |||
| private readonly DatabaseContext _databaseContext; | |||
| private readonly IEmailer _emailer; | |||
| private readonly ILogger<UserService> _logger; | |||
| public UserService(IOptions<AuthorizationSettings> authSettings, UserManager<User> userManager, IMapper mapper, DatabaseContext databaseContext) | |||
| public UserService(IOptions<AuthorizationSettings> authSettings, UserManager<User> userManager, IMapper mapper, DatabaseContext databaseContext, IEmailer emailer, ILogger<UserService> logger) | |||
| { | |||
| _authSettings = authSettings.Value; | |||
| _userManager = userManager; | |||
| _mapper = mapper; | |||
| _databaseContext = databaseContext; | |||
| _emailer = emailer; | |||
| _logger = logger; | |||
| } | |||
| public async Task<IEnumerable<User?>> GetAll() => | |||
| @@ -95,10 +106,10 @@ namespace Diligent.WebAPI.Business.Services | |||
| return await GenerateToken(user); | |||
| } | |||
| public async Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(GoogleApiModel model) | |||
| { | |||
| if (!IsTokenValid(model.Token)) | |||
| { | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| @@ -184,7 +195,7 @@ namespace Diligent.WebAPI.Business.Services | |||
| var existRefreshToken = await _databaseContext.RefreshTokens.Where(x => x.UserId == user.Id).FirstOrDefaultAsync(); | |||
| if(existRefreshToken != null) | |||
| if (existRefreshToken != null) | |||
| { | |||
| existRefreshToken.Token = writedToken; | |||
| existRefreshToken.JwtId = token.Id; | |||
| @@ -230,7 +241,7 @@ namespace Diligent.WebAPI.Business.Services | |||
| var userk = await _databaseContext.Users.Where(u => u.Id == storedRefreshToken.UserId).FirstOrDefaultAsync(); | |||
| if(userk == null) | |||
| if (userk == null) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "There is no user which is associated with refresh token" }; | |||
| } | |||
| @@ -277,7 +288,7 @@ namespace Diligent.WebAPI.Business.Services | |||
| var user = await _userManager.FindByIdAsync(validatedToken.Claims.Single(x => x.Type == "id").Value); | |||
| var token = await GenerateJwtToken(user); | |||
| var token = await GenerateJwtToken(user); | |||
| return new RefreshTokenResultDto | |||
| { | |||
| @@ -372,5 +383,66 @@ namespace Diligent.WebAPI.Business.Services | |||
| await _databaseContext.SaveChangesAsync(); | |||
| } | |||
| public async Task<ServiceResponseDTO<object>> GetEmailConfirmationUrlAsync(string email) | |||
| { | |||
| var user = await _userManager.FindByEmailAsync(email); | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Email did not find." | |||
| }; | |||
| } | |||
| var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); | |||
| token = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(token)); | |||
| await _emailer.SendEmailAndWriteToDbAsync(email, "Reset password", $"<a href='http://localhost:3000/reset-password?token={token}&email={email}'>RESET PASSWORD LINK</a>", isHtml: true); | |||
| user.PasswordResetToken = token; | |||
| await _databaseContext.SaveChangesAsync(); | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| Data = new { code = token, email = email } | |||
| }; | |||
| } | |||
| public async Task<ServiceResponseDTO<object>> PasswordResetAsync(string email, string code, string password) | |||
| { | |||
| var user = await _userManager.FindByEmailAsync(email); | |||
| if (user == null) | |||
| { | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Email did not find." | |||
| }; | |||
| } | |||
| // FOR SOME REASON USERMANAGER.RESETPASSWORDASYNC returns InvalidToken. In future change this | |||
| //var passwordResetToken = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); | |||
| //IdentityResult resetResult = await _userManager.ResetPasswordAsync(user, passwordResetToken, password); | |||
| //if (resetResult.Succeeded) | |||
| await _userManager.RemovePasswordAsync(user); | |||
| await _userManager.AddPasswordAsync(user, password); | |||
| if(user.PasswordResetToken == code) | |||
| { | |||
| if (await _userManager.IsLockedOutAsync(user)) | |||
| { | |||
| await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow); | |||
| } | |||
| return new ServiceResponseDTO<object> { Data = true }; | |||
| } | |||
| //var errors = resetResult.Errors.Select(x => x.Description); | |||
| return new ServiceResponseDTO<object> | |||
| { | |||
| IsError = true, | |||
| ErrorMessage = "Invalid reset password token" | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,6 @@ | |||
| namespace Diligent.WebAPI.Business.Settings | |||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; | |||
| namespace Diligent.WebAPI.Business.Settings | |||
| { | |||
| public class AuthorizationSettings | |||
| { | |||
| @@ -7,5 +9,13 @@ | |||
| public int JwtExpiredTime { get; set; } | |||
| public int JwtRefreshExpiredTime { get; set; } | |||
| public string SmtpFrom { get; set; } | |||
| public string SmtpFromName { get; set; } | |||
| public string SmtpServer { get; set; } | |||
| public int SmtpPort { get; set; } | |||
| public bool SmtpUseSSL { get; set; } | |||
| public string SmtpUsername { get; set; } | |||
| public string SmtpPassword { get; set; } | |||
| public string ResetPasswordUrl { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| namespace Diligent.WebAPI.Contracts.Models | |||
| { | |||
| public class DiligEmail | |||
| { | |||
| public long Id { get; set; } | |||
| public string To { get; set; } | |||
| public string Subject { get; set; } | |||
| public bool IsHtml { get; set; } | |||
| public string Body { get; set; } | |||
| public DateTime? DontSendBefore { get; set; } | |||
| public DateTime? CreateTime { get; set; } | |||
| public DateTime? SentTime { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| namespace Diligent.WebAPI.Contracts.Models | |||
| { | |||
| public class ResetPasswordModel | |||
| { | |||
| public string Email { get; set; } | |||
| public string Code { get; set; } | |||
| public string Password { get; set; } | |||
| } | |||
| } | |||
| @@ -6,4 +6,5 @@ public class User : IdentityUser<int> | |||
| { | |||
| public string FirstName { get; set; } | |||
| public string LastName { get; set; } | |||
| public string? PasswordResetToken { get; set; } | |||
| } | |||
| @@ -0,0 +1,600 @@ | |||
| // <auto-generated /> | |||
| using System; | |||
| using Diligent.WebAPI.Data; | |||
| using Microsoft.EntityFrameworkCore; | |||
| using Microsoft.EntityFrameworkCore.Infrastructure; | |||
| using Microsoft.EntityFrameworkCore.Metadata; | |||
| using Microsoft.EntityFrameworkCore.Migrations; | |||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | |||
| #nullable disable | |||
| namespace Diligent.WebAPI.Data.Migrations | |||
| { | |||
| [DbContext(typeof(DatabaseContext))] | |||
| [Migration("20221102121707_AddedPasswordResetToken")] | |||
| partial class AddedPasswordResetToken | |||
| { | |||
| protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
| { | |||
| #pragma warning disable 612, 618 | |||
| modelBuilder | |||
| .HasAnnotation("ProductVersion", "6.0.10") | |||
| .HasAnnotation("Relational:MaxIdentifierLength", 128); | |||
| SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.AppRole", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<string>("ConcurrencyStamp") | |||
| .IsConcurrencyToken() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Name") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<string>("NormalizedName") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("NormalizedName") | |||
| .IsUnique() | |||
| .HasDatabaseName("RoleNameIndex") | |||
| .HasFilter("[NormalizedName] IS NOT NULL"); | |||
| b.ToTable("AspNetRoles", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.InsuranceCompany", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<string>("City") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Country") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Fax") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("LegalAddress") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("LegalEmail") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Name") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PostalCode") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.ToTable("InsuranceCompanies"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.InsurancePolicy", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("EndDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<long>("InsurerId") | |||
| .HasColumnType("bigint"); | |||
| b.Property<decimal>("Premium") | |||
| .HasColumnType("decimal(18,2)"); | |||
| b.Property<DateTime>("StartDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Type") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("InsurerId"); | |||
| b.ToTable("InsurancePolicies"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.Insurer", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<string>("Address") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("City") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Country") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("DateOfBirth") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Email") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("FirstName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<long>("InsuranceCompanyId") | |||
| .HasColumnType("bigint"); | |||
| b.Property<string>("LastName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PostalCode") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("InsuranceCompanyId"); | |||
| b.ToTable("Insurers"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.RefreshToken", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreationDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("ExpiryDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<bool>("Invalidated") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("JwtId") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Token") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("Used") | |||
| .HasColumnType("bit"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("RefreshTokens"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.User", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<int>("AccessFailedCount") | |||
| .HasColumnType("int"); | |||
| b.Property<string>("ConcurrencyStamp") | |||
| .IsConcurrencyToken() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Email") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<bool>("EmailConfirmed") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("FirstName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("LastName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("LockoutEnabled") | |||
| .HasColumnType("bit"); | |||
| b.Property<DateTimeOffset?>("LockoutEnd") | |||
| .HasColumnType("datetimeoffset"); | |||
| b.Property<string>("NormalizedEmail") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<string>("NormalizedUserName") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<string>("PasswordHash") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PasswordResetToken") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("PhoneNumberConfirmed") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("SecurityStamp") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("TwoFactorEnabled") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("UserName") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("NormalizedEmail") | |||
| .HasDatabaseName("EmailIndex"); | |||
| b.HasIndex("NormalizedUserName") | |||
| .IsUnique() | |||
| .HasDatabaseName("UserNameIndex") | |||
| .HasFilter("[NormalizedUserName] IS NOT NULL"); | |||
| b.ToTable("AspNetUsers", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookDefinition", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Description") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("DisplayName") | |||
| .IsRequired() | |||
| .HasMaxLength(100) | |||
| .HasColumnType("nvarchar(100)"); | |||
| b.Property<string>("Name") | |||
| .IsRequired() | |||
| .HasMaxLength(100) | |||
| .HasColumnType("nvarchar(100)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.ToTable("WebhookDefinitions"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookSubscription", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<bool>("IsActive") | |||
| .HasColumnType("bit"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<long>("WebhookDefinitionId") | |||
| .HasColumnType("bigint"); | |||
| b.Property<string>("WebhookURL") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("WebhookDefinitionId"); | |||
| b.ToTable("WebhookSubscriptions"); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<string>("ClaimType") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("ClaimValue") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<int>("RoleId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("RoleId"); | |||
| b.ToTable("AspNetRoleClaims", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<string>("ClaimType") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("ClaimValue") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("AspNetUserClaims", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b => | |||
| { | |||
| b.Property<string>("LoginProvider") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("ProviderKey") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("ProviderDisplayName") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("LoginProvider", "ProviderKey"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("AspNetUserLogins", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<int>", b => | |||
| { | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.Property<int>("RoleId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("UserId", "RoleId"); | |||
| b.HasIndex("RoleId"); | |||
| b.ToTable("AspNetUserRoles", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b => | |||
| { | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.Property<string>("LoginProvider") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("Name") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("Value") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.HasKey("UserId", "LoginProvider", "Name"); | |||
| b.ToTable("AspNetUserTokens", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.InsurancePolicy", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.Insurer", "Insurer") | |||
| .WithMany() | |||
| .HasForeignKey("InsurerId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("Insurer"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.Insurer", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.InsuranceCompany", "InsuranceCompany") | |||
| .WithMany() | |||
| .HasForeignKey("InsuranceCompanyId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("InsuranceCompany"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.RefreshToken", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", "User") | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("User"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookSubscription", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.WebhookDefinition", "WebhookDefinition") | |||
| .WithMany() | |||
| .HasForeignKey("WebhookDefinitionId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("WebhookDefinition"); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.AppRole", null) | |||
| .WithMany() | |||
| .HasForeignKey("RoleId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.AppRole", null) | |||
| .WithMany() | |||
| .HasForeignKey("RoleId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| #pragma warning restore 612, 618 | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| using Microsoft.EntityFrameworkCore.Migrations; | |||
| #nullable disable | |||
| namespace Diligent.WebAPI.Data.Migrations | |||
| { | |||
| public partial class AddedPasswordResetToken : Migration | |||
| { | |||
| protected override void Up(MigrationBuilder migrationBuilder) | |||
| { | |||
| migrationBuilder.AddColumn<string>( | |||
| name: "PasswordResetToken", | |||
| table: "AspNetUsers", | |||
| type: "nvarchar(max)", | |||
| nullable: true); | |||
| } | |||
| protected override void Down(MigrationBuilder migrationBuilder) | |||
| { | |||
| migrationBuilder.DropColumn( | |||
| name: "PasswordResetToken", | |||
| table: "AspNetUsers"); | |||
| } | |||
| } | |||
| } | |||
| @@ -394,6 +394,9 @@ namespace Diligent.WebAPI.Data.Migrations | |||
| b.Property<string>("PasswordHash") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PasswordResetToken") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .HasColumnType("nvarchar(max)"); | |||
| @@ -6,19 +6,34 @@ | |||
| public class UsersController : ControllerBase | |||
| { | |||
| private readonly IUserService _userService; | |||
| private readonly IEmailer _emailer; | |||
| public UsersController(IUserService userService) | |||
| public UsersController(IUserService userService, IEmailer emailer) | |||
| { | |||
| _userService = userService; | |||
| _emailer = emailer; | |||
| } | |||
| [Authorize] | |||
| [HttpGet] | |||
| public IActionResult GetAll() | |||
| public async Task<IActionResult> GetAll() | |||
| { | |||
| return Ok("Hello from protected route"); | |||
| } | |||
| [HttpGet("ForgotPassword")] | |||
| public async Task<IActionResult> ForgotPassword(string email) | |||
| { | |||
| var result = await _userService.GetEmailConfirmationUrlAsync(email); | |||
| return Ok(result); | |||
| } | |||
| [HttpPost("RessetPassword")] | |||
| public async Task<IActionResult> ResetPassword([FromBody]ResetPasswordModel model) | |||
| { | |||
| var result = await _userService.PasswordResetAsync(email:model.Email,code: model.Code,password: model.Password); | |||
| return Ok(result); | |||
| } | |||
| [HttpPost] | |||
| public async Task<IActionResult> CreateUser([FromBody] CreateUserRequestDto model) | |||
| { | |||
| @@ -14,6 +14,7 @@ | |||
| services.AddAutoMapper(typeof(ApplicantMappingProfile)); | |||
| services.AddScoped<IInsurersService, InsurersService>(); | |||
| services.AddScoped<IEmailer, Emailer>(); | |||
| services.AddScoped<IInsuranceCompaniesService, InsuranceCompaniesService>(); | |||
| services.AddScoped<IInsurancePoliciesService, InsurancePoliciesService>(); | |||
| services.AddScoped<IWebhookSubscriptionService, WebhookSubscriptionService>(); | |||
| @@ -5,6 +5,13 @@ | |||
| "Authorization": { | |||
| "JwtExpiredTime": "5", | |||
| "JwtRefreshExpiredTime": "30", | |||
| "Secret": "SECRET_ASKGFH#$_#((Y)#I%EWJGDSJTGKEOS@$SAF" | |||
| "Secret": "SECRET_ASKGFH#$_#((Y)#I%EWJGDSJTGKEOS@$SAF", | |||
| "SmtpServer": "smtp.mailtrap.io", | |||
| "SmtpPort": 2525, | |||
| "SmtpUseSSL": true, | |||
| "SmtpUsername": "460e3c49f02e37", | |||
| "SmtpPassword": "66443869eaad55", | |||
| "SmtpFrom": "noreply@hrcenter.net", | |||
| "SmtpFromName": "HRCenter Team" | |||
| } | |||
| } | |||