feat: add email confirmation on registration
This commit is contained in:
parent
dc829f03c8
commit
59c3a9f704
9
Server/Configurations/SmtpCredentials.cs
Normal file
9
Server/Configurations/SmtpCredentials.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Server.Configurations;
|
||||||
|
|
||||||
|
public class SmtpCredentials
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = null!;
|
||||||
|
public int Port { get; set; }
|
||||||
|
public string User { get; set; } = null!;
|
||||||
|
public string Password { get; set; } = null!;
|
||||||
|
}
|
128
Server/Constants/JwtStandardClaims.cs
Normal file
128
Server/Constants/JwtStandardClaims.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
namespace Server.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard JWT claims according to https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||||
|
/// </summary>
|
||||||
|
public static class JwtStandardClaimNames
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Subject - Identifier for the End-User at the Issuer.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Sub = "sub";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's full name in displayable form including all name parts, possibly
|
||||||
|
/// including titles and suffixes, ordered according to the End-User's locale and preferences.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Name = "name";
|
||||||
|
/// <summary>
|
||||||
|
/// Given name(s) or first name(s) of the End-User. Note that in some cultures,
|
||||||
|
/// people can have multiple given names; all can be present, with the names
|
||||||
|
/// being separated by space characters.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string GivenName = "given_name";
|
||||||
|
/// <summary>
|
||||||
|
/// Surname(s) or last name(s) of the End-User. Note that in some cultures,
|
||||||
|
/// people can have multiple family names or no family name; all can be present,
|
||||||
|
/// with the names being separated by space characters.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string FamilyName = "family_name";
|
||||||
|
/// <summary>
|
||||||
|
/// Middle name(s) of the End-User. Note that in some cultures, people can have
|
||||||
|
/// multiple middle names; all can be present, with the names being separated by
|
||||||
|
/// space characters. Also note that in some cultures, middle names are not used.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string MiddleName = "middle_name";
|
||||||
|
/// <summary>
|
||||||
|
/// Casual name of the End-User that may or may not be the same as the given_name.
|
||||||
|
/// For instance, a nickname value of Mike might be returned alongside a given_name value of Michael.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Nickname = "nickname";
|
||||||
|
/// <summary>
|
||||||
|
/// Shorthand name by which the End-User wishes to be referred to at the RP,
|
||||||
|
/// such as janedoe or j.doe. This value MAY be any valid JSON string including special
|
||||||
|
/// characters such as @, /, or whitespace. The RP MUST NOT rely upon this value being unique.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string PreferredUsername = "preferred_username";
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Profile = "profile";
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the End-User's profile picture. This URL MUST refer to an image file
|
||||||
|
/// (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image.
|
||||||
|
/// Note that this URL SHOULD specifically reference a profile photo of the End-User suitable
|
||||||
|
/// for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Picture = "picture";
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the End-User's Web page or blog. This Web page SHOULD contain information
|
||||||
|
/// published by the End-User or an organization that the End-User is affiliated with.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Website = "website";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's preferred e-mail address. Its value MUST conform to the RFC 5322 [RFC5322]
|
||||||
|
/// addr-spec syntax. The RP MUST NOT rely upon this value being unique.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Email = "email";
|
||||||
|
/// <summary>
|
||||||
|
/// True if the End-User's e-mail address has been verified; otherwise false.
|
||||||
|
/// When this Claim Value is true, this means that the OP took affirmative steps
|
||||||
|
/// to ensure that this e-mail address was controlled by the End-User at the time
|
||||||
|
/// the verification was performed. The means by which an e-mail address is verified
|
||||||
|
/// is context-specific, and dependent upon the trust framework or contractual agreements
|
||||||
|
/// within which the parties are operating.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string EmailVerified = "email_verified";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's gender. Values defined by this specification are female and male.
|
||||||
|
/// Other values MAY be used when neither of the defined values are applicable.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Gender = "gender";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format.
|
||||||
|
/// The year MAY be 0000, indicating that it is omitted. To represent only the year, YYYY format is allowed.
|
||||||
|
/// Note that depending on the underlying platform's date related function, providing just year can result in
|
||||||
|
/// varying month and day, so the implementers need to take this factor into account to correctly process the dates.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string BirthDate = "birthdate";
|
||||||
|
/// <summary>
|
||||||
|
/// String from zoneinfo [zoneinfo] time zone database representing the End-User's time zone.
|
||||||
|
/// For example, Europe/Paris or America/Los_Angeles.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string ZoneInfo = "zoneinfo";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's locale, represented as a BCP47 [RFC5646] language tag.
|
||||||
|
/// This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase
|
||||||
|
/// and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash.
|
||||||
|
/// For example, en-US or fr-CA. As a compatibility note, some implementations have used
|
||||||
|
/// an underscore as the separator rather than a dash, for example, en_US;
|
||||||
|
/// Relying Parties MAY choose to accept this locale syntax as well.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Locale = "locale";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's preferred telephone number. E.164 [E.164] is RECOMMENDED as the format of this Claim,
|
||||||
|
/// for example, +1 (425) 555-1212 or +56 (2) 687 2400. If the phone number contains an extension,
|
||||||
|
/// it is RECOMMENDED that the extension be represented using the RFC 3966 [RFC3966] extension syntax,
|
||||||
|
/// for example, +1 (604) 555-1234;ext=5678.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string PhoneNumber = "phone_number";
|
||||||
|
/// <summary>
|
||||||
|
/// True if the End-User's phone number has been verified; otherwise false.
|
||||||
|
/// When this Claim Value is true, this means that the OP took affirmative steps to ensure
|
||||||
|
/// that this phone number was controlled by the End-User at the time the verification was performed.
|
||||||
|
/// The means by which a phone number is verified is context-specific,
|
||||||
|
/// and dependent upon the trust framework or contractual agreements within which the parties are operating.
|
||||||
|
/// When true, the phone_number Claim MUST be in E.164 format and any extensions MUST be represented in RFC 3966 format.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string PhoneNumberVerified = "phone_number_verified";
|
||||||
|
/// <summary>
|
||||||
|
/// End-User's preferred postal address. The value of the address member is a JSON [RFC4627] structure
|
||||||
|
/// containing some or all of the members defined in Section 5.1.1.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string Address = "address";
|
||||||
|
/// <summary>
|
||||||
|
/// Time the End-User's information was last updated. Its value is a JSON number representing
|
||||||
|
/// the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly string UpdatedAt = "updated_at";
|
||||||
|
}
|
@ -24,8 +24,7 @@ public class AuthenticationController : ControllerBase
|
|||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
public async Task<IActionResult> RegisterAsync([FromBody] RegistrationRequest registerRequest)
|
public async Task<IActionResult> RegisterAsync([FromBody] RegistrationRequest registerRequest)
|
||||||
{
|
{
|
||||||
var (succeeded, message) =
|
var (succeeded, message) = await _authService.RegisterAsync(registerRequest);
|
||||||
await _authService.RegisterAsync(registerRequest);
|
|
||||||
|
|
||||||
if (!succeeded)
|
if (!succeeded)
|
||||||
{
|
{
|
||||||
@ -35,6 +34,20 @@ public class AuthenticationController : ControllerBase
|
|||||||
return Ok(new ResponseBase{ Message = message });
|
return Ok(new ResponseBase{ Message = message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("confirmEmail")]
|
||||||
|
public async Task<IActionResult> ConfirmEmailAsync([FromQuery] string email, [FromQuery] string token,
|
||||||
|
[FromQuery] string redirectionUrl)
|
||||||
|
{
|
||||||
|
var (succeeded, message) = await _authService.ConfirmEmailAsync(email, token);
|
||||||
|
|
||||||
|
if (!succeeded)
|
||||||
|
{
|
||||||
|
return BadRequest(new ResponseBase {Message = message});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Redirect(redirectionUrl);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("authenticate")]
|
[HttpPost("authenticate")]
|
||||||
public async Task<IActionResult> GetTokenAsync(AuthenticationRequest authRequest)
|
public async Task<IActionResult> GetTokenAsync(AuthenticationRequest authRequest)
|
||||||
{
|
{
|
||||||
|
@ -25,6 +25,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(options => {
|
|||||||
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Error;
|
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Error;
|
||||||
options.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
|
options.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
|
||||||
});
|
});
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
@ -57,10 +58,11 @@ builder.Services.AddCors(options => {
|
|||||||
.AllowAnyHeader().AllowAnyMethod());
|
.AllowAnyHeader().AllowAnyMethod());
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddIdentityCore<User>(options => {
|
builder.Services.AddIdentityCore<User>(options =>
|
||||||
|
{
|
||||||
options.User.RequireUniqueEmail = true;
|
options.User.RequireUniqueEmail = true;
|
||||||
options.Password.RequiredLength = 8;
|
options.Password.RequiredLength = 8;
|
||||||
}).AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
|
}).AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
|
||||||
|
|
||||||
// Configuration from AppSettings
|
// Configuration from AppSettings
|
||||||
builder.Services.Configure<Jwt>(builder.Configuration.GetSection("Jwt"));
|
builder.Services.Configure<Jwt>(builder.Configuration.GetSection("Jwt"));
|
||||||
@ -81,6 +83,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|||||||
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
|
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
builder.Services.Configure<SmtpCredentials>(builder.Configuration.GetSection("SmtpCredentials"));
|
||||||
|
|
||||||
builder.Services.AddAuthorization(options => {
|
builder.Services.AddAuthorization(options => {
|
||||||
// options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
|
// options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
|
||||||
@ -99,6 +102,7 @@ builder.Services.AddAuthorization(options => {
|
|||||||
|
|
||||||
builder.Services.AddAutoMapper(typeof(MapperInitializer));
|
builder.Services.AddAutoMapper(typeof(MapperInitializer));
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IEmailSenderService, EmailSenderService>();
|
||||||
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
|
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<ICountryManagementService, CountryManagementService>();
|
builder.Services.AddScoped<ICountryManagementService, CountryManagementService>();
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
|
||||||
|
<PackageReference Include="MailKit" Version="4.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.14" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.14" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||||
|
@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Server.Configurations;
|
using Server.Configurations;
|
||||||
|
using Server.Constants;
|
||||||
using Server.Models;
|
using Server.Models;
|
||||||
using SharedModels.Requests;
|
using SharedModels.Requests;
|
||||||
using SharedModels.Responses;
|
using SharedModels.Responses;
|
||||||
@ -19,30 +20,38 @@ public class AuthenticationService : IAuthenticationService
|
|||||||
private readonly UserManager<User> _userManager;
|
private readonly UserManager<User> _userManager;
|
||||||
private readonly RoleManager<IdentityRole> _roleManager;
|
private readonly RoleManager<IdentityRole> _roleManager;
|
||||||
private readonly Jwt _jwt;
|
private readonly Jwt _jwt;
|
||||||
|
private readonly IHttpContextAccessor _contextAccessor;
|
||||||
|
private readonly LinkGenerator _linkGenerator;
|
||||||
|
private readonly IEmailSenderService _emailSender;
|
||||||
|
|
||||||
public AuthenticationService(UserManager<User> userManager,
|
public AuthenticationService(UserManager<User> userManager,
|
||||||
RoleManager<IdentityRole> roleManager, IOptions<Jwt> jwt)
|
RoleManager<IdentityRole> roleManager, IOptions<Jwt> jwt,
|
||||||
|
IHttpContextAccessor contextAccessor, LinkGenerator linkGenerator,
|
||||||
|
IEmailSenderService emailSender)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_roleManager = roleManager;
|
_roleManager = roleManager;
|
||||||
_jwt = jwt.Value;
|
_jwt = jwt.Value;
|
||||||
|
_contextAccessor = contextAccessor;
|
||||||
|
_linkGenerator = linkGenerator;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
|
||||||
|
_userManager.UserValidators.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest)
|
public async Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest)
|
||||||
{
|
{
|
||||||
_userManager.UserValidators.Clear();
|
|
||||||
|
|
||||||
var userWithSameEmail = await _userManager.FindByEmailAsync(regRequest.Email);
|
var userWithSameEmail = await _userManager.FindByEmailAsync(regRequest.Email);
|
||||||
if (userWithSameEmail != null)
|
if (userWithSameEmail != null)
|
||||||
{
|
{
|
||||||
return (false, $"Email is already registered.");
|
return (false, "Email is already registered.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var userWithSamePhone = await _userManager.Users
|
var userWithSamePhone = await _userManager.Users
|
||||||
.SingleOrDefaultAsync(u => u.PhoneNumber == regRequest.PhoneNumber);
|
.SingleOrDefaultAsync(u => u.PhoneNumber == regRequest.PhoneNumber);
|
||||||
if (userWithSamePhone != null)
|
if (userWithSamePhone != null)
|
||||||
{
|
{
|
||||||
return (false, $"Phone is already registered.");
|
return (false, "Phone is already registered.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = new User
|
var user = new User
|
||||||
@ -55,14 +64,40 @@ public class AuthenticationService : IAuthenticationService
|
|||||||
PhoneNumber = regRequest.PhoneNumber
|
PhoneNumber = regRequest.PhoneNumber
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _userManager.CreateAsync(user, regRequest.Password);
|
var createUserResult = await _userManager.CreateAsync(user, regRequest.Password);
|
||||||
|
if (!createUserResult.Succeeded)
|
||||||
|
{
|
||||||
|
return (false, $"{createUserResult.Errors?.First().Description}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
|
var confirmationLink = _linkGenerator.GetUriByAction(_contextAccessor.HttpContext,
|
||||||
|
"confirmEmail", "authentication",
|
||||||
|
new { email = user.Email, token = token, redirectionUrl = regRequest.EmailConfirmationRedirectUrl },
|
||||||
|
_contextAccessor.HttpContext.Request.Scheme);
|
||||||
|
|
||||||
|
await _emailSender.SendMail(user.Email, "Email confirmation", confirmationLink);
|
||||||
|
|
||||||
|
await _userManager.AddToRoleAsync(user, Constants.Identity.DefaultRole.ToString());
|
||||||
|
return (true, $"User registered with email {user.Email}. Before signing in confirm your email" +
|
||||||
|
$"by following a link sent to registered email address.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool succeeded, string? message)> ConfirmEmailAsync(string email, string token)
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(email);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return (false, $"Email {email} not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userManager.ConfirmEmailAsync(user, token);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return (false, $"{result.Errors?.First().Description}");
|
return (false, $"Error confirming email {email} with token {token}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userManager.AddToRoleAsync(user, Constants.Identity.DefaultRole.ToString());
|
return (true, $"Email {email} confirmed");
|
||||||
return (true, $"User registered with email {user.Email}.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool succeeded, AuthenticationResponse authResponse,
|
public async Task<(bool succeeded, AuthenticationResponse authResponse,
|
||||||
@ -186,11 +221,13 @@ public class AuthenticationService : IAuthenticationService
|
|||||||
|
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
|
new Claim(JwtStandardClaimNames.Sub, user.Id),
|
||||||
new Claim(JwtRegisteredClaimNames.Name, user.LastName + user.FirstName + user.Patronymic),
|
new Claim(JwtStandardClaimNames.Name, user.LastName + user.FirstName + user.Patronymic),
|
||||||
new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
|
new Claim(JwtStandardClaimNames.GivenName, user.FirstName),
|
||||||
new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
|
new Claim(JwtStandardClaimNames.FamilyName, user.LastName),
|
||||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
new Claim(JwtStandardClaimNames.MiddleName, user.Patronymic),
|
||||||
|
new Claim(JwtStandardClaimNames.Email, user.Email),
|
||||||
|
new Claim(JwtStandardClaimNames.EmailVerified, user.EmailConfirmed.ToString()),
|
||||||
new Claim(JwtRegisteredClaimNames.Exp, DateTime.UtcNow.AddMinutes(_jwt.ValidityInMinutes).ToString(CultureInfo.InvariantCulture))
|
new Claim(JwtRegisteredClaimNames.Exp, DateTime.UtcNow.AddMinutes(_jwt.ValidityInMinutes).ToString(CultureInfo.InvariantCulture))
|
||||||
}
|
}
|
||||||
.Union(userClaims)
|
.Union(userClaims)
|
||||||
|
51
Server/Services/EmailSenderService.cs
Normal file
51
Server/Services/EmailSenderService.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text;
|
||||||
|
using MailKit.Net.Smtp;
|
||||||
|
using MailKit.Security;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MimeKit;
|
||||||
|
using Server.Configurations;
|
||||||
|
|
||||||
|
namespace Server.Services;
|
||||||
|
|
||||||
|
public class EmailSenderService : IEmailSenderService
|
||||||
|
{
|
||||||
|
private readonly SmtpCredentials _smtpCredentials;
|
||||||
|
private readonly ISmtpClient _smtpClient;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public EmailSenderService(IOptions<SmtpCredentials> smtpCredentials, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
|
||||||
|
_smtpCredentials = smtpCredentials.Value;
|
||||||
|
_smtpClient = new SmtpClient();
|
||||||
|
_smtpClient.SslProtocols = SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool succeeded, string message)> SendMail(string toEmail, string subject, string message)
|
||||||
|
{
|
||||||
|
string applicationName = _configuration.GetValue<string>("ApplicationName");
|
||||||
|
|
||||||
|
MimeMessage mailMessage = new MimeMessage();
|
||||||
|
|
||||||
|
mailMessage.From.Add(new MailboxAddress("auto.bus", _smtpCredentials.User));
|
||||||
|
mailMessage.To.Add(new MailboxAddress("auto.bus client", toEmail));
|
||||||
|
mailMessage.Subject = $"{applicationName}. {subject}";
|
||||||
|
mailMessage.Body = new TextPart(MimeKit.Text.TextFormat.Text) { Text = message};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _smtpClient.ConnectAsync(_smtpCredentials.Host, _smtpCredentials.Port, true);
|
||||||
|
await _smtpClient.AuthenticateAsync(Encoding.ASCII, _smtpCredentials.User, _smtpCredentials.Password);
|
||||||
|
await _smtpClient.SendAsync(mailMessage);
|
||||||
|
await _smtpClient.DisconnectAsync(true);
|
||||||
|
return (true, "Letter has been sent successfully");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,14 @@ namespace Server.Services;
|
|||||||
public interface IAuthenticationService
|
public interface IAuthenticationService
|
||||||
{
|
{
|
||||||
Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest);
|
Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest);
|
||||||
|
|
||||||
Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> AuthenticateAsync(AuthenticationRequest authRequest);
|
|
||||||
|
|
||||||
Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> RenewRefreshTokenAsync(string? token);
|
Task<(bool succeeded, string message)> ConfirmEmailAsync(string email, string token);
|
||||||
|
|
||||||
|
Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)>
|
||||||
|
AuthenticateAsync(AuthenticationRequest authRequest);
|
||||||
|
|
||||||
|
Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)>
|
||||||
|
RenewRefreshTokenAsync(string? token);
|
||||||
|
|
||||||
Task<bool> RevokeRefreshToken(string? token);
|
Task<bool> RevokeRefreshToken(string? token);
|
||||||
}
|
}
|
||||||
|
6
Server/Services/IEmailSenderService.cs
Normal file
6
Server/Services/IEmailSenderService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Server.Services;
|
||||||
|
|
||||||
|
public interface IEmailSenderService
|
||||||
|
{
|
||||||
|
Task<(bool succeeded, string message)> SendMail(string toEmail, string subject, string message);
|
||||||
|
}
|
@ -5,9 +5,17 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "host=localhost;database=auto.bus;user id=postgres;password=postgres;Include Error Detail = true"
|
"DefaultConnection": "host=localhost;database=auto.bus;user id=postgres;password=postgres;Include Error Detail = true"
|
||||||
},
|
},
|
||||||
|
"ApplicationName": "auto.bus",
|
||||||
|
"SmtpCredentials": {
|
||||||
|
"Host": "",
|
||||||
|
"Port": "",
|
||||||
|
"User": "",
|
||||||
|
"Password": ""
|
||||||
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "Secret which will never be exposed",
|
"Key": "Secret which will never be exposed",
|
||||||
"Audience": "Application URL",
|
"Audience": "Application URL",
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Host=localhost;Database=auto.bus;Username=postgres;Password=postgres;"
|
"DefaultConnection": "Host=localhost;Database=auto.bus;Username=postgres;Password=postgres;"
|
||||||
},
|
},
|
||||||
|
"SmtpCredentials": {
|
||||||
|
"Host": "",
|
||||||
|
"Port": "",
|
||||||
|
"User": "",
|
||||||
|
"Password": ""
|
||||||
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "Secret which will never be exposed",
|
"Key": "Secret which will never be exposed",
|
||||||
"Audience": "Application URL",
|
"Audience": "Application URL",
|
||||||
|
@ -24,4 +24,7 @@ public class RegistrationRequest
|
|||||||
[Required(ErrorMessage = "Password is required")]
|
[Required(ErrorMessage = "Password is required")]
|
||||||
[DataType(DataType.Password)]
|
[DataType(DataType.Password)]
|
||||||
public string Password { get; set; } = null!;
|
public string Password { get; set; } = null!;
|
||||||
|
|
||||||
|
[Url]
|
||||||
|
public string EmailConfirmationRedirectUrl { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user