add passkey and login management to User Management
This commit is contained in:
@ -143,15 +143,15 @@ else
|
||||
|
||||
if (PageState.QueryString.ContainsKey("key"))
|
||||
{
|
||||
user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
|
||||
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);
|
||||
await logger.LogInformation(LogFunction.Security, "User 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);
|
||||
await logger.LogError(LogFunction.Security, "User Login Linkage Failed For Username {Username}", _username);
|
||||
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
|
||||
}
|
||||
_username = "";
|
||||
|
||||
@ -654,7 +654,7 @@
|
||||
{
|
||||
if (_allowpasskeys)
|
||||
{
|
||||
_passkeys = await UserService.GetPasskeysAsync();
|
||||
_passkeys = await UserService.GetPasskeysAsync(PageState.User.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -709,7 +709,7 @@
|
||||
|
||||
private async Task DeletePasskey(UserPasskey passkey)
|
||||
{
|
||||
await UserService.DeletePasskeyAsync(passkey.CredentialId);
|
||||
await UserService.DeletePasskeyAsync(PageState.User.UserId, passkey.CredentialId);
|
||||
await GetPasskeys();
|
||||
StateHasChanged();
|
||||
}
|
||||
@ -718,7 +718,7 @@
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_passkeyName))
|
||||
{
|
||||
await UserService.UpdatePasskeyAsync(new UserPasskey { CredentialId = _passkeyId, Name = _passkeyName });
|
||||
await UserService.UpdatePasskeyAsync(new UserPasskey { CredentialId = _passkeyId, Name = _passkeyName, UserId = PageState.User.UserId });
|
||||
await GetPasskeys();
|
||||
_passkeyName = "";
|
||||
StateHasChanged();
|
||||
@ -736,13 +736,13 @@
|
||||
{
|
||||
if (_allowexternallogin)
|
||||
{
|
||||
_logins = await UserService.GetLoginsAsync();
|
||||
_logins = await UserService.GetLoginsAsync(PageState.User.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteLogin(UserLogin login)
|
||||
{
|
||||
await UserService.DeleteLoginAsync(login.Provider, login.Key);
|
||||
await UserService.DeleteLoginAsync(PageState.User.UserId, login.Provider, login.Key);
|
||||
await GetLogins();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@ -103,6 +103,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br /><br />
|
||||
@if (_allowpasskeys)
|
||||
{
|
||||
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
|
||||
@if (_passkeys != null && _passkeys.Count > 0)
|
||||
{
|
||||
<Pager Items="@_passkeys">
|
||||
<Header>
|
||||
<th style="width: 1px;"> </th>
|
||||
<th>@Localizer["Passkey"]</th>
|
||||
</Header>
|
||||
<Row>
|
||||
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeletePasskey(context))" ResourceKey="DeletePasskey" Class="btn btn-danger" Header="Delete Passkey" Message="@string.Format(Localizer["Confirm.Passkey.Delete", context.Name])" /></td>
|
||||
<td>@context.Name</td>
|
||||
</Row>
|
||||
</Pager>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@Localizer["Message.Passkeys.None"]</div>
|
||||
}
|
||||
</Section>
|
||||
<br />
|
||||
}
|
||||
@if (_allowexternallogin)
|
||||
{
|
||||
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
|
||||
@if (_logins != null && _logins.Count > 0)
|
||||
{
|
||||
<Pager Items="@_logins">
|
||||
<Header>
|
||||
<th style="width: 1px;"> </th>
|
||||
<th>@Localizer["Login"]</th>
|
||||
</Header>
|
||||
<Row>
|
||||
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeleteLogin(context))" ResourceKey="DeleteLogin" Class="btn btn-danger" Header="Delete Login" Message="@string.Format(Localizer["Confirm.Login.Delete", context.Name])" /></td>
|
||||
<td>@context.Name</td>
|
||||
</Row>
|
||||
</Pager>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@Localizer["Message.Logins.None"]</div>
|
||||
}
|
||||
</Section>
|
||||
<br />
|
||||
}
|
||||
</TabPanel>
|
||||
<TabPanel Name="Profile" Heading="Profile" ResourceKey="Profile">
|
||||
<div class="container">
|
||||
@ -173,24 +220,30 @@
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<Models.TimeZone> _timezones;
|
||||
private bool _initialized = false;
|
||||
private string _passwordrequirements;
|
||||
private bool _allowpasskeys = false;
|
||||
private bool _allowexternallogin = false;
|
||||
|
||||
private int _userid;
|
||||
private string _username = string.Empty;
|
||||
private string _password = string.Empty;
|
||||
private string _passwordtype = "password";
|
||||
private string _togglepassword = string.Empty;
|
||||
private string _confirm = string.Empty;
|
||||
private string _email = string.Empty;
|
||||
private string _confirmed = string.Empty;
|
||||
private string _displayname = string.Empty;
|
||||
private List<Models.TimeZone> _timezones;
|
||||
private string _timezoneid = string.Empty;
|
||||
private string _isdeleted;
|
||||
private string _lastlogin;
|
||||
private string _lastipaddress;
|
||||
private bool _ishost = false;
|
||||
|
||||
private string _passwordrequirements;
|
||||
private string _password = string.Empty;
|
||||
private string _passwordtype = "password";
|
||||
private string _togglepassword = string.Empty;
|
||||
private string _confirm = string.Empty;
|
||||
private List<UserPasskey> _passkeys;
|
||||
private List<UserLogin> _logins;
|
||||
|
||||
private List<Profile> _profiles;
|
||||
private Dictionary<string, string> _settings;
|
||||
private string _category = string.Empty;
|
||||
@ -208,19 +261,8 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
|
||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
|
||||
foreach (var profile in _profiles)
|
||||
{
|
||||
if (profile.Options.ToLower().StartsWith("entityname:"))
|
||||
{
|
||||
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
|
||||
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
|
||||
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
||||
}
|
||||
}
|
||||
_timezones = TimeZoneService.GetTimeZones();
|
||||
_allowpasskeys = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false") == "true");
|
||||
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
|
||||
|
||||
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
|
||||
{
|
||||
@ -232,13 +274,30 @@
|
||||
_email = user.Email;
|
||||
_confirmed = user.EmailConfirmed.ToString();
|
||||
_displayname = user.DisplayName;
|
||||
_timezones = TimeZoneService.GetTimeZones();
|
||||
_timezoneid = PageState.User.TimeZoneId;
|
||||
_isdeleted = user.IsDeleted.ToString();
|
||||
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
|
||||
_lastipaddress = user.LastIPAddress;
|
||||
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
|
||||
|
||||
_settings = user.Settings;
|
||||
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
|
||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||
await GetPasskeys();
|
||||
await GetLogins();
|
||||
|
||||
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
|
||||
foreach (var profile in _profiles)
|
||||
{
|
||||
if (profile.Options.ToLower().StartsWith("entityname:"))
|
||||
{
|
||||
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
|
||||
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
|
||||
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
||||
}
|
||||
}
|
||||
_settings = user.Settings;
|
||||
|
||||
_createdby = user.CreatedBy;
|
||||
_createdon = user.CreatedOn;
|
||||
_modifiedby = user.ModifiedBy;
|
||||
@ -358,6 +417,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetPasskeys()
|
||||
{
|
||||
if (_allowpasskeys)
|
||||
{
|
||||
_passkeys = await UserService.GetPasskeysAsync(_userid);
|
||||
}
|
||||
}
|
||||
private async Task DeletePasskey(UserPasskey passkey)
|
||||
{
|
||||
await UserService.DeletePasskeyAsync(_userid, passkey.CredentialId);
|
||||
await GetPasskeys();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task GetLogins()
|
||||
{
|
||||
if (_allowexternallogin)
|
||||
{
|
||||
_logins = await UserService.GetLoginsAsync(_userid);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteLogin(UserLogin login)
|
||||
{
|
||||
await UserService.DeleteLoginAsync(_userid, login.Provider, login.Key);
|
||||
await GetLogins();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool ValidateProfiles()
|
||||
{
|
||||
foreach (Profile profile in _profiles)
|
||||
|
||||
@ -222,4 +222,34 @@
|
||||
<data name="Security.Heading" xml:space="preserve">
|
||||
<value>Security</value>
|
||||
</data>
|
||||
<data name="Passkeys.Heading" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="Logins.Heading" xml:space="preserve">
|
||||
<value>Logins</value>
|
||||
</data>
|
||||
<data name="Passkey" xml:space="preserve">
|
||||
<value>Passkey</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
<data name="DeletePasskey.Header" xml:space="preserve">
|
||||
<value>Delete Passkey</value>
|
||||
</data>
|
||||
<data name="DeleteLogin.Header" xml:space="preserve">
|
||||
<value>Delete Login</value>
|
||||
</data>
|
||||
<data name="Confirm.Passkey.Delete" xml:space="preserve">
|
||||
<value>Are You Sure You Wish To Delete {0}?</value>
|
||||
</data>
|
||||
<data name="Confirm.Login.Delete" xml:space="preserve">
|
||||
<value>Are You Sure You Wish To Delete {0}?</value>
|
||||
</data>
|
||||
<data name="Message.Passkeys.None" xml:space="preserve">
|
||||
<value>You Have Not Created Any Passkeys</value>
|
||||
</data>
|
||||
<data name="Message.Logins.None" xml:space="preserve">
|
||||
<value>You Do Not Have Any External Logins For This Site</value>
|
||||
</data>
|
||||
</root>
|
||||
@ -147,17 +147,6 @@ namespace Oqtane.Services
|
||||
/// <returns></returns>
|
||||
Task<string> GetPersonalAccessTokenAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Link an external login with a local user account
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="User"/> we're verifying</param>
|
||||
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
|
||||
/// <param name="type">External Login provider type</param>
|
||||
/// <param name="key">External Login provider key</param>
|
||||
/// <param name="name">External Login provider display name</param>
|
||||
/// <returns></returns>
|
||||
Task<User> LinkUserAsync(User user, string token, string type, string key, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Get password requirements for site
|
||||
/// </summary>
|
||||
@ -177,8 +166,9 @@ namespace Oqtane.Services
|
||||
/// <summary>
|
||||
/// Get passkeys for a user
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<UserPasskey>> GetPasskeysAsync();
|
||||
Task<List<UserPasskey>> GetPasskeysAsync(int userId);
|
||||
|
||||
/// <summary>
|
||||
/// Update a user passkey
|
||||
@ -190,23 +180,37 @@ namespace Oqtane.Services
|
||||
/// <summary>
|
||||
/// Delete a user passkey
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="credentialId"></param>
|
||||
/// <returns></returns>
|
||||
Task DeletePasskeyAsync(byte[] credentialId);
|
||||
Task DeletePasskeyAsync(int userId, byte[] credentialId);
|
||||
|
||||
/// <summary>
|
||||
/// Get logins for a user
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<UserLogin>> GetLoginsAsync();
|
||||
Task<List<UserLogin>> GetLoginsAsync(int userId);
|
||||
|
||||
/// <summary>
|
||||
/// Link an external login with a local user account
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="User"/> we're verifying</param>
|
||||
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
|
||||
/// <param name="type">External Login provider type</param>
|
||||
/// <param name="key">External Login provider key</param>
|
||||
/// <param name="name">External Login provider display name</param>
|
||||
/// <returns></returns>
|
||||
Task<User> AddLoginAsync(User user, string token, string type, string key, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a user login
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
Task DeleteLoginAsync(string provider, string key);
|
||||
Task DeleteLoginAsync(int userId, string provider, string key);
|
||||
}
|
||||
|
||||
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
|
||||
@ -253,7 +257,7 @@ namespace Oqtane.Services
|
||||
|
||||
public async Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent)
|
||||
{
|
||||
return await PostJsonAsync<User>($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user);
|
||||
return await PostJsonAsync<User>($"{Apiurl}/signin?setcookie={setCookie}&persistent={isPersistent}", user);
|
||||
}
|
||||
|
||||
public async Task LogoutUserAsync(User user)
|
||||
@ -306,11 +310,6 @@ namespace Oqtane.Services
|
||||
return await GetStringAsync($"{Apiurl}/personalaccesstoken");
|
||||
}
|
||||
|
||||
public async Task<User> LinkUserAsync(User user, string token, string type, string key, string name)
|
||||
{
|
||||
return await PostJsonAsync<User>($"{Apiurl}/link?token={token}&type={type}&key={key}&name={name}", user);
|
||||
}
|
||||
|
||||
public async Task<string> GetPasswordRequirementsAsync(int siteId)
|
||||
{
|
||||
var requirements = await GetJsonAsync<Dictionary<string, string>>($"{Apiurl}/passwordrequirements/{siteId}");
|
||||
@ -338,9 +337,9 @@ namespace Oqtane.Services
|
||||
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}¬ify={notify}", null);
|
||||
}
|
||||
|
||||
public async Task<List<UserPasskey>> GetPasskeysAsync()
|
||||
public async Task<List<UserPasskey>> GetPasskeysAsync(int userId)
|
||||
{
|
||||
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey");
|
||||
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}");
|
||||
}
|
||||
|
||||
public async Task<UserPasskey> UpdatePasskeyAsync(UserPasskey passkey)
|
||||
@ -348,19 +347,24 @@ namespace Oqtane.Services
|
||||
return await PutJsonAsync<UserPasskey>($"{Apiurl}/passkey", passkey);
|
||||
}
|
||||
|
||||
public async Task DeletePasskeyAsync(byte[] credentialId)
|
||||
public async Task DeletePasskeyAsync(int userId, byte[] credentialId)
|
||||
{
|
||||
await DeleteAsync($"{Apiurl}/passkey?id={Base64Url.EncodeToString(credentialId)}");
|
||||
await DeleteAsync($"{Apiurl}/passkey?id={userId}&credential={Base64Url.EncodeToString(credentialId)}");
|
||||
}
|
||||
|
||||
public async Task<List<UserLogin>> GetLoginsAsync()
|
||||
public async Task<List<UserLogin>> GetLoginsAsync(int userId)
|
||||
{
|
||||
return await GetJsonAsync<List<UserLogin>>($"{Apiurl}/login");
|
||||
return await GetJsonAsync<List<UserLogin>>($"{Apiurl}/login?id={userId}");
|
||||
}
|
||||
|
||||
public async Task DeleteLoginAsync(string provider, string key)
|
||||
public async Task<User> AddLoginAsync(User user, string token, string type, string key, string name)
|
||||
{
|
||||
await DeleteAsync($"{Apiurl}/login?provider={provider}&key={key}");
|
||||
return await PostJsonAsync<User>($"{Apiurl}/login?token={token}&type={type}&key={key}&name={name}", user);
|
||||
}
|
||||
|
||||
public async Task DeleteLoginAsync(int userId, string provider, string key)
|
||||
{
|
||||
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user