open changes
This commit is contained in:
14
wishlist/App.razor
Normal file
14
wishlist/App.razor
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<CascadingAuthenticationState>
|
||||||
|
<Router AppAssembly="@typeof(App).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<PageTitle>Not found</PageTitle>
|
||||||
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
|
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
|
</CascadingAuthenticationState>
|
||||||
10
wishlist/Areas/Identity/Pages/Account/AccessDenied.cshtml
Normal file
10
wishlist/Areas/Identity/Pages/Account/AccessDenied.cshtml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@page
|
||||||
|
@model AccessDeniedModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Access denied";
|
||||||
|
}
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1 class="text-danger">@ViewData["Title"]</h1>
|
||||||
|
<p class="text-danger">You do not have access to this resource.</p>
|
||||||
|
</header>
|
||||||
23
wishlist/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs
Normal file
23
wishlist/Areas/Identity/Pages/Account/AccessDenied.cshtml.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessDeniedModel : PageModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@page
|
||||||
|
@model ConfirmEmailModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Confirm email";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||||
49
wishlist/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
49
wishlist/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class ConfirmEmailModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
|
||||||
|
public ConfirmEmailModel(UserManager<ApplicationUser> userManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@page
|
||||||
|
@model ConfirmEmailChangeModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Confirm email change";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class ConfirmEmailChangeModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
|
||||||
|
public ConfirmEmailChangeModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
wishlist/Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
33
wishlist/Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@page
|
||||||
|
@model ExternalLoginModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Register";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<h2 id="external-login-title">Associate your @Model.ProviderDisplayName account.</h2>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p id="external-login-description" class="text-info">
|
||||||
|
You've successfully authenticated with <strong>@Model.ProviderDisplayName</strong>.
|
||||||
|
Please enter an email address for this site below and click the Register button to finish
|
||||||
|
logging in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form asp-page-handler="Confirmation" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Email" class="form-control" autocomplete="email" />
|
||||||
|
<label asp-for="Input.Email" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
289
wishlist/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
289
wishlist/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
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 System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class ExternalLoginModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IUserStore<ApplicationUser> _userStore;
|
||||||
|
private readonly IUserEmailStore<ApplicationUser> _emailStore;
|
||||||
|
private readonly IEmailSender _emailSender;
|
||||||
|
private readonly ILogger<ExternalLoginModel> _logger;
|
||||||
|
|
||||||
|
public ExternalLoginModel(
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IUserStore<ApplicationUser> userStore,
|
||||||
|
ILogger<ExternalLoginModel> logger,
|
||||||
|
IEmailSender emailSender)
|
||||||
|
{
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_userStore = userStore;
|
||||||
|
_emailStore = GetEmailStore();
|
||||||
|
_logger = logger;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderDisplayName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OnGet() => RedirectToPage("./Login");
|
||||||
|
|
||||||
|
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<IActionResult> 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 async Task<IActionResult> 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 = await CreateUser();
|
||||||
|
|
||||||
|
// fill user info (name, givenname, surname ...)
|
||||||
|
FillExternalProviderUserInfo(user, info);
|
||||||
|
|
||||||
|
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
|
||||||
|
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Add User role
|
||||||
|
await _userManager.AddToRoleAsync(user, "User");
|
||||||
|
|
||||||
|
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 <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SupportedExternalLoginProviderNames
|
||||||
|
{
|
||||||
|
public const string GOOGLE = "Google";
|
||||||
|
public const string FACEBOOK = "Facebook";
|
||||||
|
public const string MICROSOFT = "Microsoft";
|
||||||
|
//public const string TWITTER = "Twitter";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FillExternalProviderUserInfo(ApplicationUser user, ExternalLoginInfo info)
|
||||||
|
{
|
||||||
|
if (info is null || string.IsNullOrEmpty(info.LoginProvider))
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (info.LoginProvider)
|
||||||
|
{
|
||||||
|
case SupportedExternalLoginProviderNames.GOOGLE:
|
||||||
|
Claim gname = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name);
|
||||||
|
if (gname is not null)
|
||||||
|
user.Name = gname.Value;
|
||||||
|
Claim ggivenName = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName);
|
||||||
|
if (ggivenName is not null)
|
||||||
|
user.GivenName = ggivenName.Value;
|
||||||
|
Claim gsurname = info.Principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname);
|
||||||
|
if (gsurname is not null)
|
||||||
|
user.Surname = gsurname.Value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SupportedExternalLoginProviderNames.FACEBOOK:
|
||||||
|
Claim fname = info.Principal.Claims.FirstOrDefault(c => c.Type == "urn:facebook:name");
|
||||||
|
if (fname is not null)
|
||||||
|
user.Name = fname.Value;
|
||||||
|
Claim fgivenName = info.Principal.Claims.FirstOrDefault(c => c.Type == "urn:facebook:first_name");
|
||||||
|
if (fgivenName is not null)
|
||||||
|
user.Name = fgivenName.Value;
|
||||||
|
Claim fsurname = info.Principal.Claims.FirstOrDefault(c => c.Type == "urn:facebook:last_name");
|
||||||
|
if (fsurname is not null)
|
||||||
|
user.Name = fsurname.Value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SupportedExternalLoginProviderNames.MICROSOFT:
|
||||||
|
Claim mname = info.Principal.Claims.FirstOrDefault(c => c.Type == "displayName");
|
||||||
|
if (mname is not null)
|
||||||
|
user.Name = mname.Value;
|
||||||
|
Claim mgivenName = info.Principal.Claims.FirstOrDefault(c => c.Type == "first_name");
|
||||||
|
if (mgivenName is not null)
|
||||||
|
user.GivenName = mgivenName.Value;
|
||||||
|
Claim msurname = info.Principal.Claims.FirstOrDefault(c => c.Type == "last_name");
|
||||||
|
if (msurname is not null)
|
||||||
|
user.Surname = msurname.Value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
//case SupportedExternalLoginProviderNames.TWITTER:
|
||||||
|
// Claim tname = info.Principal.Claims.FirstOrDefault(c => c.Type == "screen_name");
|
||||||
|
// if (tname is not null)
|
||||||
|
// user.Name = tname.Value;
|
||||||
|
// break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ApplicationUser> CreateUser()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = Activator.CreateInstance<ApplicationUser>();
|
||||||
|
await user.SaveToDb();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
|
||||||
|
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor, or alternatively " +
|
||||||
|
$"override the external login page in /Areas/Identity/Pages/Account/ExternalLogin.cshtml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUserEmailStore<ApplicationUser> GetEmailStore()
|
||||||
|
{
|
||||||
|
if (!_userManager.SupportsUserEmail)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||||
|
}
|
||||||
|
return (IUserEmailStore<ApplicationUser>)_userStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
wishlist/Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
26
wishlist/Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@page
|
||||||
|
@model ForgotPasswordModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Forgot your password?";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<h2>Enter your email.</h2>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
|
||||||
|
<label asp-for="Input.Email" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class ForgotPasswordModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IEmailSender _emailSender;
|
||||||
|
|
||||||
|
public ForgotPasswordModel(UserManager<ApplicationUser> userManager, IEmailSender emailSender)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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 <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
return RedirectToPage("./ForgotPasswordConfirmation");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@page
|
||||||
|
@model ForgotPasswordConfirmation
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Forgot password confirmation";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<p>
|
||||||
|
Please check your email to reset your password.
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class ForgotPasswordConfirmation : PageModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wishlist/Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
10
wishlist/Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@page
|
||||||
|
@model LockoutModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Locked out";
|
||||||
|
}
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1 class="text-danger">@ViewData["Title"]</h1>
|
||||||
|
<p class="text-danger">This account has been locked out, please try again later.</p>
|
||||||
|
</header>
|
||||||
25
wishlist/Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
25
wishlist/Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class LockoutModel : PageModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
wishlist/Areas/Identity/Pages/Account/LogOut.cshtml
Normal file
16
wishlist/Areas/Identity/Pages/Account/LogOut.cshtml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@page
|
||||||
|
@using Wishlist.Models;
|
||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@attribute [IgnoreAntiforgeryToken]
|
||||||
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
@functions {
|
||||||
|
public async Task<IActionResult> OnPost()
|
||||||
|
{
|
||||||
|
if (SignInManager.IsSignedIn(User))
|
||||||
|
{
|
||||||
|
await SignInManager.SignOutAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Redirect("~/");
|
||||||
|
}
|
||||||
|
}
|
||||||
101
wishlist/Areas/Identity/Pages/Account/Login.cshtml
Normal file
101
wishlist/Areas/Identity/Pages/Account/Login.cshtml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
@page
|
||||||
|
@model LoginModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Log in";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<section>
|
||||||
|
<form id="account" method="post">
|
||||||
|
<h2>Use a local account to log in.</h2>
|
||||||
|
<hr />
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
|
||||||
|
<label asp-for="Input.Email" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.Password" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label asp-for="Input.RememberMe" class="form-label">
|
||||||
|
<input class="form-check-input" asp-for="Input.RememberMe" />
|
||||||
|
@Html.DisplayNameFor(m => m.Input.RememberMe)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-md-offset-2">
|
||||||
|
<section>
|
||||||
|
<h3>Use another service to log in.</h3>
|
||||||
|
<hr />
|
||||||
|
@{
|
||||||
|
if ((Model.ExternalLogins?.Count ?? 0) == 0)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">
|
||||||
|
article
|
||||||
|
about setting up this ASP.NET application to support logging in via external services
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
@foreach (var provider in Model.ExternalLogins!)
|
||||||
|
{
|
||||||
|
switch (provider.Name)
|
||||||
|
{
|
||||||
|
// case "Google":
|
||||||
|
// <script src="https://accounts.google.com/gsi/client" async></script>
|
||||||
|
// <div id="g_id_onload"
|
||||||
|
// data-client_id="396640764717-ugh7obgucmg799iq6rq1eiqp565m4h6q.apps.googleusercontent.com"
|
||||||
|
// data-login_uri="https://wishlist.arnemoerman.be/signin"
|
||||||
|
// data-auto_prompt="false">
|
||||||
|
// </div>
|
||||||
|
// <div class="g_id_signin" data-type="standard" data-size="large" data-theme="outline" data-text="sign_in_with" data-shape="rectangular" data-logo_alignment="left"> </div>
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
134
wishlist/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
134
wishlist/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class LoginModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ILogger<LoginModel> _logger;
|
||||||
|
|
||||||
|
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger)
|
||||||
|
{
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Display(Name = "Remember me?")]
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
wishlist/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
41
wishlist/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@page
|
||||||
|
@model LoginWith2faModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Two-factor authentication";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<hr />
|
||||||
|
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form method="post" asp-route-returnUrl="@Model.ReturnUrl">
|
||||||
|
<input asp-for="RememberMe" type="hidden" />
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" />
|
||||||
|
<label asp-for="Input.TwoFactorCode" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label asp-for="Input.RememberMachine" class="form-label">
|
||||||
|
<input asp-for="Input.RememberMachine" />
|
||||||
|
@Html.DisplayNameFor(m => m.Input.RememberMachine)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Don't have access to your authenticator device? You can
|
||||||
|
<a id="recovery-code-login" asp-page="./LoginWithRecoveryCode" asp-route-returnUrl="@Model.ReturnUrl">log in with a recovery code</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
127
wishlist/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
127
wishlist/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class LoginWith2faModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<LoginWith2faModel> _logger;
|
||||||
|
|
||||||
|
public LoginWith2faModel(
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<LoginWith2faModel> logger)
|
||||||
|
{
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Display(Name = "Remember this machine")]
|
||||||
|
public bool RememberMachine { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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);
|
||||||
|
|
||||||
|
var userId = await _userManager.GetUserIdAsync(user);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@page
|
||||||
|
@model LoginWithRecoveryCodeModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Recovery code verification";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" />
|
||||||
|
<label asp-for="Input.RecoveryCode" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.RecoveryCode" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class LoginWithRecoveryCodeModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<LoginWithRecoveryCodeModel> _logger;
|
||||||
|
|
||||||
|
public LoginWithRecoveryCodeModel(
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<LoginWithRecoveryCodeModel> logger)
|
||||||
|
{
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Text)]
|
||||||
|
[Display(Name = "Recovery Code")]
|
||||||
|
public string RecoveryCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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);
|
||||||
|
|
||||||
|
var userId = await _userManager.GetUserIdAsync(user);
|
||||||
|
|
||||||
|
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 account locked out.");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@page
|
||||||
|
@model ChangePasswordModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Change password";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form id="change-password-form" method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.OldPassword" class="form-control" autocomplete="current-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.OldPassword" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.OldPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.NewPassword" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Update password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class ChangePasswordModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ILogger<ChangePasswordModel> _logger;
|
||||||
|
|
||||||
|
public ChangePasswordModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
ILogger<ChangePasswordModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Current password")]
|
||||||
|
public string OldPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@page
|
||||||
|
@model DeletePersonalDataModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Delete Personal Data";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<p>
|
||||||
|
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form id="delete-user" method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
@if (Model.RequirePassword)
|
||||||
|
{
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.Password" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button class="w-100 btn btn-lg btn-danger" type="submit">Delete data and close my account</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class DeletePersonalDataModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ILogger<DeletePersonalDataModel> _logger;
|
||||||
|
|
||||||
|
public DeletePersonalDataModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
ILogger<DeletePersonalDataModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequirePassword { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _signInManager.SignOutAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
|
||||||
|
|
||||||
|
return Redirect("~/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@page
|
||||||
|
@model Disable2faModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Disable two-factor authentication (2FA)";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<p>
|
||||||
|
<strong>This action only disables 2FA.</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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 <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form method="post">
|
||||||
|
<button class="btn btn-danger" type="submit">Disable 2FA</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class Disable2faModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<Disable2faModel> _logger;
|
||||||
|
|
||||||
|
public Disable2faModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<Disable2faModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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 as it's not currently enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@page
|
||||||
|
@model DownloadPersonalDataModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Download Your Data";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class DownloadPersonalDataModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<DownloadPersonalDataModel> _logger;
|
||||||
|
|
||||||
|
public DownloadPersonalDataModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<DownloadPersonalDataModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OnGet()
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<string, string>();
|
||||||
|
var personalDataProps = typeof(ApplicationUser).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);
|
||||||
|
}
|
||||||
|
|
||||||
|
personalData.Add($"Authenticator Key", await _userManager.GetAuthenticatorKeyAsync(user));
|
||||||
|
|
||||||
|
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
|
||||||
|
return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
wishlist/Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
44
wishlist/Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
@page
|
||||||
|
@model EmailModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Manage Email";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.Email;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form id="email-form" method="post">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
@if (Model.IsEmailConfirmed)
|
||||||
|
{
|
||||||
|
<div class="form-floating input-group">
|
||||||
|
<input asp-for="Email" class="form-control" disabled />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="h-100 input-group-text text-success font-weight-bold">✓</span>
|
||||||
|
</div>
|
||||||
|
<label asp-for="Email" class="form-label"></label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Email" class="form-control" disabled />
|
||||||
|
<label asp-for="Email" class="form-label"></label>
|
||||||
|
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" />
|
||||||
|
<label asp-for="Input.NewEmail" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button id="change-email-button" type="submit" asp-page-handler="ChangeEmail" class="w-100 btn btn-lg btn-primary">Change email</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
170
wishlist/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
170
wishlist/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class EmailModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly IEmailSender _emailSender;
|
||||||
|
|
||||||
|
public EmailModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
IEmailSender emailSender)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmailConfirmed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
[Display(Name = "New email")]
|
||||||
|
public string NewEmail { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
var email = await _userManager.GetEmailAsync(user);
|
||||||
|
Email = email;
|
||||||
|
|
||||||
|
Input = new InputModel
|
||||||
|
{
|
||||||
|
NewEmail = email,
|
||||||
|
};
|
||||||
|
|
||||||
|
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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 { area = "Identity", userId = userId, email = Input.NewEmail, code = code },
|
||||||
|
protocol: Request.Scheme);
|
||||||
|
await _emailSender.SendEmailAsync(
|
||||||
|
Input.NewEmail,
|
||||||
|
"Confirm your email",
|
||||||
|
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
StatusMessage = "Confirmation link to change email sent. Please check your email.";
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusMessage = "Your email is unchanged.";
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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 <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
StatusMessage = "Verification email sent. Please check your email.";
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@page
|
||||||
|
@model EnableAuthenticatorModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Configure authenticator app";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<div>
|
||||||
|
<p>To use an authenticator app go through the following steps:</p>
|
||||||
|
<ol class="list">
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
Download a two-factor authenticator app like Microsoft Authenticator for
|
||||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
|
||||||
|
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or
|
||||||
|
Google Authenticator for
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">Android</a> and
|
||||||
|
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||||
|
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
|
||||||
|
<div id="qrCode"></div>
|
||||||
|
<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form id="send-code" method="post">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Code" class="form-control" autocomplete="off" />
|
||||||
|
<label asp-for="Input.Code" class="control-label form-label">Verification Code</label>
|
||||||
|
<span asp-validation-for="Input.Code" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Verify</button>
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class EnableAuthenticatorModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<EnableAuthenticatorModel> _logger;
|
||||||
|
private readonly UrlEncoder _urlEncoder;
|
||||||
|
|
||||||
|
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||||
|
|
||||||
|
public EnableAuthenticatorModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<EnableAuthenticatorModel> logger,
|
||||||
|
UrlEncoder urlEncoder)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
_urlEncoder = urlEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string SharedKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string AuthenticatorUri { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string[] RecoveryCodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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 hyphens
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatKey(string unformattedKey)
|
||||||
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
int currentPosition = 0;
|
||||||
|
while (currentPosition + 4 < unformattedKey.Length)
|
||||||
|
{
|
||||||
|
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
|
||||||
|
currentPosition += 4;
|
||||||
|
}
|
||||||
|
if (currentPosition < unformattedKey.Length)
|
||||||
|
{
|
||||||
|
result.Append(unformattedKey.AsSpan(currentPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||||
|
{
|
||||||
|
return string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
AuthenticatorUriFormat,
|
||||||
|
_urlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
|
||||||
|
_urlEncoder.Encode(email),
|
||||||
|
unformattedKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@page
|
||||||
|
@model ExternalLoginsModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Manage your external logins";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.ExternalLogins;
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
@if (Model.CurrentLogins?.Count > 0)
|
||||||
|
{
|
||||||
|
<h3>Registered Logins</h3>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
@foreach (var login in Model.CurrentLogins)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td id="@($"login-provider-{login.LoginProvider}")">@login.ProviderDisplayName</td>
|
||||||
|
<td>
|
||||||
|
@if (Model.ShowRemoveButton)
|
||||||
|
{
|
||||||
|
<form id="@($"remove-login-{login.LoginProvider}")" asp-page-handler="RemoveLogin" method="post">
|
||||||
|
<div>
|
||||||
|
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" />
|
||||||
|
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" />
|
||||||
|
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@:
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
@if (Model.OtherLogins?.Count > 0)
|
||||||
|
{
|
||||||
|
<h4>Add another service to log in.</h4>
|
||||||
|
<hr />
|
||||||
|
<form id="link-login-form" asp-page-handler="LinkLogin" method="post" class="form-horizontal">
|
||||||
|
<div id="socialLoginList">
|
||||||
|
<p>
|
||||||
|
@foreach (var provider in Model.OtherLogins)
|
||||||
|
{
|
||||||
|
<button id="@($"link-login-button-{provider.Name}")" type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class ExternalLoginsModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly IUserStore<ApplicationUser> _userStore;
|
||||||
|
|
||||||
|
public ExternalLoginsModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
IUserStore<ApplicationUser> userStore)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_userStore = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public IList<UserLoginInfo> CurrentLogins { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public IList<AuthenticationScheme> OtherLogins { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowRemoveButton { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync()
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentLogins = await _userManager.GetLoginsAsync(user);
|
||||||
|
OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
|
||||||
|
.Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
string passwordHash = null;
|
||||||
|
if (_userStore is IUserPasswordStore<ApplicationUser> userPasswordStore)
|
||||||
|
{
|
||||||
|
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowRemoveButton = passwordHash != null || CurrentLogins.Count > 1;
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostRemoveLoginAsync(string loginProvider, string providerKey)
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> OnGetLinkLoginCallbackAsync()
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = await _userManager.GetUserIdAsync(user);
|
||||||
|
var info = await _signInManager.GetExternalLoginInfoAsync(userId);
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Unexpected error occurred loading external login info.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
@page
|
||||||
|
@model GenerateRecoveryCodesModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<p>
|
||||||
|
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||||
|
<strong>Put these codes in a safe place.</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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 <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form method="post">
|
||||||
|
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class GenerateRecoveryCodesModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<GenerateRecoveryCodesModel> _logger;
|
||||||
|
|
||||||
|
public GenerateRecoveryCodesModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<GenerateRecoveryCodesModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string[] RecoveryCodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Cannot generate recovery codes for user because they do not have 2FA enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
wishlist/Areas/Identity/Pages/Account/Manage/Index.cshtml
Normal file
30
wishlist/Areas/Identity/Pages/Account/Manage/Index.cshtml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@page
|
||||||
|
@model IndexModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Profile";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form id="profile-form" method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Username" class="form-control" disabled />
|
||||||
|
<label asp-for="Username" class="form-label"></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.PhoneNumber" class="form-control" />
|
||||||
|
<label asp-for="Input.PhoneNumber" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
116
wishlist/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
Normal file
116
wishlist/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
|
||||||
|
public IndexModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Phone]
|
||||||
|
[Display(Name = "Phone number")]
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
var userName = await _userManager.GetUserNameAsync(user);
|
||||||
|
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
|
||||||
|
|
||||||
|
Username = userName;
|
||||||
|
|
||||||
|
Input = new InputModel
|
||||||
|
{
|
||||||
|
PhoneNumber = phoneNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
wishlist/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
Normal file
122
wishlist/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static class ManageNavPages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string Index => "Index";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string Email => "Email";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string ChangePassword => "ChangePassword";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string DownloadPersonalData => "DownloadPersonalData";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string DeletePersonalData => "DeletePersonalData";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string ExternalLogins => "ExternalLogins";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string PersonalData => "PersonalData";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
@page
|
||||||
|
@model PersonalDataModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Personal Data";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p>Your account contains personal data that you have given us. This page allows you to download or delete that data.</p>
|
||||||
|
<p>
|
||||||
|
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
||||||
|
</p>
|
||||||
|
<form id="download-data" asp-page="DownloadPersonalData" method="post">
|
||||||
|
<button class="btn btn-primary" type="submit">Download</button>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
<a id="delete" asp-page="DeletePersonalData" class="btn btn-danger">Delete</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class PersonalDataModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly ILogger<PersonalDataModel> _logger;
|
||||||
|
|
||||||
|
public PersonalDataModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
ILogger<PersonalDataModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGet()
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@page
|
||||||
|
@model ResetAuthenticatorModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Reset authenticator key";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<p>
|
||||||
|
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||||
|
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form id="reset-authenticator-form" method="post">
|
||||||
|
<button id="reset-authenticator-button" class="btn btn-danger" type="submit">Reset authenticator key</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class ResetAuthenticatorModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ILogger<ResetAuthenticatorModel> _logger;
|
||||||
|
|
||||||
|
public ResetAuthenticatorModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
ILogger<ResetAuthenticatorModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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);
|
||||||
|
var userId = await _userManager.GetUserIdAsync(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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@page
|
||||||
|
@model SetPasswordModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Set password";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Set your password</h3>
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<p class="text-info">
|
||||||
|
You do not have a local username/password for this site. Add a local
|
||||||
|
account so you can log in without an external login.
|
||||||
|
</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form id="set-password-form" method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" />
|
||||||
|
<label asp-for="Input.NewPassword" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" />
|
||||||
|
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class SetPasswordModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
|
||||||
|
public SetPasswordModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
SignInManager<ApplicationUser> signInManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@page
|
||||||
|
@model ShowRecoveryCodesModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Recovery codes";
|
||||||
|
ViewData["ActivePage"] = "TwoFactorAuthentication";
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<p>
|
||||||
|
<strong>Put these codes in a safe place.</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
@for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
|
||||||
|
{
|
||||||
|
<code class="recovery-code">@Model.RecoveryCodes[row]</code><text> </text><code class="recovery-code">@Model.RecoveryCodes[row + 1]</code><br />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class ShowRecoveryCodesModel : PageModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string[] RecoveryCodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public IActionResult OnGet()
|
||||||
|
{
|
||||||
|
if (RecoveryCodes == null || RecoveryCodes.Length == 0)
|
||||||
|
{
|
||||||
|
return RedirectToPage("./TwoFactorAuthentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@page
|
||||||
|
@using Microsoft.AspNetCore.Http.Features
|
||||||
|
@model TwoFactorAuthenticationModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Two-factor authentication (2FA)";
|
||||||
|
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
<partial name="_StatusMessage" for="StatusMessage" />
|
||||||
|
<h3>@ViewData["Title"]</h3>
|
||||||
|
@{
|
||||||
|
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
|
||||||
|
@if (consentFeature?.CanTrack ?? true)
|
||||||
|
{
|
||||||
|
@if (Model.Is2faEnabled)
|
||||||
|
{
|
||||||
|
if (Model.RecoveryCodesLeft == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>You have no recovery codes left.</strong>
|
||||||
|
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Model.RecoveryCodesLeft == 1)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>You have 1 recovery code left.</strong>
|
||||||
|
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Model.RecoveryCodesLeft <= 3)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||||
|
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Model.IsMachineRemembered)
|
||||||
|
{
|
||||||
|
<form method="post" style="display: inline-block">
|
||||||
|
<button type="submit" class="btn btn-primary">Forget this browser</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
|
||||||
|
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h4>Authenticator app</h4>
|
||||||
|
@if (!Model.HasAuthenticator)
|
||||||
|
{
|
||||||
|
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
|
||||||
|
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>Privacy and cookie policy have not been accepted.</strong>
|
||||||
|
<p>You must accept the policy before you can enable two factor authentication.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
|
{
|
||||||
|
public class TwoFactorAuthenticationModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ILogger<TwoFactorAuthenticationModel> _logger;
|
||||||
|
|
||||||
|
public TwoFactorAuthenticationModel(
|
||||||
|
UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, ILogger<TwoFactorAuthenticationModel> logger)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasAuthenticator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public int RecoveryCodesLeft { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public bool Is2faEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsMachineRemembered { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[TempData]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync()
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null;
|
||||||
|
Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||||
|
IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user);
|
||||||
|
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user);
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync()
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _signInManager.ForgetTwoFactorClientAsync();
|
||||||
|
StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.";
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
wishlist/Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
29
wishlist/Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@{
|
||||||
|
if (ViewData.TryGetValue("ParentLayout", out var parentLayout) && parentLayout != null)
|
||||||
|
{
|
||||||
|
Layout = parentLayout.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Layout = "/Areas/Identity/Pages/_Layout.cshtml";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Manage your account</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Change your account settings</h2>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<partial name="_ManageNav" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
@RenderBody()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@RenderSection("Scripts", required: false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@using Wishlist.Models;
|
||||||
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
@{
|
||||||
|
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||||
|
}
|
||||||
|
<ul class="nav nav-pills flex-column">
|
||||||
|
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
|
||||||
|
@if (hasExternalLogins)
|
||||||
|
{
|
||||||
|
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
|
||||||
|
}
|
||||||
|
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@model string
|
||||||
|
|
||||||
|
@if (!String.IsNullOrEmpty(Model))
|
||||||
|
{
|
||||||
|
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||||
|
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
@Model
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@using Wishlist.Areas.Identity.Pages.Account.Manage
|
||||||
83
wishlist/Areas/Identity/Pages/Account/Register.cshtml
Normal file
83
wishlist/Areas/Identity/Pages/Account/Register.cshtml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
@page
|
||||||
|
@model RegisterModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Register";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||||
|
<h2>Create a new account.</h2>
|
||||||
|
<hr />
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
|
||||||
|
<label asp-for="Input.Email"></label>
|
||||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.Password"></label>
|
||||||
|
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.ConfirmPassword"></label>
|
||||||
|
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-md-offset-2">
|
||||||
|
<section>
|
||||||
|
<h3>Use another service to register.</h3>
|
||||||
|
<hr />
|
||||||
|
@{
|
||||||
|
if ((Model.ExternalLogins?.Count ?? 0) == 0)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">
|
||||||
|
article
|
||||||
|
about setting up this ASP.NET application to support logging in via external services
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
@foreach (var provider in Model.ExternalLogins!)
|
||||||
|
{
|
||||||
|
switch (provider.Name)
|
||||||
|
{
|
||||||
|
// case "Google":
|
||||||
|
// <script src="https://accounts.google.com/gsi/client" async></script>
|
||||||
|
// <div id="g_id_onload"
|
||||||
|
// data-client_id="396640764717-ugh7obgucmg799iq6rq1eiqp565m4h6q.apps.googleusercontent.com"
|
||||||
|
// data-login_uri="https://wishlist.arnemoerman.be/signin"
|
||||||
|
// data-auto_prompt="false">
|
||||||
|
// </div>
|
||||||
|
// <div class="g_id_register" data-type="standard" data-size="large" data-theme="outline" data-text="register_with" data-shape="rectangular" data-logo_alignment="left"></div>
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
176
wishlist/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
176
wishlist/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
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.AspNetCore.WebUtilities;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class RegisterModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IUserStore<ApplicationUser> _userStore;
|
||||||
|
private readonly IUserEmailStore<ApplicationUser> _emailStore;
|
||||||
|
private readonly ILogger<RegisterModel> _logger;
|
||||||
|
private readonly IEmailSender _emailSender;
|
||||||
|
|
||||||
|
public RegisterModel(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IUserStore<ApplicationUser> userStore,
|
||||||
|
SignInManager<ApplicationUser> signInManager,
|
||||||
|
ILogger<RegisterModel> logger,
|
||||||
|
IEmailSender emailSender)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_userStore = userStore;
|
||||||
|
_emailStore = GetEmailStore();
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_logger = logger;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
[Display(Name = "Email")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Confirm password")]
|
||||||
|
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||||
|
public string ConfirmPassword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnGetAsync(string returnUrl = null)
|
||||||
|
{
|
||||||
|
ReturnUrl = returnUrl;
|
||||||
|
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
||||||
|
{
|
||||||
|
returnUrl ??= Url.Content("~/");
|
||||||
|
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
var user = CreateUser();
|
||||||
|
|
||||||
|
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
|
||||||
|
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
|
||||||
|
var result = await _userManager.CreateAsync(user, Input.Password);
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("User created a new account with password.");
|
||||||
|
|
||||||
|
// Add User role
|
||||||
|
await _userManager.AddToRoleAsync(user, "User");
|
||||||
|
|
||||||
|
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, returnUrl = returnUrl },
|
||||||
|
protocol: Request.Scheme);
|
||||||
|
|
||||||
|
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
|
||||||
|
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
if (_userManager.Options.SignIn.RequireConfirmedAccount)
|
||||||
|
{
|
||||||
|
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApplicationUser CreateUser()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Activator.CreateInstance<ApplicationUser>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
|
||||||
|
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor, or alternatively " +
|
||||||
|
$"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUserEmailStore<ApplicationUser> GetEmailStore()
|
||||||
|
{
|
||||||
|
if (!_userManager.SupportsUserEmail)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||||
|
}
|
||||||
|
return (IUserEmailStore<ApplicationUser>)_userStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@page
|
||||||
|
@model RegisterConfirmationModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Register confirmation";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
@{
|
||||||
|
if (@Model.DisplayConfirmAccountLink)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
|
||||||
|
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your account</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
Please check your email to confirm your account.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
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 System.Text;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class RegisterConfirmationModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IEmailSender _sender;
|
||||||
|
|
||||||
|
public RegisterConfirmationModel(UserManager<ApplicationUser> userManager, IEmailSender sender)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool DisplayConfirmAccountLink { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string EmailConfirmationUrl { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
|
||||||
|
{
|
||||||
|
if (email == null)
|
||||||
|
{
|
||||||
|
return RedirectToPage("/Index");
|
||||||
|
}
|
||||||
|
returnUrl = returnUrl ?? Url.Content("~/");
|
||||||
|
|
||||||
|
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 = true;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@page
|
||||||
|
@model ResendEmailConfirmationModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Resend email confirmation";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<h2>Enter your email.</h2>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form method="post">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Email" class="form-control" aria-required="true" />
|
||||||
|
<label asp-for="Input.Email" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Resend</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
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 System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class ResendEmailConfirmationModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IEmailSender _emailSender;
|
||||||
|
|
||||||
|
public ResendEmailConfirmationModel(UserManager<ApplicationUser> userManager, IEmailSender emailSender)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_emailSender = emailSender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> 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 <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
wishlist/Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
37
wishlist/Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@page
|
||||||
|
@model ResetPasswordModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Reset password";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<h2>Reset your password.</h2>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<form method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<input asp-for="Input.Code" type="hidden" />
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
|
||||||
|
<label asp-for="Input.Email" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.Password" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
|
||||||
|
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||||
|
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
}
|
||||||
114
wishlist/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
Normal file
114
wishlist/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
public class ResetPasswordModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
|
||||||
|
public ResetPasswordModel(UserManager<ApplicationUser> userManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[BindProperty]
|
||||||
|
public InputModel Input { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Confirm password")]
|
||||||
|
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||||
|
public string ConfirmPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string Code { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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<IActionResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@page
|
||||||
|
@model ResetPasswordConfirmationModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Reset password confirmation";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
<p>
|
||||||
|
Your password has been reset. Please <a asp-page="./Login">click here to log in</a>.
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages.Account
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class ResetPasswordConfirmationModel : PageModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wishlist/Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
10
wishlist/Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@model string
|
||||||
|
|
||||||
|
@if (!String.IsNullOrEmpty(Model))
|
||||||
|
{
|
||||||
|
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||||
|
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
@Model
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@using Wishlist.Areas.Identity.Pages.Account
|
||||||
23
wishlist/Areas/Identity/Pages/Error.cshtml
Normal file
23
wishlist/Areas/Identity/Pages/Error.cshtml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@page
|
||||||
|
@model ErrorModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (Model.ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Development environment should not be enabled in deployed applications</strong>, 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 <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
|
||||||
|
</p>
|
||||||
41
wishlist/Areas/Identity/Pages/Error.cshtml.cs
Normal file
41
wishlist/Areas/Identity/Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity.Pages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public class ErrorModel : PageModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public string RequestId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||||
|
/// directly from your code. This API may change or be removed in future releases.
|
||||||
|
/// </summary>
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
wishlist/Areas/Identity/Pages/Shared/_LoginPartial.cshtml
Normal file
28
wishlist/Areas/Identity/Pages/Shared/_LoginPartial.cshtml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@using Wishlist.Models;
|
||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
@inject UserManager<ApplicationUser> UserManager
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
@if (SignInManager.IsSignedIn(User))
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity?.Name!</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/" method="post">
|
||||||
|
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
4
wishlist/Areas/Identity/Pages/_ViewImports.cshtml
Normal file
4
wishlist/Areas/Identity/Pages/_ViewImports.cshtml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@using Wishlist.Areas.Identity
|
||||||
|
@using Wishlist.Areas.Identity.Pages
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
4
wishlist/Areas/Identity/Pages/_ViewStart.cshtml
Normal file
4
wishlist/Areas/Identity/Pages/_ViewStart.cshtml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
@{
|
||||||
|
Layout = "/Pages/Shared/_Layout.cshtml";
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Wishlist.Areas.Identity
|
||||||
|
{
|
||||||
|
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
|
||||||
|
: RevalidatingServerAuthenticationStateProvider where TUser : class
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly IdentityOptions _options;
|
||||||
|
|
||||||
|
public RevalidatingIdentityAuthenticationStateProvider(
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
IOptions<IdentityOptions> optionsAccessor)
|
||||||
|
: base(loggerFactory)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_options = optionsAccessor.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
|
protected override async Task<bool> ValidateAuthenticationStateAsync(
|
||||||
|
AuthenticationState authenticationState, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Get the user manager from a new scope to ensure it fetches fresh data
|
||||||
|
var scope = _scopeFactory.CreateScope();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
|
||||||
|
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (scope is IAsyncDisposable asyncDisposable)
|
||||||
|
{
|
||||||
|
await asyncDisposable.DisposeAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scope.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
|
||||||
|
{
|
||||||
|
var user = await userManager.GetUserAsync(principal);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (!userManager.SupportsUserSecurityStamp)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
|
||||||
|
var userStamp = await userManager.GetSecurityStampAsync(user);
|
||||||
|
return principalStamp == userStamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
wishlist/Data/ApplicationDbContext.cs
Normal file
195
wishlist/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Wishlist.Data;
|
||||||
|
|
||||||
|
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
|
||||||
|
ApplicationUserClaim, ApplicationUserRole,
|
||||||
|
ApplicationUserLogin, ApplicationRoleClaim, ApplicationUserToken>
|
||||||
|
{
|
||||||
|
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
|
||||||
|
|
||||||
|
public DbSet<ApplicationUser> ApplicationUsers => Set<ApplicationUser>();
|
||||||
|
public DbSet<ApplicationRole> ApplicationRoles => Set<ApplicationRole>();
|
||||||
|
|
||||||
|
public DbSet<ApplicationUserClaim> ApplicationUserClaims => Set<ApplicationUserClaim>();
|
||||||
|
public DbSet<ApplicationUserRole> ApplicationUserRoles => Set<ApplicationUserRole>();
|
||||||
|
public DbSet<ApplicationUserLogin> ApplicationUserLogins => Set<ApplicationUserLogin>();
|
||||||
|
public DbSet<ApplicationRoleClaim> ApplicationRoleClaims => Set<ApplicationRoleClaim>();
|
||||||
|
public DbSet<ApplicationUserToken> ApplicationUserTokens => Set<ApplicationUserToken>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
//models
|
||||||
|
modelBuilder.Entity<ApplicationUser>(b =>
|
||||||
|
{
|
||||||
|
// Primary key
|
||||||
|
b.HasKey(u => u.Id);
|
||||||
|
|
||||||
|
// Indexes for "normalized" username and email, to allow efficient lookups
|
||||||
|
b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
|
||||||
|
b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");
|
||||||
|
|
||||||
|
// Maps to the AspNetUsers table
|
||||||
|
b.ToTable("AspNetUsers");
|
||||||
|
|
||||||
|
// A concurrency token for use with the optimistic concurrency checking
|
||||||
|
b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();
|
||||||
|
|
||||||
|
// Limit the size of columns to use efficient database types
|
||||||
|
b.Property(u => u.UserName).HasMaxLength(256);
|
||||||
|
b.Property(u => u.NormalizedUserName).HasMaxLength(256);
|
||||||
|
b.Property(u => u.Email).HasMaxLength(256);
|
||||||
|
b.Property(u => u.NormalizedEmail).HasMaxLength(256);
|
||||||
|
|
||||||
|
// The relationships between User and other entity types
|
||||||
|
// Note that these relationships are configured with no navigation properties
|
||||||
|
|
||||||
|
// Each User can have many UserClaims
|
||||||
|
b.HasMany<ApplicationUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
|
||||||
|
|
||||||
|
// Each User can have many UserLogins
|
||||||
|
b.HasMany<ApplicationUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
|
||||||
|
|
||||||
|
// Each User can have many UserTokens
|
||||||
|
b.HasMany<ApplicationUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
|
||||||
|
|
||||||
|
// Each User can have many entries in the UserRole join table
|
||||||
|
b.HasMany<ApplicationUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationUserClaim>(b =>
|
||||||
|
{
|
||||||
|
// Primary key
|
||||||
|
b.HasKey(uc => uc.Id);
|
||||||
|
|
||||||
|
// Maps to the AspNetUserClaims table
|
||||||
|
b.ToTable("AspNetUserClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationUserLogin>(b =>
|
||||||
|
{
|
||||||
|
// Composite primary key consisting of the LoginProvider and the key to use
|
||||||
|
// with that provider
|
||||||
|
b.HasKey(l => new { l.LoginProvider, l.ProviderKey });
|
||||||
|
|
||||||
|
// Limit the size of the composite key columns due to common DB restrictions
|
||||||
|
b.Property(l => l.LoginProvider).HasMaxLength(128);
|
||||||
|
b.Property(l => l.ProviderKey).HasMaxLength(128);
|
||||||
|
|
||||||
|
// Maps to the AspNetUserLogins table
|
||||||
|
b.ToTable("AspNetUserLogins");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationUserToken>(b =>
|
||||||
|
{
|
||||||
|
// Composite primary key consisting of the UserId, LoginProvider and Name
|
||||||
|
b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });
|
||||||
|
|
||||||
|
// Limit the size of the composite key columns due to common DB restrictions
|
||||||
|
//b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
|
||||||
|
//b.Property(t => t.Name).HasMaxLength(maxKeyLength);
|
||||||
|
|
||||||
|
// Maps to the AspNetUserTokens table
|
||||||
|
b.ToTable("AspNetUserTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationRole>(b =>
|
||||||
|
{
|
||||||
|
// Primary key
|
||||||
|
b.HasKey(r => r.Id);
|
||||||
|
|
||||||
|
// Index for "normalized" role name to allow efficient lookups
|
||||||
|
b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex").IsUnique();
|
||||||
|
|
||||||
|
// Maps to the AspNetRoles table
|
||||||
|
b.ToTable("AspNetRoles");
|
||||||
|
|
||||||
|
// A concurrency token for use with the optimistic concurrency checking
|
||||||
|
b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();
|
||||||
|
|
||||||
|
// Limit the size of columns to use efficient database types
|
||||||
|
b.Property(u => u.Id).HasMaxLength(50);
|
||||||
|
b.Property(u => u.Name).HasMaxLength(256);
|
||||||
|
b.Property(u => u.NormalizedName).HasMaxLength(256);
|
||||||
|
|
||||||
|
// The relationships between Role and other entity types
|
||||||
|
// Note that these relationships are configured with no navigation properties
|
||||||
|
|
||||||
|
// Each Role can have many entries in the UserRole join table
|
||||||
|
b.HasMany<ApplicationUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
|
||||||
|
|
||||||
|
// Each Role can have many associated RoleClaims
|
||||||
|
b.HasMany<ApplicationRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationRoleClaim>(b =>
|
||||||
|
{
|
||||||
|
// Primary key
|
||||||
|
b.HasKey(rc => rc.Id);
|
||||||
|
|
||||||
|
// Maps to the AspNetRoleClaims table
|
||||||
|
b.ToTable("AspNetRoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationUserRole>(b =>
|
||||||
|
{
|
||||||
|
// Primary key
|
||||||
|
b.HasKey(r => new { r.UserId, r.RoleId });
|
||||||
|
|
||||||
|
// Maps to the AspNetUserRoles table
|
||||||
|
b.ToTable("AspNetUserRoles");
|
||||||
|
});
|
||||||
|
|
||||||
|
// navigation props
|
||||||
|
modelBuilder.Entity<ApplicationUser>(b =>
|
||||||
|
{
|
||||||
|
// Each User can have many UserClaims
|
||||||
|
b.HasMany(e => e.Claims)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(uc => uc.UserId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Each User can have many UserLogins
|
||||||
|
b.HasMany(e => e.Logins)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(ul => ul.UserId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Each User can have many UserTokens
|
||||||
|
b.HasMany(e => e.Tokens)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(ut => ut.UserId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Each User can have many entries in the UserRole join table
|
||||||
|
b.HasMany(e => e.UserRoles)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(ur => ur.UserId)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ApplicationRole>(b =>
|
||||||
|
{
|
||||||
|
// Each Role can have many entries in the UserRole join table
|
||||||
|
b.HasMany(e => e.UserRoles)
|
||||||
|
.WithOne(e => e.Role)
|
||||||
|
.HasForeignKey(ur => ur.RoleId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Each Role can have many associated RoleClaims
|
||||||
|
b.HasMany(e => e.RoleClaims)
|
||||||
|
.WithOne(e => e.Role)
|
||||||
|
.HasForeignKey(rc => rc.RoleId)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
14
wishlist/Data/DB/DatabaseModel.cs
Normal file
14
wishlist/Data/DB/DatabaseModel.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class DatabaseModel : Database<DatabaseModel>
|
||||||
|
{
|
||||||
|
public Table<User> Users { get; set; }
|
||||||
|
public Table<Wishlist> Wishlists { get; set; }
|
||||||
|
public Table<WishlistItem> WishlistItems { get; set; }
|
||||||
|
public Table<UserMayViewWishList> UsersMayViewWishList { get; set; }
|
||||||
|
public Table<UserMarkedAsGettingItem> UsersMarkedAsGettingItem { get; set; }
|
||||||
|
public Table<Friend> Friends { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wishlist/Data/DB/Friend.cs
Normal file
10
wishlist/Data/DB/Friend.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class Friend
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public int FriendId { get; set; }
|
||||||
|
public FriendStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
94
wishlist/Data/DB/Migrations/20240630_2015_InitialTables.cs
Normal file
94
wishlist/Data/DB/Migrations/20240630_2015_InitialTables.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
|
||||||
|
namespace Wishlist.Data.DB.Migrations
|
||||||
|
{
|
||||||
|
[Migration(2024_06_30__20_15)]
|
||||||
|
public class _20240630_2015_InitialTables : Migration
|
||||||
|
{
|
||||||
|
public override void Up()
|
||||||
|
{
|
||||||
|
Create.Table("Users")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
|
||||||
|
.WithColumn("Username").AsString(50).NotNullable()
|
||||||
|
.WithColumn("Name").AsString(255).NotNullable()
|
||||||
|
.WithColumn("GivenName").AsString(255).NotNullable()
|
||||||
|
.WithColumn("Surname").AsString(255).NotNullable()
|
||||||
|
.WithColumn("Email").AsString(255).NotNullable()
|
||||||
|
.WithColumn("Password").AsString().NotNullable()
|
||||||
|
.WithColumn("ProjectRolesName").AsString(50).NotNullable()
|
||||||
|
.WithColumn("LoginType").AsInt32().NotNullable();
|
||||||
|
|
||||||
|
Create.Table("Wishlists")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
|
||||||
|
.WithColumn("Name").AsString(50).NotNullable()
|
||||||
|
.WithColumn("Description").AsString().NotNullable()
|
||||||
|
.WithColumn("UserId").AsInt32().NotNullable()
|
||||||
|
.WithColumn("IsPublic").AsBoolean().NotNullable();
|
||||||
|
Create.ForeignKey("FK_Wishlists_Users_UserId")
|
||||||
|
.FromTable("Wishlists").ForeignColumn("UserId")
|
||||||
|
.ToTable("Users").PrimaryColumn("Id");
|
||||||
|
|
||||||
|
Create.Table("WishlistItems")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
|
||||||
|
.WithColumn("WishlistId").AsInt32().NotNullable()
|
||||||
|
.WithColumn("Name").AsString(50).NotNullable()
|
||||||
|
.WithColumn("Description").AsString().Nullable()
|
||||||
|
.WithColumn("Price").AsDecimal().NotNullable()
|
||||||
|
.WithColumn("Link").AsString().Nullable();
|
||||||
|
Create.ForeignKey("FK_WishlistItems_Wishlists_WishlistId")
|
||||||
|
.FromTable("WishlistItems").ForeignColumn("WishlistId")
|
||||||
|
.ToTable("Wishlists").PrimaryColumn("Id");
|
||||||
|
|
||||||
|
Create.Table("UserMayViewWishList")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
|
||||||
|
.WithColumn("UserId").AsInt32().NotNullable()
|
||||||
|
.WithColumn("WishlistId").AsInt32().NotNullable();
|
||||||
|
Create.ForeignKey("FK_UserMayViewWishList_Users_UserId")
|
||||||
|
.FromTable("UserMayViewWishList").ForeignColumn("UserId")
|
||||||
|
.ToTable("Users").PrimaryColumn("Id");
|
||||||
|
Create.ForeignKey("FK_UserMayViewWishList_Wishlists_WishlistId")
|
||||||
|
.FromTable("UserMayViewWishList").ForeignColumn("WishlistId")
|
||||||
|
.ToTable("Wishlists").PrimaryColumn("Id");
|
||||||
|
|
||||||
|
Create.Table("UserMarkedAsGettingItem")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
|
||||||
|
.WithColumn("UserId").AsInt32().NotNullable()
|
||||||
|
.WithColumn("WishlistItemId").AsInt32().NotNullable();
|
||||||
|
Create.ForeignKey("FK_UserMarkedAsGettingItem_Users_UserId")
|
||||||
|
.FromTable("UserMarkedAsGettingItem").ForeignColumn("UserId")
|
||||||
|
.ToTable("Users").PrimaryColumn("Id");
|
||||||
|
Create.ForeignKey("FK_UserMarkedAsGettingItem_WishlistItems_WishlistItemId")
|
||||||
|
.FromTable("UserMarkedAsGettingItem").ForeignColumn("WishlistItemId")
|
||||||
|
.ToTable("WishlistItems").PrimaryColumn("Id");
|
||||||
|
|
||||||
|
Create.Table("Friends")
|
||||||
|
.WithColumn("Id").AsInt32().PrimaryKey().Identity()
|
||||||
|
.WithColumn("UserId").AsInt32().NotNullable()
|
||||||
|
.WithColumn("FriendUserId").AsInt32().NotNullable();
|
||||||
|
Create.ForeignKey("FK_Friends_Users_UserId")
|
||||||
|
.FromTable("Friends").ForeignColumn("UserId")
|
||||||
|
.ToTable("Users").PrimaryColumn("Id");
|
||||||
|
Create.ForeignKey("FK_Friends_Users_FriendUserId")
|
||||||
|
.FromTable("Friends").ForeignColumn("FriendUserId")
|
||||||
|
.ToTable("Users").PrimaryColumn("Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Down()
|
||||||
|
{
|
||||||
|
Delete.ForeignKey("FK_Friends_Users_FriendUserId");
|
||||||
|
Delete.ForeignKey("FK_Friends_Users_UserId");
|
||||||
|
Delete.Table("Friends");
|
||||||
|
Delete.ForeignKey("FK_UserMarkedAsGettingItem_WishlistItems_WishlistItemId");
|
||||||
|
Delete.ForeignKey("FK_UserMarkedAsGettingItem_Users_UserId");
|
||||||
|
Delete.Table("UserMarkedAsGettingItem");
|
||||||
|
Delete.ForeignKey("FK_UserMayViewWishList_Wishlists_WishlistId");
|
||||||
|
Delete.ForeignKey("FK_UserMayViewWishList_Users_UserId");
|
||||||
|
Delete.Table("UserMayViewWishList");
|
||||||
|
Delete.ForeignKey("FK_WishlistItems_Wishlists_WishlistId");
|
||||||
|
Delete.Table("WishlistItems");
|
||||||
|
Delete.ForeignKey("FK_Wishlists_Users_UserId");
|
||||||
|
Delete.Table("Wishlists");
|
||||||
|
Delete.Table("Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
wishlist/Data/DB/User.cs
Normal file
15
wishlist/Data/DB/User.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? GivenName { get; set; }
|
||||||
|
public string? Surname { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? ProjectRolesName { get; set; }
|
||||||
|
public LoginType LoginType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
wishlist/Data/DB/UserMarkedAsGettingItem.cs
Normal file
11
wishlist/Data/DB/UserMarkedAsGettingItem.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class UserMarkedAsGettingItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
public int WishlistItemId { get; set; }
|
||||||
|
public WishlistItem WishlistItem { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
wishlist/Data/DB/UserMayViewWishList.cs
Normal file
11
wishlist/Data/DB/UserMayViewWishList.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class UserMayViewWishList
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
public int WishlistId { get; set; }
|
||||||
|
public Wishlist Wishlist { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
wishlist/Data/DB/Wishlist.cs
Normal file
12
wishlist/Data/DB/Wishlist.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class Wishlist
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
public bool IsPublic { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
wishlist/Data/DB/WishlistItem.cs
Normal file
13
wishlist/Data/DB/WishlistItem.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Wishlist.Data.DB
|
||||||
|
{
|
||||||
|
public class WishlistItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int WishlistId { get; set; }
|
||||||
|
public Wishlist Wishlist { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
public string? Link { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
50
wishlist/Data/EmailSender.cs
Normal file
50
wishlist/Data/EmailSender.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||||
|
using System.Net.Mail;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Wishlist.Data;
|
||||||
|
|
||||||
|
public class EmailSender : IEmailSender
|
||||||
|
{
|
||||||
|
private ILogger<EmailSender> _logger;
|
||||||
|
|
||||||
|
public EmailSender(ILogger<EmailSender> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SendEmailAsync(string email, string subject, string htmlMessage)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Email sender started.");
|
||||||
|
|
||||||
|
string host = "wishlist.arnemoerman.be";
|
||||||
|
string from = "wishlist@arnemoerman.be";
|
||||||
|
int port = 25;
|
||||||
|
bool enableSsl = false;
|
||||||
|
|
||||||
|
MailMessage message = new(from, email, subject, htmlMessage)
|
||||||
|
{
|
||||||
|
HeadersEncoding = Encoding.UTF8,
|
||||||
|
SubjectEncoding = Encoding.UTF8,
|
||||||
|
BodyEncoding = Encoding.UTF8,
|
||||||
|
IsBodyHtml = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var smtp = new SmtpClient(host))
|
||||||
|
{
|
||||||
|
smtp.Port = port;
|
||||||
|
smtp.EnableSsl = enableSsl;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
smtp.Send(message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(ex, "Sending Email Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wishlist/Data/FriendStatus.cs
Normal file
10
wishlist/Data/FriendStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wishlist.Data
|
||||||
|
{
|
||||||
|
public enum FriendStatus
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Pending = 1,
|
||||||
|
Accepted = 2,
|
||||||
|
Rejected = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
13
wishlist/Data/LoginType.cs
Normal file
13
wishlist/Data/LoginType.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Wishlist.Data
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum LoginType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Email = 1,
|
||||||
|
Google = 2,
|
||||||
|
Facebook = 4,
|
||||||
|
Microsoft = 8,
|
||||||
|
//Twitter = 16
|
||||||
|
}
|
||||||
|
}
|
||||||
59
wishlist/Data/ProjectAccesses.cs
Normal file
59
wishlist/Data/ProjectAccesses.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
namespace Wishlist.Data;
|
||||||
|
|
||||||
|
public static class ProjectRoles
|
||||||
|
{
|
||||||
|
public static ProjectRole UserRole => new("User", "USER", "User Role");
|
||||||
|
public static ProjectRole AdminRole => new("Admin", "ADMIN", "Admin Role");
|
||||||
|
|
||||||
|
public static ProjectRole[] Roles => [UserRole, AdminRole];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProjectPolicies
|
||||||
|
{
|
||||||
|
public static ProjectPolicy BlogPostCreateClaimPolicy => new(nameof(BlogPostCreateClaimPolicy), [ProjectClaims.BlogPostCreate]);
|
||||||
|
public static ProjectPolicy BlogPostReadClaimPolicy => new(nameof(BlogPostReadClaimPolicy), [ProjectClaims.BlogPostRead]);
|
||||||
|
public static ProjectPolicy BlogPostUpdateClaimPolicy => new(nameof(BlogPostUpdateClaimPolicy), [ProjectClaims.BlogPostUpdate]);
|
||||||
|
public static ProjectPolicy BlogPostDeleteClaimPolicy => new(nameof(BlogPostDeleteClaimPolicy), [ProjectClaims.BlogPostDelete]);
|
||||||
|
|
||||||
|
public static ProjectPolicy WishlistViewPolicy => new(nameof(WishlistViewPolicy), [ProjectClaims.WishlistView]);
|
||||||
|
public static ProjectPolicy WishlistCreatePolicy => new(nameof(WishlistCreatePolicy), [ProjectClaims.WishlistCreate]);
|
||||||
|
public static ProjectPolicy WishlistMarkAsBuyingPolicy => new(nameof(WishlistMarkAsBuyingPolicy), [ProjectClaims.WishlistMarkAsBuying]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProjectClaims
|
||||||
|
{
|
||||||
|
public static ProjectClaim BlogPostCreate => new("BlogPost", "Create");
|
||||||
|
public static ProjectClaim BlogPostRead => new("BlogPost", "Read");
|
||||||
|
public static ProjectClaim BlogPostUpdate => new("BlogPost", "Update");
|
||||||
|
public static ProjectClaim BlogPostDelete => new("BlogPost", "Delete");
|
||||||
|
|
||||||
|
public static ProjectClaim WishlistView => new("Wishlist", "View");
|
||||||
|
public static ProjectClaim WishlistCreate => new("Wishlist", "Create");
|
||||||
|
public static ProjectClaim WishlistMarkAsBuying => new("Wishlist", "MarkAsBuying");
|
||||||
|
|
||||||
|
public static List<ProjectClaim> GetBlogPostClaims => [BlogPostCreate, BlogPostRead, BlogPostUpdate, BlogPostDelete];
|
||||||
|
|
||||||
|
public static List<ProjectClaim> GetWishlistClaims => [WishlistView, WishlistCreate, WishlistMarkAsBuying];
|
||||||
|
|
||||||
|
public static List<List<ProjectClaim>> GetAllClaims = [GetBlogPostClaims, GetWishlistClaims];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProjectPolicy(string name, ProjectClaim[] requiredClaims)
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = name;
|
||||||
|
public ProjectClaim[] RequiredClaims { get; set; } = requiredClaims;
|
||||||
|
public ProjectRole[] RequiredRoles { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProjectClaim(string type, string value)
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = type;
|
||||||
|
public string Value { get; set; } = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProjectRole(string roleName, string normalizedName, string description)
|
||||||
|
{
|
||||||
|
public string RoleName { get; set; } = roleName;
|
||||||
|
public string NormalizedName { get; set; } = normalizedName;
|
||||||
|
public string Description { get; set; } = description;
|
||||||
|
}
|
||||||
163
wishlist/Data/SeedData.cs
Normal file
163
wishlist/Data/SeedData.cs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
using Wishlist.Models;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Wishlist.Data;
|
||||||
|
|
||||||
|
public class SeedData
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IUserStore<ApplicationUser> _userStore;
|
||||||
|
private readonly IUserEmailStore<ApplicationUser> _emailStore;
|
||||||
|
|
||||||
|
public SeedData(ApplicationDbContext context,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IUserStore<ApplicationUser> userStore)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_userManager = userManager;
|
||||||
|
_userStore = userStore;
|
||||||
|
_emailStore = GetEmailStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateInitialData()
|
||||||
|
{
|
||||||
|
foreach (var projectRole in ProjectRoles.Roles)
|
||||||
|
{
|
||||||
|
var applicationRole = new ApplicationRole { Id = Guid.NewGuid().ToString(), Name = projectRole.RoleName, NormalizedName = projectRole.NormalizedName, Description = projectRole.Description };
|
||||||
|
_context.Roles.Add(applicationRole);
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var adminRole = _context.Roles.First(x => x.Name == ProjectRoles.AdminRole.RoleName);
|
||||||
|
foreach (var claims in ProjectClaims.GetAllClaims)
|
||||||
|
foreach (var claim in claims)
|
||||||
|
_context.RoleClaims.Add(new ApplicationRoleClaim { RoleId = adminRole.Id, ClaimType = claim.Type, ClaimValue = claim.Value });
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var userRole = _context.Roles.First(x => x.Name == ProjectRoles.UserRole.RoleName);
|
||||||
|
_context.RoleClaims.Add(new ApplicationRoleClaim { RoleId = userRole.Id, ClaimType = ProjectClaims.BlogPostRead.Type, ClaimValue = ProjectClaims.BlogPostRead.Value });
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var admin = Activator.CreateInstance<ApplicationUser>();
|
||||||
|
admin.Name = "AdminG AdminS";
|
||||||
|
admin.GivenName = "AdminG";
|
||||||
|
admin.Surname = "AdminS";
|
||||||
|
string adminEmail = "admin@admin.com";
|
||||||
|
string adminPass = "123456";
|
||||||
|
await _userStore.SetUserNameAsync(admin, adminEmail, CancellationToken.None);
|
||||||
|
await _emailStore.SetEmailAsync(admin, adminEmail, CancellationToken.None);
|
||||||
|
var resultAdmin = await _userManager.CreateAsync(admin, adminPass);
|
||||||
|
if (!resultAdmin.Succeeded) throw new Exception($"Error on Adding Admin user. Errors:{string.Join(",", resultAdmin.Errors)}");
|
||||||
|
foreach (var role in ProjectRoles.Roles)
|
||||||
|
await _userManager.AddToRoleAsync(admin, role.RoleName);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var user = Activator.CreateInstance<ApplicationUser>();
|
||||||
|
user.Name = "UserG UserS";
|
||||||
|
user.GivenName = "UserG";
|
||||||
|
user.Surname = "UserS";
|
||||||
|
string userEmail = "user@user.com";
|
||||||
|
string userPass = "123456";
|
||||||
|
await _userStore.SetUserNameAsync(user, userEmail, CancellationToken.None);
|
||||||
|
await _emailStore.SetEmailAsync(user, userEmail, CancellationToken.None);
|
||||||
|
var resultUser = await _userManager.CreateAsync(user, userPass);
|
||||||
|
if (!resultUser.Succeeded) throw new Exception($"Error on Adding User user. Errors:{string.Join(",", resultUser.Errors)}");
|
||||||
|
await _userManager.AddToRoleAsync(user, ProjectRoles.UserRole.RoleName);
|
||||||
|
|
||||||
|
// add BlogPosts
|
||||||
|
var posts = GetAllBlogPosts();
|
||||||
|
await _context.BlogPosts.AddRangeAsync(posts);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUserEmailStore<ApplicationUser> GetEmailStore()
|
||||||
|
{
|
||||||
|
if (!_userManager.SupportsUserEmail)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||||
|
}
|
||||||
|
return (IUserEmailStore<ApplicationUser>)_userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<BlogPost> GetAllBlogPosts()
|
||||||
|
{
|
||||||
|
List<BlogPost> posts = [];
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
BlogPost post = new() { Id = i + 1, Title = titles[i], Content = contents[i % 10] };
|
||||||
|
posts.Add(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] titles = {
|
||||||
|
"Introduction to Object-Oriented Programming",
|
||||||
|
"Mastering Data Structures and Algorithms",
|
||||||
|
"Building Web Applications with ASP.NET",
|
||||||
|
"Creating Mobile Apps with Xamarin",
|
||||||
|
"Exploring Artificial Intelligence and Machine Learning",
|
||||||
|
"Understanding Functional Programming Concepts",
|
||||||
|
"Developing Games with Unity",
|
||||||
|
"Securing Web Applications from Cyber Attacks",
|
||||||
|
"Optimizing Code Performance for Better Efficiency",
|
||||||
|
"Implementing Design Patterns in Software Development",
|
||||||
|
"Testing and Debugging Strategies for Reliable Software",
|
||||||
|
"Working with Databases and SQL",
|
||||||
|
"Building Responsive User Interfaces with HTML and CSS",
|
||||||
|
"Exploring Cloud Computing and Serverless Architecture",
|
||||||
|
"Developing Cross-Platform Applications with React Native",
|
||||||
|
"Introduction to Internet of Things (IoT)",
|
||||||
|
"Creating Scalable Microservices with Docker and Kubernetes",
|
||||||
|
"Understanding Network Protocols and TCP/IP",
|
||||||
|
"Building RESTful APIs with Node.js and Express",
|
||||||
|
"Exploring Big Data Analytics and Apache Hadoop",
|
||||||
|
"Mastering Version Control with Git and GitHub",
|
||||||
|
"Developing Desktop Applications with WPF",
|
||||||
|
"Securing Mobile Applications from Malicious Attacks",
|
||||||
|
"Optimizing Database Performance with Indexing",
|
||||||
|
"Implementing Continuous Integration and Deployment",
|
||||||
|
"Testing Mobile Apps on Different Platforms",
|
||||||
|
"Working with NoSQL Databases like MongoDB",
|
||||||
|
"Building Progressive Web Apps with React",
|
||||||
|
"Exploring Quantum Computing and Quantum Algorithms",
|
||||||
|
"Introduction to Cybersecurity and Ethical Hacking",
|
||||||
|
"Creating Chatbots with Natural Language Processing",
|
||||||
|
"Understanding Software Development Life Cycle",
|
||||||
|
"Developing Augmented Reality (AR) Applications",
|
||||||
|
"Securing Web APIs with OAuth and JWT",
|
||||||
|
"Optimizing Front-End Performance for Better User Experience",
|
||||||
|
"Implementing Machine Learning Models with TensorFlow",
|
||||||
|
"Testing Web Applications for Cross-Browser Compatibility",
|
||||||
|
"Working with Blockchain Technology and Smart Contracts",
|
||||||
|
"Building Real-Time Applications with SignalR",
|
||||||
|
"Exploring Cryptography and Encryption Techniques",
|
||||||
|
"Introduction to Agile Software Development",
|
||||||
|
"Creating Voice User Interfaces with Amazon Alexa",
|
||||||
|
"Understanding Web Accessibility and Inclusive Design",
|
||||||
|
"Developing Natural Language Processing Applications",
|
||||||
|
"Securing Cloud Infrastructure and Services",
|
||||||
|
"Optimizing Backend Performance for Scalability",
|
||||||
|
"Implementing Continuous Monitoring and Alerting",
|
||||||
|
"Testing APIs with Postman and Swagger",
|
||||||
|
"Working with Data Visualization Libraries like D3.js",
|
||||||
|
"Building E-commerce Applications with Shopify",
|
||||||
|
"Exploring Robotic Process Automation (RPA)",
|
||||||
|
"Introduction to DevOps and CI/CD Pipelines"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] contents = new string[]
|
||||||
|
{
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur t.",
|
||||||
|
"Sed ut perspiciatis unde omnis iste natuccusantium doloremque laudantium.",
|
||||||
|
"Nemo enim ipsam voluptatem quia voluptas aut fugit.",
|
||||||
|
"Quis autem vel eum iure reprehenderit quesse quam nihil molestiae consequatur.",
|
||||||
|
"At vero eos et accusamus et iusto odio d.",
|
||||||
|
"Similique sunt in culpa qui officia de.",
|
||||||
|
"Et harum quidem rerum facilis est et expio.",
|
||||||
|
"Nam libero tempore, cum soluta nobis est.",
|
||||||
|
"Omnis voluptas assumenda est, omnis dolo",
|
||||||
|
"Temporibus autem quibusdam et aut offic"
|
||||||
|
};
|
||||||
|
}
|
||||||
8
wishlist/Models/BlogPost.cs
Normal file
8
wishlist/Models/BlogPost.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Wishlist.Models;
|
||||||
|
|
||||||
|
public class BlogPost
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
85
wishlist/Models/IdentityModels.cs
Normal file
85
wishlist/Models/IdentityModels.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Wishlist.Data.DB;
|
||||||
|
|
||||||
|
namespace Wishlist.Models;
|
||||||
|
|
||||||
|
public class ApplicationUser : IdentityUser<string>
|
||||||
|
{
|
||||||
|
DatabaseModel _database;
|
||||||
|
|
||||||
|
public ApplicationUser(DatabaseModel database)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
Id = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User User { get; set; } = new();
|
||||||
|
|
||||||
|
//public string Name { get; set; } => string.Empty;
|
||||||
|
//public string GivenName { get; set; } = String.Empty;
|
||||||
|
//public string Surname { get; set; } = String.Empty;
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => User.Name ?? string.Empty;
|
||||||
|
set => User.Name = value;
|
||||||
|
}
|
||||||
|
public string GivenName
|
||||||
|
{
|
||||||
|
get => User.GivenName ?? string.Empty;
|
||||||
|
set => User.GivenName = value;
|
||||||
|
}
|
||||||
|
public string Surname
|
||||||
|
{
|
||||||
|
get => User.Surname ?? string.Empty;
|
||||||
|
set => User.Surname = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ICollection<ApplicationUserClaim> Claims { get; set; }
|
||||||
|
public virtual ICollection<ApplicationUserLogin> Logins { get; set; }
|
||||||
|
public virtual ICollection<ApplicationUserToken> Tokens { get; set; }
|
||||||
|
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
|
||||||
|
|
||||||
|
public async Task SaveToDb() => await _database.Users.InsertAsync(User);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationRole : IdentityRole<string>
|
||||||
|
{
|
||||||
|
public ApplicationRole()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Id { get; set; }
|
||||||
|
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
|
||||||
|
public virtual ICollection<ApplicationRoleClaim> RoleClaims { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationUserRole : IdentityUserRole<string>
|
||||||
|
{
|
||||||
|
public virtual ApplicationUser User { get; set; }
|
||||||
|
public virtual ApplicationRole Role { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationUserClaim : IdentityUserClaim<string>
|
||||||
|
{
|
||||||
|
public virtual ApplicationUser User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationUserLogin : IdentityUserLogin<string>
|
||||||
|
{
|
||||||
|
public virtual ApplicationUser User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationRoleClaim : IdentityRoleClaim<string>
|
||||||
|
{
|
||||||
|
public virtual ApplicationRole Role { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationUserToken : IdentityUserToken<string>
|
||||||
|
{
|
||||||
|
public virtual ApplicationUser User { get; set; }
|
||||||
|
}
|
||||||
52
wishlist/Pages/BlogPost/Create.razor
Normal file
52
wishlist/Pages/BlogPost/Create.razor
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
@page "/BlogPost/Create"
|
||||||
|
|
||||||
|
<PageTitle>Create</PageTitle>
|
||||||
|
|
||||||
|
@*<AuthorizeView Roles="Admin">*@
|
||||||
|
<AuthorizeView Policy="@ProjectPolicies.BlogPostCreateClaimPolicy.Name">
|
||||||
|
<Authorized>
|
||||||
|
<h1>Create</h1>
|
||||||
|
<h4>BlogPost</h4>
|
||||||
|
<hr />
|
||||||
|
@if (blogPost == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<EditForm Model="@blogPost" OnValidSubmit="@HandleValidSubmit" Context="createBlogPost">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(BlogPostViewModel.Title)</label>
|
||||||
|
<InputText @bind-Value="blogPost.Title" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => blogPost.Title)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(BlogPostViewModel.Content)</label>
|
||||||
|
<InputText @bind-Value="blogPost.Content" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => blogPost.Content)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Create" class="btn btn-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/BlogPost">Back to List</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>Not Authorized</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
26
wishlist/Pages/BlogPost/Create.razor.cs
Normal file
26
wishlist/Pages/BlogPost/Create.razor.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using global::Wishlist.Models;
|
||||||
|
using global::Wishlist.Services;
|
||||||
|
|
||||||
|
namespace Wishlist.Pages.BlogPost
|
||||||
|
{
|
||||||
|
public partial class Create
|
||||||
|
{
|
||||||
|
|
||||||
|
private BlogPostViewModel? blogPost;
|
||||||
|
|
||||||
|
private async void HandleValidSubmit()
|
||||||
|
{
|
||||||
|
var model = Mapper.Map<BlogPostViewModel, Models.BlogPost>(blogPost);
|
||||||
|
bool result = await BlogPostService.AddBlogPostAsync(model);
|
||||||
|
if (result)
|
||||||
|
NavigationManager.NavigateTo("/BlogPost");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
blogPost = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
53
wishlist/Pages/BlogPost/Delete.razor
Normal file
53
wishlist/Pages/BlogPost/Delete.razor
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@page "/BlogPost/Delete/{id:int}"
|
||||||
|
|
||||||
|
<PageTitle>Delete</PageTitle>
|
||||||
|
|
||||||
|
@*<AuthorizeView Roles="Admin">*@
|
||||||
|
<AuthorizeView Policy="@ProjectPolicies.BlogPostDeleteClaimPolicy.Name">
|
||||||
|
<Authorized>
|
||||||
|
|
||||||
|
<h1>Delete</h1>
|
||||||
|
|
||||||
|
<h3>Are you sure you want to delete this?</h3>
|
||||||
|
|
||||||
|
@if (blogPost == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<h4>BlogPost</h4>
|
||||||
|
<hr />
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(BlogPost.Id)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@blogPost.Id
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(BlogPost.Title)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@blogPost.Title
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(BlogPost.Content)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@blogPost.Content
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-danger" @onclick="DeleteButtonClick">Delete</button> |
|
||||||
|
<a href="/BlogPost">Back to List</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>Not Authorized</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
33
wishlist/Pages/BlogPost/Delete.razor.cs
Normal file
33
wishlist/Pages/BlogPost/Delete.razor.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using global::Wishlist.Models;
|
||||||
|
using global::Wishlist.Services;
|
||||||
|
|
||||||
|
namespace Wishlist.Pages.BlogPost
|
||||||
|
{
|
||||||
|
public partial class Delete
|
||||||
|
{
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int id { get; set; }
|
||||||
|
|
||||||
|
private BlogPostViewModel? blogPost;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (blogPost == null)
|
||||||
|
{
|
||||||
|
var result = await BlogPostService.GetbyId(id);
|
||||||
|
if (result != null)
|
||||||
|
blogPost = Mapper.Map<Models.BlogPost, BlogPostViewModel>(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void DeleteButtonClick()
|
||||||
|
{
|
||||||
|
bool result = await BlogPostService.DeletebyIdAsync(id);
|
||||||
|
if (result)
|
||||||
|
NavigationManager.NavigateTo("/BlogPost");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
49
wishlist/Pages/BlogPost/Details.razor
Normal file
49
wishlist/Pages/BlogPost/Details.razor
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@page "/BlogPost/Details/{id:int}"
|
||||||
|
|
||||||
|
<PageTitle>Details</PageTitle>
|
||||||
|
|
||||||
|
<AuthorizeView Policy="@ProjectPolicies.BlogPostReadClaimPolicy.Name">
|
||||||
|
<Authorized>
|
||||||
|
<h1>Details</h1>
|
||||||
|
@if (blogPost == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<h4>BlogPost</h4>
|
||||||
|
<hr />
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(BlogPost.Id)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@blogPost.Id
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(BlogPost.Title)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@blogPost.Title
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(BlogPost.Content)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@blogPost.Content
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/BlogPost/Edit/@blogPost.Id">Edit</a> |
|
||||||
|
<a href="/BlogPost">Back to List</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>Not Authorized</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
|
||||||
26
wishlist/Pages/BlogPost/Details.razor.cs
Normal file
26
wishlist/Pages/BlogPost/Details.razor.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using global::Wishlist.Models;
|
||||||
|
using global::Wishlist.Services;
|
||||||
|
|
||||||
|
namespace Wishlist.Pages.BlogPost
|
||||||
|
{
|
||||||
|
public partial class Details
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int id { get; set; }
|
||||||
|
|
||||||
|
private BlogPostViewModel? blogPost;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (blogPost == null)
|
||||||
|
{
|
||||||
|
var result = await BlogPostService.GetbyId(id);
|
||||||
|
if (result != null)
|
||||||
|
blogPost = Mapper.Map<Models.BlogPost, BlogPostViewModel>(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
wishlist/Pages/BlogPost/Edit.razor
Normal file
56
wishlist/Pages/BlogPost/Edit.razor
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@page "/BlogPost/Edit/{id:int}"
|
||||||
|
|
||||||
|
<PageTitle>Edit</PageTitle>
|
||||||
|
|
||||||
|
@*<AuthorizeView Roles="Admin">*@
|
||||||
|
<AuthorizeView Policy="@ProjectPolicies.BlogPostUpdateClaimPolicy.Name">
|
||||||
|
<Authorized>
|
||||||
|
<h1>Edit</h1>
|
||||||
|
<h4>BlogPost</h4>
|
||||||
|
<hr />
|
||||||
|
@if (blogPost == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<EditForm Model="@blogPost" OnValidSubmit="@HandleValidSubmit" Context="editBlogPost">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(BlogPostViewModel.Title)</label>
|
||||||
|
<InputText @bind-Value="blogPost.Title" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => blogPost.Title)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(BlogPostViewModel.Content)</label>
|
||||||
|
<InputText @bind-Value="blogPost.Content" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => blogPost.Content)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Save" class="btn btn-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/BlogPost">Back to List</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>Not Authorized</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
|
||||||
36
wishlist/Pages/BlogPost/Edit.razor.cs
Normal file
36
wishlist/Pages/BlogPost/Edit.razor.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Wishlist.Models;
|
||||||
|
using Wishlist.Services;
|
||||||
|
|
||||||
|
namespace Wishlist.Pages.BlogPost
|
||||||
|
{
|
||||||
|
public partial class Edit
|
||||||
|
{
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int id { get; set; }
|
||||||
|
|
||||||
|
private BlogPostViewModel? blogPost;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (blogPost == null)
|
||||||
|
{
|
||||||
|
var result = await BlogPostService.GetbyId(id);
|
||||||
|
if (result != null)
|
||||||
|
blogPost = Mapper.Map<Models.BlogPost, BlogPostViewModel>(result);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleValidSubmit()
|
||||||
|
{
|
||||||
|
var model = Mapper.Map<BlogPostViewModel, Models.BlogPost>(blogPost);
|
||||||
|
bool result = await BlogPostService.UpdateBlogPostAsync(id, model);
|
||||||
|
if (result)
|
||||||
|
NavigationManager.NavigateTo("/BlogPost");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
53
wishlist/Pages/BlogPost/Index.razor
Normal file
53
wishlist/Pages/BlogPost/Index.razor
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@page "/BlogPost"
|
||||||
|
|
||||||
|
<PageTitle>Index</PageTitle>
|
||||||
|
|
||||||
|
<AuthorizeView Policy="@ProjectPolicies.BlogPostReadClaimPolicy.Name">
|
||||||
|
<Authorized>
|
||||||
|
<h1>Index</h1>
|
||||||
|
<p>
|
||||||
|
<a href="/BlogPost/Create">Create New</a>
|
||||||
|
</p>
|
||||||
|
@if (blogPosts == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>@nameof(BlogPostViewModel.Id)</th>
|
||||||
|
<th>@nameof(BlogPostViewModel.Title)</th>
|
||||||
|
<th>@nameof(BlogPostViewModel.Content)</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var blogpost in blogPosts)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@blogpost.Id</td>
|
||||||
|
<td>@blogpost.Title</td>
|
||||||
|
<td>@blogpost.Content</td>
|
||||||
|
<td>
|
||||||
|
<a href="/BlogPost/Details/@blogpost.Id">Details</a> |
|
||||||
|
<AuthorizeView Policy="@nameof(ProjectPolicies.BlogPostUpdateClaimPolicy)" Context="editLink">
|
||||||
|
<a href="/BlogPost/Edit/@blogpost.Id">Edit</a> |
|
||||||
|
</AuthorizeView>
|
||||||
|
<AuthorizeView Policy="@nameof(ProjectPolicies.BlogPostDeleteClaimPolicy)" Context="deleteLink">
|
||||||
|
<a href="/BlogPost/Delete/@blogpost.Id">Delete</a>
|
||||||
|
</AuthorizeView>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>Not Authorized</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
|
||||||
20
wishlist/Pages/BlogPost/Index.razor.cs
Normal file
20
wishlist/Pages/BlogPost/Index.razor.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using global::Wishlist.Models;
|
||||||
|
using global::Wishlist.Services;
|
||||||
|
|
||||||
|
namespace Wishlist.Pages.BlogPost
|
||||||
|
{
|
||||||
|
public partial class Index
|
||||||
|
{
|
||||||
|
private IEnumerable<BlogPostViewModel>? blogPosts;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (blogPosts == null)
|
||||||
|
{
|
||||||
|
var result = await BlogPostService.GetAllAsync();
|
||||||
|
blogPosts = Mapper.Map<IEnumerable<Models.BlogPost>, IEnumerable<BlogPostViewModel>>(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
8
wishlist/Pages/BlogPost/_Imports.razor
Normal file
8
wishlist/Pages/BlogPost/_Imports.razor
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@using AutoMapper;
|
||||||
|
@using global::Wishlist.Data;
|
||||||
|
@using global::Wishlist.Models;
|
||||||
|
@using global::Wishlist.Services;
|
||||||
|
|
||||||
|
@inject IMapper Mapper;
|
||||||
|
@inject NavigationManager NavigationManager;
|
||||||
|
@inject BlogPostService BlogPostService;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user