Merge pull request #5295 from sbwalker/dev

add time zone support for sites and users
This commit is contained in:
Shaun Walker
2025-05-13 09:24:33 -04:00
committed by GitHub
20 changed files with 417 additions and 172 deletions

View File

@ -54,6 +54,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>(); services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>(); services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<IOutputCacheService, OutputCacheService>(); services.AddScoped<IOutputCacheService, OutputCacheService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -3,6 +3,7 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IUserService UserService @inject IUserService UserService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService @inject ISettingService SettingService
@ -56,6 +57,18 @@
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" /> <input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div> </div>
</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="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button> <button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
@ -77,6 +90,7 @@ else
} }
@code { @code {
private List<Models.TimeZone> _timezones;
private string _passwordrequirements; private string _passwordrequirements;
private string _username = string.Empty; private string _username = string.Empty;
private ElementReference form; private ElementReference form;
@ -87,6 +101,7 @@ else
private string _confirm = string.Empty; private string _confirm = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private bool _userCreated = false; private bool _userCreated = false;
private bool _allowsitelogin = true; private bool _allowsitelogin = true;
@ -96,6 +111,8 @@ else
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezoneid = PageState.Site.TimeZoneId;
} }
protected override void OnParametersSet() protected override void OnParametersSet()
@ -124,6 +141,7 @@ else
Password = _password, Password = _password,
Email = _email, Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname), DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
PhotoFileId = null PhotoFileId = null
}; };
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);

View File

@ -10,6 +10,7 @@
@inject IAliasService AliasService @inject IAliasService AliasService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@ -41,6 +42,18 @@
</select> </select>
</div> </div>
</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="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</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="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label> <Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -133,7 +146,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label> <Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" /> <FileManager FileId="@_logofileid" Filter="@_imagefiles" @ref="_logofilemanager" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -416,9 +429,11 @@
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages; private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private string _name = string.Empty; private string _name = string.Empty;
private string _homepageid = "-"; private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _isdeleted; private string _isdeleted;
private string _sitemap = ""; private string _sitemap = "";
private string _siteguid = ""; private string _siteguid = "";
@ -435,7 +450,7 @@
private Dictionary<string, string> _textEditors = new Dictionary<string, string>(); private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = ""; private string _textEditor = "";
private string _imageFiles = string.Empty; private string _imagefiles = string.Empty;
private string _headcontent = string.Empty; private string _headcontent = string.Empty;
private string _bodycontent = string.Empty; private string _bodycontent = string.Empty;
@ -493,11 +508,13 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null) if (site != null)
{ {
_timezones = await TimeZoneService.GetTimeZonesAsync();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name; _name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null) if (site.HomePageId != null)
{ {
_homepageid = site.HomePageId.Value.ToString(); _homepageid = site.HomePageId.Value.ToString();
@ -531,8 +548,8 @@
_textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName)); _textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName));
} }
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor); _textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); _imagefiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
// page content // page content
_headcontent = site.HeadContent; _headcontent = site.HeadContent;
@ -650,6 +667,7 @@
if (site != null) if (site != null)
{ {
site.Name = _name; site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));

View File

