From 902a8dce8f4611fed078428da28edec8f7e990f1 Mon Sep 17 00:00:00 2001 From: weegee Date: Fri, 3 Apr 2020 21:52:39 +0530 Subject: [PATCH] Forgot password flow (#19) * Add flow for forgot password * Update gitignore * Fix untracked files * Remove unused property Rename files and adopted the options pattern. Remove the placeholder and place the username there. Added entry in appSettings.json file in docs and other changes * Replace file from entities to Options folder; Propagate returnurl * Remove necessary config overrides, add returnUrl override logic * Make the project build again. * The register page incorrectly used the display name as username. * Add a development mail service. Co-authored-by: Paul Scharnofske --- .config/dotnet-tools.json | 18 ++ .gitignore | 28 ++ docs/defaults/src/WebApp/appsettings.json | 45 +-- .../Common/Interfaces/IMailService.cs | 12 + src/Application/Options/MailOptions.cs | 11 + src/Domain/Entities/ApplicationUser.cs | 2 +- src/Infrastructure/DependencyInjection.cs | 16 +- src/Infrastructure/Infrastructure.csproj | 16 +- .../20200403160315_AddDisplayName.Designer.cs | 275 ++++++++++++++++++ .../20200403160315_AddDisplayName.cs | 22 ++ .../ApplicationDbContextModelSnapshot.cs | 3 + .../Services/DevelopmentMailService.cs | 31 ++ src/Infrastructure/Services/MailService.cs | 58 ++++ .../Pages/Account/Forgot-Password.cshtml | 31 ++ .../Pages/Account/Forgot-Password.cshtml.cs | 64 ++++ src/WebApp/Pages/Account/Login.cshtml | 1 + src/WebApp/Pages/Account/Register.cshtml.cs | 4 +- .../Pages/Account/Reset-Password.cshtml | 34 +++ .../Pages/Account/Reset-Password.cshtml.cs | 81 ++++++ src/WebApp/Startup.cs | 3 + 20 files changed, 724 insertions(+), 31 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 src/Application/Common/Interfaces/IMailService.cs create mode 100644 src/Application/Options/MailOptions.cs create mode 100644 src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.Designer.cs create mode 100644 src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.cs create mode 100644 src/Infrastructure/Services/DevelopmentMailService.cs create mode 100644 src/Infrastructure/Services/MailService.cs create mode 100644 src/WebApp/Pages/Account/Forgot-Password.cshtml create mode 100644 src/WebApp/Pages/Account/Forgot-Password.cshtml.cs create mode 100644 src/WebApp/Pages/Account/Reset-Password.cshtml create mode 100644 src/WebApp/Pages/Account/Reset-Password.cshtml.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..09c15ee --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-format": { + "version": "3.3.111304", + "commands": [ + "dotnet-format" + ] + }, + "dotnet-ef": { + "version": "3.1.3", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d5aa7b..96cec59 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,31 @@ obj/ /.vs/ /.vscode/ *.user + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/docs/defaults/src/WebApp/appsettings.json b/docs/defaults/src/WebApp/appsettings.json index 362d4b8..3333633 100644 --- a/docs/defaults/src/WebApp/appsettings.json +++ b/docs/defaults/src/WebApp/appsettings.json @@ -6,29 +6,32 @@ } }, "IdentityServer": { - "Clients": [ - { - "ClientId": "codidact_client", - "ClientSecrets": [ - { - "Value": "LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=" - } - ], - "AllowedGrantTypes": [ - "authorization_code" - ], - "AllowedScopes": [ - "openid", - "profile" - ], - "RedirectUris": [ - "http://localhost:8000/signin-oidc" - ], - "RequireConsent": false - } - ] + "Clients": [{ + "ClientId": "codidact_client", + "ClientSecrets": [{ + "Value": "LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=" + }], + "AllowedGrantTypes": [ + "authorization_code" + ], + "AllowedScopes": [ + "openid", + "profile" + ], + "RedirectUris": [ + "http://localhost:8000/signin-oidc" + ], + "RequireConsent": false + }] }, "ConnectionStrings": { "Authentication": "Data Source=authentication.db" + }, + "Mail": { + "Host": "smtp.gmail.com", + "Port": 465, + "Sender": "example@gmail.com", + "SenderName": "example", + "EnableSsl": true } } diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs new file mode 100644 index 0000000..72ba3e8 --- /dev/null +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Codidact.Authentication.Domain.Entities; + + +namespace Codidact.Authentication.Application.Common.Interfaces +{ + public interface IMailService + { + Task SendEmailAsync(ApplicationUser user, string subject, string message); + Task SendResetPassword(ApplicationUser user, string token, string returnUrl); + } +} diff --git a/src/Application/Options/MailOptions.cs b/src/Application/Options/MailOptions.cs new file mode 100644 index 0000000..cb28a99 --- /dev/null +++ b/src/Application/Options/MailOptions.cs @@ -0,0 +1,11 @@ +namespace Codidact.Authentication.Application.Options +{ + public class MailOptions + { + public string Host { get; set; } + public int Port { get; set; } + public string SenderName { get; set; } + public string Sender { get; set; } + public bool EnableSsl { get; set; } + } +} diff --git a/src/Domain/Entities/ApplicationUser.cs b/src/Domain/Entities/ApplicationUser.cs index f24b8b0..92a7f8c 100644 --- a/src/Domain/Entities/ApplicationUser.cs +++ b/src/Domain/Entities/ApplicationUser.cs @@ -4,6 +4,6 @@ namespace Codidact.Authentication.Domain.Entities { public class ApplicationUser : IdentityUser { - + public string DisplayName { get; set; } } } diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index ce8467b..bc0a35d 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -9,6 +9,7 @@ using Codidact.Authentication.Application.Common.Interfaces; using Codidact.Authentication.Infrastructure.Services; using Codidact.Authentication.Domain.Entities; +using Codidact.Authentication.Application.Options; namespace Codidact.Authentication.Infrastructure { @@ -45,7 +46,8 @@ public static IServiceCollection AddInfrastructure( options.Password.RequireUppercase = false; }) .AddEntityFrameworkStores() - .AddSignInManager(); + .AddSignInManager() + .AddDefaultTokenProviders(); var identityServerBuilder = services.AddIdentityServer() .AddInMemoryClients(configuration.GetSection("IdentityServer:Clients")) @@ -60,6 +62,18 @@ public static IServiceCollection AddInfrastructure( services.AddAuthentication() .AddIdentityCookies(); + services.AddSingleton(configuration.GetSection("Mail").Get()); + + + if (environment.IsDevelopment()) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } + return services; } } diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 24477fd..35edfc7 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -5,13 +5,15 @@ Codidact.Authentication.Infrastructure - - - - - - - + + + + + + + + + diff --git a/src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.Designer.cs b/src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.Designer.cs new file mode 100644 index 0000000..98f4f2c --- /dev/null +++ b/src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using Codidact.Authentication.Infrastructure.Persistance; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Codidact.Authentication.Infrastructure.Persistance.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20200403160315_AddDisplayName")] + partial class AddDisplayName + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Codidact.Authentication.Domain.Entities.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("character varying(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("character varying(256)") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("character varying(256)") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("character varying(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("identity_user"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("character varying(256)") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("character varying(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex"); + + b.ToTable("identity_role"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("identity_role_claim"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("identity_user_claim"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("identity_user_login"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("identity_user_role"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("identity_user_token"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Codidact.Authentication.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Codidact.Authentication.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Codidact.Authentication.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Codidact.Authentication.Domain.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.cs b/src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.cs new file mode 100644 index 0000000..1b21a73 --- /dev/null +++ b/src/Infrastructure/Persistance/Migrations/20200403160315_AddDisplayName.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Codidact.Authentication.Infrastructure.Persistance.Migrations +{ + public partial class AddDisplayName : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DisplayName", + table: "identity_user", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DisplayName", + table: "identity_user"); + } + } +} diff --git a/src/Infrastructure/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs index 56a9eed..03aa568 100644 --- a/src/Infrastructure/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs @@ -33,6 +33,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsConcurrencyToken() .HasColumnType("text"); + b.Property("DisplayName") + .HasColumnType("text"); + b.Property("Email") .HasColumnType("character varying(256)") .HasMaxLength(256); diff --git a/src/Infrastructure/Services/DevelopmentMailService.cs b/src/Infrastructure/Services/DevelopmentMailService.cs new file mode 100644 index 0000000..b8a21f3 --- /dev/null +++ b/src/Infrastructure/Services/DevelopmentMailService.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using System; + +using Microsoft.Extensions.Logging; + +using Codidact.Authentication.Application.Common.Interfaces; +using Codidact.Authentication.Domain.Entities; + +namespace Codidact.Authentication.Infrastructure.Services +{ + public class DevelopmentMailService : IMailService + { + private readonly ILogger _logger; + + public DevelopmentMailService(ILogger logger) + { + _logger = logger; + } + + public Task SendEmailAsync(ApplicationUser user, string subject, string textMessage) + { + throw new NotImplementedException(); + } + + public Task SendResetPassword(ApplicationUser user, string token, string returnUrl) + { + _logger.LogInformation($"Sending password reset email to {user.Email} with the return rul {returnUrl}."); + return Task.CompletedTask; + } + } +} diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs new file mode 100644 index 0000000..92ea455 --- /dev/null +++ b/src/Infrastructure/Services/MailService.cs @@ -0,0 +1,58 @@ +using MailKit.Net.Smtp; +using MimeKit; +using MimeKit.Text; +using System.Threading.Tasks; +using System.Web; +using System; + +using Codidact.Authentication.Application.Common.Interfaces; +using Codidact.Authentication.Domain.Entities; +using Codidact.Authentication.Application.Options; + +using Microsoft.Extensions.DependencyInjection; + +namespace Codidact.Authentication.Infrastructure.Services +{ + public class MailService : IMailService + { + private MailOptions _emailConfiguration; + private IServiceProvider _secretsService; + public MailService(MailOptions emailConfiguration, IServiceProvider secretsService) + { + _emailConfiguration = emailConfiguration; + _secretsService = secretsService; + } + public async Task SendEmailAsync(ApplicationUser user, string subject, string textMessage) + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_emailConfiguration.SenderName, _emailConfiguration.Sender)); + message.To.Add(new MailboxAddress(user.UserName, user.Email)); + message.Subject = subject; + + message.Body = new TextPart(TextFormat.Html) + { + Text = textMessage + }; + var secrets = _secretsService.GetService(); + + using (var emailClient = new SmtpClient()) + { + emailClient.Connect(_emailConfiguration.Host, _emailConfiguration.Port, _emailConfiguration.EnableSsl); + + emailClient.AuthenticationMechanisms.Remove("XOAUTH2"); + + emailClient.Authenticate(_emailConfiguration.Sender, secrets.Get("EmailConfiguration:SenderEmailPassword").GetAwaiter().GetResult()); + + await emailClient.SendAsync(message); + + emailClient.Disconnect(true); + } + + } + public async Task SendResetPassword(ApplicationUser user, string token, string returnUrl) + { + var email = HttpUtility.UrlEncode(user.Email); + await SendEmailAsync(user, "Reset your Password", $"Click here to reset your password Test"); + } + } +} diff --git a/src/WebApp/Pages/Account/Forgot-Password.cshtml b/src/WebApp/Pages/Account/Forgot-Password.cshtml new file mode 100644 index 0000000..12e26b3 --- /dev/null +++ b/src/WebApp/Pages/Account/Forgot-Password.cshtml @@ -0,0 +1,31 @@ +@page +@model ForgotPasswordModel + +
+
+

Forgot Password

+

Sometimes we forget our password but thats okay, we can help you out. Don't have an account? Sign up instead.

+
+ +
+ @if (Model.Sent == false) + { +
+
+ + + +
+
+ + } + else + { +
+

Email has been sent to the provided email.

+
+ } +
+ diff --git a/src/WebApp/Pages/Account/Forgot-Password.cshtml.cs b/src/WebApp/Pages/Account/Forgot-Password.cshtml.cs new file mode 100644 index 0000000..cf18469 --- /dev/null +++ b/src/WebApp/Pages/Account/Forgot-Password.cshtml.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; + +using Codidact.Authentication.Domain.Entities; +using Codidact.Authentication.Application.Common.Interfaces; + +namespace Codidact.Authentication.WebApp.Pages.Account +{ + [BindProperties] + public class ForgotPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly IMailService _emailService; + + public ForgotPasswordModel( + UserManager userManager, + IMailService emailService + ) + { + _userManager = userManager; + _emailService = emailService; + } + [Required] + public string ReturnUrl { get; set; } = "/index"; + public void OnGet([FromQuery] string returnUrl) + { + if (returnUrl != null) + { + ReturnUrl = returnUrl; + } + } + + + [Required(ErrorMessage = "E-Mail Address is required")] + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + + public bool Sent { get; set; } + + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(Email); + if (user != null) + { + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + await _emailService.SendResetPassword(user, token, ReturnUrl); + Sent = true; + } + else + { + ModelState.AddModelError("Email", "No user found with this email"); + } + } + + return Page(); + } + } +} diff --git a/src/WebApp/Pages/Account/Login.cshtml b/src/WebApp/Pages/Account/Login.cshtml index 268a05b..a3f7ba8 100644 --- a/src/WebApp/Pages/Account/Login.cshtml +++ b/src/WebApp/Pages/Account/Login.cshtml @@ -5,6 +5,7 @@

