diff --git a/GWMS.UI/Areas/Identity/IdentityHostingStartup.cs b/GWMS.UI/Areas/Identity/IdentityHostingStartup.cs
new file mode 100644
index 0000000..d0b852e
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/IdentityHostingStartup.cs
@@ -0,0 +1,27 @@
+using System;
+using GWMS.Data;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: HostingStartup(typeof(GWMS.UI.Areas.Identity.IdentityHostingStartup))]
+
+namespace GWMS.UI.Areas.Identity
+{
+ public class IdentityHostingStartup : IHostingStartup
+ {
+ #region Public Methods
+
+ public void Configure(IWebHostBuilder builder)
+ {
+ builder.ConfigureServices((context, services) =>
+ {
+ });
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/AccessDenied.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/AccessDenied.cshtml
new file mode 100644
index 0000000..017f6ff
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/AccessDenied.cshtml
@@ -0,0 +1,10 @@
+@page
+@model AccessDeniedModel
+@{
+ ViewData["Title"] = "Access denied";
+}
+
+
+
@ViewData["Title"]
+
You do not have access to this resource.
+
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs
new file mode 100644
index 0000000..8046adf
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ public class AccessDeniedModel : PageModel
+ {
+ #region Public Methods
+
+ public void OnGet()
+ {
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml
new file mode 100644
index 0000000..d84e93b
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml
@@ -0,0 +1,7 @@
+@page
+@model ConfirmEmailModel
+@{
+ ViewData["Title"] = "Confirm email";
+}
+
+
@ViewData["Title"]
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
new file mode 100644
index 0000000..8e7f147
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ConfirmEmailModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ConfirmEmailModel(UserManager userManager)
+ {
+ _userManager = userManager;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(string userId, string code)
+ {
+ if (userId == null || code == null)
+ {
+ return RedirectToPage("/Index");
+ }
+
+ var user = await _userManager.FindByIdAsync(userId);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{userId}'.");
+ }
+
+ code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
+ var result = await _userManager.ConfirmEmailAsync(user, code);
+ StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
+ return Page();
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml
new file mode 100644
index 0000000..cbe5275
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml
@@ -0,0 +1,8 @@
+@page
+@model ConfirmEmailChangeModel
+@{
+ ViewData["Title"] = "Confirm email change";
+}
+
+
@ViewData["Title"]
+
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs
new file mode 100644
index 0000000..d0e261d
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ConfirmEmailChangeModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ConfirmEmailChangeModel(UserManager userManager, SignInManager signInManager)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(string userId, string email, string code)
+ {
+ if (userId == null || email == null || code == null)
+ {
+ return RedirectToPage("/Index");
+ }
+
+ var user = await _userManager.FindByIdAsync(userId);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{userId}'.");
+ }
+
+ code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
+ var result = await _userManager.ChangeEmailAsync(user, email, code);
+ if (!result.Succeeded)
+ {
+ StatusMessage = "Error changing email.";
+ return Page();
+ }
+
+ // In our UI email and user name are one and the same, so when we update the email
+ // we need to update the user name.
+ var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
+ if (!setUserNameResult.Succeeded)
+ {
+ StatusMessage = "Error changing user name.";
+ return Page();
+ }
+
+ await _signInManager.RefreshSignInAsync(user);
+ StatusMessage = "Thank you for confirming your email change.";
+ return Page();
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml
new file mode 100644
index 0000000..f7dc967
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml
@@ -0,0 +1,33 @@
+@page
+@model ExternalLoginModel
+@{
+ ViewData["Title"] = "Register";
+}
+
+
@ViewData["Title"]
+
Associate your @Model.ProviderDisplayName account.
+
+
+
+ You've successfully authenticated with @Model.ProviderDisplayName.
+ Please enter an email address for this site below and click the Register button to finish
+ logging in.
+
+
+
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
new file mode 100644
index 0000000..16e460f
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ExternalLoginModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly IEmailSender _emailSender;
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ExternalLoginModel(
+ SignInManager signInManager,
+ UserManager userManager,
+ ILogger logger,
+ IEmailSender emailSender)
+ {
+ _signInManager = signInManager;
+ _userManager = userManager;
+ _logger = logger;
+ _emailSender = emailSender;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string ErrorMessage { get; set; }
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public string ProviderDisplayName { get; set; }
+
+ public string ReturnUrl { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public IActionResult OnGetAsync()
+ {
+ return RedirectToPage("./Login");
+ }
+
+ public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
+ {
+ returnUrl = returnUrl ?? Url.Content("~/");
+ if (remoteError != null)
+ {
+ ErrorMessage = $"Error from external provider: {remoteError}";
+ return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
+ }
+ var info = await _signInManager.GetExternalLoginInfoAsync();
+ if (info == null)
+ {
+ ErrorMessage = "Error loading external login information.";
+ return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
+ }
+
+ // Sign in the user with this external login provider if the user already has a login.
+ var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
+ if (result.Succeeded)
+ {
+ _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
+ return LocalRedirect(returnUrl);
+ }
+ if (result.IsLockedOut)
+ {
+ return RedirectToPage("./Lockout");
+ }
+ else
+ {
+ // If the user does not have an account, then ask the user to create an account.
+ ReturnUrl = returnUrl;
+ ProviderDisplayName = info.ProviderDisplayName;
+ if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
+ {
+ Input = new InputModel
+ {
+ Email = info.Principal.FindFirstValue(ClaimTypes.Email)
+ };
+ }
+ return Page();
+ }
+ }
+
+ public IActionResult OnPost(string provider, string returnUrl = null)
+ {
+ // Request a redirect to the external login provider.
+ var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
+ var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
+ return new ChallengeResult(provider, properties);
+ }
+
+ public async Task OnPostConfirmationAsync(string returnUrl = null)
+ {
+ returnUrl = returnUrl ?? Url.Content("~/");
+ // Get the information about the user from the external login provider
+ var info = await _signInManager.GetExternalLoginInfoAsync();
+ if (info == null)
+ {
+ ErrorMessage = "Error loading external login information during confirmation.";
+ return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
+ }
+
+ if (ModelState.IsValid)
+ {
+ var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
+
+ var result = await _userManager.CreateAsync(user);
+ if (result.Succeeded)
+ {
+ result = await _userManager.AddLoginAsync(user, info);
+ if (result.Succeeded)
+ {
+ _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
+
+ var userId = await _userManager.GetUserIdAsync(user);
+ var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = Url.Page(
+ "/Account/ConfirmEmail",
+ pageHandler: null,
+ values: new { area = "Identity", userId = userId, code = code },
+ protocol: Request.Scheme);
+
+ await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
+ $"Please confirm your account by clicking here.");
+
+ // If account confirmation is required, we need to show the link if we don't have a real email sender
+ if (_userManager.Options.SignIn.RequireConfirmedAccount)
+ {
+ return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
+ }
+
+ await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
+
+ return LocalRedirect(returnUrl);
+ }
+ }
+ foreach (var error in result.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ }
+
+ ProviderDisplayName = info.ProviderDisplayName;
+ ReturnUrl = returnUrl;
+ return Page();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml
new file mode 100644
index 0000000..94f46b2
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml
@@ -0,0 +1,26 @@
+@page
+@model ForgotPasswordModel
+@{
+ ViewData["Title"] = "Forgot your password?";
+}
+
+
@ViewData["Title"]
+
Enter your email.
+
+
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs
new file mode 100644
index 0000000..d90835b
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Encodings.Web;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ForgotPasswordModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly IEmailSender _emailSender;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender)
+ {
+ _userManager = userManager;
+ _emailSender = emailSender;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnPostAsync()
+ {
+ if (ModelState.IsValid)
+ {
+ var user = await _userManager.FindByEmailAsync(Input.Email);
+ if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
+ {
+ // Don't reveal that the user does not exist or is not confirmed
+ return RedirectToPage("./ForgotPasswordConfirmation");
+ }
+
+ // For more information on how to enable account confirmation and password reset please
+ // visit https://go.microsoft.com/fwlink/?LinkID=532713
+ var code = await _userManager.GeneratePasswordResetTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = Url.Page(
+ "/Account/ResetPassword",
+ pageHandler: null,
+ values: new { area = "Identity", code },
+ protocol: Request.Scheme);
+
+ await _emailSender.SendEmailAsync(
+ Input.Email,
+ "Reset Password",
+ $"Please reset your password by clicking here.");
+
+ return RedirectToPage("./ForgotPasswordConfirmation");
+ }
+
+ return Page();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml
new file mode 100644
index 0000000..1a1b7f9
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml
@@ -0,0 +1,11 @@
+@page
+@model ForgotPasswordConfirmation
+@{
+ ViewData["Title"] = "Forgot password confirmation";
+}
+
+
@ViewData["Title"]
+
+ Please check your email to reset your password.
+
+
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs
new file mode 100644
index 0000000..7dccea2
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ForgotPasswordConfirmation.cshtml.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ForgotPasswordConfirmation : PageModel
+ {
+ #region Public Methods
+
+ public void OnGet()
+ {
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Lockout.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Lockout.cshtml
new file mode 100644
index 0000000..4eded88
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Lockout.cshtml
@@ -0,0 +1,10 @@
+@page
+@model LockoutModel
+@{
+ ViewData["Title"] = "Locked out";
+}
+
+
+
@ViewData["Title"]
+
This account has been locked out, please try again later.
+
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs
new file mode 100644
index 0000000..52f40f3
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Lockout.cshtml.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class LockoutModel : PageModel
+ {
+ #region Public Methods
+
+ public void OnGet()
+ {
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/LogOut.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/LogOut.cshtml
index 21b7fe2..d240159 100644
--- a/GWMS.UI/Areas/Identity/Pages/Account/LogOut.cshtml
+++ b/GWMS.UI/Areas/Identity/Pages/Account/LogOut.cshtml
@@ -1,15 +1,21 @@
@page
-@using Microsoft.AspNetCore.Identity
-@attribute [IgnoreAntiforgeryToken]
-@inject SignInManager SignInManager
-@functions {
- public async Task OnPost()
- {
- if (SignInManager.IsSignedIn(User))
- {
- await SignInManager.SignOutAsync();
- }
-
- return Redirect("~/");
- }
+@model LogoutModel
+@{
+ ViewData["Title"] = "Log out";
}
+
+
+
+ There are no external authentication services configured. See this article
+ for details on setting up this ASP.NET application to support logging in via external services.
+
+
+ }
+ else
+ {
+
+ }
+ }
+
+
*@
+
+
+@section Scripts {
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Login.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Login.cshtml.cs
new file mode 100644
index 0000000..4d31d98
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Login.cshtml.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class LoginModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public LoginModel(SignInManager signInManager,
+ ILogger logger,
+ UserManager userManager)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string ErrorMessage { get; set; }
+
+ public IList ExternalLogins { get; set; }
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public string ReturnUrl { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(string returnUrl = null)
+ {
+ if (!string.IsNullOrEmpty(ErrorMessage))
+ {
+ ModelState.AddModelError(string.Empty, ErrorMessage);
+ }
+
+ returnUrl ??= Url.Content("~/");
+
+ // Clear the existing external cookie to ensure a clean login process
+ await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
+
+ ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
+
+ ReturnUrl = returnUrl;
+ }
+
+ public async Task OnPostAsync(string returnUrl = null)
+ {
+ returnUrl ??= Url.Content("~/");
+
+ ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
+
+ if (ModelState.IsValid)
+ {
+ // This doesn't count login failures towards account lockout
+ // To enable password failures to trigger account lockout, set lockoutOnFailure: true
+ var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
+ if (result.Succeeded)
+ {
+ _logger.LogInformation("User logged in.");
+ return LocalRedirect(returnUrl);
+ }
+ if (result.RequiresTwoFactor)
+ {
+ return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
+ }
+ if (result.IsLockedOut)
+ {
+ _logger.LogWarning("User account locked out.");
+ return RedirectToPage("./Lockout");
+ }
+ else
+ {
+ ModelState.AddModelError(string.Empty, "Invalid login attempt.");
+ return Page();
+ }
+ }
+
+ // If we got this far, something failed, redisplay form
+ return Page();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; }
+
+ [Required]
+ [DataType(DataType.Password)]
+ public string Password { get; set; }
+
+ [Display(Name = "Remember me?")]
+ public bool RememberMe { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
new file mode 100644
index 0000000..780b4ec
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
@@ -0,0 +1,41 @@
+@page
+@model LoginWith2faModel
+@{
+ ViewData["Title"] = "Two-factor authentication";
+}
+
+
@ViewData["Title"]
+
+
Your login is protected with an authenticator app. Enter your authenticator code below.
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
new file mode 100644
index 0000000..2780d46
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class LoginWith2faModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public LoginWith2faModel(SignInManager signInManager, ILogger logger)
+ {
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public bool RememberMe { get; set; }
+
+ public string ReturnUrl { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(bool rememberMe, string returnUrl = null)
+ {
+ // Ensure the user has gone through the username & password screen first
+ var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
+
+ if (user == null)
+ {
+ throw new InvalidOperationException($"Unable to load two-factor authentication user.");
+ }
+
+ ReturnUrl = returnUrl;
+ RememberMe = rememberMe;
+
+ return Page();
+ }
+
+ public async Task OnPostAsync(bool rememberMe, string returnUrl = null)
+ {
+ if (!ModelState.IsValid)
+ {
+ return Page();
+ }
+
+ returnUrl = returnUrl ?? Url.Content("~/");
+
+ var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
+ if (user == null)
+ {
+ throw new InvalidOperationException($"Unable to load two-factor authentication user.");
+ }
+
+ var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
+
+ var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine);
+
+ if (result.Succeeded)
+ {
+ _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id);
+ return LocalRedirect(returnUrl);
+ }
+ else if (result.IsLockedOut)
+ {
+ _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
+ return RedirectToPage("./Lockout");
+ }
+ else
+ {
+ _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id);
+ ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
+ return Page();
+ }
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Display(Name = "Remember this machine")]
+ public bool RememberMachine { get; set; }
+
+ [Required]
+ [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Authenticator code")]
+ public string TwoFactorCode { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml
new file mode 100644
index 0000000..d866adb
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml
@@ -0,0 +1,29 @@
+@page
+@model LoginWithRecoveryCodeModel
+@{
+ ViewData["Title"] = "Recovery code verification";
+}
+
+
@ViewData["Title"]
+
+
+ You have requested to log in with a recovery code. This login will not be remembered until you provide
+ an authenticator app code at log in or disable 2FA and log in again.
+
+
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs
new file mode 100644
index 0000000..b20f3b5
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/LoginWithRecoveryCode.cshtml.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class LoginWithRecoveryCodeModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger)
+ {
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public string ReturnUrl { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(string returnUrl = null)
+ {
+ // Ensure the user has gone through the username & password screen first
+ var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
+ if (user == null)
+ {
+ throw new InvalidOperationException($"Unable to load two-factor authentication user.");
+ }
+
+ ReturnUrl = returnUrl;
+
+ return Page();
+ }
+
+ public async Task OnPostAsync(string returnUrl = null)
+ {
+ if (!ModelState.IsValid)
+ {
+ return Page();
+ }
+
+ var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
+ if (user == null)
+ {
+ throw new InvalidOperationException($"Unable to load two-factor authentication user.");
+ }
+
+ var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
+
+ var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
+
+ if (result.Succeeded)
+ {
+ _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id);
+ return LocalRedirect(returnUrl ?? Url.Content("~/"));
+ }
+ if (result.IsLockedOut)
+ {
+ _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
+ return RedirectToPage("./Lockout");
+ }
+ else
+ {
+ _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id);
+ ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
+ return Page();
+ }
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [BindProperty]
+ [Required]
+ [DataType(DataType.Text)]
+ [Display(Name = "Recovery Code")]
+ public string RecoveryCode { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Logout.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Logout.cshtml.cs
new file mode 100644
index 0000000..13fe794
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Logout.cshtml.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class LogoutModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public LogoutModel(SignInManager signInManager, ILogger logger)
+ {
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Methods
+
+ public void OnGet()
+ {
+ }
+
+ public async Task OnPost(string returnUrl = null)
+ {
+ await _signInManager.SignOutAsync();
+ _logger.LogInformation("User logged out.");
+ if (returnUrl != null)
+ {
+ return LocalRedirect(returnUrl);
+ }
+ else
+ {
+ return RedirectToPage();
+ }
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml
new file mode 100644
index 0000000..31a2ea5
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml
@@ -0,0 +1,36 @@
+@page
+@model ChangePasswordModel
+@{
+ ViewData["Title"] = "Change password";
+ ViewData["ActivePage"] = ManageNavPages.ChangePassword;
+}
+
+
@ViewData["Title"]
+
+
+
+
+
+
+
+@section Scripts {
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs
new file mode 100644
index 0000000..9b87ac1
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ChangePassword.cshtml.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class ChangePasswordModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ChangePasswordModel(
+ UserManager userManager,
+ SignInManager signInManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var hasPassword = await _userManager.HasPasswordAsync(user);
+ if (!hasPassword)
+ {
+ return RedirectToPage("./SetPassword");
+ }
+
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ if (!ModelState.IsValid)
+ {
+ return Page();
+ }
+
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
+ if (!changePasswordResult.Succeeded)
+ {
+ foreach (var error in changePasswordResult.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ return Page();
+ }
+
+ await _signInManager.RefreshSignInAsync(user);
+ _logger.LogInformation("User changed their password successfully.");
+ StatusMessage = "Your password has been changed.";
+
+ return RedirectToPage();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm new password")]
+ [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; }
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "New password")]
+ public string NewPassword { get; set; }
+
+ [Required]
+ [DataType(DataType.Password)]
+ [Display(Name = "Current password")]
+ public string OldPassword { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml
new file mode 100644
index 0000000..c95ab92
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml
@@ -0,0 +1,33 @@
+@page
+@model DeletePersonalDataModel
+@{
+ ViewData["Title"] = "Delete Personal Data";
+ ViewData["ActivePage"] = ManageNavPages.PersonalData;
+}
+
+
@ViewData["Title"]
+
+
+
+ Deleting this data will permanently remove your account, and this cannot be recovered.
+
+
+
+
+
+
+
+@section Scripts {
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs
new file mode 100644
index 0000000..fe5574b
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml.cs
@@ -0,0 +1,107 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class DeletePersonalDataModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public DeletePersonalDataModel(
+ UserManager userManager,
+ SignInManager signInManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public bool RequirePassword { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGet()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ RequirePassword = await _userManager.HasPasswordAsync(user);
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ RequirePassword = await _userManager.HasPasswordAsync(user);
+ if (RequirePassword)
+ {
+ if (!await _userManager.CheckPasswordAsync(user, Input.Password))
+ {
+ ModelState.AddModelError(string.Empty, "Incorrect password.");
+ return Page();
+ }
+ }
+
+ var result = await _userManager.DeleteAsync(user);
+ var userId = await _userManager.GetUserIdAsync(user);
+ if (!result.Succeeded)
+ {
+ throw new InvalidOperationException($"Unexpected error occurred deleting user with ID '{userId}'.");
+ }
+
+ await _signInManager.SignOutAsync();
+
+ _logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
+
+ return Redirect("~/");
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [DataType(DataType.Password)]
+ public string Password { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml
new file mode 100644
index 0000000..96df752
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml
@@ -0,0 +1,25 @@
+@page
+@model Disable2faModel
+@{
+ ViewData["Title"] = "Disable two-factor authentication (2FA)";
+ ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
+}
+
+
+
@ViewData["Title"]
+
+
+
+ This action only disables 2FA.
+
+
+ Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
+ used in an authenticator app you should reset your authenticator keys.
+
+
+
+
+
+
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs
new file mode 100644
index 0000000..476d169
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Disable2fa.cshtml.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class Disable2faModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public Disable2faModel(
+ UserManager userManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGet()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ if (!await _userManager.GetTwoFactorEnabledAsync(user))
+ {
+ throw new InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled.");
+ }
+
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
+ if (!disable2faResult.Succeeded)
+ {
+ throw new InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
+ StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
+ return RedirectToPage("./TwoFactorAuthentication");
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml
new file mode 100644
index 0000000..87470c2
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml
@@ -0,0 +1,12 @@
+@page
+@model DownloadPersonalDataModel
+@{
+ ViewData["Title"] = "Download Your Data";
+ ViewData["ActivePage"] = ManageNavPages.PersonalData;
+}
+
+
@ViewData["Title"]
+
+@section Scripts {
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs
new file mode 100644
index 0000000..24dd4aa
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class DownloadPersonalDataModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public DownloadPersonalDataModel(
+ UserManager userManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Methods
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ _logger.LogInformation("User with ID '{UserId}' asked for their personal data.", _userManager.GetUserId(User));
+
+ // Only include personal data for download
+ var personalData = new Dictionary();
+ var personalDataProps = typeof(IdentityUser).GetProperties().Where(
+ prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
+ foreach (var p in personalDataProps)
+ {
+ personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
+ }
+
+ var logins = await _userManager.GetLoginsAsync(user);
+ foreach (var l in logins)
+ {
+ personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
+ }
+
+ Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
+ return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json");
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/Email.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Email.cshtml
new file mode 100644
index 0000000..2a599ee
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Email.cshtml
@@ -0,0 +1,43 @@
+@page
+@model EmailModel
+@{
+ ViewData["Title"] = "Manage Email";
+ ViewData["ActivePage"] = ManageNavPages.Email;
+}
+
+
@ViewData["Title"]
+
+
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
new file mode 100644
index 0000000..d25d758
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public partial class EmailModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly IEmailSender _emailSender;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public EmailModel(
+ UserManager userManager,
+ SignInManager signInManager,
+ IEmailSender emailSender)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _emailSender = emailSender;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ public string Email { get; set; }
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public bool IsEmailConfirmed { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ public string Username { get; set; }
+
+ #endregion Public Properties
+
+ #region Private Methods
+
+ private async Task LoadAsync(IdentityUser user)
+ {
+ var email = await _userManager.GetEmailAsync(user);
+ Email = email;
+
+ Input = new InputModel
+ {
+ NewEmail = email,
+ };
+
+ IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
+ }
+
+ #endregion Private Methods
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ await LoadAsync(user);
+ return Page();
+ }
+
+ public async Task OnPostChangeEmailAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ if (!ModelState.IsValid)
+ {
+ await LoadAsync(user);
+ return Page();
+ }
+
+ var email = await _userManager.GetEmailAsync(user);
+ if (Input.NewEmail != email)
+ {
+ var userId = await _userManager.GetUserIdAsync(user);
+ var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = Url.Page(
+ "/Account/ConfirmEmailChange",
+ pageHandler: null,
+ values: new { userId = userId, email = Input.NewEmail, code = code },
+ protocol: Request.Scheme);
+ await _emailSender.SendEmailAsync(
+ Input.NewEmail,
+ "Confirm your email",
+ $"Please confirm your account by clicking here.");
+
+ StatusMessage = "Confirmation link to change email sent. Please check your email.";
+ return RedirectToPage();
+ }
+
+ StatusMessage = "Your email is unchanged.";
+ return RedirectToPage();
+ }
+
+ public async Task OnPostSendVerificationEmailAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ if (!ModelState.IsValid)
+ {
+ await LoadAsync(user);
+ return Page();
+ }
+
+ var userId = await _userManager.GetUserIdAsync(user);
+ var email = await _userManager.GetEmailAsync(user);
+ var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = Url.Page(
+ "/Account/ConfirmEmail",
+ pageHandler: null,
+ values: new { area = "Identity", userId = userId, code = code },
+ protocol: Request.Scheme);
+ await _emailSender.SendEmailAsync(
+ email,
+ "Confirm your email",
+ $"Please confirm your account by clicking here.");
+
+ StatusMessage = "Verification email sent. Please check your email.";
+ return RedirectToPage();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [EmailAddress]
+ [Display(Name = "New email")]
+ public string NewEmail { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml
new file mode 100644
index 0000000..5506f50
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml
@@ -0,0 +1,63 @@
+@page
+@model EnableAuthenticatorModel
+@{
+ ViewData["Title"] = "Configure authenticator app";
+ ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
+}
+
+
+
@ViewData["Title"]
+
+
To use an authenticator app go through the following steps:
+
+
+
+ Download a two-factor authenticator app like Microsoft Authenticator for
+ Android and
+ iOS or
+ Google Authenticator for
+ Android and
+ iOS.
+
+
+
+
Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.
+ Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
+ with a unique code. Enter the code in the confirmation box below.
+
+
+
+
+
+
+
+
+
+
+@section Scripts {
+
+
+
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs
new file mode 100644
index 0000000..f1c876f
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs
@@ -0,0 +1,183 @@
+using System;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class EnableAuthenticatorModel : PageModel
+ {
+ #region Private Fields
+
+ private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
+ private readonly ILogger _logger;
+ private readonly UrlEncoder _urlEncoder;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public EnableAuthenticatorModel(
+ UserManager userManager,
+ ILogger logger,
+ UrlEncoder urlEncoder)
+ {
+ _userManager = userManager;
+ _logger = logger;
+ _urlEncoder = urlEncoder;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ public string AuthenticatorUri { get; set; }
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ [TempData]
+ public string[] RecoveryCodes { get; set; }
+
+ public string SharedKey { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Private Methods
+
+ private string FormatKey(string unformattedKey)
+ {
+ var result = new StringBuilder();
+ int currentPosition = 0;
+ while (currentPosition + 4 < unformattedKey.Length)
+ {
+ result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
+ currentPosition += 4;
+ }
+ if (currentPosition < unformattedKey.Length)
+ {
+ result.Append(unformattedKey.Substring(currentPosition));
+ }
+
+ return result.ToString().ToLowerInvariant();
+ }
+
+ private string GenerateQrCodeUri(string email, string unformattedKey)
+ {
+ return string.Format(
+ AuthenticatorUriFormat,
+ _urlEncoder.Encode("GWMS.User"),
+ _urlEncoder.Encode(email),
+ unformattedKey);
+ }
+
+ private async Task LoadSharedKeyAndQrCodeUriAsync(IdentityUser user)
+ {
+ // Load the authenticator key & QR code URI to display on the form
+ var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
+ if (string.IsNullOrEmpty(unformattedKey))
+ {
+ await _userManager.ResetAuthenticatorKeyAsync(user);
+ unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
+ }
+
+ SharedKey = FormatKey(unformattedKey);
+
+ var email = await _userManager.GetEmailAsync(user);
+ AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey);
+ }
+
+ #endregion Private Methods
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ await LoadSharedKeyAndQrCodeUriAsync(user);
+
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ if (!ModelState.IsValid)
+ {
+ await LoadSharedKeyAndQrCodeUriAsync(user);
+ return Page();
+ }
+
+ // Strip spaces and hypens
+ var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
+
+ var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
+ user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
+
+ if (!is2faTokenValid)
+ {
+ ModelState.AddModelError("Input.Code", "Verification code is invalid.");
+ await LoadSharedKeyAndQrCodeUriAsync(user);
+ return Page();
+ }
+
+ await _userManager.SetTwoFactorEnabledAsync(user, true);
+ var userId = await _userManager.GetUserIdAsync(user);
+ _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
+
+ StatusMessage = "Your authenticator app has been verified.";
+
+ if (await _userManager.CountRecoveryCodesAsync(user) == 0)
+ {
+ var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
+ RecoveryCodes = recoveryCodes.ToArray();
+ return RedirectToPage("./ShowRecoveryCodes");
+ }
+ else
+ {
+ return RedirectToPage("./TwoFactorAuthentication");
+ }
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Verification Code")]
+ public string Code { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml
new file mode 100644
index 0000000..d7a3c42
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml
@@ -0,0 +1,53 @@
+@page
+@model ExternalLoginsModel
+@{
+ ViewData["Title"] = "Manage your external logins";
+ ViewData["ActivePage"] = ManageNavPages.ExternalLogins;
+}
+
+
+@if (Model.CurrentLogins?.Count > 0)
+{
+
Registered Logins
+
+
+ @foreach (var login in Model.CurrentLogins)
+ {
+
+
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs
new file mode 100644
index 0000000..3f123de
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ExternalLogins.cshtml.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class ExternalLoginsModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ExternalLoginsModel(
+ UserManager userManager,
+ SignInManager signInManager)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ public IList CurrentLogins { get; set; }
+
+ public IList OtherLogins { get; set; }
+
+ public bool ShowRemoveButton { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID 'user.Id'.");
+ }
+
+ CurrentLogins = await _userManager.GetLoginsAsync(user);
+ OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
+ .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
+ .ToList();
+ ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1;
+ return Page();
+ }
+
+ public async Task OnGetLinkLoginCallbackAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID 'user.Id'.");
+ }
+
+ var info = await _signInManager.GetExternalLoginInfoAsync(user.Id);
+ if (info == null)
+ {
+ throw new InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'.");
+ }
+
+ var result = await _userManager.AddLoginAsync(user, info);
+ if (!result.Succeeded)
+ {
+ StatusMessage = "The external login was not added. External logins can only be associated with one account.";
+ return RedirectToPage();
+ }
+
+ // Clear the existing external cookie to ensure a clean login process
+ await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
+
+ StatusMessage = "The external login was added.";
+ return RedirectToPage();
+ }
+
+ public async Task OnPostLinkLoginAsync(string provider)
+ {
+ // Clear the existing external cookie to ensure a clean login process
+ await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
+
+ // Request a redirect to the external login provider to link a login for the current user
+ var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback");
+ var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
+ return new ChallengeResult(provider, properties);
+ }
+
+ public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey)
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID 'user.Id'.");
+ }
+
+ var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey);
+ if (!result.Succeeded)
+ {
+ StatusMessage = "The external login was not removed.";
+ return RedirectToPage();
+ }
+
+ await _signInManager.RefreshSignInAsync(user);
+ StatusMessage = "The external login was removed.";
+ return RedirectToPage();
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml
new file mode 100644
index 0000000..284ab59
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml
@@ -0,0 +1,27 @@
+@page
+@model GenerateRecoveryCodesModel
+@{
+ ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
+ ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
+}
+
+
+
@ViewData["Title"]
+
+
+
+ Put these codes in a safe place.
+
+
+ If you lose your device and don't have the recovery codes you will lose access to your account.
+
+
+ Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
+ used in an authenticator app you should reset your authenticator keys.
+
+
+
+
+
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs
new file mode 100644
index 0000000..fbb77d9
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class GenerateRecoveryCodesModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public GenerateRecoveryCodesModel(
+ UserManager userManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string[] RecoveryCodes { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
+ if (!isTwoFactorEnabled)
+ {
+ var userId = await _userManager.GetUserIdAsync(user);
+ throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled.");
+ }
+
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
+ var userId = await _userManager.GetUserIdAsync(user);
+ if (!isTwoFactorEnabled)
+ {
+ throw new InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled.");
+ }
+
+ var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
+ RecoveryCodes = recoveryCodes.ToArray();
+
+ _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
+ StatusMessage = "You have generated new recovery codes.";
+ return RedirectToPage("./ShowRecoveryCodes");
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/Index.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Index.cshtml
new file mode 100644
index 0000000..e018437
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Index.cshtml
@@ -0,0 +1,30 @@
+@page
+@model IndexModel
+@{
+ ViewData["Title"] = "Profile";
+ ViewData["ActivePage"] = ManageNavPages.Index;
+}
+
+
@ViewData["Title"]
+
+
+
+
+
+
+
+@section Scripts {
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
new file mode 100644
index 0000000..a226a06
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public partial class IndexModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public IndexModel(
+ UserManager userManager,
+ SignInManager signInManager)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ public string Username { get; set; }
+
+ #endregion Public Properties
+
+ #region Private Methods
+
+ private async Task LoadAsync(IdentityUser user)
+ {
+ var userName = await _userManager.GetUserNameAsync(user);
+ var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
+
+ Username = userName;
+
+ Input = new InputModel
+ {
+ PhoneNumber = phoneNumber
+ };
+ }
+
+ #endregion Private Methods
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ await LoadAsync(user);
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ if (!ModelState.IsValid)
+ {
+ await LoadAsync(user);
+ return Page();
+ }
+
+ var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
+ if (Input.PhoneNumber != phoneNumber)
+ {
+ var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
+ if (!setPhoneResult.Succeeded)
+ {
+ StatusMessage = "Unexpected error when trying to set phone number.";
+ return RedirectToPage();
+ }
+ }
+
+ await _signInManager.RefreshSignInAsync(user);
+ StatusMessage = "Your profile has been updated";
+ return RedirectToPage();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Phone]
+ [Display(Name = "Phone number")]
+ public string PhoneNumber { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
new file mode 100644
index 0000000..2fb6ac7
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public static class ManageNavPages
+ {
+ #region Public Properties
+
+ public static string ChangePassword => "ChangePassword";
+ public static string DeletePersonalData => "DeletePersonalData";
+ public static string DownloadPersonalData => "DownloadPersonalData";
+ public static string Email => "Email";
+ public static string ExternalLogins => "ExternalLogins";
+ public static string Index => "Index";
+ public static string PersonalData => "PersonalData";
+
+ public static string TwoFactorAuthentication => "TwoFactorAuthentication";
+
+ #endregion Public Properties
+
+ #region Private Methods
+
+ private static string PageNavClass(ViewContext viewContext, string page)
+ {
+ var activePage = viewContext.ViewData["ActivePage"] as string
+ ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName);
+ return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
+ }
+
+ #endregion Private Methods
+
+ #region Public Methods
+
+ public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
+
+ public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData);
+
+ public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData);
+
+ public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
+
+ public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
+
+ public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
+
+ public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
+
+ public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml
new file mode 100644
index 0000000..d64bd82
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml
@@ -0,0 +1,27 @@
+@page
+@model PersonalDataModel
+@{
+ ViewData["Title"] = "Personal Data";
+ ViewData["ActivePage"] = ManageNavPages.PersonalData;
+}
+
+
@ViewData["Title"]
+
+
+
+
Your account contains personal data that you have given us. This page allows you to download or delete that data.
+
+ Deleting this data will permanently remove your account, and this cannot be recovered.
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs
new file mode 100644
index 0000000..f3a2f0f
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/PersonalData.cshtml.cs
@@ -0,0 +1,45 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class PersonalDataModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly ILogger _logger;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public PersonalDataModel(
+ UserManager userManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Methods
+
+ public async Task OnGet()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ return Page();
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml
new file mode 100644
index 0000000..081c824
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml
@@ -0,0 +1,24 @@
+@page
+@model ResetAuthenticatorModel
+@{
+ ViewData["Title"] = "Reset authenticator key";
+ ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
+}
+
+
+
@ViewData["Title"]
+
+
+
+ If you reset your authenticator key your authenticator app will not work until you reconfigure it.
+
+
+ This process disables 2FA until you verify your authenticator app.
+ If you do not complete your authenticator app configuration you may lose access to your account.
+
+
+
+
+
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs
new file mode 100644
index 0000000..28b8c6a
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ResetAuthenticator.cshtml.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class ResetAuthenticatorModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly SignInManager _signInManager;
+ private ILogger _logger;
+ private UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ResetAuthenticatorModel(
+ UserManager userManager,
+ SignInManager signInManager,
+ ILogger logger)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _logger = logger;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGet()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ await _userManager.SetTwoFactorEnabledAsync(user, false);
+ await _userManager.ResetAuthenticatorKeyAsync(user);
+ _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
+
+ await _signInManager.RefreshSignInAsync(user);
+ StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
+
+ return RedirectToPage("./EnableAuthenticator");
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml
new file mode 100644
index 0000000..f1817aa
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml
@@ -0,0 +1,35 @@
+@page
+@model SetPasswordModel
+@{
+ ViewData["Title"] = "Set password";
+ ViewData["ActivePage"] = ManageNavPages.ChangePassword;
+}
+
+
Set your password
+
+
+ You do not have a local username/password for this site. Add a local
+ account so you can log in without an external login.
+
+
+
+
+
+
+
+@section Scripts {
+
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs
new file mode 100644
index 0000000..83e247f
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/SetPassword.cshtml.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account.Manage
+{
+ public class SetPasswordModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public SetPasswordModel(
+ UserManager userManager,
+ SignInManager signInManager)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ [TempData]
+ public string StatusMessage { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync()
+ {
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var hasPassword = await _userManager.HasPasswordAsync(user);
+
+ if (hasPassword)
+ {
+ return RedirectToPage("./ChangePassword");
+ }
+
+ return Page();
+ }
+
+ public async Task OnPostAsync()
+ {
+ if (!ModelState.IsValid)
+ {
+ return Page();
+ }
+
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
+ }
+
+ var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword);
+ if (!addPasswordResult.Succeeded)
+ {
+ foreach (var error in addPasswordResult.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ return Page();
+ }
+
+ await _signInManager.RefreshSignInAsync(user);
+ StatusMessage = "Your password has been set.";
+
+ return RedirectToPage();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm new password")]
+ [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; }
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "New password")]
+ public string NewPassword { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml
new file mode 100644
index 0000000..23fa27b
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Manage/ShowRecoveryCodes.cshtml
@@ -0,0 +1,25 @@
+@page
+@model ShowRecoveryCodesModel
+@{
+ ViewData["Title"] = "Recovery codes";
+ ViewData["ActivePage"] = "TwoFactorAuthentication";
+}
+
+
+
@ViewData["Title"]
+
+
+ Put these codes in a safe place.
+
+
+ If you lose your device and don't have the recovery codes you will lose access to your account.
+
+ There are no external authentication services configured. See this article
+ for details on setting up this ASP.NET application to support logging in via external services.
+
+
+ }
+ else
+ {
+
+ }
+ }
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/Register.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/Register.cshtml.cs
new file mode 100644
index 0000000..19d0077
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/Register.cshtml.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Logging;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class RegisterModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly IEmailSender _emailSender;
+ private readonly ILogger _logger;
+ private readonly SignInManager _signInManager;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public RegisterModel(
+ UserManager userManager,
+ SignInManager signInManager,
+ ILogger logger,
+ IEmailSender emailSender)
+ {
+ _userManager = userManager;
+ _signInManager = signInManager;
+ _logger = logger;
+ _emailSender = emailSender;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ public IList ExternalLogins { get; set; }
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ public string ReturnUrl { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(string returnUrl = null)
+ {
+ ReturnUrl = returnUrl;
+ ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
+ }
+
+ public async Task OnPostAsync(string returnUrl = null)
+ {
+ returnUrl ??= Url.Content("~/");
+ ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
+ if (ModelState.IsValid)
+ {
+ var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
+ var result = await _userManager.CreateAsync(user, Input.Password);
+ if (result.Succeeded)
+ {
+ _logger.LogInformation("User created a new account with password.");
+
+ var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = Url.Page(
+ "/Account/ConfirmEmail",
+ pageHandler: null,
+ values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
+ protocol: Request.Scheme);
+
+ await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
+ $"Please confirm your account by clicking here.");
+
+ if (_userManager.Options.SignIn.RequireConfirmedAccount)
+ {
+ return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
+ }
+ else
+ {
+ // aggiunta ruolo UNDEF (da sistemare poi)
+ // https://code-maze.com/using-roles-in-blazor-webassembly-hosted-applications/
+ await _userManager.AddToRoleAsync(user, "Undef");
+ // continuo come default
+ await _signInManager.SignInAsync(user, isPersistent: false);
+ return LocalRedirect(returnUrl);
+ }
+ }
+ foreach (var error in result.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ }
+
+ // If we got this far, something failed, redisplay form
+ return Page();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm password")]
+ [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; }
+
+ [Required]
+ [EmailAddress]
+ [Display(Name = "Email")]
+ public string Email { get; set; }
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ [Display(Name = "Password")]
+ public string Password { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml
new file mode 100644
index 0000000..b851192
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml
@@ -0,0 +1,22 @@
+@page
+@model RegisterConfirmationModel
+@{
+ ViewData["Title"] = "Register confirmation";
+}
+
+
@ViewData["Title"]
+@{
+ if (@Model.DisplayConfirmAccountLink)
+ {
+
+ This app does not currently have a real email sender registered, see these docs for how to configure a real email sender.
+ Normally this would be emailed: Click here to confirm your account
+
+ }
+ else
+ {
+
+ Please check your email to confirm your account.
+
+ }
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs
new file mode 100644
index 0000000..74abbcc
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs
@@ -0,0 +1,75 @@
+using Microsoft.AspNetCore.Authorization;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class RegisterConfirmationModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly IEmailSender _sender;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public RegisterConfirmationModel(UserManager userManager, IEmailSender sender)
+ {
+ _userManager = userManager;
+ _sender = sender;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ public bool DisplayConfirmAccountLink { get; set; }
+ public string Email { get; set; }
+ public string EmailConfirmationUrl { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public async Task OnGetAsync(string email, string returnUrl = null)
+ {
+ if (email == null)
+ {
+ return RedirectToPage("/Index");
+ }
+
+ var user = await _userManager.FindByEmailAsync(email);
+ if (user == null)
+ {
+ return NotFound($"Unable to load user with email '{email}'.");
+ }
+
+ Email = email;
+ // Once you add a real email sender, you should remove this code that lets you confirm the account
+ DisplayConfirmAccountLink = false;
+ if (DisplayConfirmAccountLink)
+ {
+ var userId = await _userManager.GetUserIdAsync(user);
+ var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ EmailConfirmationUrl = Url.Page(
+ "/Account/ConfirmEmail",
+ pageHandler: null,
+ values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
+ protocol: Request.Scheme);
+ }
+
+ return Page();
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
new file mode 100644
index 0000000..8578c23
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml
@@ -0,0 +1,26 @@
+@page
+@model ResendEmailConfirmationModel
+@{
+ ViewData["Title"] = "Resend email confirmation";
+}
+
+
@ViewData["Title"]
+
Enter your email.
+
+
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs
new file mode 100644
index 0000000..e811871
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs
@@ -0,0 +1,97 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.UI.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ResendEmailConfirmationModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly IEmailSender _emailSender;
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender)
+ {
+ _userManager = userManager;
+ _emailSender = emailSender;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public void OnGet()
+ {
+ }
+
+ public async Task OnPostAsync()
+ {
+ if (!ModelState.IsValid)
+ {
+ return Page();
+ }
+
+ var user = await _userManager.FindByEmailAsync(Input.Email);
+ if (user == null)
+ {
+ ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
+ return Page();
+ }
+
+ var userId = await _userManager.GetUserIdAsync(user);
+ var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
+ code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
+ var callbackUrl = Url.Page(
+ "/Account/ConfirmEmail",
+ pageHandler: null,
+ values: new { userId = userId, code = code },
+ protocol: Request.Scheme);
+ await _emailSender.SendEmailAsync(
+ Input.Email,
+ "Confirm your email",
+ $"Please confirm your account by clicking here.");
+
+ ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
+ return Page();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ResetPassword.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ResetPassword.cshtml
new file mode 100644
index 0000000..27bc951
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ResetPassword.cshtml
@@ -0,0 +1,37 @@
+@page
+@model ResetPasswordModel
+@{
+ ViewData["Title"] = "Reset password";
+}
+
+
@ViewData["Title"]
+
Reset your password.
+
+
+
+
+
+
+
+@section Scripts {
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
new file mode 100644
index 0000000..d7fa1aa
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ResetPasswordModel : PageModel
+ {
+ #region Private Fields
+
+ private readonly UserManager _userManager;
+
+ #endregion Private Fields
+
+ #region Public Constructors
+
+ public ResetPasswordModel(UserManager userManager)
+ {
+ _userManager = userManager;
+ }
+
+ #endregion Public Constructors
+
+ #region Public Properties
+
+ [BindProperty]
+ public InputModel Input { get; set; }
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public IActionResult OnGet(string code = null)
+ {
+ if (code == null)
+ {
+ return BadRequest("A code must be supplied for password reset.");
+ }
+ else
+ {
+ Input = new InputModel
+ {
+ Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))
+ };
+ return Page();
+ }
+ }
+
+ public async Task OnPostAsync()
+ {
+ if (!ModelState.IsValid)
+ {
+ return Page();
+ }
+
+ var user = await _userManager.FindByEmailAsync(Input.Email);
+ if (user == null)
+ {
+ // Don't reveal that the user does not exist
+ return RedirectToPage("./ResetPasswordConfirmation");
+ }
+
+ var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
+ if (result.Succeeded)
+ {
+ return RedirectToPage("./ResetPasswordConfirmation");
+ }
+
+ foreach (var error in result.Errors)
+ {
+ ModelState.AddModelError(string.Empty, error.Description);
+ }
+ return Page();
+ }
+
+ #endregion Public Methods
+
+ #region Public Classes
+
+ public class InputModel
+ {
+ #region Public Properties
+
+ public string Code { get; set; }
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm password")]
+ [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; }
+
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; }
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
+ [DataType(DataType.Password)]
+ public string Password { get; set; }
+
+ #endregion Public Properties
+ }
+
+ #endregion Public Classes
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml
new file mode 100644
index 0000000..c52552f
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml
@@ -0,0 +1,10 @@
+@page
+@model ResetPasswordConfirmationModel
+@{
+ ViewData["Title"] = "Reset password confirmation";
+}
+
+
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs
new file mode 100644
index 0000000..fe7be64
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/ResetPasswordConfirmation.cshtml.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages.Account
+{
+ [AllowAnonymous]
+ public class ResetPasswordConfirmationModel : PageModel
+ {
+ #region Public Methods
+
+ public void OnGet()
+ {
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/_StatusMessage.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/_StatusMessage.cshtml
new file mode 100644
index 0000000..e996841
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/_StatusMessage.cshtml
@@ -0,0 +1,10 @@
+@model string
+
+@if (!String.IsNullOrEmpty(Model))
+{
+ var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
+
+
+ @Model
+
+}
diff --git a/GWMS.UI/Areas/Identity/Pages/Account/_ViewImports.cshtml b/GWMS.UI/Areas/Identity/Pages/Account/_ViewImports.cshtml
new file mode 100644
index 0000000..436510e
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Account/_ViewImports.cshtml
@@ -0,0 +1 @@
+@using GWMS.UI.Areas.Identity.Pages.Account
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/Error.cshtml b/GWMS.UI/Areas/Identity/Pages/Error.cshtml
new file mode 100644
index 0000000..b1f3143
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Error.cshtml
@@ -0,0 +1,23 @@
+@page
+@model ErrorModel
+@{
+ ViewData["Title"] = "Error";
+}
+
+
Error.
+
An error occurred while processing your request.
+
+@if (Model.ShowRequestId)
+{
+
+ Request ID:@Model.RequestId
+
+}
+
+
Development Mode
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application.
+
diff --git a/GWMS.UI/Areas/Identity/Pages/Error.cshtml.cs b/GWMS.UI/Areas/Identity/Pages/Error.cshtml.cs
new file mode 100644
index 0000000..9c40e59
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/Error.cshtml.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace GWMS.UI.Areas.Identity.Pages
+{
+ [AllowAnonymous]
+ [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+ public class ErrorModel : PageModel
+ {
+ #region Public Properties
+
+ public string RequestId { get; set; }
+
+ public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+ #endregion Public Properties
+
+ #region Public Methods
+
+ public void OnGet()
+ {
+ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/_ViewImports.cshtml b/GWMS.UI/Areas/Identity/Pages/_ViewImports.cshtml
new file mode 100644
index 0000000..57fa35d
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/_ViewImports.cshtml
@@ -0,0 +1,4 @@
+@using Microsoft.AspNetCore.Identity
+@using GWMS.UI.Areas.Identity
+@using GWMS.UI.Areas.Identity.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
\ No newline at end of file
diff --git a/GWMS.UI/Areas/Identity/Pages/_ViewStart.cshtml b/GWMS.UI/Areas/Identity/Pages/_ViewStart.cshtml
new file mode 100644
index 0000000..55cd025
--- /dev/null
+++ b/GWMS.UI/Areas/Identity/Pages/_ViewStart.cshtml
@@ -0,0 +1,4 @@
+
+@{
+ Layout = "/Pages/Shared/_Layout.cshtml";
+}