@ -9,6 +9,7 @@
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IFileService FileService @inject IFileService FileService
@inject IFolderService FolderService @inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject IJSRuntime jsRuntime @inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@ -16,9 +17,9 @@
@if (_initialized) @if (_initialized)
{ {
@if (PageState.User != null && photo != null) @if (PageState.User != null && _photo != null)
{ {
<img src="@ImageUrl(photofileid, 400, 400)" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block"> <img src="@ImageUrl(_photofileid, 400, 400)" alt="@_displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
} }
else else
{ {
@ -31,7 +32,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label> <Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly /> <input id="username" class="form-control" @bind="@_username" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -47,17 +48,17 @@
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" /> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@if (allowtwofactor) @if (_allowtwofactor)
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label> <Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required> <select id="twofactor" class="form-select" @bind="@_twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
@ -67,19 +68,31 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label> <Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label> <Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" /> <input id="displayname" class="form-control" @bind="@_displayname" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label> <Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager FileId="@photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" /> <select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@_photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@_folderid" @ref="_filemanager" />
</div> </div>
</div> </div>
</div> </div>
@ -91,17 +104,17 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
@p.Category @p.Category
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@ -150,12 +163,12 @@
@if (p.IsRequired) @if (p.IsRequired)
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" <input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" /> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
} }
else else
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" <input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" /> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
} }
} }
else else
@ -163,12 +176,12 @@
@if (p.IsRequired) @if (p.IsRequired)
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" <input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" /> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
} }
else else
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" <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)" /> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
} }
} }
} }
@ -179,12 +192,12 @@
@if (p.IsRequired) @if (p.IsRequired)
{ {
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" <textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
} }
else else
{ {
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" <textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
} }
} }
else else
@ -192,12 +205,12 @@
@if (p.IsRequired) @if (p.IsRequired)
{ {
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" <textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
} }
else else
{ {
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" <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> @attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
} }
} }
} }
@ -220,11 +233,11 @@
<option value="from">@Localizer["Items.Sent"]</option> <option value="from">@Localizer["Items.Sent"]</option>
</select> </select>
<br /> <br />
@if (filter == "to") @if (_filter == "to")
{ {
@if (notifications.Any()) @if (_notifications.Any())
{ {
<Pager Items="@notifications"> <Pager Items="@_notifications">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
@ -260,15 +273,15 @@
context.Body = context.Body.Replace("\n", ""); context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", ""); context.Body = context.Body.Replace("\r", "");
} }
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; _notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
} }
@if (context.IsRead) @if (context.IsRead)
{ {
@notificationSummary @_notificationSummary
} }
else else
{ {
<b>@notificationSummary</b> <b>@_notificationSummary</b>
} }
</td> </td>
</Detail> </Detail>
@ -285,9 +298,9 @@
} }
else else
{ {
@if (notifications.Any()) @if (_notifications.Any())
{ {
<Pager Items="@notifications"> <Pager Items="@_notifications">
<Header> <Header>
<th style="width: 1px;"></th> <th style="width: 1px;"></th>
<th style="width: 1px;"></th> <th style="width: 1px;"></th>
@ -324,15 +337,15 @@
context.Body = context.Body.Replace("\n", ""); context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", ""); context.Body = context.Body.Replace("\r", "");
} }
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; _notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
} }
@if (context.IsRead) @if (context.IsRead)
{ {
@notificationSummary @_notificationSummary
} }
else else
{ {
<b>@notificationSummary</b> <b>@_notificationSummary</b>
} }
</td> </td>
</Detail> </Detail>
@ -354,29 +367,32 @@
} }
@code { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _passwordrequirements; private string _passwordrequirements;
private string username = string.Empty; private string _username = string.Empty;
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 string confirm = string.Empty; private string _confirm = string.Empty;
private bool allowtwofactor = false; private bool _allowtwofactor = false;
private string twofactor = "False"; private string _twofactor = "False";
private string email = string.Empty; private string _email = string.Empty;
private string displayname = string.Empty; private string _displayname = string.Empty;
private FileManager filemanager; private FileManager _filemanager;
private int folderid = -1; private int _folderid = -1;
private int photofileid = -1; private string _timezoneid = string.Empty;
private File photo = null; private int _photofileid = -1;
private string _ImageFiles = string.Empty; private File _photo = null;
private List<Profile> profiles; private string _imagefiles = string.Empty;
private Dictionary<string, string> userSettings;
private string category = string.Empty;
private string filter = "to"; private List<Profile> _profiles;
private List<Notification> notifications; private Dictionary<string, string> _userSettings;
private string notificationSummary = string.Empty; private string _category = string.Empty;
private string _filter = "to";
private List<Notification> _notifications;
private string _notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
@ -386,17 +402,19 @@
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); _allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
if (PageState.User != null) if (PageState.User != null)
{ {
username = PageState.User.Username; _username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString(); _twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email; _email = PageState.User.Email;
displayname = PageState.User.DisplayName; _displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
if (string.IsNullOrEmpty(email)) if (string.IsNullOrEmpty(_email))
{ {
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning); AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
} }
@ -405,24 +423,24 @@
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null) if (folder != null)
{ {
folderid = folder.FolderId; _folderid = folder.FolderId;
} }
if (PageState.User.PhotoFileId != null) if (PageState.User.PhotoFileId != null)
{ {
photofileid = PageState.User.PhotoFileId.Value; _photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); _photo = await FileService.GetFileAsync(_photofileid);
} }
else else
{ {
photofileid = -1; _photofileid = -1;
photo = null; _photo = null;
} }
userSettings = PageState.User.Settings; _userSettings = PageState.User.Settings;
var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_ImageFiles = SettingService.GetSetting(userSettings, "ImageFiles", Constants.ImageFiles); _imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles);
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
await LoadNotificationsAsync(); await LoadNotificationsAsync();
@ -442,13 +460,13 @@
private async Task LoadNotificationsAsync() private async Task LoadNotificationsAsync()
{ {
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); _notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); _notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@ -460,38 +478,39 @@
{ {
try try
{ {
if (username != string.Empty && email != string.Empty) if (_username != string.Empty && _email != string.Empty)
{ {
if (_password == confirm) if (_password == _confirm)
{ {
if (ValidateProfiles()) if (ValidateProfiles())
{ {
var user = PageState.User; var user = PageState.User;
user.Username = username; user.Username = _username;
user.Password = _password; user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor); user.TwoFactorRequired = bool.Parse(_twofactor);
user.Email = email; user.Email = _email;
user.DisplayName = (displayname == string.Empty ? username : displayname); user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.PhotoFileId = filemanager.GetFileId(); user.TimeZoneId = _timezoneid;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1) if (user.PhotoFileId == -1)
{ {
user.PhotoFileId = null; user.PhotoFileId = null;
} }
if (user.PhotoFileId != null) if (user.PhotoFileId != null)
{ {
photofileid = user.PhotoFileId.Value; _photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); _photo = await FileService.GetFileAsync(_photofileid);
} }
else else
{ {
photofileid = -1; _photofileid = -1;
photo = null; _photo = null;
} }
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(userSettings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(_userSettings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved"); await logger.LogInformation("User Profile Saved");
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
@ -557,12 +576,12 @@
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{ {
userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue); _userSettings = SettingService.SetSetting(_userSettings, profile.Name, profile.DefaultValue);
} }
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@ -594,7 +613,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value); _userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
} }
private async Task Delete(Notification Notification) private async Task Delete(Notification Notification)
@ -624,7 +643,7 @@
private async void FilterChanged(ChangeEventArgs e) private async void FilterChanged(ChangeEventArgs e)
{ {
filter = (string)e.Value; _filter = (string)e.Value;
await LoadNotificationsAsync(); await LoadNotificationsAsync();
StateHasChanged(); StateHasChanged();
} }
@ -634,7 +653,7 @@
try try
{ {
ShowProgressIndicator(); ShowProgressIndicator();
foreach(var Notification in notifications) foreach(var Notification in _notifications)
{ {
if (!Notification.IsDeleted) if (!Notification.IsDeleted)
{ {

View File

@ -5,6 +5,7 @@
@inject IUserService UserService @inject IUserService UserService
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Add> Localizer @inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -12,7 +13,7 @@
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity"> <TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null) @if (_profiles != null)
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -33,6 +34,18 @@
<input id="displayname" class="form-control" @bind="@_displayname" /> <input id="displayname" class="form-control" @bind="@_displayname" />
</div> </div>
</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="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</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="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label> <Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -48,20 +61,20 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
<strong>@p.Category</strong> <strong>@p.Category</strong>
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9"> <div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options)) @if (!string.IsNullOrEmpty(p.Options))
{ {
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))"> <select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
@ -103,14 +116,16 @@
@code { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True"; private string _notify = "True";
private List<Profile> profiles; private List<Profile> _profiles;
private Dictionary<string, string> settings; private Dictionary<string, string> _settings;
private string category = string.Empty; private string _category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -118,8 +133,10 @@
{ {
try try
{ {
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _timezones = await TimeZoneService.GetTimeZonesAsync();
settings = new Dictionary<string, string>(); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true; _initialized = true;
} }
catch (Exception ex) catch (Exception ex)
@ -131,7 +148,7 @@
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue); string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@ -153,6 +170,7 @@
user.Password = ""; // will be auto generated user.Password = ""; // will be auto generated
user.Email = _email; user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null; user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify); user.SuppressNotification = !bool.Parse(_notify);
@ -160,7 +178,7 @@
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Created {User}", user); await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@ -185,12 +203,12 @@
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{ {
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue); _settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
} }
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@ -217,6 +235,6 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value); _settings = SettingService.SetSetting(_settings, SettingName, value);
} }
} }

View File

@ -6,6 +6,7 @@
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IFileService FileService @inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -19,7 +20,7 @@
<div class="row mb-1 align-items-center"> <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"></Label> <Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly /> <input id="username" class="form-control" @bind="@_username" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -35,7 +36,7 @@
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label> <Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" /> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
@ -43,13 +44,25 @@
<div class="row mb-1 align-items-center"> <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"></Label> <Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label> <Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" /> <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="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div> </div>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -57,7 +70,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label> <Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted"> <select id="isdeleted" class="form-select" @bind="@_isdeleted">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
@ -67,13 +80,13 @@
<div class="row mb-1 align-items-center"> <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"></Label> <Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@lastlogin" readonly /> <input id="lastlogin" class="form-control" @bind="@_lastlogin" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"></Label> <Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@lastipaddress" readonly /> <input id="lastipaddress" class="form-control" @bind="@_lastipaddress" readonly />
</div> </div>
</div> </div>
</div> </div>
@ -81,15 +94,15 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
<strong>@p.Category</strong> <strong>@p.Category</strong>
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@ -131,44 +144,46 @@
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost) @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> <button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
} }
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") @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" /> <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 /> <br /><br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
} }
@code { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _passwordrequirements; private string _passwordrequirements;
private int userid; private int _userid;
private string username = string.Empty; private string _username = string.Empty;
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 string confirm = string.Empty; private string _confirm = string.Empty;
private string email = string.Empty; private string _email = string.Empty;
private string displayname = string.Empty; private string _displayname = string.Empty;
private string isdeleted; private string _timezoneid = string.Empty;
private string lastlogin; private string _isdeleted;
private string lastipaddress; private string _lastlogin;
private bool ishost = false; private string _lastipaddress;
private bool _ishost = false;
private List<Profile> profiles; private List<Profile> _profiles;
private Dictionary<string, string> userSettings; private Dictionary<string, string> _settings;
private string category = string.Empty; private string _category = string.Empty;
private string createdby; private string _createdby;
private DateTime createdon; private DateTime _createdon;
private string modifiedby; private string _modifiedby;
private DateTime modifiedon; private DateTime _modifiedon;
private string deletedby; private string _deletedby;
private DateTime? deletedon; private DateTime? _deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -178,29 +193,31 @@
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{ {
userid = UserId; _userid = UserId;
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
username = user.Username; _username = user.Username;
email = user.Email; _email = user.Email;
displayname = user.DisplayName; _displayname = user.DisplayName;
isdeleted = user.IsDeleted.ToString(); _timezoneid = PageState.User.TimeZoneId;
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); _isdeleted = user.IsDeleted.ToString();
lastipaddress = user.LastIPAddress; _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); _lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
userSettings = user.Settings; _settings = user.Settings;
createdby = user.CreatedBy; _createdby = user.CreatedBy;
createdon = user.CreatedOn; _createdon = user.CreatedOn;
modifiedby = user.ModifiedBy; _modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn; _modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy; _deletedby = user.DeletedBy;
deletedon = user.DeletedOn; _deletedon = user.DeletedOn;
} }
} }
@ -208,14 +225,14 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message); await logger.LogError(ex, "Error Loading User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
} }
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@ -227,27 +244,28 @@
{ {
try try
{ {
if (username != string.Empty && email != string.Empty) if (_username != string.Empty && _email != string.Empty)
{ {
if (_password == confirm) if (_password == _confirm)
{ {
if (ValidateProfiles()) if (ValidateProfiles())
{ {
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId; user.SiteId = PageState.Site.SiteId;
user.Username = username; user.Username = _username;
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
} }
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId); await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Saved {User}", user); await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@ -269,7 +287,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", username, email, ex.Message); await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
} }
} }
@ -278,17 +296,17 @@
{ {
try try
{ {
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username); 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 // post back to the server so that the cookies are set correctly
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = username, returnurl = PageState.Alias.Path }; var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, returnurl = PageState.Alias.Path };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", username, ex.Message); await logger.LogError(ex, "Error Impersonating User {Username} {Error}", _username, ex.Message);
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error); AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
} }
} }
@ -297,9 +315,9 @@
{ {
try try
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _userid != PageState.User.UserId)
{ {
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Permanently Deleted {User}", user); await logger.LogInformation("User Permanently Deleted {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
@ -307,19 +325,19 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message); await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
} }
} }
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{ {
userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue); _settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
} }
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@ -346,7 +364,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value); _settings = SettingService.SetSetting(_settings, SettingName, value);
} }
private void TogglePassword() private void TogglePassword()