Sign in

Welcome to Codidact! You can login to your account here. Don't have an account? Sign up instead.

+

Forgot your password? Click here to recover it.

diff --git a/src/WebApp/Pages/Account/Register.cshtml.cs b/src/WebApp/Pages/Account/Register.cshtml.cs index 50f1f11..6427f51 100644 --- a/src/WebApp/Pages/Account/Register.cshtml.cs +++ b/src/WebApp/Pages/Account/Register.cshtml.cs @@ -56,8 +56,10 @@ public async Task OnPostAsync() var result = await _userManager.CreateAsync(new ApplicationUser { Email = Email, - UserName = DisplayName, + UserName = Email, + DisplayName = DisplayName, }, Password); + if (result.Succeeded) { return LocalRedirect(ReturnUrl); diff --git a/src/WebApp/Pages/Account/Reset-Password.cshtml b/src/WebApp/Pages/Account/Reset-Password.cshtml new file mode 100644 index 0000000..8af7198 --- /dev/null +++ b/src/WebApp/Pages/Account/Reset-Password.cshtml @@ -0,0 +1,34 @@ +@page +@model ResetPasswordModel + +
+
+

Reset your password

+
+ + + + + + +
+
+
+
+ + +

Choose a strong one. At least 8 characters are recommended. Don't choose common words or names.

+
+ +
+ + +

We want to make sure, that you don't accidentally misspell your password.

+
+
+ +
+
+ diff --git a/src/WebApp/Pages/Account/Reset-Password.cshtml.cs b/src/WebApp/Pages/Account/Reset-Password.cshtml.cs new file mode 100644 index 0000000..052d89f --- /dev/null +++ b/src/WebApp/Pages/Account/Reset-Password.cshtml.cs @@ -0,0 +1,81 @@ +using System.Web; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Codidact.Authentication.Domain.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Codidact.Authentication.WebApp.Pages.Account +{ + + [BindProperties] + public class ResetPasswordModel : PageModel + { + + private readonly UserManager _userManager; + + public ResetPasswordModel( + UserManager userManager + ) + { + _userManager = userManager; + } + + [Required(ErrorMessage = "Password is required")] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Required(ErrorMessage = "Password Confirmaton is required")] + [DataType(DataType.Password)] + public string ConfirmPassword { get; set; } + + [Required] + public string ReturnUrl { get; set; } = "/index"; + + [Required(ErrorMessage = "Invalid reset password form")] + public string Token { get; set; } + + [Required(ErrorMessage = "Invalid reset password form")] + public string Email { get; set; } + + public void OnGet([FromQuery] string token, [FromQuery]string returnUrl, [FromQuery]string email) + { + Token = token; + ReturnUrl = returnUrl; + Email = email; + } + + public async Task OnPostAsync() + { + if (!Password.Equals(ConfirmPassword, System.StringComparison.InvariantCulture)) + { + ModelState.AddModelError("ConfirmPassword", "Password and Password Confirmation must match"); + } + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(HttpUtility.UrlDecode(Email)); + if (user == null) + { + ModelState.AddModelError("Email", "Email not found"); + } + else + { + var result = await _userManager.ResetPasswordAsync(user, HttpUtility.UrlDecode(Token), Password); + if (!result.Succeeded) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + } + else + { + return RedirectToPage("Login"); + } + } + } + return Page(); + } + } +} diff --git a/src/WebApp/Startup.cs b/src/WebApp/Startup.cs index 33850d7..97abcd3 100644 --- a/src/WebApp/Startup.cs +++ b/src/WebApp/Startup.cs @@ -11,6 +11,7 @@ using Codidact.Authentication.Application; using Codidact.Authentication.Infrastructure; using Codidact.Authentication.Infrastructure.Persistance; +using Codidact.Authentication.Application.Options; namespace Codidact.Authentication.WebApp { @@ -40,6 +41,8 @@ public void ConfigureServices(IServiceCollection services) options.LowercaseUrls = true; }); + services.Configure(_configuration.GetSection("Mail")); + services.AddRazorPages() .AddRazorRuntimeCompilation();