504 lines
23 KiB
Plaintext
504 lines
23 KiB
Plaintext
@using System.Net
|
|
@namespace Oqtane.Modules.Admin.Login
|
|
@inherits ModuleBase
|
|
@inject NavigationManager NavigationManager
|
|
@inject IUserService UserService
|
|
@inject ISettingService SettingService
|
|
@inject IServiceProvider ServiceProvider
|
|
@inject IStringLocalizer<Index> Localizer
|
|
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
|
|
|
@if (PageState.User != null)
|
|
{
|
|
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
|
|
}
|
|
else
|
|
{
|
|
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
|
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
|
|
@switch (_action)
|
|
{
|
|
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" @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>
|
|
@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>
|
|
}
|
|
|
|
@if (_allowloginlink)
|
|
{
|
|
<hr class="app-rule mt-3" />
|
|
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="@(() => SetAction("LoginLink"))">@Localizer["UseLoginLink"]</button>
|
|
}
|
|
|
|
@if (_allowpasskeys)
|
|
{
|
|
<hr class="app-rule mt-3" />
|
|
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="PasskeyLogin">@Localizer["Passkey"]</button>
|
|
}
|
|
|
|
@if (PageState.Site.AllowRegistration)
|
|
{
|
|
<hr class="app-rule mt-3" />
|
|
<div class="text-center mt-2">
|
|
<NavLink href="@_registerurl">@Localizer["Register"]</NavLink>
|
|
</div>
|
|
}
|
|
break;
|
|
case "ForgotPassword":
|
|
<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" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
|
|
</div>
|
|
<div class="btn-group mt-4 col-12" role="group">
|
|
<button type="button" class="btn btn-primary col-6" @onclick="ForgotPassword">@SharedLocalizer["Send"]</button>
|
|
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="@(() => SetAction("ForgotUsername"))">@Localizer["ForgotUsername"]</button>
|
|
break;
|
|
case "ForgotUsername":
|
|
<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="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 {
|
|
private string _action = "Login";
|
|
private bool _allowexternallogin = false;
|
|
private bool _allowsitelogin = true;
|
|
private bool _allowloginlink = false;
|
|
private bool _allowpasskeys = false;
|
|
|
|
private ElementReference login;
|
|
private bool validated = false;
|
|
private string _username = string.Empty;
|
|
private ElementReference username;
|
|
private string _password = string.Empty;
|
|
private string _passwordtype = "password";
|
|
private string _togglepassword = string.Empty;
|
|
private ElementReference password;
|
|
private bool _remember = false;
|
|
private bool _alwaysremember = false;
|
|
private string _registerurl = string.Empty;
|
|
private string _email = string.Empty;
|
|
private string _code = string.Empty;
|
|
|
|
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
|
|
public override bool? Prerender => true;
|
|
|
|
public override List<Resource> Resources => new List<Resource>()
|
|
{
|
|
new Stylesheet(ModulePath() + "Module.css")
|
|
};
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
try
|
|
{
|
|
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
|
|
_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"));
|
|
_alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
|
|
|
|
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
|
|
{
|
|
_registerurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "");
|
|
}
|
|
else
|
|
{
|
|
_registerurl = NavigateUrl("register");
|
|
}
|
|
|
|
_togglepassword = SharedLocalizer["ShowPassword"];
|
|
|
|
if (PageState.QueryString.ContainsKey("name"))
|
|
{
|
|
_username = PageState.QueryString["name"];
|
|
}
|
|
|
|
if (PageState.QueryString.ContainsKey("token") && !string.IsNullOrEmpty(_username))
|
|
{
|
|
var user = new User();
|
|
user.SiteId = PageState.Site.SiteId;
|
|
user.Username = _username;
|
|
|
|
if (PageState.QueryString.ContainsKey("key"))
|
|
{
|
|
user = await UserService.AddLoginAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
|
|
if (user != null)
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Success.Account.Linked"], MessageType.Info);
|
|
}
|
|
else
|
|
{
|
|
await logger.LogError(LogFunction.Security, "External Login Linkage Failed For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
|
|
}
|
|
_username = "";
|
|
}
|
|
else
|
|
{
|
|
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
|
|
if (user != null)
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
|
|
}
|
|
else
|
|
{
|
|
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Message.Account.NotVerified"], MessageType.Warning);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (PageState.QueryString.ContainsKey("status"))
|
|
{
|
|
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Loading Login {Error}", ex.Message);
|
|
AddModuleMessage(Localizer["Error.LoadLogin"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
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;
|
|
_username = "";
|
|
_password = "";
|
|
_email = "";
|
|
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()
|
|
{
|
|
try
|
|
{
|
|
validated = true;
|
|
var interop = new Interop(JSRuntime);
|
|
if (await interop.FormValid(login))
|
|
{
|
|
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
|
|
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
|
|
|
|
if (_action == "Login")
|
|
{
|
|
_remember = _alwaysremember || _remember;
|
|
user = await UserService.LoginUserAsync(user, hybrid, _remember);
|
|
}
|
|
else
|
|
{
|
|
user = await UserService.VerifyTwoFactorAsync(user, _code);
|
|
}
|
|
|
|
if (user != null && user.IsAuthenticated)
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress);
|
|
|
|
// return url is not specified if user navigated directly to login page
|
|
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
|
|
|
|
if (hybrid)
|
|
{
|
|
// hybrid apps utilize an interactive login
|
|
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
|
|
authstateprovider.NotifyAuthenticationChanged();
|
|
NavigationManager.NavigateTo(NavigateUrl(returnurl, true));
|
|
}
|
|
else
|
|
{
|
|
// post back to the Login page so that the cookies are set correctly
|
|
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(returnurl) };
|
|
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
|
|
await interop.SubmitForm(url, fields);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
|
|
{
|
|
_action = "TwoFactor";
|
|
validated = false;
|
|
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
|
|
}
|
|
else
|
|
{
|
|
if (_action != "TwoFactor")
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
|
|
}
|
|
else
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Performing Login {Error}", ex.Message);
|
|
AddModuleMessage(Localizer["Error.Login"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
private void CancelLogin()
|
|
{
|
|
NavigationManager.NavigateTo(PageState.ReturnUrl);
|
|
}
|
|
|
|
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
|
|
{
|
|
if (!string.IsNullOrEmpty(_username))
|
|
{
|
|
if (await UserService.ForgotPasswordAsync(_username))
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
|
|
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
|
|
}
|
|
else
|
|
{
|
|
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Resetting Password {Error}", ex.Message);
|
|
AddModuleMessage(Localizer["Error.ResetPassword"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
private async Task ForgotUsername()
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(_email))
|
|
{
|
|
if (await UserService.ForgotUsernameAsync(_email))
|
|
{
|
|
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 async Task LoginLink()
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(_email))
|
|
{
|
|
if (await UserService.SendLoginLinkAsync(_email))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Sending Login Link {Error}", ex.Message);
|
|
AddModuleMessage(Localizer["Error.SendLoginLink"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && PageState.QueryString.ContainsKey("options"))
|
|
{
|
|
// user has initiated a passkey login
|
|
try
|
|
{
|
|
var interop = new Interop(JSRuntime);
|
|
var credential = await interop.RequestCredential(WebUtility.UrlDecode(PageState.QueryString["options"]));
|
|
if (!string.IsNullOrEmpty(credential))
|
|
{
|
|
// post back to the Passkey page so that the cookies are set correctly
|
|
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path + "/";
|
|
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "login", credential = credential, returnurl = returnurl };
|
|
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
|
|
await interop.SubmitForm(url, fields);
|
|
}
|
|
else
|
|
{
|
|
await logger.LogError("Passkey Login Was Not Successful");
|
|
AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Passkey Login Was Not Successful");
|
|
AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (firstRender && PageState.User == null && _allowsitelogin && _action == "Login")
|
|
{
|
|
if (string.IsNullOrEmpty(_username))
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// redirect logged in user to specified page
|
|
if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
|
{
|
|
NavigationManager.NavigateTo(PageState.ReturnUrl);
|
|
}
|
|
}
|
|
}
|