View File

@ -180,4 +180,10 @@
<data name="Login" xml:space="preserve"> <data name="Login" xml:space="preserve">
<value>Already have account? Login now.</value> <value>Already have account? Login now.</value>
</data> </data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
</root> </root>

View File

@ -447,4 +447,10 @@
<data name="Success.SiteMap.CacheEvicted" xml:space="preserve"> <data name="Success.SiteMap.CacheEvicted" xml:space="preserve">
<value>Site Map Cache Cleared</value> <value>Site Map Cache Cleared</value>
</data> </data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The default time zone for the site</value>
</data>
</root> </root>

View File

@ -246,4 +246,10 @@
<data name="Logout Everywhere" xml:space="preserve"> <data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value> <value>Logout Everywhere</value>
</data> </data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
</root> </root>

View File

@ -156,4 +156,10 @@
<data name="Notify.Text" xml:space="preserve"> <data name="Notify.Text" xml:space="preserve">
<value>Notify?</value> <value>Notify?</value>
</data> </data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
</root> </root>

View File

@ -210,4 +210,10 @@
<data name="Error.User.Impersonate" xml:space="preserve"> <data name="Error.User.Impersonate" xml:space="preserve">
<value>Unable To Impersonate User</value> <value>Unable To Impersonate User</value>
</data> </data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
</root> </root>

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Services
{
/// <summary>
/// Service to store and retrieve <see cref="TimeZone"/> entries
/// </summary>
public interface ITimeZoneService
{
/// <summary>
/// Get the list of time zones
/// </summary>
/// <returns></returns>
Task<List<TimeZone>> GetTimeZonesAsync();
}
}

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class TimeZoneService : ServiceBase, ITimeZoneService
{
public TimeZoneService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("TimeZone");
public async Task<List<TimeZone>> GetTimeZonesAsync()
{
return await GetJsonAsync<List<TimeZone>>($"{Apiurl}");
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class TimeZoneController : Controller
{
public TimeZoneController() {}
// GET: api/<controller>
[HttpGet]
public IEnumerable<Models.TimeZone> Get()
{
return TimeZoneInfo.GetSystemTimeZones()
.Select(item => new Models.TimeZone
{
Id = item.Id,
DisplayName = item.DisplayName
})
.OrderBy(item => item.DisplayName);
}
}
}

View File

@ -135,6 +135,7 @@ namespace Oqtane.Controllers
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId) if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId)
{ {
filtered.Email = user.Email; filtered.Email = user.Email;
filtered.TimeZoneId = user.TimeZoneId;
filtered.PhotoFileId = user.PhotoFileId; filtered.PhotoFileId = user.PhotoFileId;
filtered.LastLoginOn = user.LastLoginOn; filtered.LastLoginOn = user.LastLoginOn;
filtered.LastIPAddress = user.LastIPAddress; filtered.LastIPAddress = user.LastIPAddress;

View File

@ -104,6 +104,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISearchProvider, DatabaseSearchProvider>(); services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
services.AddScoped<IImageService, ImageService>(); services.AddScoped<IImageService, ImageService>();
services.AddScoped<ICookieConsentService, ServerCookieConsentService>(); services.AddScoped<ICookieConsentService, ServerCookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.06.01.03.01")]
public class AddTimeZone : MultiDatabaseMigration
{
public AddTimeZone(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.AddStringColumn("TimeZoneId", 50, true);
var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase);
userEntityBuilder.AddStringColumn("TimeZoneId", 50, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -26,6 +26,11 @@ namespace Oqtane.Models
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// The default time zone for the site
/// </summary>
public string TimeZoneId { get; set; }
/// <summary> /// <summary>
/// Reference to a <see cref="File"/> which has the Logo for this site. /// Reference to a <see cref="File"/> which has the Logo for this site.
/// Should be an image. /// Should be an image.
@ -200,6 +205,7 @@ namespace Oqtane.Models
SiteId = SiteId, SiteId = SiteId,
TenantId = TenantId, TenantId = TenantId,
Name = Name, Name = Name,
TimeZoneId = TimeZoneId,
LogoFileId = LogoFileId, LogoFileId = LogoFileId,
FaviconFileId = FaviconFileId, FaviconFileId = FaviconFileId,
DefaultThemeType = DefaultThemeType, DefaultThemeType = DefaultThemeType,

View File

@ -0,0 +1,10 @@
namespace Oqtane.Models
{
public class TimeZone
{
public string Id { get; set; }
public string DisplayName { get; set; }
}
}

View File

@ -29,6 +29,11 @@ namespace Oqtane.Models
/// </summary> /// </summary>
public string Email { get; set; } public string Email { get; set; }
/// <summary>
/// User time zone
/// </summary>
public string TimeZoneId { get; set; }
/// <summary> /// <summary>
/// Reference to a <see cref="File"/> containing the users photo. /// Reference to a <see cref="File"/> containing the users photo.
/// </summary> /// </summary>