added support for Forgot Username and Use Login Link
This commit is contained in:
@ -14,93 +14,133 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@if (!twofactor)
|
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
||||||
{
|
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
|
||||||
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
@switch (_action)
|
||||||
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
|
{
|
||||||
@if (_allowexternallogin)
|
case "Login":
|
||||||
{
|
@if (_allowexternallogin)
|
||||||
<button type="button" class="btn btn-primary col-12" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
|
|
||||||
<hr class="app-rule mt-3 mb-2" />
|
|
||||||
}
|
|
||||||
@if (_allowsitelogin)
|
|
||||||
{
|
|
||||||
<div class="form-group text-center">
|
|
||||||
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
|
|
||||||
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group text-center mt-2">
|
|
||||||
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" @bind:event="oninput" required />
|
|
||||||
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (!_alwaysremember)
|
|
||||||
{
|
{
|
||||||
|
<button type="button" class="btn btn-primary col-12" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
|
||||||
|
<hr class="app-rule mt-3 mb-2" />
|
||||||
|
}
|
||||||
|
@if (_allowsitelogin)
|
||||||
|
{
|
||||||
|
<div class="form-group text-center">
|
||||||
|
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
|
||||||
|
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
|
||||||
|
</div>
|
||||||
<div class="form-group text-center mt-2">
|
<div class="form-group text-center mt-2">
|
||||||
<div>
|
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
|
||||||
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
|
<div class="input-group">
|
||||||
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
|
<input id="password" type="@_passwordtype" @ref="password" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" @bind:event="oninput" required />
|
||||||
|
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (!_alwaysremember)
|
||||||
|
{
|
||||||
|
<div class="form-group text-center mt-2">
|
||||||
|
<div>
|
||||||
|
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
|
||||||
|
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Stay Signed In?</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="btn-group mt-2 col-12" role="group">
|
||||||
|
<button type="button" class="btn btn-primary col-6" @onclick="Login">@SharedLocalizer["Login"]</button>
|
||||||
|
<button type="button" class="btn btn-secondary col-6" @onclick="CancelLogin">@SharedLocalizer["Cancel"]</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="@(() => SetAction("ForgotPassword"))">@Localizer["ForgotPassword"]</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="btn-group mt-2 col-12" role="group">
|
@if (_allowloginlink)
|
||||||
<button type="button" class="btn btn-primary col-6" @onclick="Login">@SharedLocalizer["Login"]</button>
|
{
|
||||||
<button type="button" class="btn btn-secondary col-6" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
|
<hr class="app-rule mt-3" />
|
||||||
</div>
|
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="@(() => SetAction("LoginLink"))">@Localizer["UseLoginLink"]</button>
|
||||||
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
|
}
|
||||||
|
|
||||||
@if (_allowpasskeys)
|
@if (_allowpasskeys)
|
||||||
{
|
{
|
||||||
<hr class="app-rule mt-3" />
|
<hr class="app-rule mt-3" />
|
||||||
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="Passkey">@Localizer["Passkey"]</button>
|
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="PasskeyLogin">@Localizer["Passkey"]</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (PageState.Site.AllowRegistration)
|
@if (PageState.Site.AllowRegistration)
|
||||||
{
|
{
|
||||||
<hr class="app-rule mt-3" />
|
<hr class="app-rule mt-3" />
|
||||||
<div class="text-center mt-2">
|
<div class="text-center mt-2">
|
||||||
<NavLink href="@_registerurl">@Localizer["Register"]</NavLink>
|
<NavLink href="@_registerurl">@Localizer["Register"]</NavLink>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
</div>
|
case "ForgotPassword":
|
||||||
</form>
|
<div class="form-group text-center">
|
||||||
}
|
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
|
||||||
else
|
<input id="username" type="text" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
|
||||||
{
|
</div>
|
||||||
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
<div class="btn-group mt-4 col-12" role="group">
|
||||||
<div class="container Oqtane-Modules-Admin-Login">
|
<button type="button" class="btn btn-primary col-6" @onclick="ForgotPassword">@SharedLocalizer["Send"]</button>
|
||||||
<div class="form-group">
|
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
|
||||||
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
|
</div>
|
||||||
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
|
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="@(() => SetAction("ForgotUsername"))">@Localizer["ForgotUsername"]</button>
|
||||||
</div>
|
break;
|
||||||
<br />
|
case "ForgotUsername":
|
||||||
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
|
<div class="form-group text-center">
|
||||||
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
|
<Label Class="control-label" For="email" HelpText="Please enter your Email" ResourceKey="Email">Email:</Label>
|
||||||
</div>
|
<input id="email" type="text" class="form-control" placeholder="@Localizer["Email.Placeholder"]" @bind="@_email" required />
|
||||||
</form>
|
</div>
|
||||||
}
|
<div class="btn-group mt-4 col-12" role="group">
|
||||||
|
<button type="button" class="btn btn-primary col-6" @onclick="ForgotUsername">@SharedLocalizer["Send"]</button>
|
||||||
|
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
case "LoginLink":
|
||||||
|
<div class="form-group text-center">
|
||||||
|
<Label Class="control-label" For="email" HelpText="Please enter your Email" ResourceKey="Email">Email:</Label>
|
||||||
|
<input id="email" type="text" class="form-control" placeholder="@Localizer["Email.Placeholder"]" @bind="@_email" required />
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mt-4 col-12" role="group">
|
||||||
|
<button type="button" class="btn btn-primary col-6" @onclick="LoginLink">@SharedLocalizer["Send"]</button>
|
||||||
|
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
case "TwoFactor":
|
||||||
|
<div class="form-group">
|
||||||
|
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
|
||||||
|
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mt-4 col-12" role="group">
|
||||||
|
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
|
||||||
|
<button type="button" class="btn btn-secondary" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private bool _allowsitelogin = true;
|
private string _action = "Login";
|
||||||
private bool _allowexternallogin = false;
|
private bool _allowexternallogin = false;
|
||||||
|
private bool _allowsitelogin = true;
|
||||||
|
private bool _allowloginlink = false;
|
||||||
private bool _allowpasskeys = false;
|
private bool _allowpasskeys = false;
|
||||||
|
|
||||||
private ElementReference login;
|
private ElementReference login;
|
||||||
private bool validated = false;
|
private bool validated = false;
|
||||||
private bool twofactor = false;
|
|
||||||
private string _username = string.Empty;
|
private string _username = string.Empty;
|
||||||
private ElementReference username;
|
private ElementReference username;
|
||||||
private string _password = string.Empty;
|
private string _password = string.Empty;
|
||||||
private string _passwordtype = "password";
|
private string _passwordtype = "password";
|
||||||
private string _togglepassword = string.Empty;
|
private string _togglepassword = string.Empty;
|
||||||
|
private ElementReference password;
|
||||||
private bool _remember = false;
|
private bool _remember = false;
|
||||||
private bool _alwaysremember = false;
|
private bool _alwaysremember = false;
|
||||||
private string _code = string.Empty;
|
|
||||||
private string _registerurl = string.Empty;
|
private string _registerurl = string.Empty;
|
||||||
|
private string _email = string.Empty;
|
||||||
|
private string _code = string.Empty;
|
||||||
|
|
||||||
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
|
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
|
||||||
public override bool? Prerender => true;
|
public override bool? Prerender => true;
|
||||||
@ -116,6 +156,7 @@ else
|
|||||||
{
|
{
|
||||||
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
|
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
|
||||||
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
|
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
|
||||||
|
_allowloginlink = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LoginLink", "false"));
|
||||||
_allowpasskeys = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false"));
|
_allowpasskeys = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false"));
|
||||||
_alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
|
_alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
|
||||||
|
|
||||||
@ -186,6 +227,45 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task KeyPressed(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Code == "Enter" || e.Code == "NumpadEnter")
|
||||||
|
{
|
||||||
|
switch (_action)
|
||||||
|
{
|
||||||
|
case "Login":
|
||||||
|
await Login();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAction(string action)
|
||||||
|
{
|
||||||
|
_action = action;
|
||||||
|
ClearModuleMessage();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExternalLogin()
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(PageState.ReturnUrl)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TogglePassword()
|
||||||
|
{
|
||||||
|
if (_passwordtype == "password")
|
||||||
|
{
|
||||||
|
_passwordtype = "text";
|
||||||
|
_togglepassword = SharedLocalizer["HidePassword"];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_passwordtype = "password";
|
||||||
|
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Login()
|
private async Task Login()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -197,7 +277,7 @@ else
|
|||||||
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
|
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
|
||||||
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
|
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
|
||||||
|
|
||||||
if (!twofactor)
|
if (_action == "Login")
|
||||||
{
|
{
|
||||||
_remember = _alwaysremember || _remember;
|
_remember = _alwaysremember || _remember;
|
||||||
user = await UserService.LoginUserAsync(user, hybrid, _remember);
|
user = await UserService.LoginUserAsync(user, hybrid, _remember);
|
||||||
@ -233,13 +313,13 @@ else
|
|||||||
{
|
{
|
||||||
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
|
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
|
||||||
{
|
{
|
||||||
twofactor = true;
|
_action = "TwoFactor";
|
||||||
validated = false;
|
validated = false;
|
||||||
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
|
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!twofactor)
|
if (_action != "TwoFactor")
|
||||||
{
|
{
|
||||||
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
|
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
|
||||||
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
|
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
|
||||||
@ -264,23 +344,32 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Cancel()
|
private void CancelLogin()
|
||||||
{
|
{
|
||||||
NavigationManager.NavigateTo(PageState.ReturnUrl);
|
NavigationManager.NavigateTo(PageState.ReturnUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Forgot()
|
private async Task PasskeyLogin()
|
||||||
|
{
|
||||||
|
// post back to the Passkey page so that the cookies are set correctly
|
||||||
|
var interop = new Interop(JSRuntime);
|
||||||
|
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = NavigateUrl() };
|
||||||
|
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
|
||||||
|
await interop.SubmitForm(url, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ForgotPassword()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_username != string.Empty)
|
if (!string.IsNullOrEmpty(_username))
|
||||||
{
|
{
|
||||||
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
|
var user = new User { Username = _username };
|
||||||
|
user = await UserService.ForgotPasswordAsync(user);
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
await UserService.ForgotPasswordAsync(user);
|
|
||||||
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
|
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
|
||||||
AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info);
|
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -289,10 +378,8 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
|
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -301,51 +388,66 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Reset()
|
private async Task ForgotUsername()
|
||||||
{
|
{
|
||||||
twofactor = false;
|
try
|
||||||
_username = "";
|
|
||||||
_password = "";
|
|
||||||
ClearModuleMessage();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task KeyPressed(KeyboardEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Code == "Enter" || e.Code == "NumpadEnter")
|
|
||||||
{
|
{
|
||||||
await Login();
|
if (!string.IsNullOrEmpty(_email))
|
||||||
|
{
|
||||||
|
var user = new User { Email = _email };
|
||||||
|
user = await UserService.ForgotUsernameAsync(user);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
AddModuleMessage(Localizer["Message.ForgotUsername"], MessageType.Info);
|
||||||
|
await logger.LogInformation(LogFunction.Security, "Username Reminder Notification Sent For Email {Email}", _email);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await logger.LogError(ex, "Error Sending Username Reminder {Error}", ex.Message);
|
||||||
|
AddModuleMessage(Localizer["Error.ForgotUsername"], MessageType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TogglePassword()
|
private async Task LoginLink()
|
||||||
{
|
{
|
||||||
if (_passwordtype == "password")
|
try
|
||||||
{
|
{
|
||||||
_passwordtype = "text";
|
if (!string.IsNullOrEmpty(_email))
|
||||||
_togglepassword = SharedLocalizer["HidePassword"];
|
{
|
||||||
|
var user = new User { Email = _email };
|
||||||
|
user = await UserService.SendLoginLinkAsync(user);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info);
|
||||||
|
await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_passwordtype = "password";
|
await logger.LogError(ex, "Error Sending Login Link {Error}", ex.Message);
|
||||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
AddModuleMessage(Localizer["Error.SendLoginLink"], MessageType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExternalLogin()
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(PageState.ReturnUrl)), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Passkey()
|
|
||||||
{
|
|
||||||
// post back to the Passkey page so that the cookies are set correctly
|
|
||||||
var interop = new Interop(JSRuntime);
|
|
||||||
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = NavigateUrl() };
|
|
||||||
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
|
|
||||||
await interop.SubmitForm(url, fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender && PageState.QueryString.ContainsKey("options"))
|
if (firstRender && PageState.QueryString.ContainsKey("options"))
|
||||||
@ -377,11 +479,21 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstRender && PageState.User == null && _allowsitelogin)
|
if (firstRender && PageState.User == null && _allowsitelogin && _action == "Login")
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(username.Id)) // ensure username is visible in UI
|
if (string.IsNullOrEmpty(_username))
|
||||||
{
|
{
|
||||||
await username.FocusAsync();
|
if (!string.IsNullOrEmpty(username.Id)) // ensure username is visible in UI
|
||||||
|
{
|
||||||
|
await username.FocusAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(password.Id)) // ensure password is visible in UI
|
||||||
|
{
|
||||||
|
await password.FocusAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,15 @@ else
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="loginlink" HelpText="Do you want to allow users to login using a time sensitive link sent by email" ResourceKey="LoginLink">Allow Login Link?</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="loginlink" class="form-select" @bind="@_loginlink">
|
||||||
|
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||||
|
<option value="false">@SharedLocalizer["No"]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row mb-1 align-items-center">
|
<div class="row mb-1 align-items-center">
|
||||||
<Label Class="col-sm-3" For="passkeys" HelpText="Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)" ResourceKey="Passkeys">Allow Passkeys?</Label>
|
<Label Class="col-sm-3" For="passkeys" HelpText="Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)" ResourceKey="Passkeys">Allow Passkeys?</Label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
@ -547,6 +556,7 @@ else
|
|||||||
private string _registerurl;
|
private string _registerurl;
|
||||||
private string _profileurl;
|
private string _profileurl;
|
||||||
private string _requireconfirmedemail;
|
private string _requireconfirmedemail;
|
||||||
|
private string _loginlink;
|
||||||
private string _passkeys;
|
private string _passkeys;
|
||||||
private string _twofactor;
|
private string _twofactor;
|
||||||
private string _cookiename;
|
private string _cookiename;
|
||||||
@ -625,6 +635,7 @@ else
|
|||||||
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
|
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
|
||||||
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
|
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
|
||||||
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
|
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
|
||||||
|
_loginlink = SettingService.GetSetting(settings, "LoginOptions:LoginLink", "false");
|
||||||
_passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false");
|
_passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false");
|
||||||
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
|
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
|
||||||
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
|
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
|
||||||
@ -764,6 +775,7 @@ else
|
|||||||
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
|
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
|
||||||
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
|
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
|
||||||
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
|
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
|
||||||
|
settings = SettingService.SetSetting(settings, "LoginOptions:LoginLink", _loginlink, false);
|
||||||
settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false);
|
settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false);
|
||||||
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
|
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
|
||||||
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
|
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
|
||||||
|
|||||||
@ -120,6 +120,12 @@
|
|||||||
<data name="ForgotPassword" xml:space="preserve">
|
<data name="ForgotPassword" xml:space="preserve">
|
||||||
<value>Forgot Password?</value>
|
<value>Forgot Password?</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ForgotUsername" xml:space="preserve">
|
||||||
|
<value>Forgot Username?</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseLoginLink" xml:space="preserve">
|
||||||
|
<value>Use Login Link</value>
|
||||||
|
</data>
|
||||||
<data name="Success.Account.Verified" xml:space="preserve">
|
<data name="Success.Account.Verified" xml:space="preserve">
|
||||||
<value>User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.</value>
|
<value>User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.</value>
|
||||||
</data>
|
</data>
|
||||||
@ -142,16 +148,19 @@
|
|||||||
<value>You Are Already Signed In</value>
|
<value>You Are Already Signed In</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Message.ForgotPassword" xml:space="preserve">
|
<data name="Message.ForgotPassword" xml:space="preserve">
|
||||||
<value>Please Enter The Username Related To Your Account And Then Select The Forgot Password Option Again</value>
|
|
||||||
</data>
|
|
||||||
<data name="Message.ForgotUser" xml:space="preserve">
|
|
||||||
<value>Please Check The Email Address Associated To Your User Account For A Password Reset Notification</value>
|
<value>Please Check The Email Address Associated To Your User Account For A Password Reset Notification</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Message.ForgotUsername" xml:space="preserve">
|
||||||
|
<value>Please Check Your Email For A Username Reminder Notification</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message.SendLoginLink" xml:space="preserve">
|
||||||
|
<value>A Login Link Has Been Sent To Your Email Address. The Link Is Only Valid For A Limited Amount Of Time.</value>
|
||||||
|
</data>
|
||||||
<data name="Message.UserDoesNotExist" xml:space="preserve">
|
<data name="Message.UserDoesNotExist" xml:space="preserve">
|
||||||
<value>User Does Not Exist</value>
|
<value>User Does Not Exist For Criteria Specified</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Code.HelpText" xml:space="preserve">
|
<data name="Code.HelpText" xml:space="preserve">
|
||||||
<value>Please Enter The Secure Verification Code Which Was Sent To You By Email.</value>
|
<value>Please enter the secure verification code which was sent to you by email</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Code.Placeholder" xml:space="preserve">
|
<data name="Code.Placeholder" xml:space="preserve">
|
||||||
<value>Verification Code</value>
|
<value>Verification Code</value>
|
||||||
@ -166,7 +175,7 @@
|
|||||||
<value>A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator.</value>
|
<value>A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Password.HelpText" xml:space="preserve">
|
<data name="Password.HelpText" xml:space="preserve">
|
||||||
<value>Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time.</value>
|
<value>Please enter the password related to your account. Remember that passwords are sase sensitive. If you attempt to login to your account multiple times unsuccessfully, you will be locked out for a period of time.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Password.Placeholder" xml:space="preserve">
|
<data name="Password.Placeholder" xml:space="preserve">
|
||||||
<value>Password</value>
|
<value>Password</value>
|
||||||
@ -175,13 +184,13 @@
|
|||||||
<value>Password:</value>
|
<value>Password:</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Remember.HelpText" xml:space="preserve">
|
<data name="Remember.HelpText" xml:space="preserve">
|
||||||
<value>Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site</value>
|
<value>Specify if you would like to be signed back in automatically the next time you visit this site</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Remember.Text" xml:space="preserve">
|
<data name="Remember.Text" xml:space="preserve">
|
||||||
<value>Remember Me?</value>
|
<value>Stay Signed In?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Username.HelpText" xml:space="preserve">
|
<data name="Username.HelpText" xml:space="preserve">
|
||||||
<value>Please Enter The Username Related To Your Account</value>
|
<value>Please enter the username related to your account</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Username.Placeholder" xml:space="preserve">
|
<data name="Username.Placeholder" xml:space="preserve">
|
||||||
<value>Username</value>
|
<value>Username</value>
|
||||||
@ -201,7 +210,13 @@
|
|||||||
<data name="Error.ResetPassword" xml:space="preserve">
|
<data name="Error.ResetPassword" xml:space="preserve">
|
||||||
<value>Error Resetting Password</value>
|
<value>Error Resetting Password</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
|
<data name="Error.ForgotUsername" xml:space="preserve">
|
||||||
|
<value>Error Sending Username Reminder</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error.SendLoginLink" xml:space="preserve">
|
||||||
|
<value>Error Sending Login Link</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
|
||||||
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
|
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExternalLoginStatus.MissingClaims" xml:space="preserve">
|
<data name="ExternalLoginStatus.MissingClaims" xml:space="preserve">
|
||||||
@ -228,6 +243,9 @@
|
|||||||
<data name="ExternalLoginStatus.ReviewClaims" xml:space="preserve">
|
<data name="ExternalLoginStatus.ReviewClaims" xml:space="preserve">
|
||||||
<value>The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider.</value>
|
<value>The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ExternalLoginStatus.LoginLinkFailed" xml:space="preserve">
|
||||||
|
<value>Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process.</value>
|
||||||
|
</data>
|
||||||
<data name="Register" xml:space="preserve">
|
<data name="Register" xml:space="preserve">
|
||||||
<value>Register as new user?</value>
|
<value>Register as new user?</value>
|
||||||
</data>
|
</data>
|
||||||
@ -237,4 +255,13 @@
|
|||||||
<data name="Error.Passkey.Fail" xml:space="preserve">
|
<data name="Error.Passkey.Fail" xml:space="preserve">
|
||||||
<value>Passkey Login Was Not Successful</value>
|
<value>Passkey Login Was Not Successful</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Email.HelpText" xml:space="preserve">
|
||||||
|
<value>Please enter the email address related to your account</value>
|
||||||
|
</data>
|
||||||
|
<data name="Email.Placeholder" xml:space="preserve">
|
||||||
|
<value>Email Address</value>
|
||||||
|
</data>
|
||||||
|
<data name="Email.Text" xml:space="preserve">
|
||||||
|
<value>Email:</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -567,4 +567,10 @@
|
|||||||
<data name="Passkeys.HelpText" xml:space="preserve">
|
<data name="Passkeys.HelpText" xml:space="preserve">
|
||||||
<value>Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)?</value>
|
<value>Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)?</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LoginLink.Text" xml:space="preserve">
|
||||||
|
<value>Allow Login Link?</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginLink.HelpText" xml:space="preserve">
|
||||||
|
<value>Do you want to allow users to login using a time sensitive link sent by email?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -101,7 +101,14 @@ namespace Oqtane.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task ForgotPasswordAsync(User user);
|
Task<User> ForgotPasswordAsync(User user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger a forgot-username e-mail for this <see cref="User"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<User> ForgotUsernameAsync(User user);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset the password of this <see cref="User"/>
|
/// Reset the password of this <see cref="User"/>
|
||||||
@ -211,6 +218,13 @@ namespace Oqtane.Services
|
|||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task DeleteLoginAsync(int userId, string provider, string key);
|
Task DeleteLoginAsync(int userId, string provider, string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a login link
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<User> SendLoginLinkAsync(User user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
|
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
|
||||||
@ -275,9 +289,14 @@ namespace Oqtane.Services
|
|||||||
return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user);
|
return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ForgotPasswordAsync(User user)
|
public async Task<User> ForgotPasswordAsync(User user)
|
||||||
{
|
{
|
||||||
await PostJsonAsync($"{Apiurl}/forgot", user);
|
return await PostJsonAsync<User>($"{Apiurl}/forgot", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> ForgotUsernameAsync(User user)
|
||||||
|
{
|
||||||
|
return await PostJsonAsync<User>($"{Apiurl}/forgotusername", user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> ResetPasswordAsync(User user, string token)
|
public async Task<User> ResetPasswordAsync(User user, string token)
|
||||||
@ -366,5 +385,10 @@ namespace Oqtane.Services
|
|||||||
{
|
{
|
||||||
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
|
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<User> SendLoginLinkAsync(User user)
|
||||||
|
{
|
||||||
|
return await PostJsonAsync<User>($"{Apiurl}/loginlink", user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -296,12 +296,24 @@ namespace Oqtane.Controllers
|
|||||||
|
|
||||||
// POST api/<controller>/forgot
|
// POST api/<controller>/forgot
|
||||||
[HttpPost("forgot")]
|
[HttpPost("forgot")]
|
||||||
public async Task Forgot([FromBody] User user)
|
public async Task<User> Forgot([FromBody] User user)
|
||||||
{
|
{
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
await _userManager.ForgotPassword(user);
|
return await _userManager.ForgotPassword(user);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST api/<controller>/forgotusername
|
||||||
|
[HttpPost("forgotusername")]
|
||||||
|
public async Task<User> ForgotUsername([FromBody] User user)
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return await _userManager.ForgotUsername(user);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/<controller>/reset
|
// POST api/<controller>/reset
|
||||||
@ -559,5 +571,16 @@ namespace Oqtane.Controllers
|
|||||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST api/<controller>/loginlink
|
||||||
|
[HttpPost("loginlink")]
|
||||||
|
public async Task<User> SendLoginLink([FromBody] User user)
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return await _userManager.SendLoginLink(user);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,8 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Policy;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Oqtane.Enums;
|
using Oqtane.Enums;
|
||||||
@ -30,7 +28,8 @@ namespace Oqtane.Managers
|
|||||||
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
|
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
|
||||||
Task LogoutUserEverywhere(User user);
|
Task LogoutUserEverywhere(User user);
|
||||||
Task<User> VerifyEmail(User user, string token);
|
Task<User> VerifyEmail(User user, string token);
|
||||||
Task ForgotPassword(User user);
|
Task<User> ForgotPassword(User user);
|
||||||
|
Task<User> ForgotUsername(User user);
|
||||||
Task<User> ResetPassword(User user, string token);
|
Task<User> ResetPassword(User user, string token);
|
||||||
User VerifyTwoFactor(User user, string token);
|
User VerifyTwoFactor(User user, string token);
|
||||||
Task<UserValidateResult> ValidateUser(string username, string email, string password);
|
Task<UserValidateResult> ValidateUser(string username, string email, string password);
|
||||||
@ -42,6 +41,7 @@ namespace Oqtane.Managers
|
|||||||
Task<List<UserLogin>> GetLogins(int userId, int siteId);
|
Task<List<UserLogin>> GetLogins(int userId, int siteId);
|
||||||
Task<User> AddLogin(User user, string token, string type, string key, string name);
|
Task<User> AddLogin(User user, string token, string type, string key, string name);
|
||||||
Task DeleteLogin(int userId, string provider, string key);
|
Task DeleteLogin(int userId, string provider, string key);
|
||||||
|
Task<User> SendLoginLink(User user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserManager : IUserManager
|
public class UserManager : IUserManager
|
||||||
@ -519,14 +519,16 @@ namespace Oqtane.Managers
|
|||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
public async Task ForgotPassword(User user)
|
|
||||||
|
public async Task<User> ForgotPassword(User user)
|
||||||
{
|
{
|
||||||
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
||||||
if (identityuser != null)
|
if (identityuser != null)
|
||||||
{
|
{
|
||||||
var alias = _tenantManager.GetAlias();
|
|
||||||
user = _users.GetUser(user.Username);
|
|
||||||
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
|
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
|
||||||
|
|
||||||
|
var alias = _tenantManager.GetAlias();
|
||||||
|
user = GetUser(user.Username, alias.SiteId);
|
||||||
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
|
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
|
||||||
string siteName = _sites.GetSite(alias.SiteId).Name;
|
string siteName = _sites.GetSite(alias.SiteId).Name;
|
||||||
string subject = _localizer["ForgotPasswordEmailSubject"];
|
string subject = _localizer["ForgotPasswordEmailSubject"];
|
||||||
@ -537,11 +539,51 @@ namespace Oqtane.Managers
|
|||||||
body = body.Replace("[SiteName]", siteName);
|
body = body.Replace("[SiteName]", siteName);
|
||||||
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
|
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
|
||||||
_notifications.AddNotification(notification);
|
_notifications.AddNotification(notification);
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
|
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
|
||||||
|
return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> ForgotUsername(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email);
|
||||||
|
if (identityuser != null)
|
||||||
|
{
|
||||||
|
var alias = _tenantManager.GetAlias();
|
||||||
|
user = GetUser(identityuser.UserName, alias.SiteId);
|
||||||
|
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username;
|
||||||
|
string siteName = _sites.GetSite(alias.SiteId).Name;
|
||||||
|
string subject = _localizer["ForgotUsernameEmailSubject"];
|
||||||
|
subject = subject.Replace("[SiteName]", siteName);
|
||||||
|
string body = _localizer["ForgotUsernameEmailBody"].Value;
|
||||||
|
body = body.Replace("[UserDisplayName]", user.DisplayName);
|
||||||
|
body = body.Replace("[URL]", url);
|
||||||
|
body = body.Replace("[SiteName]", siteName);
|
||||||
|
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
|
||||||
|
_notifications.AddNotification(notification);
|
||||||
|
|
||||||
|
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Forgot Username Notification Sent For {Email}", user.Email);
|
||||||
|
return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// email may not be unique
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", user.Email);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -588,6 +630,7 @@ namespace Oqtane.Managers
|
|||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
|
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
|
||||||
{
|
{
|
||||||
var validateResult = new UserValidateResult { Succeeded = true };
|
var validateResult = new UserValidateResult { Succeeded = true };
|
||||||
@ -914,5 +957,49 @@ namespace Oqtane.Managers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<User> SendLoginLink(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(user.Email);
|
||||||
|
if (identityuser != null)
|
||||||
|
{
|
||||||
|
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
|
||||||
|
|
||||||
|
var alias = _tenantManager.GetAlias();
|
||||||
|
user = GetUser(identityuser.UserName, alias.SiteId);
|
||||||
|
user.TwoFactorCode = token;
|
||||||
|
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
|
||||||
|
_users.UpdateUser(user);
|
||||||
|
|
||||||
|
string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
|
||||||
|
string siteName = _sites.GetSite(alias.SiteId).Name;
|
||||||
|
string subject = _localizer["LoginLinkEmailSubject"];
|
||||||
|
subject = subject.Replace("[SiteName]", siteName);
|
||||||
|
string body = _localizer["LoginLinkEmailBody"].Value;
|
||||||
|
body = body.Replace("[UserDisplayName]", user.DisplayName);
|
||||||
|
body = body.Replace("[URL]", url);
|
||||||
|
body = body.Replace("[SiteName]", siteName);
|
||||||
|
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
|
||||||
|
_notifications.AddNotification(notification);
|
||||||
|
|
||||||
|
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email);
|
||||||
|
return new User { UserId = user.UserId, Username = user.Username, Email = user.Email }; // minimal object
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// email may not be unique
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", user.Email);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
Oqtane.Server/Pages/LoginLink.cshtml
Normal file
3
Oqtane.Server/Pages/LoginLink.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@page "/pages/loginlink"
|
||||||
|
@namespace Oqtane.Pages
|
||||||
|
@model Oqtane.Pages.LoginLinkModel
|
||||||
69
Oqtane.Server/Pages/LoginLink.cshtml.cs
Normal file
69
Oqtane.Server/Pages/LoginLink.cshtml.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Oqtane.Enums;
|
||||||
|
using Oqtane.Extensions;
|
||||||
|
using Oqtane.Infrastructure;
|
||||||
|
using Oqtane.Managers;
|
||||||
|
using Oqtane.Shared;
|
||||||
|
|
||||||
|
namespace Oqtane.Pages
|
||||||
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class LoginLinkModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly UserManager<IdentityUser> _identityUserManager;
|
||||||
|
private readonly SignInManager<IdentityUser> _identitySignInManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly ILogManager _logger;
|
||||||
|
|
||||||
|
public LoginLinkModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager, ILogManager logger)
|
||||||
|
{
|
||||||
|
_identityUserManager = identityUserManager;
|
||||||
|
_identitySignInManager = identitySignInManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync(string name, string token)
|
||||||
|
{
|
||||||
|
var returnurl = "/login";
|
||||||
|
|
||||||
|
if (bool.Parse(HttpContext.GetSiteSettings().GetValue("LoginOptions:LoginLink", "false")) &&
|
||||||
|
!User.Identity.IsAuthenticated && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
var validuser = false;
|
||||||
|
|
||||||
|
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name);
|
||||||
|
if (identityuser != null)
|
||||||
|
{
|
||||||
|
var user = _userManager.GetUser(identityuser.UserName, HttpContext.GetAlias().SiteId);
|
||||||
|
if (user != null && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
|
||||||
|
{
|
||||||
|
await _identitySignInManager.SignInAsync(identityuser, false);
|
||||||
|
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name);
|
||||||
|
validuser = true;
|
||||||
|
returnurl = "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validuser)
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name);
|
||||||
|
returnurl += $"?status={ExternalLoginStatus.LoginLinkFailed}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name);
|
||||||
|
returnurl = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalRedirect(Url.Content("~" + returnurl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -121,7 +121,19 @@
|
|||||||
<value>Dear [UserDisplayName]<br><br>You recently requested to reset your password. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Reset Password</a></b><br><br>Please note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site.<br><br>If you did not request to reset your password you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team</value>
|
<value>Dear [UserDisplayName]<br><br>You recently requested to reset your password. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Reset Password</a></b><br><br>Please note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site.<br><br>If you did not request to reset your password you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ForgotPasswordEmailSubject" xml:space="preserve">
|
<data name="ForgotPasswordEmailSubject" xml:space="preserve">
|
||||||
<value>Password Reset Notification Sent For [SiteName]</value>
|
<value>Password Reset Notification For [SiteName]</value>
|
||||||
|
</data>
|
||||||
|
<data name="ForgotUsernameEmailBody" xml:space="preserve">
|
||||||
|
<value>Dear [UserDisplayName]<br><br>You recently requested a username reminder. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Login</a></b><br><br>If you did not request a username reminder you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team</value>
|
||||||
|
</data>
|
||||||
|
<data name="ForgotUsernameEmailSubject" xml:space="preserve">
|
||||||
|
<value>Forgotten Username Reminder For [SiteName]</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginLinkEmailBody" xml:space="preserve">
|
||||||
|
<value>Dear [UserDisplayName]<br><br>You recently requested a login link. Please use the link below to complete the process: <b><a href="[URL]"><br><br>Click here to Login</a></b><br><br>Please note that the link is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate another login link request on the site.<br><br>If you did not request a login link you can safely ignore this message.<br><br>Thank You!<br>[SiteName] Team</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoginLinkEmailSubject" xml:space="preserve">
|
||||||
|
<value>Login Link Notification For [SiteName]</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NoVerificationEmailBody" xml:space="preserve">
|
<data name="NoVerificationEmailBody" xml:space="preserve">
|
||||||
<value>Dear [UserDisplayName],<br><br>A user account has been successfully created for you with the username <b>[Username]</b>. Please <b><a href="[URL]">click here to login</a></b>. If you do not know your password, use the forgot password option on the login page to reset your account.<br><br>Thank You!<br>[SiteName] Team</value>
|
<value>Dear [UserDisplayName],<br><br>A user account has been successfully created for you with the username <b>[Username]</b>. Please <b><a href="[URL]">click here to login</a></b>. If you do not know your password, use the forgot password option on the login page to reset your account.<br><br>Thank You!<br>[SiteName] Team</value>
|
||||||
|
|||||||
@ -10,5 +10,6 @@ namespace Oqtane.Shared {
|
|||||||
public const string AccessDenied = "AccessDenied";
|
public const string AccessDenied = "AccessDenied";
|
||||||
public const string RemoteFailure = "RemoteFailure";
|
public const string RemoteFailure = "RemoteFailure";
|
||||||
public const string ReviewClaims = "ReviewClaims";
|
public const string ReviewClaims = "ReviewClaims";
|
||||||
|
public const string LoginLinkFailed = "LoginLinkFailed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user