396 lines
19 KiB
Plaintext
396 lines
19 KiB
Plaintext
@namespace Oqtane.Modules.Admin.Users
|
|
@using System.Text.RegularExpressions;
|
|
@inherits ModuleBase
|
|
@inject NavigationManager NavigationManager
|
|
@inject IUserService UserService
|
|
@inject IProfileService ProfileService
|
|
@inject ISettingService SettingService
|
|
@inject IFileService FileService
|
|
@inject ITimeZoneService TimeZoneService
|
|
@inject IServiceProvider ServiceProvider
|
|
@inject IStringLocalizer<Edit> Localizer
|
|
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
|
|
|
@if (_initialized)
|
|
{
|
|
<TabStrip>
|
|
<TabPanel Name="Identity" ResourceKey="Identity">
|
|
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
|
|
<div class="container">
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username">Username:</Label>
|
|
<div class="col-sm-9">
|
|
<input id="username" class="form-control" @bind="@_username" readonly />
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">Password:</Label>
|
|
<div class="col-sm-9">
|
|
<div class="input-group">
|
|
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
|
|
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</Label>
|
|
<div class="col-sm-9">
|
|
<div class="input-group">
|
|
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
|
|
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email">Email:</Label>
|
|
<div class="col-sm-9">
|
|
<input id="email" class="form-control" @bind="@_email" />
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Confirmed?</Label>
|
|
<div class="col-sm-9">
|
|
<select id="confirmed" class="form-select" @bind="@_confirmed">
|
|
<option value="True">@SharedLocalizer["Yes"]</option>
|
|
<option value="False">@SharedLocalizer["No"]</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName">Full Name:</Label>
|
|
<div class="col-sm-9">
|
|
<input id="displayname" class="form-control" @bind="@_displayname" />
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
|
|
<div class="col-sm-9">
|
|
<select id="timezone" class="form-select" @bind="@_timezoneid">
|
|
<option value=""><@SharedLocalizer["Not Specified"]></option>
|
|
@foreach (var timezone in _timezones)
|
|
{
|
|
<option value="@timezone.Id">@timezone.DisplayName</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
|
|
{
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted">Deleted?</Label>
|
|
<div class="col-sm-9">
|
|
<select id="isdeleted" class="form-select" @bind="@_isdeleted">
|
|
<option value="True">@SharedLocalizer["Yes"]</option>
|
|
<option value="False">@SharedLocalizer["No"]</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
}
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin">Last Login:</Label>
|
|
<div class="col-sm-9">
|
|
<input id="lastlogin" class="form-control" @bind="@_lastlogin" readonly />
|
|
</div>
|
|
</div>
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress">Last IP Address:</Label>
|
|
<div class="col-sm-9">
|
|
<input id="lastipaddress" class="form-control" @bind="@_lastipaddress" readonly />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
<TabPanel Name="Profile" ResourceKey="Profile">
|
|
<div class="container">
|
|
<div class="row mb-1 align-items-center">
|
|
@foreach (Profile profile in _profiles)
|
|
{
|
|
var p = profile;
|
|
if (p.Category != _category)
|
|
{
|
|
<div class="col text-center pb-2">
|
|
<strong>@p.Category</strong>
|
|
</div>
|
|
_category = p.Category;
|
|
}
|
|
<div class="row mb-1 align-items-center">
|
|
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
|
|
<div class="col-sm-9">
|
|
@if (!string.IsNullOrEmpty(p.Options))
|
|
{
|
|
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
|
|
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option))
|
|
{
|
|
<option value="@option" selected>@option</option>
|
|
}
|
|
else
|
|
{
|
|
<option value="@option">@option</option>
|
|
}
|
|
}
|
|
</select>
|
|
}
|
|
else
|
|
{
|
|
@if (p.Rows == 1)
|
|
{
|
|
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
|
|
}
|
|
else
|
|
{
|
|
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
</TabStrip>
|
|
<br />
|
|
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
|
|
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
|
|
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !_ishost)
|
|
{
|
|
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
|
|
}
|
|
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True")
|
|
{
|
|
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
|
|
}
|
|
<br /><br />
|
|
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
|
|
}
|
|
|
|
@code {
|
|
private List<Models.TimeZone> _timezones;
|
|
private bool _initialized = false;
|
|
private string _passwordrequirements;
|
|
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 string _timezoneid = string.Empty;
|
|
private string _isdeleted;
|
|
private string _lastlogin;
|
|
private string _lastipaddress;
|
|
private bool _ishost = false;
|
|
|
|
private List<Profile> _profiles;
|
|
private Dictionary<string, string> _settings;
|
|
private string _category = string.Empty;
|
|
|
|
private string _createdby;
|
|
private DateTime _createdon;
|
|
private string _modifiedby;
|
|
private DateTime _modifiedon;
|
|
private string _deletedby;
|
|
private DateTime? _deletedon;
|
|
|
|
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
try
|
|
{
|
|
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
|
|
_togglepassword = SharedLocalizer["ShowPassword"];
|
|
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
|
|
_timezones = await TimeZoneService.GetTimeZonesAsync();
|
|
|
|
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
|
|
{
|
|
_userid = UserId;
|
|
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
|
|
if (user != null)
|
|
{
|
|
_username = user.Username;
|
|
_email = user.Email;
|
|
_confirmed = user.EmailConfirmed.ToString();
|
|
_displayname = user.DisplayName;
|
|
_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;
|
|
_createdby = user.CreatedBy;
|
|
_createdon = user.CreatedOn;
|
|
_modifiedby = user.ModifiedBy;
|
|
_modifiedon = user.ModifiedOn;
|
|
_deletedby = user.DeletedBy;
|
|
_deletedon = user.DeletedOn;
|
|
}
|
|
}
|
|
|
|
_initialized = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Loading User {UserId} {Error}", _userid, ex.Message);
|
|
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
private string GetProfileValue(string SettingName, string DefaultValue)
|
|
{
|
|
string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
|
|
if (value.Contains("]"))
|
|
{
|
|
value = value.Substring(value.IndexOf("]") + 1);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private async Task SaveUser()
|
|
{
|
|
try
|
|
{
|
|
if (_username != string.Empty && _email != string.Empty)
|
|
{
|
|
if (_password == _confirm)
|
|
{
|
|
if (ValidateProfiles())
|
|
{
|
|
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
|
|
user.SiteId = PageState.Site.SiteId;
|
|
user.Username = _username;
|
|
user.Password = _password;
|
|
user.Email = _email;
|
|
user.EmailConfirmed = bool.Parse(_confirmed);
|
|
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
|
|
user.TimeZoneId = _timezoneid;
|
|
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
|
|
{
|
|
user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
|
|
}
|
|
|
|
user = await UserService.UpdateUserAsync(user);
|
|
if (user != null)
|
|
{
|
|
await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
|
|
await logger.LogInformation("User Saved {User}", user);
|
|
NavigationManager.NavigateTo(NavigateUrl());
|
|
}
|
|
else
|
|
{
|
|
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", _username, _email, ex.Message);
|
|
AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
private async Task ImpersonateUser()
|
|
{
|
|
try
|
|
{
|
|
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", _username, PageState.User.Username);
|
|
|
|
// post back to the server so that the cookies are set correctly
|
|
var interop = new Interop(JSRuntime);
|
|
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, returnurl = PageState.Alias.Path };
|
|
string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
|
|
await interop.SubmitForm(url, fields);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", _username, ex.Message);
|
|
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
private async Task DeleteUser()
|
|
{
|
|
try
|
|
{
|
|
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _userid != PageState.User.UserId)
|
|
{
|
|
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
|
|
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
|
|
await logger.LogInformation("User Permanently Deleted {User}", user);
|
|
NavigationManager.NavigateTo(NavigateUrl());
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", _userid, ex.Message);
|
|
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
|
|
}
|
|
}
|
|
|
|
private bool ValidateProfiles()
|
|
{
|
|
foreach (Profile profile in _profiles)
|
|
{
|
|
var value = GetProfileValue(profile.Name, string.Empty);
|
|
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
|
|
{
|
|
_settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
|
|
}
|
|
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
|
{
|
|
if (profile.IsRequired && string.IsNullOrEmpty(value))
|
|
{
|
|
AddModuleMessage(string.Format(SharedLocalizer["ProfileRequired"], profile.Title), MessageType.Warning);
|
|
return false;
|
|
}
|
|
if (!string.IsNullOrEmpty(profile.Validation))
|
|
{
|
|
Regex regex = new Regex(profile.Validation);
|
|
bool valid = regex.Match(value).Success;
|
|
if (!valid)
|
|
{
|
|
AddModuleMessage(string.Format(SharedLocalizer["ProfileInvalid"], profile.Title), MessageType.Warning);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void ProfileChanged(ChangeEventArgs e, string SettingName)
|
|
{
|
|
var value = (string)e.Value;
|
|
_settings = SettingService.SetSetting(_settings, SettingName, value);
|
|
}
|
|
|
|
private void TogglePassword()
|
|
{
|
|
if (_passwordtype == "password")
|
|
{
|
|
_passwordtype = "text";
|
|
_togglepassword = SharedLocalizer["HidePassword"];
|
|
}
|
|
else
|
|
{
|
|
_passwordtype = "password";
|
|
_togglepassword = SharedLocalizer["ShowPassword"];
|
|
}
|
|
}
|
|
}
|