mirror of
https://github.com/claunia/marechai.git
synced 2025-12-16 19:14:25 +00:00
Scaffold all ASP.NET Identity pages.
This commit is contained in:
12
Marechai/Areas/Identity/IdentityHostingStartup.cs
Normal file
12
Marechai/Areas/Identity/IdentityHostingStartup.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Marechai.Areas.Identity;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
[assembly: HostingStartup(typeof(IdentityHostingStartup))]
|
||||
|
||||
namespace Marechai.Areas.Identity
|
||||
{
|
||||
public class IdentityHostingStartup : IHostingStartup
|
||||
{
|
||||
public void Configure(IWebHostBuilder builder) => builder.ConfigureServices((context, services) => { });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@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>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class AccessDeniedModel : PageModel
|
||||
{
|
||||
public void OnGet() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@page
|
||||
@model ConfirmEmailModel
|
||||
@{
|
||||
ViewData["Title"] = "Confirm email";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
44
Marechai/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
44
Marechai/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ConfirmEmailModel : PageModel
|
||||
{
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ConfirmEmailModel(UserManager<ApplicationUser> userManager) => _userManager = userManager;
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string userId, string code)
|
||||
{
|
||||
if(userId == null ||
|
||||
code == null)
|
||||
{
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
ApplicationUser user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
||||
IdentityResult result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@page
|
||||
@model ConfirmEmailChangeModel
|
||||
@{
|
||||
ViewData["Title"] = "Confirm email change";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ConfirmEmailChangeModel : PageModel
|
||||
{
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ConfirmEmailChangeModel(UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
ApplicationUser user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
||||
IdentityResult 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.
|
||||
IdentityResult 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Marechai/Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
31
Marechai/Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
@@ -0,0 +1,31 @@
|
||||
@page
|
||||
@model ExternalLoginModel
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h4 id="external-login-title">Associate your @Model.ProviderDisplayName account.</h4>
|
||||
<hr />
|
||||
<p class="text-info" id="external-login-description">
|
||||
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-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input asp-for="Input.Email" class="form-control" />
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
200
Marechai/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
200
Marechai/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ExternalLoginModel : PageModel
|
||||
{
|
||||
readonly IEmailSender _emailSender;
|
||||
readonly ILogger<ExternalLoginModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ExternalLoginModel(SignInManager<ApplicationUser> signInManager,
|
||||
UserManager<ApplicationUser> userManager, ILogger<ExternalLoginModel> logger,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public string ProviderDisplayName { get; set; }
|
||||
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public IActionResult OnGetAsync() => RedirectToPage("./Login");
|
||||
|
||||
public IActionResult OnPost(string provider, string returnUrl = null)
|
||||
{
|
||||
// Request a redirect to the external login provider.
|
||||
string redirectUrl = Url.Page("./ExternalLogin", "Callback", new
|
||||
{
|
||||
returnUrl
|
||||
});
|
||||
|
||||
AuthenticationProperties 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
|
||||
});
|
||||
}
|
||||
|
||||
ExternalLoginInfo 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.
|
||||
SignInResult result =
|
||||
await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false, 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");
|
||||
}
|
||||
|
||||
// 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
|
||||
ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
|
||||
if(info == null)
|
||||
{
|
||||
ErrorMessage = "Error loading external login information during confirmation.";
|
||||
|
||||
return RedirectToPage("./Login", new
|
||||
{
|
||||
ReturnUrl = returnUrl
|
||||
});
|
||||
}
|
||||
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = Input.Email, Email = Input.Email
|
||||
};
|
||||
|
||||
IdentityResult 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);
|
||||
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
string code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
string callbackUrl = Url.Page("/Account/ConfirmEmail", null, new
|
||||
{
|
||||
area = "Identity", userId, code
|
||||
}, 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
|
||||
{
|
||||
Input.Email
|
||||
});
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, false, info.LoginProvider);
|
||||
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
}
|
||||
|
||||
foreach(IdentityError error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
ProviderDisplayName = info.ProviderDisplayName;
|
||||
ReturnUrl = returnUrl;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Marechai/Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
25
Marechai/Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
@@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model ForgotPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Forgot your password?";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h4>Enter your email.</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input asp-for="Input.Email" class="form-control" />
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.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;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ForgotPasswordModel : PageModel
|
||||
{
|
||||
readonly IEmailSender _emailSender;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ForgotPasswordModel(UserManager<ApplicationUser> userManager, IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
ApplicationUser 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
|
||||
string code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
string callbackUrl = Url.Page("/Account/ResetPassword", null, new
|
||||
{
|
||||
area = "Identity", code
|
||||
}, 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();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@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,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ForgotPasswordConfirmation : PageModel
|
||||
{
|
||||
public void OnGet() { }
|
||||
}
|
||||
}
|
||||
9
Marechai/Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
9
Marechai/Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
@@ -0,0 +1,9 @@
|
||||
@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>
|
||||
11
Marechai/Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
11
Marechai/Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class LockoutModel : PageModel
|
||||
{
|
||||
public void OnGet() { }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@page
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Marechai.Database.Models
|
||||
@attribute [IgnoreAntiforgeryToken]
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
||||
@functions {
|
||||
|
||||
|
||||
85
Marechai/Areas/Identity/Pages/Account/Login.cshtml
Normal file
85
Marechai/Areas/Identity/Pages/Account/Login.cshtml
Normal file
@@ -0,0 +1,85 @@
|
||||
@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">
|
||||
<h4>Use a local account to log in.</h4>
|
||||
<hr />
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Username"></label>
|
||||
<input asp-for="Input.Username" class="form-control" />
|
||||
<span asp-validation-for="Input.Username" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input asp-for="Input.Password" class="form-control" />
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label asp-for="Input.RememberMe">
|
||||
<input asp-for="Input.RememberMe" />
|
||||
@Html.DisplayNameFor(m => m.Input.RememberMe)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Log in</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p>
|
||||
<a asp-page="./ForgotPassword" id="forgot-password">Forgot your password?</a>
|
||||
</p>
|
||||
<p>
|
||||
<a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
|
||||
</p>
|
||||
<p>
|
||||
<a asp-page="./ResendEmailConfirmation" id="resend-confirmation">Resend email confirmation</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-2">
|
||||
<section>
|
||||
<h4>Use another service to log in.</h4>
|
||||
<hr />
|
||||
@{
|
||||
if ((Model.ExternalLogins?.Count ?? 0) == 0)
|
||||
{
|
||||
<div>
|
||||
<p>
|
||||
There are no external authentication services configured. See
|
||||
<a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
|
||||
for details on setting up this ASP.NET application to support logging in via external services.
|
||||
</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)
|
||||
{
|
||||
<button 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>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
112
Marechai/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
112
Marechai/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class LoginModel : PageModel
|
||||
{
|
||||
readonly ILogger<LoginModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
||||
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ErrorMessage);
|
||||
}
|
||||
|
||||
returnUrl = 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 = returnUrl ?? Url.Content("~/");
|
||||
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
SignInResult result =
|
||||
await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, false);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
if(result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToPage("./LoginWith2fa", new
|
||||
{
|
||||
ReturnUrl = returnUrl, Input.RememberMe
|
||||
});
|
||||
}
|
||||
|
||||
if(result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required, DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Marechai/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
40
Marechai/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
@@ -0,0 +1,40 @@
|
||||
@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="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.TwoFactorCode"></label>
|
||||
<input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" />
|
||||
<span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label asp-for="Input.RememberMachine">
|
||||
<input asp-for="Input.RememberMachine" />
|
||||
@Html.DisplayNameFor(m => m.Input.RememberMachine)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">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" />
|
||||
}
|
||||
102
Marechai/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
102
Marechai/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class LoginWith2faModel : PageModel
|
||||
{
|
||||
readonly ILogger<LoginWith2faModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
|
||||
public LoginWith2faModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginWith2faModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
ApplicationUser 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("~/");
|
||||
|
||||
ApplicationUser user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
string authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
SignInResult result =
|
||||
await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe,
|
||||
Input.RememberMachine);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id);
|
||||
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
if(result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
|
||||
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[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; }
|
||||
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@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="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.RecoveryCode"></label>
|
||||
<input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" />
|
||||
<span asp-validation-for="Input.RecoveryCode" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Log in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class LoginWithRecoveryCodeModel : PageModel
|
||||
{
|
||||
readonly ILogger<LoginWithRecoveryCodeModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
|
||||
public LoginWithRecoveryCodeModel(SignInManager<ApplicationUser> signInManager,
|
||||
ILogger<LoginWithRecoveryCodeModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
ApplicationUser 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();
|
||||
}
|
||||
|
||||
ApplicationUser user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
string recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
|
||||
|
||||
SignInResult result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id);
|
||||
|
||||
return LocalRedirect(returnUrl ?? Url.Content("~/"));
|
||||
}
|
||||
|
||||
if(result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
|
||||
|
||||
return RedirectToPage("./Lockout");
|
||||
}
|
||||
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[BindProperty, Required, DataType(DataType.Text), Display(Name = "Recovery Code")]
|
||||
public string RecoveryCode { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Marechai/Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
38
Marechai/Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class LogoutModel : PageModel
|
||||
{
|
||||
readonly ILogger<LogoutModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
|
||||
public LogoutModel(SignInManager<ApplicationUser> signInManager, ILogger<LogoutModel> logger)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet() { }
|
||||
|
||||
public async Task<IActionResult> OnPost(string returnUrl = null)
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
_logger.LogInformation("User logged out.");
|
||||
|
||||
if(returnUrl != null)
|
||||
{
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model ChangePasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Change password";
|
||||
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||
}
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="change-password-form" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.OldPassword"></label>
|
||||
<input asp-for="Input.OldPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.OldPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.NewPassword"></label>
|
||||
<input asp-for="Input.NewPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.ConfirmPassword"></label>
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ChangePasswordModel : PageModel
|
||||
{
|
||||
readonly ILogger<ChangePasswordModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ChangePasswordModel(UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager, ILogger<ChangePasswordModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
bool hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
|
||||
if(!hasPassword)
|
||||
{
|
||||
return RedirectToPage("./SetPassword");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
IdentityResult changePasswordResult =
|
||||
await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
|
||||
|
||||
if(!changePasswordResult.Succeeded)
|
||||
{
|
||||
foreach(IdentityError 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();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, DataType(DataType.Password), Display(Name = "Current password")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
[Required,
|
||||
StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",
|
||||
MinimumLength = 6), DataType(DataType.Password), Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model DeletePersonalDataModel
|
||||
@{
|
||||
ViewData["Title"] = "Delete Personal Data";
|
||||
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||
}
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<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 class="form-group" id="delete-user" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
@if (Model.RequirePassword)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input asp-for="Input.Password" class="form-control" />
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
<button class="btn btn-danger" type="submit">Delete data and close my account</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class DeletePersonalDataModel : PageModel
|
||||
{
|
||||
readonly ILogger<DeletePersonalDataModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public DeletePersonalDataModel(UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
ILogger<DeletePersonalDataModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public bool RequirePassword { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
ApplicationUser 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()
|
||||
{
|
||||
ApplicationUser 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();
|
||||
}
|
||||
}
|
||||
|
||||
IdentityResult result = await _userManager.DeleteAsync(user);
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected error occurred deleting user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
await _signInManager.SignOutAsync();
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
|
||||
|
||||
return Redirect("~/");
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@page
|
||||
@model Disable2faModel
|
||||
@{
|
||||
ViewData["Title"] = "Disable two-factor authentication (2FA)";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<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 class="form-group" method="post">
|
||||
<button class="btn btn-danger" type="submit">Disable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class Disable2faModel : PageModel
|
||||
{
|
||||
readonly ILogger<Disable2faModel> _logger;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public Disable2faModel(UserManager<ApplicationUser> userManager, ILogger<Disable2faModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if(!await _userManager.GetTwoFactorEnabledAsync(user))
|
||||
{
|
||||
throw new
|
||||
InvalidOperationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
IdentityResult disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
|
||||
if(!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new
|
||||
InvalidOperationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
|
||||
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
|
||||
|
||||
return RedirectToPage("./TwoFactorAuthentication");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@page
|
||||
@model DownloadPersonalDataModel
|
||||
@{
|
||||
ViewData["Title"] = "Download Your Data";
|
||||
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||
}
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class DownloadPersonalDataModel : PageModel
|
||||
{
|
||||
readonly ILogger<DownloadPersonalDataModel> _logger;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public DownloadPersonalDataModel(UserManager<ApplicationUser> userManager,
|
||||
ILogger<DownloadPersonalDataModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
ApplicationUser 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
|
||||
Dictionary<string, string> personalData = new Dictionary<string, string>();
|
||||
|
||||
IEnumerable<PropertyInfo> personalDataProps = typeof(ApplicationUser).
|
||||
GetProperties().
|
||||
Where(prop =>
|
||||
Attribute.IsDefined(prop,
|
||||
typeof(PersonalDataAttribute)));
|
||||
|
||||
foreach(PropertyInfo p in personalDataProps)
|
||||
{
|
||||
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
|
||||
}
|
||||
|
||||
IList<UserLoginInfo> logins = await _userManager.GetLoginsAsync(user);
|
||||
|
||||
foreach(UserLoginInfo l in logins)
|
||||
{
|
||||
personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey);
|
||||
}
|
||||
|
||||
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
|
||||
|
||||
return new FileContentResult(JsonSerializer.SerializeToUtf8Bytes(personalData), "application/json");
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Marechai/Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
42
Marechai/Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
@@ -0,0 +1,42 @@
|
||||
@page
|
||||
@model EmailModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage Email";
|
||||
ViewData["ActivePage"] = ManageNavPages.Email;
|
||||
}
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="email-form" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email"></label>
|
||||
@if (Model.IsEmailConfirmed)
|
||||
{
|
||||
<div class="input-group">
|
||||
<input asp-for="Email" class="form-control" disabled />
|
||||
<div class="input-group-append">
|
||||
<span class="font-weight-bold input-group-text text-success">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="Email" class="form-control" disabled />
|
||||
<button asp-page-handler="SendVerificationEmail" class="btn btn-link" id="email-verification" type="submit">Send verification email</button>
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.NewEmail"></label>
|
||||
<input asp-for="Input.NewEmail" class="form-control" />
|
||||
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<button asp-page-handler="ChangeEmail" class="btn btn-primary" id="change-email-button" type="submit">Change email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
148
Marechai/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
148
Marechai/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class EmailModel : PageModel
|
||||
{
|
||||
readonly IEmailSender _emailSender;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public EmailModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public bool IsEmailConfirmed { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
async Task LoadAsync(ApplicationUser user)
|
||||
{
|
||||
string email = await _userManager.GetEmailAsync(user);
|
||||
Email = email;
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
NewEmail = email
|
||||
};
|
||||
|
||||
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser 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()
|
||||
{
|
||||
ApplicationUser 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();
|
||||
}
|
||||
|
||||
string email = await _userManager.GetEmailAsync(user);
|
||||
|
||||
if(Input.NewEmail != email)
|
||||
{
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
string code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
|
||||
|
||||
string callbackUrl = Url.Page("/Account/ConfirmEmailChange", null, new
|
||||
{
|
||||
userId, email = Input.NewEmail, code
|
||||
}, 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()
|
||||
{
|
||||
ApplicationUser 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();
|
||||
}
|
||||
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
string email = await _userManager.GetEmailAsync(user);
|
||||
string code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
string callbackUrl = Url.Page("/Account/ConfirmEmail", null, new
|
||||
{
|
||||
area = "Identity", userId, code
|
||||
}, 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();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, EmailAddress, Display(Name = "New email")]
|
||||
public string NewEmail { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
@page
|
||||
@model EnableAuthenticatorModel
|
||||
@{
|
||||
ViewData["Title"] = "Configure authenticator app";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<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="@Html.Raw(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-group">
|
||||
<label asp-for="Input.Code" class="control-label">Verification Code</label>
|
||||
<input asp-for="Input.Code" class="form-control" autocomplete="off" />
|
||||
<span asp-validation-for="Input.Code" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">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,154 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class EnableAuthenticatorModel : PageModel
|
||||
{
|
||||
const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
readonly ILogger<EnableAuthenticatorModel> _logger;
|
||||
readonly UrlEncoder _urlEncoder;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public EnableAuthenticatorModel(UserManager<ApplicationUser> userManager,
|
||||
ILogger<EnableAuthenticatorModel> logger, UrlEncoder urlEncoder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
}
|
||||
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
public string AuthenticatorUri { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser 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()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
string verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
bool 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);
|
||||
string 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)
|
||||
{
|
||||
IEnumerable<string> recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
RecoveryCodes = recoveryCodes.ToArray();
|
||||
|
||||
return RedirectToPage("./ShowRecoveryCodes");
|
||||
}
|
||||
|
||||
return RedirectToPage("./TwoFactorAuthentication");
|
||||
}
|
||||
|
||||
async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user)
|
||||
{
|
||||
// Load the authenticator key & QR code URI to display on the form
|
||||
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
|
||||
if(string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
SharedKey = FormatKey(unformattedKey);
|
||||
|
||||
string email = await _userManager.GetEmailAsync(user);
|
||||
AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey);
|
||||
}
|
||||
|
||||
string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
|
||||
while(currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
|
||||
currentPosition += 4;
|
||||
}
|
||||
|
||||
if(currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
string GenerateQrCodeUri(string email, string unformattedKey) =>
|
||||
string.Format(AuthenticatorUriFormat, _urlEncoder.Encode("Marechai"), _urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
@page
|
||||
@model ExternalLoginsModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage your external logins";
|
||||
ViewData["ActivePage"] = ManageNavPages.ExternalLogins;
|
||||
}
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
@if (Model.CurrentLogins?.Count > 0)
|
||||
{
|
||||
<h4>Registered Logins</h4>
|
||||
<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 asp-page-handler="LinkLogin" class="form-horizontal" id="link-login-form" method="post">
|
||||
<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,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ExternalLoginsModel : PageModel
|
||||
{
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ExternalLoginsModel(UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public IList<UserLoginInfo> CurrentLogins { get; set; }
|
||||
|
||||
public IList<AuthenticationScheme> OtherLogins { get; set; }
|
||||
|
||||
public bool ShowRemoveButton { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound("Unable to load user with ID 'user.Id'.");
|
||||
}
|
||||
|
||||
CurrentLogins = await _userManager.GetLoginsAsync(user);
|
||||
|
||||
OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).
|
||||
Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();
|
||||
|
||||
ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostRemoveLoginAsync(string loginProvider, string providerKey)
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound("Unable to load user with ID 'user.Id'.");
|
||||
}
|
||||
|
||||
IdentityResult 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
|
||||
string redirectUrl = Url.Page("./ExternalLogins", "LinkLoginCallback");
|
||||
|
||||
AuthenticationProperties properties =
|
||||
_signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl,
|
||||
_userManager.GetUserId(User));
|
||||
|
||||
return new ChallengeResult(provider, properties);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetLinkLoginCallbackAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound("Unable to load user with ID 'user.Id'.");
|
||||
}
|
||||
|
||||
ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync(user.Id);
|
||||
|
||||
if(info == null)
|
||||
{
|
||||
throw new
|
||||
InvalidOperationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
IdentityResult 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" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<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 class="form-group" method="post">
|
||||
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class GenerateRecoveryCodesModel : PageModel
|
||||
{
|
||||
readonly ILogger<GenerateRecoveryCodesModel> _logger;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public GenerateRecoveryCodesModel(UserManager<ApplicationUser> userManager,
|
||||
ILogger<GenerateRecoveryCodesModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
bool isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
|
||||
if(!isTwoFactorEnabled)
|
||||
{
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
throw new
|
||||
InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' because they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
bool isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
if(!isTwoFactorEnabled)
|
||||
{
|
||||
throw new
|
||||
InvalidOperationException($"Cannot generate recovery codes for user with ID '{userId}' as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
IEnumerable<string> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Marechai/Areas/Identity/Pages/Account/Manage/Index.cshtml
Normal file
29
Marechai/Areas/Identity/Pages/Account/Manage/Index.cshtml
Normal file
@@ -0,0 +1,29 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "Profile";
|
||||
ViewData["ActivePage"] = ManageNavPages.Index;
|
||||
}
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage" model="Model.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-group">
|
||||
<label asp-for="Username"></label>
|
||||
<input asp-for="Username" class="form-control" disabled />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.PhoneNumber"></label>
|
||||
<input asp-for="Input.PhoneNumber" class="form-control" />
|
||||
<span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="update-profile-button" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
98
Marechai/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
Normal file
98
Marechai/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public IndexModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
async Task LoadAsync(ApplicationUser user)
|
||||
{
|
||||
string userName = await _userManager.GetUserNameAsync(user);
|
||||
string phoneNumber = await _userManager.GetPhoneNumberAsync(user);
|
||||
|
||||
Username = userName;
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
PhoneNumber = phoneNumber
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser 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()
|
||||
{
|
||||
ApplicationUser 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();
|
||||
}
|
||||
|
||||
string phoneNumber = await _userManager.GetPhoneNumberAsync(user);
|
||||
|
||||
if(Input.PhoneNumber != phoneNumber)
|
||||
{
|
||||
IdentityResult 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();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Phone, Display(Name = "Phone number")]
|
||||
public string PhoneNumber { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public static class ManageNavPages
|
||||
{
|
||||
public static string Index => "Index";
|
||||
|
||||
public static string Email => "Email";
|
||||
|
||||
public static string ChangePassword => "ChangePassword";
|
||||
|
||||
public static string DownloadPersonalData => "DownloadPersonalData";
|
||||
|
||||
public static string DeletePersonalData => "DeletePersonalData";
|
||||
|
||||
public static string ExternalLogins => "ExternalLogins";
|
||||
|
||||
public static string PersonalData => "PersonalData";
|
||||
|
||||
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
|
||||
|
||||
public static string ChangePasswordNavClass(ViewContext viewContext) =>
|
||||
PageNavClass(viewContext, ChangePassword);
|
||||
|
||||
public static string DownloadPersonalDataNavClass(ViewContext viewContext) =>
|
||||
PageNavClass(viewContext, DownloadPersonalData);
|
||||
|
||||
public static string DeletePersonalDataNavClass(ViewContext viewContext) =>
|
||||
PageNavClass(viewContext, DeletePersonalData);
|
||||
|
||||
public static string ExternalLoginsNavClass(ViewContext viewContext) =>
|
||||
PageNavClass(viewContext, ExternalLogins);
|
||||
|
||||
public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData);
|
||||
|
||||
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) =>
|
||||
PageNavClass(viewContext, TwoFactorAuthentication);
|
||||
|
||||
static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
string activePage = viewContext.ViewData["ActivePage"] as string ??
|
||||
Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName);
|
||||
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model PersonalDataModel
|
||||
@{
|
||||
ViewData["Title"] = "Personal Data";
|
||||
ViewData["ActivePage"] = ManageNavPages.PersonalData;
|
||||
}
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<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 asp-page="DownloadPersonalData" class="form-group" id="download-data" method="post">
|
||||
<button class="btn btn-primary" type="submit">Download</button>
|
||||
</form>
|
||||
<p>
|
||||
<a asp-page="DeletePersonalData" class="btn btn-primary" id="delete">Delete</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class PersonalDataModel : PageModel
|
||||
{
|
||||
readonly ILogger<PersonalDataModel> _logger;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public PersonalDataModel(UserManager<ApplicationUser> userManager, ILogger<PersonalDataModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@page
|
||||
@model ResetAuthenticatorModel
|
||||
@{
|
||||
ViewData["Title"] = "Reset authenticator key";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<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 class="form-group" id="reset-authenticator-form" method="post">
|
||||
<button class="btn btn-danger" id="reset-authenticator-button" type="submit">Reset authenticator key</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ResetAuthenticatorModel : PageModel
|
||||
{
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly ILogger<ResetAuthenticatorModel> _logger;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ResetAuthenticatorModel(UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
ILogger<ResetAuthenticatorModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
ApplicationUser 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()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
|
||||
StatusMessage =
|
||||
"Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
|
||||
|
||||
return RedirectToPage("./EnableAuthenticator");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@page
|
||||
@model SetPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Set password";
|
||||
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||
}
|
||||
<h4>Set your password</h4>
|
||||
<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-group">
|
||||
<label asp-for="Input.NewPassword"></label>
|
||||
<input asp-for="Input.NewPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.ConfirmPassword"></label>
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Set password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class SetPasswordModel : PageModel
|
||||
{
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public SetPasswordModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
bool hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
|
||||
if(hasPassword)
|
||||
{
|
||||
return RedirectToPage("./ChangePassword");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
ApplicationUser user = await _userManager.GetUserAsync(User);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
IdentityResult addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword);
|
||||
|
||||
if(!addPasswordResult.Succeeded)
|
||||
{
|
||||
foreach(IdentityError error in addPasswordResult.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Your password has been set.";
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[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; }
|
||||
|
||||
[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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@page
|
||||
@model ShowRecoveryCodesModel
|
||||
@{
|
||||
ViewData["Title"] = "Recovery codes";
|
||||
ViewData["ActivePage"] = "TwoFactorAuthentication";
|
||||
}
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<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,25 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ShowRecoveryCodesModel : PageModel
|
||||
{
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if(RecoveryCodes == null ||
|
||||
RecoveryCodes.Length == 0)
|
||||
{
|
||||
return RedirectToPage("./TwoFactorAuthentication");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@page
|
||||
@model TwoFactorAuthenticationModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication (2FA)";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
}
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@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 class="btn btn-default" type="submit">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-default">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
|
||||
}
|
||||
<h5>Authenticator app</h5>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a asp-page="./EnableAuthenticator" class="btn btn-default" id="enable-authenticator">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-page="./EnableAuthenticator" class="btn btn-default" id="enable-authenticator">Setup authenticator app</a>
|
||||
<a asp-page="./ResetAuthenticator" class="btn btn-default" id="reset-authenticator">Reset authenticator app</a>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class TwoFactorAuthenticationModel : PageModel
|
||||
{
|
||||
const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}";
|
||||
readonly ILogger<TwoFactorAuthenticationModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public TwoFactorAuthenticationModel(UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
ILogger<TwoFactorAuthenticationModel> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool HasAuthenticator { get; set; }
|
||||
|
||||
public int RecoveryCodesLeft { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public bool Is2faEnabled { get; set; }
|
||||
|
||||
public bool IsMachineRemembered { get; set; }
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
ApplicationUser 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> OnPost()
|
||||
{
|
||||
ApplicationUser 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Marechai/Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
23
Marechai/Areas/Identity/Pages/Account/Manage/_Layout.cshtml
Normal file
@@ -0,0 +1,23 @@
|
||||
@{
|
||||
if (ViewData.TryGetValue("ParentLayout", out var parentLayout))
|
||||
{
|
||||
Layout = (string)parentLayout;
|
||||
}
|
||||
}
|
||||
<h2>Manage your account</h2>
|
||||
<div>
|
||||
<h4>Change your account settings</h4>
|
||||
<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", false)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@using Marechai.Database.Models
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@{
|
||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||
}
|
||||
<ul class="flex-column nav nav-pills">
|
||||
<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 class="nav-item" id="external-logins">
|
||||
<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,12 @@
|
||||
@model string
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model))
|
||||
{
|
||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||
<button aria-label="Close" class="close" data-dismiss="alert" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@Model
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@using Marechai.Areas.Identity.Pages.Account.Manage
|
||||
66
Marechai/Areas/Identity/Pages/Account/Register.cshtml
Normal file
66
Marechai/Areas/Identity/Pages/Account/Register.cshtml
Normal file
@@ -0,0 +1,66 @@
|
||||
@page
|
||||
@model RegisterModel
|
||||
@{
|
||||
ViewData["Title"] = "Register";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||
<h4>Create a new account.</h4>
|
||||
<hr />
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input asp-for="Input.Email" class="form-control" />
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input asp-for="Input.Password" class="form-control" />
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.ConfirmPassword"></label>
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-2">
|
||||
<section>
|
||||
<h4>Use another service to register.</h4>
|
||||
<hr />
|
||||
@{
|
||||
if ((Model.ExternalLogins?.Count ?? 0) == 0)
|
||||
{
|
||||
<div>
|
||||
<p>
|
||||
There are no external authentication services configured. See
|
||||
<a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
|
||||
for details on setting up this ASP.NET application to support logging in via external services.
|
||||
</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)
|
||||
{
|
||||
<button 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>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
116
Marechai/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
116
Marechai/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class RegisterModel : PageModel
|
||||
{
|
||||
readonly IEmailSender _emailSender;
|
||||
readonly ILogger<RegisterModel> _logger;
|
||||
readonly SignInManager<ApplicationUser> _signInManager;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public RegisterModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager,
|
||||
ILogger<RegisterModel> logger, IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public IList<AuthenticationScheme> ExternalLogins { 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 = returnUrl ?? Url.Content("~/");
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = Input.Email, Email = Input.Email
|
||||
};
|
||||
|
||||
IdentityResult result = await _userManager.CreateAsync(user, Input.Password);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
|
||||
string code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
string callbackUrl = Url.Page("/Account/ConfirmEmail", null, new
|
||||
{
|
||||
area = "Identity", userId = user.Id, code, returnUrl
|
||||
}, 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
|
||||
});
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, false);
|
||||
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
foreach(IdentityError error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, EmailAddress, Display(Name = "Email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required,
|
||||
StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",
|
||||
MinimumLength = 6), DataType(DataType.Password), Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password), Display(Name = "Confirm password"),
|
||||
Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,65 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.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;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class RegisterConfirmationModel : PageModel
|
||||
{
|
||||
readonly IEmailSender _sender;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public RegisterConfirmationModel(UserManager<ApplicationUser> userManager, IEmailSender sender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public bool DisplayConfirmAccountLink { get; set; }
|
||||
|
||||
public string EmailConfirmationUrl { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
|
||||
{
|
||||
if(email == null)
|
||||
{
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
ApplicationUser 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)
|
||||
{
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
string code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
EmailConfirmationUrl = Url.Page("/Account/ConfirmEmail", null, new
|
||||
{
|
||||
area = "Identity", userId, code, returnUrl
|
||||
}, Request.Scheme);
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model ResendEmailConfirmationModel
|
||||
@{
|
||||
ViewData["Title"] = "Resend email confirmation";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h4>Enter your email.</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input asp-for="Input.Email" class="form-control" />
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Resend</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.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;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public abstract class ResendEmailConfirmationModel : PageModel
|
||||
{
|
||||
readonly IEmailSender _emailSender;
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ResendEmailConfirmationModel(UserManager<ApplicationUser> userManager, IEmailSender emailSender)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public void OnGet() { }
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
ApplicationUser user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
string userId = await _userManager.GetUserIdAsync(user);
|
||||
string code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
string callbackUrl = Url.Page("/Account/ConfirmEmail", null, new
|
||||
{
|
||||
userId, code
|
||||
}, 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();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Marechai/Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
36
Marechai/Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
@@ -0,0 +1,36 @@
|
||||
@page
|
||||
@model ResetPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Reset password";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
<h4>Reset your password.</h4>
|
||||
<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-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input asp-for="Input.Email" class="form-control" />
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input asp-for="Input.Password" class="form-control" />
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.ConfirmPassword"></label>
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" />
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.Database.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ResetPasswordModel : PageModel
|
||||
{
|
||||
readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public ResetPasswordModel(UserManager<ApplicationUser> userManager) => _userManager = userManager;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
public IActionResult OnGet(string code = null)
|
||||
{
|
||||
if(code == null)
|
||||
{
|
||||
return BadRequest("A code must be supplied for password reset.");
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
ApplicationUser user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
|
||||
if(user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToPage("./ResetPasswordConfirmation");
|
||||
}
|
||||
|
||||
IdentityResult result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
|
||||
|
||||
if(result.Succeeded)
|
||||
{
|
||||
return RedirectToPage("./ResetPasswordConfirmation");
|
||||
}
|
||||
|
||||
foreach(IdentityError error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required,
|
||||
StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",
|
||||
MinimumLength = 6), DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[DataType(DataType.Password), Display(Name = "Confirm password"),
|
||||
Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ResetPasswordConfirmationModel : PageModel
|
||||
{
|
||||
public void OnGet() { }
|
||||
}
|
||||
}
|
||||
12
Marechai/Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
12
Marechai/Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
@@ -0,0 +1,12 @@
|
||||
@model string
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model))
|
||||
{
|
||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||
<button aria-label="Close" class="close" data-dismiss="alert" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
@Model
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@using Marechai.Areas.Identity.Pages.Account
|
||||
24
Marechai/Areas/Identity/Pages/Error.cshtml
Normal file
24
Marechai/Areas/Identity/Pages/Error.cshtml
Normal file
@@ -0,0 +1,24 @@
|
||||
@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>
|
||||
17
Marechai/Areas/Identity/Pages/Error.cshtml.cs
Normal file
17
Marechai/Areas/Identity/Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Marechai.Areas.Identity.Pages
|
||||
{
|
||||
[AllowAnonymous, ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
public string RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet() => RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,26 @@
|
||||
@using Marechai.Shared
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Microsoft.Extensions.Localization
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@inject UserManager<IdentityUser> UserManager
|
||||
@inject StringLocalizer<NavMenu> L
|
||||
@using Marechai.Database.Models
|
||||
@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">@string.Format(L["Hello {0}!"], User.Identity.Name)</a>
|
||||
<a asp-area="Identity" asp-page="/Account/Manage/Index" class="nav-link text-dark" 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">@L["Log out"]</button>
|
||||
<form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/" class="form-inline" method="post">
|
||||
<button class="btn btn-link nav-link text-dark" type="submit">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">@L["Register"]</a>
|
||||
<a asp-area="Identity" asp-page="/Account/Register" class="nav-link text-dark">Register</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">@L["Log in"]</a>
|
||||
<a asp-area="Identity" asp-page="/Account/Login" class="nav-link text-dark">Login</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
4
Marechai/Areas/Identity/Pages/_ViewImports.cshtml
Normal file
4
Marechai/Areas/Identity/Pages/_ViewImports.cshtml
Normal file
@@ -0,0 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Marechai.Areas.Identity
|
||||
@using Marechai.Areas.Identity.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
Marechai/Areas/Identity/Pages/_ViewStart.cshtml
Normal file
3
Marechai/Areas/Identity/Pages/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "/Pages/Shared/_Layout.cshtml";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<Version>3.0.99.1018</Version>
|
||||
<Version>3.0.99.1036</Version>
|
||||
<Company>Canary Islands Computer Museum</Company>
|
||||
<Copyright>Copyright © 2003-2020 Natalia Portillo</Copyright>
|
||||
<Product>Canary Islands Computer Museum Website</Product>
|
||||
<ApplicationVersion>$(Version)</ApplicationVersion>
|
||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||
<RootNamespace>Marechai</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@@ -37,61 +36,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marechai.Database\Marechai.Database.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="App.razor">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</Content>
|
||||
<Content Include="Areas\Identity\Pages\Account\LogOut.cshtml" />
|
||||
<Content Include="Areas\Identity\Pages\Shared\_LoginPartial.cshtml" />
|
||||
<Content Include="Pages\**\*.razor" />
|
||||
<Content Include="Pages\_Host.cshtml" />
|
||||
<Content Include="Shared\LoginDisplay.razor" />
|
||||
<Content Include="Shared\MainLayout.razor" />
|
||||
<Content Include="Shared\NavMenu.razor" />
|
||||
<Content Include="wwwroot\css\open-iconic\FONT-LICENSE">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\font\fonts\open-iconic.eot">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\font\fonts\open-iconic.otf">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\font\fonts\open-iconic.svg">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\font\fonts\open-iconic.ttf">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\font\fonts\open-iconic.woff">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\ICON-LICENSE">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\open-iconic\README.md">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="wwwroot\css\site.css">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="_Imports.razor">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources\**\*.es.resx" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/******************************************************************************
|
||||
/******************************************************************************
|
||||
// MARECHAI: Master repository of computing history artifacts information
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
@@ -48,7 +48,6 @@ using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Marechai
|
||||
{
|
||||
// DO NOT MAKE STATIC
|
||||
public class Startup
|
||||
{
|
||||
readonly CultureInfo[] supportedCultures =
|
||||
@@ -58,9 +57,10 @@ namespace Marechai
|
||||
|
||||
public Startup(IConfiguration configuration) => Configuration = configuration;
|
||||
|
||||
IConfiguration Configuration { get; }
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddBlazorise(options => options.ChangeTextOnKeyPress = true).AddBootstrapProviders().
|
||||
@@ -78,7 +78,8 @@ namespace Marechai
|
||||
services.AddServerSideBlazor();
|
||||
|
||||
services.
|
||||
AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
|
||||
AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>
|
||||
>();
|
||||
|
||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user