添加项目文件。
This commit is contained in:
11
TightWiki/Areas/Identity/Pages/Account/AccessDenied.cshtml
Normal file
11
TightWiki/Areas/Identity/Pages/Account/AccessDenied.cshtml
Normal file
@@ -0,0 +1,11 @@
|
||||
@page
|
||||
@model AccessDeniedModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Access denied.
|
||||
</h3>
|
||||
|
||||
<p class="text-danger">You do not have access to this resource.</p>
|
||||
@@ -0,0 +1,28 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class AccessDeniedModel : PageModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
public AccessDeniedModel(SignInManager<IdentityUser> signInManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
11
TightWiki/Areas/Identity/Pages/Account/ConfirmEmail.cshtml
Normal file
11
TightWiki/Areas/Identity/Pages/Account/ConfirmEmail.cshtml
Normal file
@@ -0,0 +1,11 @@
|
||||
@page
|
||||
@model ConfirmEmailModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Confirm your email address.
|
||||
</h3>
|
||||
|
||||
<p><partial name="_StatusMessage" model="Model.StatusMessage" /></p>
|
||||
@@ -0,0 +1,48 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Text;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ConfirmEmailModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
|
||||
public ConfirmEmailModel(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
public async Task<IActionResult> OnGetAsync(string userId, string code)
|
||||
{
|
||||
if (userId == null || code == null)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Index");
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@page
|
||||
@model ConfirmEmailChangeModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Confirm email change
|
||||
</h3>
|
||||
|
||||
<p><partial name="_StatusMessage" model="Model.StatusMessage" /></p>
|
||||
@@ -0,0 +1,67 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Text;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ConfirmEmailChangeModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public ConfirmEmailChangeModel(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string userId, string email, string code)
|
||||
{
|
||||
if (userId == null || email == null || code == null)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Index");
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{userId}'.");
|
||||
}
|
||||
|
||||
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
|
||||
var result = await _userManager.ChangeEmailAsync(user, email, code);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
StatusMessage = "Error changing email.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
// In our UI email and user name are one and the same, so when we update the email
|
||||
// we need to update the user name.
|
||||
var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
|
||||
if (!setUserNameResult.Succeeded)
|
||||
{
|
||||
StatusMessage = "Error changing user name.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Thank you for confirming your email change.";
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
TightWiki/Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
37
TightWiki/Areas/Identity/Pages/Account/ExternalLogin.cshtml
Normal file
@@ -0,0 +1,37 @@
|
||||
@page
|
||||
@using System.Text.Encodings.Web
|
||||
@model ExternalLoginModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Register
|
||||
</h3>
|
||||
|
||||
<p>Associate your @Model.ProviderDisplayName account.</p>
|
||||
<hr />
|
||||
|
||||
<p id="external-login-description" class="text-info">
|
||||
You've successfully authenticated with <strong>@Model.ProviderDisplayName</strong>.
|
||||
Please enter an email address for this site below and click the Register button to finish
|
||||
logging in.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form asp-page-handler="Confirmation" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl)" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email."/>
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
239
TightWiki/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
239
TightWiki/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using TightWiki.Library.Interfaces;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ExternalLoginModel : PageModelBase
|
||||
{
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly IUserStore<IdentityUser> _userStore;
|
||||
private readonly IUserEmailStore<IdentityUser> _emailStore;
|
||||
private readonly IWikiEmailSender _emailSender;
|
||||
private readonly ILogger<ExternalLoginModel> _logger;
|
||||
|
||||
public ExternalLoginModel(
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
IUserStore<IdentityUser> userStore,
|
||||
ILogger<ExternalLoginModel> logger,
|
||||
IWikiEmailSender emailSender)
|
||||
: base(signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_userStore = userStore;
|
||||
_emailStore = GetEmailStore();
|
||||
_logger = logger;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ProviderDisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public IActionResult OnGet() => RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Login");
|
||||
|
||||
public IActionResult OnPost(string provider, string returnUrl = null)
|
||||
{
|
||||
// Request a redirect to the external login provider.
|
||||
var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return new ChallengeResult(provider, properties);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
|
||||
{
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
if (remoteError != null)
|
||||
{
|
||||
ErrorMessage = $"Error from external provider: {remoteError}";
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Login", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
ErrorMessage = "Error loading external login information.";
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Login", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login.
|
||||
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not have an account, then ask the user to create an account.
|
||||
ReturnUrl = returnUrl;
|
||||
ProviderDisplayName = info.ProviderDisplayName;
|
||||
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
Email = info.Principal.FindFirstValue(ClaimTypes.Email)
|
||||
};
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
|
||||
{
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
// Get the information about the user from the external login provider
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
ErrorMessage = "Error loading external login information during confirmation.";
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Login", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = CreateUser();
|
||||
|
||||
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
|
||||
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
|
||||
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (result.Succeeded == false)
|
||||
{
|
||||
result = await _userManager.AddLoginAsync(user, info);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var encodedCode = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = encodedCode },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
var emailTemplate = new StringBuilder(ConfigurationRepository.Get<string>("Membership", "Template: Account Verification Email"));
|
||||
var basicConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Basic");
|
||||
var siteName = basicConfig.Value<string>("Name");
|
||||
var address = basicConfig.Value<string>("Address");
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(Guid.Parse(userId));
|
||||
|
||||
var emailSubject = "Confirm your email";
|
||||
emailTemplate.Replace("##SUBJECT##", emailSubject);
|
||||
emailTemplate.Replace("##ACCOUNTCOUNTRY##", profile.Country);
|
||||
emailTemplate.Replace("##ACCOUNTTIMEZONE##", profile.TimeZone);
|
||||
emailTemplate.Replace("##ACCOUNTLANGUAGE##", profile.Language);
|
||||
emailTemplate.Replace("##ACCOUNTEMAIL##", profile.EmailAddress);
|
||||
emailTemplate.Replace("##ACCOUNTNAME##", profile.AccountName);
|
||||
emailTemplate.Replace("##PERSONNAME##", $"{profile.FirstName} {profile.LastName}");
|
||||
emailTemplate.Replace("##CODE##", code);
|
||||
emailTemplate.Replace("##USERID##", userId);
|
||||
emailTemplate.Replace("##SITENAME##", siteName);
|
||||
emailTemplate.Replace("##SITEADDRESS##", address);
|
||||
emailTemplate.Replace("##CALLBACKURL##", HtmlEncoder.Default.Encode(callbackUrl));
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.Email, emailSubject, emailTemplate.ToString());
|
||||
|
||||
// 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($"{GlobalConfiguration.BasePath}/Identity/RegisterConfirmation", new { Email = Input.Email });
|
||||
}
|
||||
|
||||
await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
}
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
ProviderDisplayName = info.ProviderDisplayName;
|
||||
ReturnUrl = returnUrl;
|
||||
return Page();
|
||||
}
|
||||
|
||||
private IdentityUser CreateUser()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Activator.CreateInstance<IdentityUser>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException($"Can't create an instance of '{nameof(IdentityUser)}'. " +
|
||||
$"Ensure that '{nameof(IdentityUser)}' is not an abstract class and has a parameterless constructor, or alternatively " +
|
||||
$"override the external login page in /Areas/Identity/Pages/Account/ExternalLogin.cshtml");
|
||||
}
|
||||
}
|
||||
|
||||
private IUserEmailStore<IdentityUser> GetEmailStore()
|
||||
{
|
||||
if (!_userManager.SupportsUserEmail)
|
||||
{
|
||||
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||
}
|
||||
return (IUserEmailStore<IdentityUser>)_userStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@page
|
||||
@using System.Text.Encodings.Web
|
||||
@model ExternalLoginSupplementalModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Signup. Almost there...
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
You have successfully signed in with the external provider but we still need a bit of information.<br /><br />
|
||||
</p>
|
||||
|
||||
<form asp-page-handler="Confirmation" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl ?? "")" method="post">
|
||||
<div class="container">
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.AccountName" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.AccountName)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.AccountName, new { @class = "form-control", placeholder = "required" })
|
||||
<small class="form-text text-muted">This account name will be visible publicly.</small>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.AccountName)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.FirstName" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.FirstName)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.FirstName, new { @class = "form-control", placeholder = "not required" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.FirstName)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.LastName" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.LastName)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.LastName, new { @class = "form-control", placeholder = "not required" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.LastName)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.Country" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.Country)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
<select name="Input.Country" id="Input.Country" class="form-control">
|
||||
<option value="" style="color:#ccc !important;">Select a country</option>
|
||||
@foreach (var item in Model.Input.Countries)
|
||||
{
|
||||
<option value="@item.Value" selected=@(Model.Input.Country == item.Value ? "selected" : null)>
|
||||
@item.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.Country)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.Language" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.Language)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
<select name="Input.Language" id="Input.Language" class="form-control">
|
||||
<option value="" style="color:#ccc !important;">Select a language</option>
|
||||
@foreach (var item in Model.Input.Languages)
|
||||
{
|
||||
<option value="@item.Value" selected=@(Model.Input.Language == item.Value ? "selected" : null)>
|
||||
@item.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.Language)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.TimeZone" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.TimeZone)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
<select name="Input.TimeZone" id="Input.TimeZone" class="form-control">
|
||||
<option value="" style="color:#ccc !important;">Select a time-zone</option>
|
||||
@foreach (var item in Model.Input.TimeZones)
|
||||
{
|
||||
<option value="@item.Value" selected=@(Model.Input.TimeZone == item.Value ? "selected" : null)>
|
||||
@item.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.TimeZone)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<div class="col-sm-10 offset-sm-2">
|
||||
<button type="submit" class="btn btn-primary rounded-0">Complete Signup!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,165 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NTDLS.Helpers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ExternalLoginSupplementalModel : PageModelBase
|
||||
{
|
||||
[BindProperty]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
private UserManager<IdentityUser> _userManager;
|
||||
|
||||
public ExternalLoginSupplementalModel(SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager, IUserStore<IdentityUser> userStore)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = new InputModel();
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
public List<TimeZoneItem> TimeZones { get; set; } = new();
|
||||
public List<CountryItem> Countries { get; set; } = new();
|
||||
public List<LanguageItem> Languages { get; set; } = new();
|
||||
|
||||
|
||||
[Display(Name = "Account Name")]
|
||||
[Required(ErrorMessage = "Account Name is required")]
|
||||
public string AccountName { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "First Name")]
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
[Display(Name = "Last Name")]
|
||||
public string? LastName { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Time-Zone")]
|
||||
[Required(ErrorMessage = "TimeZone is required")]
|
||||
public string TimeZone { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Country")]
|
||||
[Required(ErrorMessage = "Country is required")]
|
||||
public string Country { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Language")]
|
||||
[Required(ErrorMessage = "Language is required")]
|
||||
public string Language { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
ReturnUrl ??= Url.Content("~/");
|
||||
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
|
||||
PopulateDefaults();
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
private void PopulateDefaults()
|
||||
{
|
||||
Input.TimeZones = TimeZoneItem.GetAll();
|
||||
Input.Countries = CountryItem.GetAll();
|
||||
Input.Languages = LanguageItem.GetAll();
|
||||
|
||||
var membershipConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Membership");
|
||||
|
||||
if (string.IsNullOrEmpty(Input.TimeZone))
|
||||
Input.TimeZone = membershipConfig.Value<string>("Default TimeZone").EnsureNotNull();
|
||||
|
||||
if (string.IsNullOrEmpty(Input.Country))
|
||||
Input.Country = membershipConfig.Value<string>("Default Country").EnsureNotNull();
|
||||
|
||||
if (string.IsNullOrEmpty(Input.Language))
|
||||
Input.Language = membershipConfig.Value<string>("Default Language").EnsureNotNull();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
|
||||
PopulateDefaults();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Input.AccountName))
|
||||
{
|
||||
ModelState.AddModelError("Input.AccountName", "Account Name is required.");
|
||||
return Page();
|
||||
}
|
||||
else if (UsersRepository.DoesProfileAccountExist(Input.AccountName))
|
||||
{
|
||||
ModelState.AddModelError("Input.AccountName", "Account Name is already in use.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
var info = await SignInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
return NotifyOfError("An error occurred retrieving user information from the external provider.");
|
||||
}
|
||||
|
||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email).EnsureNotNull();
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
return NotifyOfError("The email address was not supplied by the external provider.");
|
||||
}
|
||||
|
||||
var user = new IdentityUser { UserName = email, Email = email };
|
||||
var result = await _userManager.CreateAsync(user);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return NotifyOfError("An error occurred while creating the user.");
|
||||
}
|
||||
|
||||
result = await _userManager.AddLoginAsync(user, info);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return NotifyOfError("An error occurred while adding the login.");
|
||||
}
|
||||
|
||||
UsersRepository.CreateProfile(Guid.Parse(user.Id), Input.AccountName);
|
||||
|
||||
var membershipConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Membership");
|
||||
var claimsToAdd = new List<Claim>
|
||||
{
|
||||
new (ClaimTypes.Role, membershipConfig.Value<string>("Default Signup Role").EnsureNotNull()),
|
||||
new ("timezone", Input.TimeZone),
|
||||
new (ClaimTypes.Country, Input.Country),
|
||||
new ("language", Input.Language),
|
||||
new ("firstname", Input.FirstName ?? ""),
|
||||
new ("lastname", Input.LastName ?? ""),
|
||||
};
|
||||
|
||||
SecurityRepository.UpsertUserClaims(_userManager, user, claimsToAdd);
|
||||
|
||||
await SignInManager.SignInAsync(user, isPersistent: false);
|
||||
|
||||
if (string.IsNullOrEmpty(ReturnUrl))
|
||||
{
|
||||
return LocalRedirect($"{GlobalConfiguration.BasePath}/");
|
||||
}
|
||||
return LocalRedirect(ReturnUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
TightWiki/Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
30
TightWiki/Areas/Identity/Pages/Account/ForgotPassword.cshtml
Normal file
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model ForgotPasswordModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Forgot your password?
|
||||
</h3>
|
||||
|
||||
<p>Enter your email.</p>
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using TightWiki.Library.Interfaces;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
|
||||
public class ForgotPasswordModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly IWikiEmailSender _emailSender;
|
||||
|
||||
public ForgotPasswordModel(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IWikiEmailSender emailSender)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var encodedCode = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ResetPassword",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", encodedCode },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
var emailTemplate = new StringBuilder(ConfigurationRepository.Get<string>("Membership", "Template: Reset Password Email"));
|
||||
var basicConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Basic");
|
||||
var siteName = basicConfig.Value<string>("Name");
|
||||
var address = basicConfig.Value<string>("Address");
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(Guid.Parse(user.Id));
|
||||
|
||||
var emailSubject = "Reset password";
|
||||
emailTemplate.Replace("##SUBJECT##", emailSubject);
|
||||
emailTemplate.Replace("##ACCOUNTCOUNTRY##", profile.Country);
|
||||
emailTemplate.Replace("##ACCOUNTTIMEZONE##", profile.TimeZone);
|
||||
emailTemplate.Replace("##ACCOUNTLANGUAGE##", profile.Language);
|
||||
emailTemplate.Replace("##ACCOUNTEMAIL##", profile.EmailAddress);
|
||||
emailTemplate.Replace("##ACCOUNTNAME##", profile.AccountName);
|
||||
emailTemplate.Replace("##PERSONNAME##", $"{profile.FirstName} {profile.LastName}");
|
||||
emailTemplate.Replace("##CODE##", code);
|
||||
emailTemplate.Replace("##USERID##", user.Id);
|
||||
emailTemplate.Replace("##SITENAME##", siteName);
|
||||
emailTemplate.Replace("##SITEADDRESS##", address);
|
||||
emailTemplate.Replace("##CALLBACKURL##", HtmlEncoder.Default.Encode(callbackUrl));
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.Email, emailSubject, emailTemplate.ToString());
|
||||
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ForgotPasswordConfirmation");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@page
|
||||
@model ForgotPasswordConfirmation
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Forgot password confirmation
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Please check your email to reset your password.
|
||||
</p>
|
||||
@@ -0,0 +1,30 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class ForgotPasswordConfirmation : PageModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
public ForgotPasswordConfirmation(SignInManager<IdentityUser> signInManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
11
TightWiki/Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
11
TightWiki/Areas/Identity/Pages/Account/Lockout.cshtml
Normal file
@@ -0,0 +1,11 @@
|
||||
@page
|
||||
@model LockoutModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Locked out
|
||||
</h3>
|
||||
|
||||
<p>This account has been locked out, please try again later.</p>
|
||||
30
TightWiki/Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
30
TightWiki/Areas/Identity/Pages/Account/Lockout.cshtml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class LockoutModel : PageModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
public LockoutModel(SignInManager<IdentityUser> signInManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
88
TightWiki/Areas/Identity/Pages/Account/Login.cshtml
Normal file
88
TightWiki/Areas/Identity/Pages/Account/Login.cshtml
Normal file
@@ -0,0 +1,88 @@
|
||||
@page
|
||||
@using System.Text.Encodings.Web
|
||||
@using TightWiki.Models
|
||||
@model LoginModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-lg-4">
|
||||
<section>
|
||||
<form id="account" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label">Email</label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
|
||||
<label asp-for="Input.Password" class="form-label">Password</label>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="checkbox mb-3">
|
||||
<label asp-for="Input.RememberMe" class="form-label">
|
||||
<input class="form-check-input" asp-for="Input.RememberMe" />
|
||||
@Html.DisplayNameFor(m => m.Input.RememberMe)
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
<br />
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
|
||||
<br />
|
||||
@if (TightWiki.Models.GlobalConfiguration.AllowSignup == true)
|
||||
{
|
||||
<a asp-page="./Register" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl)">Register as a new user</a>
|
||||
<br />
|
||||
}
|
||||
<a id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
@{
|
||||
if ((Model.ExternalLogins?.Count ?? 0) > 0)
|
||||
{
|
||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl)" method="post" class="form-horizontal">
|
||||
<div>
|
||||
<p>
|
||||
@foreach (var provider in Model.ExternalLogins!)
|
||||
{
|
||||
if (provider.Name == "Google")
|
||||
{
|
||||
<button type="submit" class="btn w-100 mb-3 d-flex align-items-center" name="provider" value="@provider.Name" title="Log in using your Google account">
|
||||
<img src="@GlobalConfiguration.BasePath/images/external/google-signin.svg" alt="Google" class="me-2" />
|
||||
</button>
|
||||
}
|
||||
else if (provider.Name == "Microsoft")
|
||||
{
|
||||
<button type="submit" class="btn w-100 mb-3 d-flex align-items-center" name="provider" value="@provider.Name" title="Log in using your Microsoft account">
|
||||
<img src="@GlobalConfiguration.BasePath/images/external/microsoft-signin.svg" alt="Microsoft" class="me-2" />
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="submit" class="btn btn-primary w-100 mb-3" 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" />
|
||||
}
|
||||
134
TightWiki/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
134
TightWiki/Areas/Identity/Pages/Account/Login.cshtml.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LoginModel : PageModelBase
|
||||
{
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly ILogger<LoginModel> _logger;
|
||||
|
||||
public LoginModel(SignInManager<IdentityUser> signInManager, ILogger<LoginModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<AuthenticationScheme> ExternalLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
public async Task OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ErrorMessage);
|
||||
}
|
||||
|
||||
returnUrl ??= Url.Content("~/");
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
||||
{
|
||||
returnUrl ??= Url.Content("~/");
|
||||
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User logged in.");
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
TightWiki/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
44
TightWiki/Areas/Identity/Pages/Account/LoginWith2fa.cshtml
Normal file
@@ -0,0 +1,44 @@
|
||||
@page
|
||||
@using System.Text.Encodings.Web
|
||||
@model LoginWith2faModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Two-factor authentication
|
||||
</h3>
|
||||
|
||||
<p>Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl)">
|
||||
<input asp-for="RememberMe" type="hidden" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.TwoFactorCode" class="form-control" autocomplete="off" />
|
||||
<label asp-for="Input.TwoFactorCode" class="form-label"></label>
|
||||
<span asp-validation-for="Input.TwoFactorCode" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="checkbox mb-3">
|
||||
<label asp-for="Input.RememberMachine" class="form-label">
|
||||
<input asp-for="Input.RememberMachine" />
|
||||
@Html.DisplayNameFor(m => m.Input.RememberMachine)
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Don't have access to your authenticator device? You can
|
||||
<a id="recovery-code-login" asp-page="./LoginWithRecoveryCode" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl)">log in with a recovery code</a>.
|
||||
</p>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
127
TightWiki/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
127
TightWiki/Areas/Identity/Pages/Account/LoginWith2fa.cshtml.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LoginWith2faModel : PageModelBase
|
||||
{
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly ILogger<LoginWith2faModel> _logger;
|
||||
|
||||
public LoginWith2faModel(
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
ILogger<LoginWith2faModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool RememberMe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Authenticator code")]
|
||||
public string TwoFactorCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
RememberMe = rememberMe;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(bool rememberMe, string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
returnUrl = returnUrl ?? Url.Content("~/");
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine);
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id);
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@page
|
||||
@model LoginWithRecoveryCodeModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Recovery code verification
|
||||
</h3>
|
||||
|
||||
<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>
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.RecoveryCode" class="form-control" autocomplete="off" placeholder="RecoveryCode" />
|
||||
<label asp-for="Input.RecoveryCode" class="form-label"></label>
|
||||
<span asp-validation-for="Input.RecoveryCode" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LoginWithRecoveryCodeModel : PageModelBase
|
||||
{
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly ILogger<LoginWithRecoveryCodeModel> _logger;
|
||||
|
||||
public LoginWithRecoveryCodeModel(
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
ILogger<LoginWithRecoveryCodeModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
[Required]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Recovery Code")]
|
||||
public string RecoveryCode { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string returnUrl = null)
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id);
|
||||
return LocalRedirect(returnUrl ?? Url.Content("~/"));
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User account locked out.");
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/Lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
TightWiki/Areas/Identity/Pages/Account/Logout.cshtml
Normal file
29
TightWiki/Areas/Identity/Pages/Account/Logout.cshtml
Normal file
@@ -0,0 +1,29 @@
|
||||
@page
|
||||
@model LogoutModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Logout
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Logging out will make sure that others cant use this session to make changes to the wiki. You'll need to log back in before making changes.<br /><br />
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
<header>
|
||||
@{
|
||||
if (User.Identity?.IsAuthenticated ?? false)
|
||||
{
|
||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
||||
<button type="submit" class="btn btn-primary rounded-0">Logout</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>You have successfully logged out of the application.</p>
|
||||
}
|
||||
}
|
||||
</header>
|
||||
107
TightWiki/Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
107
TightWiki/Areas/Identity/Pages/Account/Logout.cshtml.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class LogoutModel : PageModelBase
|
||||
{
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly ILogger<LogoutModel> _logger;
|
||||
private readonly IAuthenticationSchemeProvider _schemeProvider;
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
|
||||
public LogoutModel(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, ILogger<LogoutModel> logger, IAuthenticationSchemeProvider schemeProvider)
|
||||
: base(signInManager)
|
||||
{
|
||||
_schemeProvider = schemeProvider;
|
||||
_signInManager = signInManager;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost(string returnUrl = null)
|
||||
{
|
||||
await _signInManager.SignOutAsync();
|
||||
|
||||
/*
|
||||
var allSchemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
foreach (var scheme in allSchemes)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HttpContext.SignOutAsync(scheme.Name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
// Explicitly delete the cookie with the correct path.
|
||||
Response.Cookies.Delete(".AspNetCore.Identity.Application", new CookieOptions
|
||||
{
|
||||
Path = "/TightWiki", // Must match the cookie's path.
|
||||
HttpOnly = true,
|
||||
Secure = true // Use this if the cookie is secure.
|
||||
});
|
||||
*/
|
||||
|
||||
/*
|
||||
if (HttpContext.Request.Cookies.Count > 0)
|
||||
{
|
||||
var siteCookies = HttpContext.Request.Cookies
|
||||
.Where(c => c.Key.Contains(".AspNetCore.")
|
||||
|| c.Key.Contains("Microsoft.Authentication"));
|
||||
foreach (var cookie in siteCookies)
|
||||
{
|
||||
Response.Cookies.Delete(cookie.Key);
|
||||
}
|
||||
}
|
||||
|
||||
await HttpContext.SignOutAsync(
|
||||
user.AuthenticationScheme);
|
||||
|
||||
HttpContext.Session.Clear();
|
||||
|
||||
await _signInManager.SignOutAsync();
|
||||
await HttpContext.SignOutAsync();
|
||||
|
||||
var allSchemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
foreach (var scheme in allSchemes)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HttpContext.SignOutAsync(scheme.Name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cookie in Request.Cookies.Keys)
|
||||
{
|
||||
Response.Cookies.Delete(cookie);
|
||||
}
|
||||
*/
|
||||
|
||||
_logger.LogInformation("User logged out.");
|
||||
if (returnUrl != null)
|
||||
{
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This needs to be a redirect so that the browser performs a new
|
||||
// request and the identity for the user gets updated.
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
@page
|
||||
@model ChangePasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Change password";
|
||||
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="change-password-form" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.OldPassword" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Please enter your old password." />
|
||||
<label asp-for="Input.OldPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.OldPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your new password." />
|
||||
<label asp-for="Input.NewPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your new password."/>
|
||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ChangePasswordModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly ILogger<ChangePasswordModel> _logger;
|
||||
|
||||
public ChangePasswordModel(
|
||||
UserManager<IdentityUser> userManager,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
ILogger<ChangePasswordModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Current password")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if (!hasPassword)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/SetPassword");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(Guid.Parse(user.Id));
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load profile with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
|
||||
if (!changePasswordResult.Succeeded)
|
||||
{
|
||||
foreach (var error in changePasswordResult.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (profile.AccountName.Equals(Constants.DEFAULTACCOUNT, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
UsersRepository.SetAdminPasswordIsChanged();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
_logger.LogInformation("User changed their password successfully.");
|
||||
StatusMessage = "Your password has been changed.";
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model Disable2faModel
|
||||
@{
|
||||
ViewData["Title"] = "Disable two-factor authentication (2FA)";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<strong>This action only disables 2FA.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
|
||||
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form method="post">
|
||||
<button class="btn btn-danger" type="submit">Disable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,67 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class Disable2faModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly ILogger<Disable2faModel> _logger;
|
||||
|
||||
public Disable2faModel(SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
ILogger<Disable2faModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!await _userManager.GetTwoFactorEnabledAsync(user))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot disable 2FA for user as it's not currently enabled.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected error occurred disabling 2FA.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User));
|
||||
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/TwoFactorAuthentication");
|
||||
}
|
||||
}
|
||||
}
|
||||
45
TightWiki/Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
45
TightWiki/Areas/Identity/Pages/Account/Manage/Email.cshtml
Normal file
@@ -0,0 +1,45 @@
|
||||
@page
|
||||
@model EmailModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage Email";
|
||||
ViewData["ActivePage"] = ManageNavPages.Email;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="email-form" method="post">
|
||||
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
|
||||
@if (Model.IsEmailConfirmed)
|
||||
{
|
||||
<div class="form-floating mb-3 input-group">
|
||||
<input asp-for="Email" class="form-control" placeholder="Please enter your email." disabled />
|
||||
<div class="input-group-append">
|
||||
<span class="h-100 input-group-text text-success font-weight-bold">✓</span>
|
||||
</div>
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Email" class="form-control" placeholder="Please enter your email." disabled />
|
||||
<label asp-for="Email" class="form-label"></label>
|
||||
<button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
|
||||
</div>
|
||||
}
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.NewEmail" class="form-control" autocomplete="email" aria-required="true" placeholder="Please enter new email." />
|
||||
<label asp-for="Input.NewEmail" class="form-label"></label>
|
||||
<span asp-validation-for="Input.NewEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<button id="change-email-button" type="submit" asp-page-handler="ChangeEmail" class="w-100 btn btn-lg btn-primary">Change email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
189
TightWiki/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
189
TightWiki/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using TightWiki.Library.Interfaces;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class EmailModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly IWikiEmailSender _emailSender;
|
||||
|
||||
public EmailModel(
|
||||
UserManager<IdentityUser> userManager,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
IWikiEmailSender emailSender)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool IsEmailConfirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "New email")]
|
||||
public string NewEmail { get; set; }
|
||||
}
|
||||
|
||||
private async Task LoadAsync(IdentityUser user)
|
||||
{
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
Email = email;
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
NewEmail = email,
|
||||
};
|
||||
|
||||
IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostChangeEmailAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
if (Input.NewEmail != email)
|
||||
{
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail);
|
||||
var encodedCode = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmailChange",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, email = Input.NewEmail, code = encodedCode },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
var emailTemplate = new StringBuilder(ConfigurationRepository.Get<string>("Membership", "Template: Account Verification Email"));
|
||||
var basicConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Basic");
|
||||
var siteName = basicConfig.Value<string>("Name");
|
||||
var address = basicConfig.Value<string>("Address");
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(Guid.Parse(userId));
|
||||
|
||||
var emailSubject = "Confirm your email";
|
||||
emailTemplate.Replace("##SUBJECT##", emailSubject);
|
||||
emailTemplate.Replace("##ACCOUNTCOUNTRY##", profile.Country);
|
||||
emailTemplate.Replace("##ACCOUNTTIMEZONE##", profile.TimeZone);
|
||||
emailTemplate.Replace("##ACCOUNTLANGUAGE##", profile.Language);
|
||||
emailTemplate.Replace("##ACCOUNTEMAIL##", profile.EmailAddress);
|
||||
emailTemplate.Replace("##ACCOUNTNAME##", profile.AccountName);
|
||||
emailTemplate.Replace("##PERSONNAME##", $"{profile.FirstName} {profile.LastName}");
|
||||
emailTemplate.Replace("##CODE##", code);
|
||||
emailTemplate.Replace("##USERID##", userId);
|
||||
emailTemplate.Replace("##SITENAME##", siteName);
|
||||
emailTemplate.Replace("##SITEADDRESS##", address);
|
||||
emailTemplate.Replace("##CALLBACKURL##", HtmlEncoder.Default.Encode(callbackUrl));
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.NewEmail, emailSubject, emailTemplate.ToString());
|
||||
StatusMessage = "Confirmation link to change email sent. Please check your email.";
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
StatusMessage = "Your email is unchanged.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = code },
|
||||
protocol: Request.Scheme);
|
||||
await _emailSender.SendEmailAsync(
|
||||
email,
|
||||
"Confirm your email",
|
||||
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||
|
||||
StatusMessage = "Verification email sent. Please check your email.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@page
|
||||
@model EnableAuthenticatorModel
|
||||
@{
|
||||
ViewData["Title"] = "Configure authenticator app";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div>
|
||||
<p>To use an authenticator app go through the following steps:</p>
|
||||
<ol class="list">
|
||||
<li>
|
||||
<p>
|
||||
Download a two-factor authenticator app like Microsoft Authenticator for
|
||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825072">Android</a> and
|
||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825073">iOS</a> or
|
||||
Google Authenticator for
|
||||
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">Android</a> and
|
||||
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
|
||||
<div id="qrCode"></div>
|
||||
<div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
|
||||
with a unique code. Enter the code in the confirmation box below.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="send-code" method="post">
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Code" class="form-control" autocomplete="off" placeholder="Please enter the code."/>
|
||||
<label asp-for="Input.Code" class="control-label form-label">Verification Code</label>
|
||||
<span asp-validation-for="Input.Code" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Verify</button>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class EnableAuthenticatorModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly ILogger<EnableAuthenticatorModel> _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
|
||||
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
public EnableAuthenticatorModel(SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
ILogger<EnableAuthenticatorModel> logger,
|
||||
UrlEncoder urlEncoder)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public string AuthenticatorUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Verification Code")]
|
||||
public string Code { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Strip spaces and hyphens
|
||||
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError("Input.Code", "Verification code is invalid.");
|
||||
await LoadSharedKeyAndQrCodeUriAsync(user);
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
_logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
|
||||
|
||||
StatusMessage = "Your authenticator app has been verified.";
|
||||
|
||||
if (await _userManager.CountRecoveryCodesAsync(user) == 0)
|
||||
{
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
RecoveryCodes = recoveryCodes.ToArray();
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ShowRecoveryCodes");
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/TwoFactorAuthentication");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSharedKeyAndQrCodeUriAsync(IdentityUser user)
|
||||
{
|
||||
// Load the authenticator key & QR code URI to display on the form
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
SharedKey = FormatKey(unformattedKey);
|
||||
|
||||
var email = await _userManager.GetEmailAsync(user);
|
||||
AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey);
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
|
||||
currentPosition += 4;
|
||||
}
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.AsSpan(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
AuthenticatorUriFormat,
|
||||
_urlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
@page
|
||||
@model ExternalLoginsModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage your external logins";
|
||||
ViewData["ActivePage"] = ManageNavPages.ExternalLogins;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
@if (Model.CurrentLogins?.Count > 0)
|
||||
{
|
||||
<h3>Registered Logins</h3>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@foreach (var login in Model.CurrentLogins)
|
||||
{
|
||||
<tr>
|
||||
<td id="@($"login-provider-{login.LoginProvider}")">@login.ProviderDisplayName</td>
|
||||
<td>
|
||||
@if (Model.ShowRemoveButton)
|
||||
{
|
||||
<form id="@($"remove-login-{login.LoginProvider}")" asp-page-handler="RemoveLogin" method="post">
|
||||
<div>
|
||||
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" />
|
||||
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" />
|
||||
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
@:
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
@if (Model.OtherLogins?.Count > 0)
|
||||
{
|
||||
<form id="link-login-form" asp-page-handler="LinkLogin" method="post" class="form-horizontal">
|
||||
<div id="socialLoginList">
|
||||
<p>
|
||||
@foreach (var provider in Model.OtherLogins)
|
||||
{
|
||||
<button id="@($"link-login-button-{provider.Name}")" type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ExternalLoginsModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly IUserStore<IdentityUser> _userStore;
|
||||
|
||||
public ExternalLoginsModel(
|
||||
UserManager<IdentityUser> userManager,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
IUserStore<IdentityUser> userStore)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_userStore = userStore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<UserLoginInfo> CurrentLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IList<AuthenticationScheme> OtherLogins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool ShowRemoveButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
CurrentLogins = await _userManager.GetLoginsAsync(user);
|
||||
OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
|
||||
.Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
|
||||
.ToList();
|
||||
|
||||
string passwordHash = null;
|
||||
if (_userStore is IUserPasswordStore<IdentityUser> userPasswordStore)
|
||||
{
|
||||
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
ShowRemoveButton = passwordHash != null || CurrentLogins.Count > 1;
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostRemoveLoginAsync(string loginProvider, string providerKey)
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
StatusMessage = "The external login was not removed.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "The external login was removed.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostLinkLoginAsync(string provider)
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
// Request a redirect to the external login provider to link a login for the current user
|
||||
var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback");
|
||||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
|
||||
return new ChallengeResult(provider, properties);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetLinkLoginCallbackAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var info = await _signInManager.GetExternalLoginInfoAsync(userId);
|
||||
if (info == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected error occurred loading external login info.");
|
||||
}
|
||||
|
||||
var result = await _userManager.AddLoginAsync(user, info);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
StatusMessage = "The external login was not added. External logins can only be associated with one account.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
StatusMessage = "The external login was added.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@page
|
||||
@model GenerateRecoveryCodesModel
|
||||
@{
|
||||
ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||
<strong>Put these codes in a safe place.</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
<p>
|
||||
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
|
||||
used in an authenticator app you should <a asp-page="./ResetAuthenticator">reset your authenticator keys.</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<form method="post">
|
||||
<button class="btn btn-danger" type="submit">Generate Recovery Codes</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class GenerateRecoveryCodesModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly ILogger<GenerateRecoveryCodesModel> _logger;
|
||||
|
||||
public GenerateRecoveryCodesModel(SignInManager<IdentityUser> signInManager,
|
||||
UserManager<IdentityUser> userManager,
|
||||
ILogger<GenerateRecoveryCodesModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
if (!isTwoFactorEnabled)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot generate recovery codes for user because they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var isTwoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
if (!isTwoFactorEnabled)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot generate recovery codes for user as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
RecoveryCodes = recoveryCodes.ToArray();
|
||||
|
||||
_logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
|
||||
StatusMessage = "You have generated new recovery codes.";
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ShowRecoveryCodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public IActionResult OnGetAsync()
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/Manage/Email");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static class ManageNavPages
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string Email => "Email";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ChangePassword => "ChangePassword";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ExternalLogins => "ExternalLogins";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
var activePage = viewContext.ViewData["ActivePage"] as string
|
||||
?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName);
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@page
|
||||
@model ResetAuthenticatorModel
|
||||
@{
|
||||
ViewData["Title"] = "Reset authenticator key";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
|
||||
</p>
|
||||
<p>
|
||||
This process disables 2FA until you verify your authenticator app.
|
||||
If you do not complete your authenticator app configuration you may lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<form id="reset-authenticator-form" method="post">
|
||||
<button id="reset-authenticator-button" class="btn btn-danger" type="submit">Reset authenticator key</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,66 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class ResetAuthenticatorModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly ILogger<ResetAuthenticatorModel> _logger;
|
||||
|
||||
public ResetAuthenticatorModel(
|
||||
UserManager<IdentityUser> userManager,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
ILogger<ResetAuthenticatorModel> logger)
|
||||
: base(signInManager)
|
||||
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
_logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.";
|
||||
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/EnableAuthenticator");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@page
|
||||
@model SetPasswordModel
|
||||
@{
|
||||
ViewData["Title"] = "Set password";
|
||||
ViewData["ActivePage"] = ManageNavPages.ChangePassword;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>Set your password</h3>
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<p class="text-info">
|
||||
You do not have a local username/password for this site. Add a local
|
||||
account so you can log in without an external login.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form id="set-password-form" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.NewPassword" class="form-control" autocomplete="new-password" placeholder="Please enter your new password."/>
|
||||
<label asp-for="Input.NewPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.NewPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" placeholder="Please confirm your new password."/>
|
||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class SetPasswordModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public SetPasswordModel(
|
||||
UserManager<IdentityUser> userManager,
|
||||
SignInManager<IdentityUser> signInManager)
|
||||
: base(signInManager)
|
||||
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
|
||||
if (hasPassword)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ChangePassword");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword);
|
||||
if (!addPasswordResult.Succeeded)
|
||||
{
|
||||
foreach (var error in addPasswordResult.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
StatusMessage = "Your password has been set.";
|
||||
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ShowRecoveryCodesModel
|
||||
@{
|
||||
ViewData["Title"] = "Recovery codes";
|
||||
ViewData["ActivePage"] = "TwoFactorAuthentication";
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p>
|
||||
<strong>Put these codes in a safe place.</strong>
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
|
||||
{
|
||||
<code class="recovery-code">@Model.RecoveryCodes[row]</code><text> </text><code class="recovery-code">@Model.RecoveryCodes[row + 1]</code><br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class ShowRecoveryCodesModel : PageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string[] RecoveryCodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if (RecoveryCodes == null || RecoveryCodes.Length == 0)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/TwoFactorAuthentication");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
@page
|
||||
@using Microsoft.AspNetCore.Http.Features
|
||||
@model TwoFactorAuthenticationModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication (2FA)";
|
||||
ViewData["ActivePage"] = ManageNavPages.TwoFactorAuthentication;
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h3>@ViewData["Title"]</h3>
|
||||
@{
|
||||
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
|
||||
@if (consentFeature?.CanTrack ?? true)
|
||||
{
|
||||
@if (Model.Is2faEnabled)
|
||||
{
|
||||
if (Model.RecoveryCodesLeft == 0)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have no recovery codes left.</strong>
|
||||
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have 1 recovery code left.</strong>
|
||||
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (Model.IsMachineRemembered)
|
||||
{
|
||||
<form method="post" style="display: inline-block">
|
||||
<button type="submit" class="btn btn-primary">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
|
||||
}
|
||||
|
||||
<h4>Authenticator app</h4>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
|
||||
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>Privacy and cookie policy have not been accepted.</strong>
|
||||
<p>You must accept the policy before you can enable two factor authentication.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
{
|
||||
public class TwoFactorAuthenticationModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly ILogger<TwoFactorAuthenticationModel> _logger;
|
||||
|
||||
public TwoFactorAuthenticationModel(
|
||||
UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, ILogger<TwoFactorAuthenticationModel> logger)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool HasAuthenticator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public int RecoveryCodesLeft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public bool Is2faEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public bool IsMachineRemembered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null;
|
||||
Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
|
||||
IsMachineRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user);
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user);
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _signInManager.ForgetTwoFactorClientAsync();
|
||||
StatusMessage = "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.";
|
||||
return RedirectToPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@{
|
||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
|
||||
@if (hasExternalLogins)
|
||||
{
|
||||
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
|
||||
}
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
|
||||
</ul>
|
||||
@@ -0,0 +1,10 @@
|
||||
@model string
|
||||
|
||||
@if (!String.IsNullOrEmpty(Model))
|
||||
{
|
||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
@Model
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@using TightWiki.Areas.Identity.Pages.Account.Manage
|
||||
161
TightWiki/Areas/Identity/Pages/Account/Register.cshtml
Normal file
161
TightWiki/Areas/Identity/Pages/Account/Register.cshtml
Normal file
@@ -0,0 +1,161 @@
|
||||
@page
|
||||
@using TightWiki.Models
|
||||
@using System.Text.Encodings.Web
|
||||
@model RegisterModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form id="registerForm" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl ?? "")" method="post">
|
||||
<h4>Create a new account.</h4>
|
||||
<hr />
|
||||
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.Email" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.Email)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.Email, new { @class = "form-control", autocomplete = "email", placeholder = "name@example.com" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.Email)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.Password" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.Password)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.PasswordFor(m => Model.Input.Password, new { @class = "form-control", autocomplete = "password", placeholder = "required" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.Password)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.ConfirmPassword" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.ConfirmPassword)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.PasswordFor(m => Model.Input.ConfirmPassword, new { @class = "form-control", autocomplete = "confirmpassword", placeholder = "required" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.ConfirmPassword)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.AccountName" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.AccountName)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.AccountName, new { @class = "form-control", placeholder = "required" })
|
||||
<small class="form-text text-muted">This account name will be visible publicly.</small>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.AccountName)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.FirstName" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.FirstName)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.FirstName, new { @class = "form-control", placeholder = "not required" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.FirstName)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-3">
|
||||
<label for="Input.Input.LastName" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.LastName)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
@Html.TextBoxFor(m => Model.Input.LastName, new { @class = "form-control", placeholder = "not required" })
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.LastName)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.Input.Country" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.Country)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
<select name="Input.Country" id="Input.Country" class="form-control">
|
||||
<option value="" style="color:#ccc !important;">Select a country</option>
|
||||
@foreach (var item in Model.Input.Countries)
|
||||
{
|
||||
<option value="@item.Value" selected=@(Model.Input.Country == item.Value ? "selected" : null)>
|
||||
@item.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.Country)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.Input.Language" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.Language)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
<select name="Input.Language" id="Input.Language" class="form-control">
|
||||
<option value="" style="color:#ccc !important;">Select a language</option>
|
||||
@foreach (var item in Model.Input.Languages)
|
||||
{
|
||||
<option value="@item.Value" selected=@(Model.Input.Language == item.Value ? "selected" : null)>
|
||||
@item.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.Language)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mb-1">
|
||||
<label for="Input.Input.TimeZone" class="col-sm-2 col-form-label"><strong>@Html.LabelFor(m => Model.Input.TimeZone)</strong></label>
|
||||
<div class="col-sm-10">
|
||||
<select name="Input.TimeZone" id="Input.TimeZone" class="form-control">
|
||||
<option value="" style="color:#ccc !important;">Select a time-zone</option>
|
||||
@foreach (var item in Model.Input.TimeZones)
|
||||
{
|
||||
<option value="@item.Value" selected=@(Model.Input.TimeZone == item.Value ? "selected" : null)>
|
||||
@item.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-danger">@Html.ValidationMessageFor(m => Model.Input.TimeZone)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="login-submit" type="submit" class="w-100 btn btn-lg btn-primary">Signup</button>
|
||||
<br /><br />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if ((Model.ExternalLogins?.Count ?? 0) > 0)
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<section>
|
||||
<h4>Use another service to register.</h4>
|
||||
<hr />
|
||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@UrlEncoder.Default.Encode(Model.ReturnUrl ?? "")" method="post" class="form-horizontal">
|
||||
<div>
|
||||
<p>
|
||||
@foreach (var provider in Model.ExternalLogins!)
|
||||
{
|
||||
if (provider.Name == "Google")
|
||||
{
|
||||
<button type="submit" class="btn w-100 mb-3 d-flex align-items-center" name="provider" value="@provider.Name" title="Log in using your Google account">
|
||||
<img src="@GlobalConfiguration.BasePath/images/external/google-signin.svg" alt="Google" class="me-2" />
|
||||
</button>
|
||||
}
|
||||
else if (provider.Name == "Microsoft")
|
||||
{
|
||||
<button type="submit" class="btn w-100 mb-3 d-flex align-items-center" name="provider" value="@provider.Name" title="Log in using your Microsoft account">
|
||||
<img src="@GlobalConfiguration.BasePath/images/external/microsoft-signin.svg" alt="Microsoft" class="me-2" />
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="submit" class="btn btn-primary w-100 mb-3" 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" />
|
||||
}
|
||||
250
TightWiki/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
250
TightWiki/Areas/Identity/Pages/Account/Register.cshtml.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using NTDLS.Helpers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Library.Interfaces;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class RegisterModel : PageModelBase
|
||||
{
|
||||
private readonly SignInManager<IdentityUser> _signInManager;
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly IUserStore<IdentityUser> _userStore;
|
||||
private readonly IUserEmailStore<IdentityUser> _emailStore;
|
||||
private readonly ILogger<RegisterModel> _logger;
|
||||
private readonly IWikiEmailSender _emailSender;
|
||||
|
||||
public RegisterModel(
|
||||
UserManager<IdentityUser> userManager,
|
||||
IUserStore<IdentityUser> userStore,
|
||||
SignInManager<IdentityUser> signInManager,
|
||||
ILogger<RegisterModel> logger,
|
||||
IWikiEmailSender emailSender)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userStore = userStore;
|
||||
_emailStore = GetEmailStore();
|
||||
_signInManager = signInManager;
|
||||
_logger = logger;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = new();
|
||||
|
||||
[BindProperty]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
public IList<AuthenticationScheme>? ExternalLogins { get; set; }
|
||||
|
||||
public class InputModel
|
||||
{
|
||||
public List<TimeZoneItem> TimeZones { get; set; } = new();
|
||||
public List<CountryItem> Countries { get; set; } = new();
|
||||
public List<LanguageItem> Languages { get; set; } = new();
|
||||
|
||||
|
||||
[Display(Name = "Account Name")]
|
||||
[Required(ErrorMessage = "Account Name is required")]
|
||||
public string AccountName { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "First Name")]
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
[Display(Name = "Last Name")]
|
||||
public string? LastName { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Time-Zone")]
|
||||
[Required(ErrorMessage = "TimeZone is required")]
|
||||
public string TimeZone { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Country")]
|
||||
[Required(ErrorMessage = "Country is required")]
|
||||
public string Country { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Language")]
|
||||
[Required(ErrorMessage = "Language is required")]
|
||||
public string Language { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[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; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private void PopulateDefaults()
|
||||
{
|
||||
Input.TimeZones = TimeZoneItem.GetAll();
|
||||
Input.Countries = CountryItem.GetAll();
|
||||
Input.Languages = LanguageItem.GetAll();
|
||||
|
||||
var membershipConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Membership");
|
||||
|
||||
if (string.IsNullOrEmpty(Input.TimeZone))
|
||||
Input.TimeZone = membershipConfig.Value<string>("Default TimeZone").EnsureNotNull();
|
||||
|
||||
if (string.IsNullOrEmpty(Input.Country))
|
||||
Input.Country = membershipConfig.Value<string>("Default Country").EnsureNotNull();
|
||||
|
||||
if (string.IsNullOrEmpty(Input.Language))
|
||||
Input.Language = membershipConfig.Value<string>("Default Language").EnsureNotNull();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string? returnUrl = null)
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
PopulateDefaults();
|
||||
|
||||
ReturnUrl = returnUrl;
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
|
||||
PopulateDefaults();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Input.AccountName))
|
||||
{
|
||||
ModelState.AddModelError("Input.AccountName", "Account Name is required.");
|
||||
return Page();
|
||||
}
|
||||
else if (UsersRepository.DoesProfileAccountExist(Input.AccountName))
|
||||
{
|
||||
ModelState.AddModelError("Input.AccountName", "Account Name is already in use.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
returnUrl ??= Url.Content("~/");
|
||||
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = new IdentityUser()
|
||||
{
|
||||
UserName = Input.Email,
|
||||
Email = Input.Email
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user, Input.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
|
||||
var membershipConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Membership");
|
||||
|
||||
UsersRepository.CreateProfile(Guid.Parse(userId), Input.AccountName);
|
||||
|
||||
var claimsToAdd = new List<Claim>
|
||||
{
|
||||
new (ClaimTypes.Role, membershipConfig.Value<string>("Default Signup Role").EnsureNotNull()),
|
||||
new ("timezone", Input.TimeZone),
|
||||
new (ClaimTypes.Country, Input.Country),
|
||||
new ("language", Input.Language),
|
||||
new ("firstname", Input.FirstName ?? ""),
|
||||
new ("lastname", Input.LastName ?? ""),
|
||||
};
|
||||
|
||||
SecurityRepository.UpsertUserClaims(_userManager, user, claimsToAdd);
|
||||
|
||||
if (_userManager.Options.SignIn.RequireConfirmedAccount)
|
||||
{
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var encodedCode = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { area = "Identity", userId = userId, code = encodedCode, returnUrl = returnUrl },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
var emailTemplate = new StringBuilder(ConfigurationRepository.Get<string>("Membership", "Template: Account Verification Email"));
|
||||
var basicConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Basic");
|
||||
var siteName = basicConfig.Value<string>("Name");
|
||||
var address = basicConfig.Value<string>("Address");
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(Guid.Parse(userId));
|
||||
|
||||
var emailSubject = "Confirm your email";
|
||||
emailTemplate.Replace("##SUBJECT##", emailSubject);
|
||||
emailTemplate.Replace("##ACCOUNTCOUNTRY##", profile.Country);
|
||||
emailTemplate.Replace("##ACCOUNTTIMEZONE##", profile.TimeZone);
|
||||
emailTemplate.Replace("##ACCOUNTLANGUAGE##", profile.Language);
|
||||
emailTemplate.Replace("##ACCOUNTEMAIL##", profile.EmailAddress);
|
||||
emailTemplate.Replace("##ACCOUNTNAME##", profile.AccountName);
|
||||
emailTemplate.Replace("##PERSONNAME##", $"{profile.FirstName} {profile.LastName}");
|
||||
emailTemplate.Replace("##CODE##", code);
|
||||
emailTemplate.Replace("##USERID##", userId);
|
||||
emailTemplate.Replace("##SITENAME##", siteName);
|
||||
emailTemplate.Replace("##SITEADDRESS##", address);
|
||||
emailTemplate.Replace("##CALLBACKURL##", HtmlEncoder.Default.Encode(callbackUrl ?? string.Empty));
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.Email, emailSubject, emailTemplate.ToString());
|
||||
|
||||
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
|
||||
}
|
||||
else
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
}
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
return Page();
|
||||
}
|
||||
|
||||
private IUserEmailStore<IdentityUser> GetEmailStore()
|
||||
{
|
||||
if (!_userManager.SupportsUserEmail)
|
||||
{
|
||||
throw new NotSupportedException("The default UI requires a user store with email support.");
|
||||
}
|
||||
return (IUserEmailStore<IdentityUser>)_userStore;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@page
|
||||
@model RegisterConfirmationModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Register confirmation
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Please check your email to confirm your account.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TightWiki.Library.Interfaces;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class RegisterConfirmationModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly IWikiEmailSender _emailSender;
|
||||
|
||||
public RegisterConfirmationModel(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IWikiEmailSender emailSender)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
public IActionResult OnGetAsync(string email, string returnUrl = null)
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@page
|
||||
@model RegistrationIsNotAllowedModel
|
||||
@{
|
||||
ViewData["Title"] = "";
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Registration is not allowed
|
||||
</h3>
|
||||
|
||||
<p class="text-danger">Registration is not allowed.</p>
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
|
||||
public class RegistrationIsNotAllowedModel : PageModelBase
|
||||
{
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
public RegistrationIsNotAllowedModel(SignInManager<IdentityUser> signInManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model ResendEmailConfirmationModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Resend email confirmation
|
||||
</h3>
|
||||
|
||||
<p>Enter your email.</p>
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Resend</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using TightWiki.Library.Interfaces;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class ResendEmailConfirmationModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
private readonly IWikiEmailSender _emailSender;
|
||||
|
||||
public ResendEmailConfirmationModel(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IWikiEmailSender emailSender)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
|
||||
return Page();
|
||||
}
|
||||
|
||||
var userId = await _userManager.GetUserIdAsync(user);
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var encodedCode = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
var callbackUrl = Url.Page(
|
||||
"/Account/ConfirmEmail",
|
||||
pageHandler: null,
|
||||
values: new { userId = userId, code = encodedCode },
|
||||
protocol: Request.Scheme);
|
||||
|
||||
var emailTemplate = new StringBuilder(ConfigurationRepository.Get<string>("Membership", "Template: Account Verification Email"));
|
||||
var basicConfig = ConfigurationRepository.GetConfigurationEntryValuesByGroupName("Basic");
|
||||
var siteName = basicConfig.Value<string>("Name");
|
||||
var address = basicConfig.Value<string>("Address");
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(Guid.Parse(userId));
|
||||
|
||||
var emailSubject = "Confirm your email";
|
||||
emailTemplate.Replace("##SUBJECT##", emailSubject);
|
||||
emailTemplate.Replace("##ACCOUNTCOUNTRY##", profile.Country);
|
||||
emailTemplate.Replace("##ACCOUNTTIMEZONE##", profile.TimeZone);
|
||||
emailTemplate.Replace("##ACCOUNTLANGUAGE##", profile.Language);
|
||||
emailTemplate.Replace("##ACCOUNTEMAIL##", profile.EmailAddress);
|
||||
emailTemplate.Replace("##ACCOUNTNAME##", profile.AccountName);
|
||||
emailTemplate.Replace("##PERSONNAME##", $"{profile.FirstName} {profile.LastName}");
|
||||
emailTemplate.Replace("##CODE##", code);
|
||||
emailTemplate.Replace("##USERID##", userId);
|
||||
emailTemplate.Replace("##SITENAME##", siteName);
|
||||
emailTemplate.Replace("##SITEADDRESS##", address);
|
||||
emailTemplate.Replace("##CALLBACKURL##", HtmlEncoder.Default.Encode(callbackUrl));
|
||||
|
||||
await _emailSender.SendEmailAsync(Input.Email, emailSubject, emailTemplate.ToString());
|
||||
|
||||
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
41
TightWiki/Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
41
TightWiki/Areas/Identity/Pages/Account/ResetPassword.cshtml
Normal file
@@ -0,0 +1,41 @@
|
||||
@page
|
||||
@model ResetPasswordModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Reset password
|
||||
</h3>
|
||||
|
||||
<p>Reset your password.</p>
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
|
||||
<input asp-for="Input.Code" type="hidden" />
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
|
||||
<label asp-for="Input.Email" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please enter your password." />
|
||||
<label asp-for="Input.Password" class="form-label"></label>
|
||||
<span asp-validation-for="Input.Password" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="Please confirm your password." />
|
||||
<label asp-for="Input.ConfirmPassword" class="form-label"></label>
|
||||
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="w-100 btn btn-lg btn-primary">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
115
TightWiki/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
Normal file
115
TightWiki/Areas/Identity/Pages/Account/ResetPassword.cshtml.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
public class ResetPasswordModel : PageModelBase
|
||||
{
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
|
||||
public ResetPasswordModel(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: base(signInManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public class InputModel
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Code { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public IActionResult OnGet(string code = null)
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
return BadRequest("A code must be supplied for password reset.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code))
|
||||
};
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(Input.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ResetPasswordConfirmation");
|
||||
}
|
||||
|
||||
var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Identity/ResetPasswordConfirmation");
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@page
|
||||
@model ResetPasswordConfirmationModel
|
||||
@{
|
||||
var sessionState = ViewData["SessionState"] as TightWiki.SessionState ?? throw new Exception("Wiki State Context cannot be null.");
|
||||
}
|
||||
|
||||
<h3>
|
||||
Reset password confirmation
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Your password has been reset. Please <a asp-page="./Login">click here to log in</a>.
|
||||
</p>
|
||||
@@ -0,0 +1,32 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TightWiki.Areas.Identity.Pages.Account
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class ResetPasswordConfirmationModel : PageModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
|
||||
/// directly from your code. This API may change or be removed in future releases.
|
||||
/// </summary>
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public ResetPasswordConfirmationModel(SignInManager<IdentityUser> signInManager)
|
||||
|
||||
: base(signInManager)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
10
TightWiki/Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
10
TightWiki/Areas/Identity/Pages/Account/_StatusMessage.cshtml
Normal file
@@ -0,0 +1,10 @@
|
||||
@model string
|
||||
|
||||
@if (!String.IsNullOrEmpty(Model))
|
||||
{
|
||||
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
|
||||
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
@Model
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@using TightWiki.Areas.Identity.Pages.Account
|
||||
Reference in New Issue
Block a user