mirror of
https://github.com/oqtane/oqtane.framework.git
synced 2025-05-19 11:04:23 +00:00
Merge remote-tracking branch 'oqtane/dev' into dev
This commit is contained in:
commit
6bc1507114
@ -64,6 +64,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="validation" HelpText="Optionally provide a regular expression (RegExp) for validating the value entered" ResourceKey="Validation">Validation: </Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input id="validation" class="form-control" @bind="@_validation" maxlength="200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row mb-1 align-items-center">
|
<div class="row mb-1 align-items-center">
|
||||||
<Label Class="col-sm-3" For="private" HelpText="Should this profile item be visible to all users?" ResourceKey="Private">Private? </Label>
|
<Label Class="col-sm-3" For="private" HelpText="Should this profile item be visible to all users?" ResourceKey="Private">Private? </Label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
@ -97,6 +103,7 @@
|
|||||||
private string _maxlength = "0";
|
private string _maxlength = "0";
|
||||||
private string _defaultvalue = string.Empty;
|
private string _defaultvalue = string.Empty;
|
||||||
private string _options = string.Empty;
|
private string _options = string.Empty;
|
||||||
|
private string _validation = string.Empty;
|
||||||
private string _isrequired = "False";
|
private string _isrequired = "False";
|
||||||
private string _isprivate = "False";
|
private string _isprivate = "False";
|
||||||
private string createdby;
|
private string createdby;
|
||||||
@ -126,6 +133,7 @@
|
|||||||
_maxlength = profile.MaxLength.ToString();
|
_maxlength = profile.MaxLength.ToString();
|
||||||
_defaultvalue = profile.DefaultValue;
|
_defaultvalue = profile.DefaultValue;
|
||||||
_options = profile.Options;
|
_options = profile.Options;
|
||||||
|
_validation = profile.Validation;
|
||||||
_isrequired = profile.IsRequired.ToString();
|
_isrequired = profile.IsRequired.ToString();
|
||||||
_isprivate = profile.IsPrivate.ToString();
|
_isprivate = profile.IsPrivate.ToString();
|
||||||
createdby = profile.CreatedBy;
|
createdby = profile.CreatedBy;
|
||||||
@ -169,6 +177,7 @@
|
|||||||
profile.MaxLength = int.Parse(_maxlength);
|
profile.MaxLength = int.Parse(_maxlength);
|
||||||
profile.DefaultValue = _defaultvalue;
|
profile.DefaultValue = _defaultvalue;
|
||||||
profile.Options = _options;
|
profile.Options = _options;
|
||||||
|
profile.Validation = _validation;
|
||||||
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));
|
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));
|
||||||
profile.IsPrivate = (_isprivate == null ? false : Boolean.Parse(_isprivate));
|
profile.IsPrivate = (_isprivate == null ? false : Boolean.Parse(_isprivate));
|
||||||
if (_profileid != -1)
|
if (_profileid != -1)
|
||||||
|
@ -145,9 +145,9 @@
|
|||||||
</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="enabledSSl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="UseSsl">SSL Enabled: </Label>
|
<Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="UseSsl">SSL Enabled: </Label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select id="enabledSSl" class="form-select" @bind="@_smtpssl" >
|
<select id="smtpssl" class="form-select" @bind="@_smtpssl" >
|
||||||
<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>
|
||||||
@ -183,7 +183,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</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="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SMTPEnabled">Enabled? </Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="smtpenabled" class="form-select" @bind="@_smtpenabled">
|
||||||
|
<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="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
|
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<input id="retention" class="form-control" @bind="@_retention" />
|
<input id="retention" class="form-control" @bind="@_retention" />
|
||||||
@ -354,6 +363,7 @@
|
|||||||
private string _togglesmtppassword = string.Empty;
|
private string _togglesmtppassword = string.Empty;
|
||||||
private string _smtpsender = string.Empty;
|
private string _smtpsender = string.Empty;
|
||||||
private string _smtprelay = "False";
|
private string _smtprelay = "False";
|
||||||
|
private string _smtpenabled = "True";
|
||||||
private string _retention = string.Empty;
|
private string _retention = string.Empty;
|
||||||
private string _pwaisenabled;
|
private string _pwaisenabled;
|
||||||
private int _pwaappiconfileid = -1;
|
private int _pwaappiconfileid = -1;
|
||||||
@ -434,6 +444,7 @@
|
|||||||
_togglesmtppassword = SharedLocalizer["ShowPassword"];
|
_togglesmtppassword = SharedLocalizer["ShowPassword"];
|
||||||
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
|
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
|
||||||
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
|
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
|
||||||
|
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
|
||||||
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
|
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
|
||||||
|
|
||||||
// aliases
|
// aliases
|
||||||
@ -600,7 +611,8 @@
|
|||||||
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
|
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
|
||||||
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
|
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
|
||||||
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
|
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
|
||||||
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
|
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
|
||||||
|
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
|
||||||
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
|
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
|
||||||
|
|
||||||
await logger.LogInformation("Site Settings Saved {Site}", site);
|
await logger.LogInformation("Site Settings Saved {Site}", site);
|
||||||
@ -612,8 +624,8 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
|
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
|
||||||
await interop.ScrollTo(0, 0, "smooth");
|
await ScrollToPageTop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -681,9 +693,8 @@
|
|||||||
|
|
||||||
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
|
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
|
||||||
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
|
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
|
||||||
var interop = new Interop(JSRuntime);
|
await ScrollToPageTop();
|
||||||
await interop.ScrollTo(0, 0, "smooth");
|
}
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await logger.LogError(ex, "Error Testing SMTP Configuration");
|
await logger.LogError(ex, "Error Testing SMTP Configuration");
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="row mb-1 align-items-center">
|
<div class="row mb-1 align-items-center">
|
||||||
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label>
|
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<input id="name" class="form-control" @bind="@_name" disabled />
|
<input id="name" class="form-control" @bind="@_name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-1 align-items-center">
|
<div class="row mb-1 align-items-center">
|
||||||
@ -89,32 +89,32 @@
|
|||||||
private string _themeName = "";
|
private string _themeName = "";
|
||||||
private string _isenabled;
|
private string _isenabled;
|
||||||
private string _name;
|
private string _name;
|
||||||
private string _version;
|
private string _version;
|
||||||
private string _packagename;
|
private string _packagename;
|
||||||
private string _owner = "";
|
private string _owner = "";
|
||||||
private string _url = "";
|
private string _url = "";
|
||||||
private string _contact = "";
|
private string _contact = "";
|
||||||
private string _license = "";
|
private string _license = "";
|
||||||
private string _createdby;
|
private string _createdby;
|
||||||
private DateTime _createdon;
|
private DateTime _createdon;
|
||||||
private string _modifiedby;
|
private string _modifiedby;
|
||||||
private DateTime _modifiedon;
|
private DateTime _modifiedon;
|
||||||
|
|
||||||
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
|
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_themeId = Int32.Parse(PageState.QueryString["id"]);
|
_themeId = Int32.Parse(PageState.QueryString["id"]);
|
||||||
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
|
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
|
||||||
if (theme != null)
|
if (theme != null)
|
||||||
{
|
{
|
||||||
_name = theme.Name;
|
_name = theme.Name;
|
||||||
_isenabled =theme.IsEnabled.ToString();
|
_isenabled =theme.IsEnabled.ToString();
|
||||||
_version = theme.Version;
|
_version = theme.Version;
|
||||||
_packagename = theme.PackageName;
|
_packagename = theme.PackageName;
|
||||||
_owner = theme.Owner;
|
_owner = theme.Owner;
|
||||||
_url = theme.Url;
|
_url = theme.Url;
|
||||||
_contact = theme.Contact;
|
_contact = theme.Contact;
|
||||||
_license = theme.License;
|
_license = theme.License;
|
||||||
@ -142,6 +142,7 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
|
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
|
||||||
|
theme.Name = _name;
|
||||||
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
|
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
|
||||||
await ThemeService.UpdateThemeAsync(theme);
|
await ThemeService.UpdateThemeAsync(theme);
|
||||||
await logger.LogInformation("Theme Saved {Theme}", theme);
|
await logger.LogInformation("Theme Saved {Theme}", theme);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@namespace Oqtane.Modules.Admin.UserProfile
|
@namespace Oqtane.Modules.Admin.UserProfile
|
||||||
|
@using System.Text.RegularExpressions;
|
||||||
@inherits ModuleBase
|
@inherits ModuleBase
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IUserService UserService
|
@inject IUserService UserService
|
||||||
@ -227,140 +228,140 @@ else
|
|||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
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 int photofileid = -1;
|
||||||
private File photo = null;
|
private File photo = null;
|
||||||
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;
|
||||||
private string filter = "to";
|
private string filter = "to";
|
||||||
private List<Notification> notifications;
|
private List<Notification> notifications;
|
||||||
|
|
||||||
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
|
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||||
|
|
||||||
if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
|
if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
|
||||||
{
|
{
|
||||||
allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
|
allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
// get user folder
|
// get user folder
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
|
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
|
||||||
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
|
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
|
||||||
|
|
||||||
await LoadNotificationsAsync();
|
await LoadNotificationsAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
|
AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
|
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
|
||||||
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
|
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(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);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Save()
|
private async Task Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (username != string.Empty && email != string.Empty && ValidateProfiles())
|
if (username != string.Empty && email != string.Empty && ValidateProfiles())
|
||||||
{
|
{
|
||||||
if (_password == confirm)
|
if (_password == confirm)
|
||||||
{
|
{
|
||||||
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.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(settings, PageState.User.UserId);
|
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
|
||||||
await logger.LogInformation("User Profile Saved");
|
await logger.LogInformation("User Profile Saved");
|
||||||
|
|
||||||
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
|
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
|
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);
|
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);
|
||||||
@ -370,6 +371,8 @@ else
|
|||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
|
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ScrollToPageTop();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -389,10 +392,15 @@ else
|
|||||||
}
|
}
|
||||||
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
||||||
{
|
{
|
||||||
if (profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
|
if (valid == true && profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
|
||||||
{
|
{
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
if (valid == true && !string.IsNullOrEmpty(profile.Validation))
|
||||||
|
{
|
||||||
|
Regex regex = new Regex(profile.Validation);
|
||||||
|
valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valid;
|
return valid;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@namespace Oqtane.Modules.Admin.Users
|
@namespace Oqtane.Modules.Admin.Users
|
||||||
|
@using System.Text.RegularExpressions;
|
||||||
@inherits ModuleBase
|
@inherits ModuleBase
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IUserService UserService
|
@inject IUserService UserService
|
||||||
@ -95,8 +96,8 @@
|
|||||||
@code {
|
@code {
|
||||||
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;
|
||||||
@ -121,15 +122,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveUser()
|
private async Task SaveUser()
|
||||||
{
|
{
|
||||||
@ -195,9 +196,17 @@
|
|||||||
{
|
{
|
||||||
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
|
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
|
||||||
}
|
}
|
||||||
if (profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
|
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
||||||
{
|
{
|
||||||
valid = false;
|
if (valid == true && profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
|
||||||
|
{
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
if (valid == true && !string.IsNullOrEmpty(profile.Validation))
|
||||||
|
{
|
||||||
|
Regex regex = new Regex(profile.Validation);
|
||||||
|
valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valid;
|
return valid;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@namespace Oqtane.Modules.Admin.Users
|
@namespace Oqtane.Modules.Admin.Users
|
||||||
|
@using System.Text.RegularExpressions;
|
||||||
@inherits ModuleBase
|
@inherits ModuleBase
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IUserService UserService
|
@inject IUserService UserService
|
||||||
@ -148,124 +149,124 @@ else
|
|||||||
<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 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 FileManager filemanager;
|
private FileManager filemanager;
|
||||||
private int photofileid = -1;
|
private int photofileid = -1;
|
||||||
private File photo = null;
|
private File photo = null;
|
||||||
private string isdeleted;
|
private string isdeleted;
|
||||||
private string lastlogin;
|
private string lastlogin;
|
||||||
private string lastipaddress;
|
private string lastipaddress;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (PageState.QueryString.ContainsKey("id"))
|
if (PageState.QueryString.ContainsKey("id"))
|
||||||
{
|
{
|
||||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
_togglepassword = SharedLocalizer["ShowPassword"];
|
||||||
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
|
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
|
||||||
userid = Int32.Parse(PageState.QueryString["id"]);
|
userid = Int32.Parse(PageState.QueryString["id"]);
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
isdeleted = user.IsDeleted.ToString();
|
isdeleted = user.IsDeleted.ToString();
|
||||||
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
|
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
|
||||||
lastipaddress = user.LastIPAddress;
|
lastipaddress = user.LastIPAddress;
|
||||||
|
|
||||||
settings = await SettingService.GetUserSettingsAsync(user.UserId);
|
settings = await SettingService.GetUserSettingsAsync(user.UserId);
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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(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);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveUser()
|
private async Task SaveUser()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (username != string.Empty && email != string.Empty && ValidateProfiles())
|
if (username != string.Empty && email != string.Empty && ValidateProfiles())
|
||||||
{
|
{
|
||||||
if (_password == confirm)
|
if (_password == confirm)
|
||||||
{
|
{
|
||||||
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.PhotoFileId = null;
|
user.PhotoFileId = null;
|
||||||
user.PhotoFileId = filemanager.GetFileId();
|
user.PhotoFileId = filemanager.GetFileId();
|
||||||
if (user.PhotoFileId == -1)
|
if (user.PhotoFileId == -1)
|
||||||
{
|
{
|
||||||
user.PhotoFileId = null;
|
user.PhotoFileId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(settings, 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());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
|
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -293,9 +294,17 @@ else
|
|||||||
{
|
{
|
||||||
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
|
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
|
||||||
}
|
}
|
||||||
if (profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
|
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
||||||
{
|
{
|
||||||
valid = false;
|
if (valid == true && profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
|
||||||
|
{
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
if (valid == true && !string.IsNullOrEmpty(profile.Validation))
|
||||||
|
{
|
||||||
|
Regex regex = new Regex(profile.Validation);
|
||||||
|
valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valid;
|
return valid;
|
||||||
|
@ -53,7 +53,6 @@
|
|||||||
|
|
||||||
public override List<Resource> Resources => new List<Resource>()
|
public override List<Resource> Resources => new List<Resource>()
|
||||||
{
|
{
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" },
|
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
|
||||||
};
|
};
|
||||||
|
@ -15,11 +15,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public override List<Resource> Resources => new List<Resource>()
|
|
||||||
{
|
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
|
|
||||||
};
|
|
||||||
|
|
||||||
private string content = "";
|
private string content = "";
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Oqtane.Documentation;
|
using Oqtane.Documentation;
|
||||||
using Oqtane.Models;
|
using Oqtane.Models;
|
||||||
|
using Oqtane.Shared;
|
||||||
|
|
||||||
namespace Oqtane.Modules.HtmlText
|
namespace Oqtane.Modules.HtmlText
|
||||||
{
|
{
|
||||||
@ -13,7 +15,11 @@ namespace Oqtane.Modules.HtmlText
|
|||||||
Version = "1.0.1",
|
Version = "1.0.1",
|
||||||
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
|
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
|
||||||
ReleaseVersions = "1.0.0,1.0.1",
|
ReleaseVersions = "1.0.0,1.0.1",
|
||||||
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"
|
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client",
|
||||||
|
Resources = new List<Resource>()
|
||||||
|
{
|
||||||
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -291,6 +291,12 @@ namespace Oqtane.Modules
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ScrollToPageTop()
|
||||||
|
{
|
||||||
|
var interop = new Interop(JSRuntime);
|
||||||
|
await interop.ScrollTo(0, 0, "smooth");
|
||||||
|
}
|
||||||
|
|
||||||
// logging methods
|
// logging methods
|
||||||
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
|
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
|
||||||
{
|
{
|
||||||
|
@ -183,4 +183,10 @@
|
|||||||
<data name="Private.Text" xml:space="preserve">
|
<data name="Private.Text" xml:space="preserve">
|
||||||
<value>Private? </value>
|
<value>Private? </value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Validation.HelpText" xml:space="preserve">
|
||||||
|
<value>Optionally provide a regular expression (RegExp) for validating the value entered</value>
|
||||||
|
</data>
|
||||||
|
<data name="Validation.Text" xml:space="preserve">
|
||||||
|
<value>Validation:</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -369,4 +369,10 @@
|
|||||||
<data name="PageContent.Heading" xml:space="preserve">
|
<data name="PageContent.Heading" xml:space="preserve">
|
||||||
<value>Page Content</value>
|
<value>Page Content</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SMTPEnabled.HelpText" xml:space="preserve">
|
||||||
|
<value>Specify if SMTP is enabled for this site</value>
|
||||||
|
</data>
|
||||||
|
<data name="SMTPEnabled.Text" xml:space="preserve">
|
||||||
|
<value>Enabled?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -1,3 +1,4 @@
|
|||||||
|
@using System.Net
|
||||||
@namespace Oqtane.Themes.Controls
|
@namespace Oqtane.Themes.Controls
|
||||||
@inherits ThemeControlBase
|
@inherits ThemeControlBase
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@ -501,7 +502,7 @@
|
|||||||
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule);
|
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule);
|
||||||
if (module != null)
|
if (module != null)
|
||||||
{
|
{
|
||||||
NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, module.ModuleId, "Index", ""));
|
NavigationManager.NavigateTo(EditUrl("admin", module.ModuleId, "Index", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Add":
|
case "Add":
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Oqtane.Documentation;
|
using Oqtane.Documentation;
|
||||||
using Oqtane.Models;
|
using Oqtane.Models;
|
||||||
|
using Oqtane.Shared;
|
||||||
|
|
||||||
namespace Oqtane.Themes.OqtaneTheme
|
namespace Oqtane.Themes.OqtaneTheme
|
||||||
{
|
{
|
||||||
@ -11,7 +13,13 @@ namespace Oqtane.Themes.OqtaneTheme
|
|||||||
Name = "Oqtane Theme",
|
Name = "Oqtane Theme",
|
||||||
Version = "1.0.0",
|
Version = "1.0.0",
|
||||||
ThemeSettingsType = "Oqtane.Themes.OqtaneTheme.ThemeSettings, Oqtane.Client",
|
ThemeSettingsType = "Oqtane.Themes.OqtaneTheme.ThemeSettings, Oqtane.Client",
|
||||||
ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client"
|
ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client",
|
||||||
|
Resources = new List<Resource>()
|
||||||
|
{
|
||||||
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.2.0/cyborg/bootstrap.min.css", Integrity = "sha512-d6pZJl/sNcj0GFkp4kTjXtPE14deuUsOqFQtxkj0KyBJQl+4e0qsEyuIDcNqrYuGoauAW3sWyDCQp49mhF4Syw==", CrossOrigin = "anonymous" },
|
||||||
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
|
||||||
|
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/js/bootstrap.bundle.min.js", Integrity = "sha512-9GacT4119eY3AcosfWtHMsT5JyZudrexyEVzTBWV3viP/YfB9e2pEy3N7WXL3SV6ASXpTU0vzzSxsbfsuUH4sQ==", CrossOrigin = "anonymous" }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,14 +110,6 @@
|
|||||||
|
|
||||||
public override string Panes => PaneNames.Default + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width,Footer";
|
public override string Panes => PaneNames.Default + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width,Footer";
|
||||||
|
|
||||||
public override List<Resource> Resources => new List<Resource>()
|
|
||||||
{
|
|
||||||
// obtained from https://cdnjs.com/libraries
|
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.2.0/cyborg/bootstrap.min.css", Integrity = "sha512-d6pZJl/sNcj0GFkp4kTjXtPE14deuUsOqFQtxkj0KyBJQl+4e0qsEyuIDcNqrYuGoauAW3sWyDCQp49mhF4Syw==", CrossOrigin = "anonymous" },
|
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" },
|
|
||||||
new Resource { ResourceType = ResourceType.Script, Bundle = "Bootstrap", Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/js/bootstrap.bundle.min.js", Integrity = "sha512-9GacT4119eY3AcosfWtHMsT5JyZudrexyEVzTBWV3viP/YfB9e2pEy3N7WXL3SV6ASXpTU0vzzSxsbfsuUH4sQ==", CrossOrigin = "anonymous" }
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool _login = true;
|
private bool _login = true;
|
||||||
private bool _register = true;
|
private bool _register = true;
|
||||||
private bool _footer = false;
|
private bool _footer = false;
|
||||||
|
@ -531,10 +531,13 @@
|
|||||||
{
|
{
|
||||||
foreach (var resource in resources)
|
foreach (var resource in resources)
|
||||||
{
|
{
|
||||||
if (!resource.Url.Contains("://") && resource.Url.StartsWith("~/"))
|
if (resource.Url.StartsWith("~"))
|
||||||
{
|
{
|
||||||
// create local path
|
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
|
||||||
resource.Url = resource.Url.Replace("~", alias.BaseUrl + "/" + type + "/" + name);
|
}
|
||||||
|
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
|
||||||
|
{
|
||||||
|
resource.Url = alias.BaseUrl + resource.Url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure resource does not exist already
|
// ensure resource does not exist already
|
||||||
|
@ -96,6 +96,30 @@
|
|||||||
{
|
{
|
||||||
await InjectScripts(PageState.Page.BodyContent, ResourceLocation.Body);
|
await InjectScripts(PageState.Page.BodyContent, ResourceLocation.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PageState.Page.Resources != null && PageState.Page.Resources.Exists(item => item.ResourceType == ResourceType.Script))
|
||||||
|
{
|
||||||
|
var interop = new Interop(JSRuntime);
|
||||||
|
var scripts = new List<object>();
|
||||||
|
var inline = 0;
|
||||||
|
foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level != ResourceLevel.Site))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(resource.Url))
|
||||||
|
{
|
||||||
|
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url;
|
||||||
|
scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module, location = resource.Location.ToString().ToLower() });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inline += 1;
|
||||||
|
await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scripts.Any())
|
||||||
|
{
|
||||||
|
await interop.IncludeScripts(scripts.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +33,10 @@ namespace Oqtane.Controllers
|
|||||||
private readonly IHttpContextAccessor _accessor;
|
private readonly IHttpContextAccessor _accessor;
|
||||||
private readonly IAliasRepository _aliases;
|
private readonly IAliasRepository _aliases;
|
||||||
private readonly ILogger<InstallationController> _filelogger;
|
private readonly ILogger<InstallationController> _filelogger;
|
||||||
|
private readonly ITenantManager _tenantManager;
|
||||||
|
private readonly ServerStateManager _serverState;
|
||||||
|
|
||||||
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger)
|
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, ServerStateManager serverState)
|
||||||
{
|
{
|
||||||
_configManager = configManager;
|
_configManager = configManager;
|
||||||
_installationManager = installationManager;
|
_installationManager = installationManager;
|
||||||
@ -44,6 +46,8 @@ namespace Oqtane.Controllers
|
|||||||
_accessor = accessor;
|
_accessor = accessor;
|
||||||
_aliases = aliases;
|
_aliases = aliases;
|
||||||
_filelogger = filelogger;
|
_filelogger = filelogger;
|
||||||
|
_tenantManager = tenantManager;
|
||||||
|
_serverState = serverState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/<controller>
|
// POST api/<controller>
|
||||||
@ -115,7 +119,9 @@ namespace Oqtane.Controllers
|
|||||||
|
|
||||||
private List<ClientAssembly> GetAssemblyList()
|
private List<ClientAssembly> GetAssemblyList()
|
||||||
{
|
{
|
||||||
return _cache.GetOrCreate("assemblieslist", entry =>
|
int siteId = _tenantManager.GetAlias().SiteId;
|
||||||
|
|
||||||
|
return _cache.GetOrCreate($"assemblieslist:{siteId}", entry =>
|
||||||
{
|
{
|
||||||
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||||
var assemblyList = new List<ClientAssembly>();
|
var assemblyList = new List<ClientAssembly>();
|
||||||
@ -127,77 +133,42 @@ namespace Oqtane.Controllers
|
|||||||
hashfilename = false;
|
hashfilename = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get list of assemblies which should be downloaded to client
|
// get site assemblies which should be downloaded to client
|
||||||
var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies();
|
var assemblies = _serverState.GetServerState(siteId).Assemblies;
|
||||||
var list = assemblies.Select(a => a.GetName().Name).ToList();
|
|
||||||
|
|
||||||
// populate assemblies
|
// populate assembly list
|
||||||
for (int i = 0; i < list.Count; i++)
|
foreach (var assembly in assemblies)
|
||||||
{
|
{
|
||||||
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, list[i] + ".dll"), hashfilename));
|
if (assembly != Constants.ClientId)
|
||||||
|
{
|
||||||
|
var filepath = Path.Combine(binFolder, assembly) + ".dll";
|
||||||
|
if (System.IO.File.Exists(filepath))
|
||||||
|
{
|
||||||
|
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert satellite assemblies at beginning of list
|
// insert satellite assemblies at beginning of list
|
||||||
foreach (var culture in _localizationManager.GetInstalledCultures())
|
foreach (var culture in _localizationManager.GetInstalledCultures())
|
||||||
{
|
{
|
||||||
var assembliesFolderPath = Path.Combine(binFolder, culture);
|
if (culture != Constants.DefaultCulture)
|
||||||
if (culture == Constants.DefaultCulture)
|
|
||||||
{
|
{
|
||||||
continue;
|
var assembliesFolderPath = Path.Combine(binFolder, culture);
|
||||||
}
|
if (Directory.Exists(assembliesFolderPath))
|
||||||
|
|
||||||
if (Directory.Exists(assembliesFolderPath))
|
|
||||||
{
|
|
||||||
foreach (var resourceFile in Directory.EnumerateFiles(assembliesFolderPath))
|
|
||||||
{
|
{
|
||||||
assemblyList.Insert(0, new ClientAssembly(resourceFile, hashfilename));
|
foreach (var assembly in assemblies)
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert module and theme dependencies at beginning of list
|
|
||||||
foreach (var assembly in assemblies)
|
|
||||||
{
|
|
||||||
foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModule))))
|
|
||||||
{
|
|
||||||
var instance = Activator.CreateInstance(type) as IModule;
|
|
||||||
foreach (string name in instance.ModuleDefinition.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Reverse())
|
|
||||||
{
|
|
||||||
var filepath = Path.Combine(binFolder, name.ToLower().EndsWith(".dll") ? name : name + ".dll");
|
|
||||||
if (System.IO.File.Exists(filepath))
|
|
||||||
{
|
{
|
||||||
if (!assemblyList.Exists(item => item.FilePath == filepath))
|
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
|
||||||
|
if (System.IO.File.Exists(filepath))
|
||||||
{
|
{
|
||||||
assemblyList.Insert(0, new ClientAssembly(filepath, hashfilename));
|
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
_filelogger.LogError(Utilities.LogMessage(this, $"Module {instance.ModuleDefinition.ModuleDefinitionName} Dependency {name}.dll Does Not Exist"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
foreach (var type in assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme))))
|
|
||||||
{
|
|
||||||
var instance = Activator.CreateInstance(type) as ITheme;
|
|
||||||
foreach (string name in instance.Theme.Dependencies.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Reverse())
|
|
||||||
{
|
{
|
||||||
var filepath = Path.Combine(binFolder, name.ToLower().EndsWith(".dll") ? name : name + ".dll");
|
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
|
||||||
if (System.IO.File.Exists(filepath))
|
|
||||||
{
|
|
||||||
if (!assemblyList.Exists(item => item.FilePath == filepath))
|
|
||||||
{
|
|
||||||
assemblyList.Insert(0, new ClientAssembly(filepath, hashfilename));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_filelogger.LogError(Utilities.LogMessage(this, $"Theme {instance.Theme.ThemeName} Dependency {name}.dll Does Not Exist"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,21 +211,24 @@ namespace Oqtane.Controllers
|
|||||||
{
|
{
|
||||||
foreach (var assembly in assemblies)
|
foreach (var assembly in assemblies)
|
||||||
{
|
{
|
||||||
if (System.IO.File.Exists(assembly.FilePath))
|
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
|
||||||
{
|
{
|
||||||
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
|
if (System.IO.File.Exists(assembly.FilePath))
|
||||||
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
|
|
||||||
{
|
{
|
||||||
filestream.CopyTo(entrystream);
|
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
|
||||||
|
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
|
||||||
|
{
|
||||||
|
filestream.CopyTo(entrystream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
|
||||||
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
|
if (System.IO.File.Exists(pdb))
|
||||||
if (System.IO.File.Exists(pdb))
|
|
||||||
{
|
|
||||||
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
|
|
||||||
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
|
|
||||||
{
|
{
|
||||||
filestream.CopyTo(entrystream);
|
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
|
||||||
|
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
|
||||||
|
{
|
||||||
|
filestream.CopyTo(entrystream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +200,8 @@ namespace Oqtane.Controllers
|
|||||||
case EntityNames.Tenant:
|
case EntityNames.Tenant:
|
||||||
case EntityNames.ModuleDefinition:
|
case EntityNames.ModuleDefinition:
|
||||||
case EntityNames.Host:
|
case EntityNames.Host:
|
||||||
|
case EntityNames.Job:
|
||||||
|
case EntityNames.Theme:
|
||||||
if (permissionName == PermissionNames.Edit)
|
if (permissionName == PermissionNames.Edit)
|
||||||
{
|
{
|
||||||
authorized = User.IsInRole(RoleNames.Host);
|
authorized = User.IsInRole(RoleNames.Host);
|
||||||
@ -262,6 +264,8 @@ namespace Oqtane.Controllers
|
|||||||
case EntityNames.Tenant:
|
case EntityNames.Tenant:
|
||||||
case EntityNames.ModuleDefinition:
|
case EntityNames.ModuleDefinition:
|
||||||
case EntityNames.Host:
|
case EntityNames.Host:
|
||||||
|
case EntityNames.Job:
|
||||||
|
case EntityNames.Theme:
|
||||||
filter = !User.IsInRole(RoleNames.Host);
|
filter = !User.IsInRole(RoleNames.Host);
|
||||||
break;
|
break;
|
||||||
case EntityNames.Site:
|
case EntityNames.Site:
|
||||||
|
@ -61,6 +61,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||||||
services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
|
services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
|
||||||
services.AddSingleton<AutoValidateAntiforgeryTokenFilter>();
|
services.AddSingleton<AutoValidateAntiforgeryTokenFilter>();
|
||||||
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
|
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
|
||||||
|
services.AddSingleton<ServerStateManager>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,119 +42,127 @@ namespace Oqtane.Infrastructure
|
|||||||
// get site settings
|
// get site settings
|
||||||
List<Setting> sitesettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToList();
|
List<Setting> sitesettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToList();
|
||||||
Dictionary<string, string> settings = GetSettings(sitesettings);
|
Dictionary<string, string> settings = GetSettings(sitesettings);
|
||||||
if (settings.ContainsKey("SMTPHost") && settings["SMTPHost"] != "" &&
|
if (!settings.ContainsKey("SMTPEnabled") || settings["SMTPEnabled"] == "True")
|
||||||
settings.ContainsKey("SMTPPort") && settings["SMTPPort"] != "" &&
|
|
||||||
settings.ContainsKey("SMTPSSL") && settings["SMTPSSL"] != "" &&
|
|
||||||
settings.ContainsKey("SMTPSender") && settings["SMTPSender"] != "")
|
|
||||||
{
|
{
|
||||||
// construct SMTP Client
|
if (settings.ContainsKey("SMTPHost") && settings["SMTPHost"] != "" &&
|
||||||
var client = new SmtpClient()
|
settings.ContainsKey("SMTPPort") && settings["SMTPPort"] != "" &&
|
||||||
|
settings.ContainsKey("SMTPSSL") && settings["SMTPSSL"] != "" &&
|
||||||
|
settings.ContainsKey("SMTPSender") && settings["SMTPSender"] != "")
|
||||||
{
|
{
|
||||||
DeliveryMethod = SmtpDeliveryMethod.Network,
|
// construct SMTP Client
|
||||||
UseDefaultCredentials = false,
|
var client = new SmtpClient()
|
||||||
Host = settings["SMTPHost"],
|
|
||||||
Port = int.Parse(settings["SMTPPort"]),
|
|
||||||
EnableSsl = bool.Parse(settings["SMTPSSL"])
|
|
||||||
};
|
|
||||||
if (settings["SMTPUsername"] != "" && settings["SMTPPassword"] != "")
|
|
||||||
{
|
|
||||||
client.Credentials = new NetworkCredential(settings["SMTPUsername"], settings["SMTPPassword"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterate through undelivered notifications
|
|
||||||
int sent = 0;
|
|
||||||
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
|
|
||||||
foreach (Notification notification in notifications)
|
|
||||||
{
|
|
||||||
// get sender and receiver information from user object if not provided
|
|
||||||
if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null)
|
|
||||||
{
|
{
|
||||||
var user = userRepository.GetUser(notification.FromUserId.Value);
|
DeliveryMethod = SmtpDeliveryMethod.Network,
|
||||||
if (user != null)
|
UseDefaultCredentials = false,
|
||||||
{
|
Host = settings["SMTPHost"],
|
||||||
notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail;
|
Port = int.Parse(settings["SMTPPort"]),
|
||||||
notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName;
|
EnableSsl = bool.Parse(settings["SMTPSSL"])
|
||||||
}
|
};
|
||||||
}
|
if (settings["SMTPUsername"] != "" && settings["SMTPPassword"] != "")
|
||||||
if ((string.IsNullOrEmpty(notification.ToEmail) || string.IsNullOrEmpty(notification.ToDisplayName)) && notification.ToUserId != null)
|
|
||||||
{
|
{
|
||||||
var user = userRepository.GetUser(notification.ToUserId.Value);
|
client.Credentials = new NetworkCredential(settings["SMTPUsername"], settings["SMTPPassword"]);
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
notification.ToEmail = (string.IsNullOrEmpty(notification.ToEmail)) ? user.Email : notification.ToEmail;
|
|
||||||
notification.ToDisplayName = (string.IsNullOrEmpty(notification.ToDisplayName)) ? user.DisplayName : notification.ToDisplayName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate recipient
|
// iterate through undelivered notifications
|
||||||
if (string.IsNullOrEmpty(notification.ToEmail))
|
int sent = 0;
|
||||||
|
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
|
||||||
|
foreach (Notification notification in notifications)
|
||||||
{
|
{
|
||||||
log += "Recipient Missing For NotificationId: " + notification.NotificationId + "<br />";
|
// get sender and receiver information from user object if not provided
|
||||||
notification.IsDeleted = true;
|
if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null)
|
||||||
notificationRepository.UpdateNotification(notification);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MailMessage mailMessage = new MailMessage();
|
|
||||||
|
|
||||||
// sender
|
|
||||||
if (settings.ContainsKey("SMTPRelay") && settings["SMTPRelay"] == "True" && !string.IsNullOrEmpty(notification.FromEmail))
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(notification.FromDisplayName))
|
var user = userRepository.GetUser(notification.FromUserId.Value);
|
||||||
|
if (user != null)
|
||||||
{
|
{
|
||||||
mailMessage.From = new MailAddress(notification.FromEmail, notification.FromDisplayName);
|
notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail;
|
||||||
|
notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((string.IsNullOrEmpty(notification.ToEmail) || string.IsNullOrEmpty(notification.ToDisplayName)) && notification.ToUserId != null)
|
||||||
|
{
|
||||||
|
var user = userRepository.GetUser(notification.ToUserId.Value);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
notification.ToEmail = (string.IsNullOrEmpty(notification.ToEmail)) ? user.Email : notification.ToEmail;
|
||||||
|
notification.ToDisplayName = (string.IsNullOrEmpty(notification.ToDisplayName)) ? user.DisplayName : notification.ToDisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate recipient
|
||||||
|
if (string.IsNullOrEmpty(notification.ToEmail))
|
||||||
|
{
|
||||||
|
log += "Recipient Missing For NotificationId: " + notification.NotificationId + "<br />";
|
||||||
|
notification.IsDeleted = true;
|
||||||
|
notificationRepository.UpdateNotification(notification);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MailMessage mailMessage = new MailMessage();
|
||||||
|
|
||||||
|
// sender
|
||||||
|
if (settings.ContainsKey("SMTPRelay") && settings["SMTPRelay"] == "True" && !string.IsNullOrEmpty(notification.FromEmail))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(notification.FromDisplayName))
|
||||||
|
{
|
||||||
|
mailMessage.From = new MailAddress(notification.FromEmail, notification.FromDisplayName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mailMessage.From = new MailAddress(notification.FromEmail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mailMessage.From = new MailAddress(notification.FromEmail);
|
mailMessage.From = new MailAddress(settings["SMTPSender"], (!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipient
|
||||||
|
if (!string.IsNullOrEmpty(notification.ToDisplayName))
|
||||||
|
{
|
||||||
|
mailMessage.To.Add(new MailAddress(notification.ToEmail, notification.ToDisplayName));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mailMessage.To.Add(new MailAddress(notification.ToEmail));
|
||||||
|
}
|
||||||
|
|
||||||
|
// subject
|
||||||
|
mailMessage.Subject = notification.Subject;
|
||||||
|
|
||||||
|
//body
|
||||||
|
mailMessage.Body = notification.Body;
|
||||||
|
|
||||||
|
// encoding
|
||||||
|
mailMessage.SubjectEncoding = System.Text.Encoding.UTF8;
|
||||||
|
mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
|
||||||
|
mailMessage.IsBodyHtml = true;
|
||||||
|
|
||||||
|
// send mail
|
||||||
|
try
|
||||||
|
{
|
||||||
|
client.Send(mailMessage);
|
||||||
|
sent++;
|
||||||
|
notification.IsDelivered = true;
|
||||||
|
notification.DeliveredOn = DateTime.UtcNow;
|
||||||
|
notificationRepository.UpdateNotification(notification);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// error
|
||||||
|
log += ex.Message + "<br />";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
mailMessage.From = new MailAddress(settings["SMTPSender"], (!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// recipient
|
|
||||||
if (!string.IsNullOrEmpty(notification.ToDisplayName))
|
|
||||||
{
|
|
||||||
mailMessage.To.Add(new MailAddress(notification.ToEmail, notification.ToDisplayName));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mailMessage.To.Add(new MailAddress(notification.ToEmail));
|
|
||||||
}
|
|
||||||
|
|
||||||
// subject
|
|
||||||
mailMessage.Subject = notification.Subject;
|
|
||||||
|
|
||||||
//body
|
|
||||||
mailMessage.Body = notification.Body;
|
|
||||||
|
|
||||||
// encoding
|
|
||||||
mailMessage.SubjectEncoding = System.Text.Encoding.UTF8;
|
|
||||||
mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
|
|
||||||
|
|
||||||
// send mail
|
|
||||||
try
|
|
||||||
{
|
|
||||||
client.Send(mailMessage);
|
|
||||||
sent++;
|
|
||||||
notification.IsDelivered = true;
|
|
||||||
notification.DeliveredOn = DateTime.UtcNow;
|
|
||||||
notificationRepository.UpdateNotification(notification);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// error
|
|
||||||
log += ex.Message + "<br />";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
log += "Notifications Delivered: " + sent + "<br />";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log += "SMTP Not Configured Properly In Site Settings - Host, Port, SSL, And Sender Are All Required" + "<br />";
|
||||||
}
|
}
|
||||||
log += "Notifications Delivered: " + sent + "<br />";
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
log += "SMTP Not Configured Properly In Site Settings - Host, Port, SSL, And Sender Are All Required" + "<br />";
|
log += "SMTP Disabled In Site Settings" + "<br />";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
Oqtane.Server/Infrastructure/ServerState.cs
Normal file
12
Oqtane.Server/Infrastructure/ServerState.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Oqtane.Models;
|
||||||
|
|
||||||
|
namespace Oqtane.Infrastructure
|
||||||
|
{
|
||||||
|
public class ServerState
|
||||||
|
{
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
public List<string> Assemblies { get; set; } = new List<string>();
|
||||||
|
public List<Resource>Scripts { get; set; } = new List<Resource>();
|
||||||
|
}
|
||||||
|
}
|
49
Oqtane.Server/Infrastructure/ServerStateManager.cs
Normal file
49
Oqtane.Server/Infrastructure/ServerStateManager.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Oqtane.Models;
|
||||||
|
|
||||||
|
namespace Oqtane.Infrastructure
|
||||||
|
{
|
||||||
|
// singleton
|
||||||
|
public class ServerStateManager
|
||||||
|
{
|
||||||
|
private List<ServerState> _serverStates { get; set; }
|
||||||
|
|
||||||
|
public ServerStateManager()
|
||||||
|
{
|
||||||
|
_serverStates = new List<ServerState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerState GetServerState(int siteId)
|
||||||
|
{
|
||||||
|
var serverState = _serverStates.FirstOrDefault(item => item.SiteId == siteId);
|
||||||
|
if (serverState == null)
|
||||||
|
{
|
||||||
|
serverState = new ServerState();
|
||||||
|
serverState.SiteId = siteId;
|
||||||
|
serverState.Assemblies = new List<string>();
|
||||||
|
serverState.Scripts = new List<Resource>();
|
||||||
|
return serverState;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return serverState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetServerState(int siteId, ServerState serverState)
|
||||||
|
{
|
||||||
|
var serverstate = _serverStates.FirstOrDefault(item => item.SiteId == siteId);
|
||||||
|
if (serverstate == null)
|
||||||
|
{
|
||||||
|
serverState.SiteId = siteId;
|
||||||
|
_serverStates.Add(serverState);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serverstate.Assemblies = serverState.Assemblies;
|
||||||
|
serverstate.Scripts = serverState.Scripts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Oqtane.Server/Migrations/Master/04000002_AddThemeName.cs
Normal file
28
Oqtane.Server/Migrations/Master/04000002_AddThemeName.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Oqtane.Databases.Interfaces;
|
||||||
|
using Oqtane.Migrations.EntityBuilders;
|
||||||
|
using Oqtane.Repository;
|
||||||
|
|
||||||
|
namespace Oqtane.Migrations.Master
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MasterDBContext))]
|
||||||
|
[Migration("Master.04.00.00.02")]
|
||||||
|
public class AddThemeName : MultiDatabaseMigration
|
||||||
|
{
|
||||||
|
public AddThemeName(IDatabase database) : base(database)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||||
|
themeEntityBuilder.AddStringColumn("Name", 200, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
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.04.00.00.03")]
|
||||||
|
public class AddProfileValidation : MultiDatabaseMigration
|
||||||
|
{
|
||||||
|
public AddProfileValidation(IDatabase database) : base(database)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
var profileEntityBuilder = new ProfileEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||||
|
profileEntityBuilder.AddStringColumn("Validation", 200, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -36,9 +36,10 @@ namespace Oqtane.Pages
|
|||||||
private readonly IVisitorRepository _visitors;
|
private readonly IVisitorRepository _visitors;
|
||||||
private readonly IAliasRepository _aliases;
|
private readonly IAliasRepository _aliases;
|
||||||
private readonly ISettingRepository _settings;
|
private readonly ISettingRepository _settings;
|
||||||
|
private readonly ServerStateManager _serverState;
|
||||||
private readonly ILogManager _logger;
|
private readonly ILogManager _logger;
|
||||||
|
|
||||||
public HostModel(IConfigManager configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, IJwtManager jwtManager, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors, IAliasRepository aliases, ISettingRepository settings, ILogManager logger)
|
public HostModel(IConfigManager configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, IJwtManager jwtManager, ISiteRepository sites, IPageRepository pages, IUrlMappingRepository urlMappings, IVisitorRepository visitors, IAliasRepository aliases, ISettingRepository settings, ServerStateManager serverState, ILogManager logger)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_tenantManager = tenantManager;
|
_tenantManager = tenantManager;
|
||||||
@ -52,6 +53,7 @@ namespace Oqtane.Pages
|
|||||||
_visitors = visitors;
|
_visitors = visitors;
|
||||||
_aliases = aliases;
|
_aliases = aliases;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
_serverState = serverState;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,32 +119,10 @@ namespace Oqtane.Pages
|
|||||||
{
|
{
|
||||||
Runtime = site.Runtime;
|
Runtime = site.Runtime;
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrEmpty(site.RenderMode))
|
if (!string.IsNullOrEmpty(site.RenderMode))
|
||||||
{
|
{
|
||||||
RenderMode = site.RenderMode;
|
RenderMode = site.RenderMode;
|
||||||
}
|
}
|
||||||
if (Runtime == "Server")
|
|
||||||
{
|
|
||||||
ReconnectScript = CreateReconnectScript();
|
|
||||||
}
|
|
||||||
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
|
|
||||||
{
|
|
||||||
PWAScript = CreatePWAScript(alias, site, route);
|
|
||||||
}
|
|
||||||
// site level scripts
|
|
||||||
HeadResources += ParseScripts(site.HeadContent);
|
|
||||||
BodyResources += ParseScripts(site.BodyContent);
|
|
||||||
|
|
||||||
// get jwt token for downstream APIs
|
|
||||||
if (User.Identity.IsAuthenticated)
|
|
||||||
{
|
|
||||||
var sitesettings = HttpContext.GetSiteSettings();
|
|
||||||
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
|
|
||||||
if (!string.IsNullOrEmpty(secret))
|
|
||||||
{
|
|
||||||
AuthorizationToken = _jwtManager.GenerateToken(alias, (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (site.VisitorTracking)
|
if (site.VisitorTracking)
|
||||||
{
|
{
|
||||||
@ -172,11 +152,33 @@ namespace Oqtane.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// include global resources
|
// get jwt token for downstream APIs
|
||||||
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
|
if (User.Identity.IsAuthenticated)
|
||||||
foreach (Assembly assembly in assemblies)
|
|
||||||
{
|
{
|
||||||
ProcessHostResources(assembly, alias);
|
var sitesettings = HttpContext.GetSiteSettings();
|
||||||
|
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
|
||||||
|
if (!string.IsNullOrEmpty(secret))
|
||||||
|
{
|
||||||
|
AuthorizationToken = _jwtManager.GenerateToken(alias, (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject scripts
|
||||||
|
if (Runtime == "Server")
|
||||||
|
{
|
||||||
|
ReconnectScript = CreateReconnectScript();
|
||||||
|
}
|
||||||
|
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
|
||||||
|
{
|
||||||
|
PWAScript = CreatePWAScript(alias, site, route);
|
||||||
|
}
|
||||||
|
HeadResources += ParseScripts(site.HeadContent);
|
||||||
|
BodyResources += ParseScripts(site.BodyContent);
|
||||||
|
_sites.InitializeSite(site.SiteId); // populates server state
|
||||||
|
var scripts = _serverState.GetServerState(site.SiteId).Scripts;
|
||||||
|
foreach (var script in scripts)
|
||||||
|
{
|
||||||
|
AddScript(script, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set culture if not specified
|
// set culture if not specified
|
||||||
@ -409,20 +411,6 @@ namespace Oqtane.Pages
|
|||||||
"</script>";
|
"</script>";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessHostResources(Assembly assembly, Alias alias)
|
|
||||||
{
|
|
||||||
var types = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IHostResources)));
|
|
||||||
foreach (var type in types)
|
|
||||||
{
|
|
||||||
var obj = Activator.CreateInstance(type) as IHostResources;
|
|
||||||
foreach (var resource in obj.Resources)
|
|
||||||
{
|
|
||||||
resource.Level = ResourceLevel.App;
|
|
||||||
ProcessResource(resource, 0, alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ParseScripts(string headcontent)
|
private string ParseScripts(string headcontent)
|
||||||
{
|
{
|
||||||
// iterate scripts
|
// iterate scripts
|
||||||
@ -439,60 +427,39 @@ namespace Oqtane.Pages
|
|||||||
return scripts;
|
return scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessResource(Resource resource, int count, Alias alias)
|
private void AddScript(Resource resource, Alias alias)
|
||||||
{
|
{
|
||||||
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
|
var script = CreateScript(resource, alias);
|
||||||
switch (resource.ResourceType)
|
if (resource.Location == Shared.ResourceLocation.Head)
|
||||||
{
|
{
|
||||||
case ResourceType.Stylesheet:
|
if (!HeadResources.Contains(script))
|
||||||
if (!HeadResources.Contains(url, StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
{
|
HeadResources += script + Environment.NewLine;
|
||||||
string id = "";
|
}
|
||||||
if (resource.Level == ResourceLevel.Page)
|
|
||||||
{
|
|
||||||
id = "id=\"app-stylesheet-" + resource.Level.ToString().ToLower() + "-" + DateTime.UtcNow.ToString("yyyyMMddHHmmssfff") + "-" + count.ToString("00") + "\" ";
|
|
||||||
}
|
|
||||||
HeadResources += "<link " + id + "rel=\"stylesheet\" href=\"" + url + "\"" + CrossOrigin(resource.CrossOrigin) + Integrity(resource.Integrity) + " type=\"text/css\"/>" + Environment.NewLine;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ResourceType.Script:
|
|
||||||
if (resource.Location == Shared.ResourceLocation.Body)
|
|
||||||
{
|
|
||||||
if (!BodyResources.Contains(url, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
BodyResources += "<script src=\"" + url + "\"" + CrossOrigin(resource.CrossOrigin) + Integrity(resource.Integrity) + "></script>" + Environment.NewLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!HeadResources.Contains(resource.Url, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
HeadResources += "<script src=\"" + url + "\"" + CrossOrigin(resource.CrossOrigin) + Integrity(resource.Integrity) + "></script>" + Environment.NewLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private string CrossOrigin(string crossorigin)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(crossorigin))
|
|
||||||
{
|
|
||||||
return " crossorigin=\"" + crossorigin + "\"";
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return "";
|
if (!BodyResources.Contains(script))
|
||||||
|
{
|
||||||
|
BodyResources += script + Environment.NewLine;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private string Integrity(string integrity)
|
|
||||||
|
private string CreateScript(Resource resource, Alias alias)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(integrity))
|
if (!string.IsNullOrEmpty(resource.Url))
|
||||||
{
|
{
|
||||||
return " integrity=\"" + integrity + "\"";
|
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
|
||||||
|
return "<script src=\"" + url + "\"" +
|
||||||
|
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
|
||||||
|
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
|
||||||
|
"></script>";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return "";
|
// inline script
|
||||||
|
return "<script>" + resource.Content + "</script>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ namespace Oqtane.Repository
|
|||||||
Site GetSite(int siteId);
|
Site GetSite(int siteId);
|
||||||
Site GetSite(int siteId, bool tracking);
|
Site GetSite(int siteId, bool tracking);
|
||||||
void DeleteSite(int siteId);
|
void DeleteSite(int siteId);
|
||||||
|
void InitializeSite(int siteId);
|
||||||
void CreatePages(Site site, List<PageTemplate> pageTemplates);
|
void CreatePages(Site site, List<PageTemplate> pageTemplates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,14 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Xml;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Oqtane.Infrastructure;
|
using Oqtane.Infrastructure;
|
||||||
using Oqtane.Models;
|
using Oqtane.Models;
|
||||||
using Oqtane.Modules;
|
using Oqtane.Modules;
|
||||||
using Oqtane.Shared;
|
using Oqtane.Shared;
|
||||||
|
using Oqtane.Themes;
|
||||||
|
|
||||||
namespace Oqtane.Repository
|
namespace Oqtane.Repository
|
||||||
{
|
{
|
||||||
@ -20,15 +22,17 @@ namespace Oqtane.Repository
|
|||||||
private readonly IPermissionRepository _permissions;
|
private readonly IPermissionRepository _permissions;
|
||||||
private readonly ITenantManager _tenants;
|
private readonly ITenantManager _tenants;
|
||||||
private readonly ISettingRepository _settings;
|
private readonly ISettingRepository _settings;
|
||||||
|
private readonly ServerStateManager _serverState;
|
||||||
private readonly string settingprefix = "SiteEnabled:";
|
private readonly string settingprefix = "SiteEnabled:";
|
||||||
|
|
||||||
public ModuleDefinitionRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings)
|
public ModuleDefinitionRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings, ServerStateManager serverState)
|
||||||
{
|
{
|
||||||
_db = context;
|
_db = context;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_permissions = permissions;
|
_permissions = permissions;
|
||||||
_tenants = tenants;
|
_tenants = tenants;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
_serverState = serverState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ModuleDefinition> GetModuleDefinitions()
|
public IEnumerable<ModuleDefinition> GetModuleDefinitions()
|
||||||
@ -182,6 +186,7 @@ namespace Oqtane.Repository
|
|||||||
var settings = _settings.GetSettings(EntityNames.ModuleDefinition).ToList();
|
var settings = _settings.GetSettings(EntityNames.ModuleDefinition).ToList();
|
||||||
|
|
||||||
// populate module definition site settings and permissions
|
// populate module definition site settings and permissions
|
||||||
|
var serverState = _serverState.GetServerState(siteId);
|
||||||
foreach (ModuleDefinition moduledefinition in ModuleDefinitions)
|
foreach (ModuleDefinition moduledefinition in ModuleDefinitions)
|
||||||
{
|
{
|
||||||
moduledefinition.SiteId = siteId;
|
moduledefinition.SiteId = siteId;
|
||||||
@ -196,6 +201,36 @@ namespace Oqtane.Repository
|
|||||||
moduledefinition.IsEnabled = moduledefinition.IsAutoEnabled;
|
moduledefinition.IsEnabled = moduledefinition.IsAutoEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (moduledefinition.IsEnabled)
|
||||||
|
{
|
||||||
|
// build list of assemblies for site
|
||||||
|
if (!serverState.Assemblies.Contains(moduledefinition.AssemblyName))
|
||||||
|
{
|
||||||
|
serverState.Assemblies.Add(moduledefinition.AssemblyName);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(moduledefinition.Dependencies))
|
||||||
|
{
|
||||||
|
foreach (var assembly in moduledefinition.Dependencies.Replace(".dll", "").Split(',', StringSplitOptions.RemoveEmptyEntries).Reverse())
|
||||||
|
{
|
||||||
|
if (!serverState.Assemblies.Contains(assembly))
|
||||||
|
{
|
||||||
|
serverState.Assemblies.Insert(0, assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// build list of scripts for site
|
||||||
|
if (moduledefinition.Resources != null)
|
||||||
|
{
|
||||||
|
foreach (var resource in moduledefinition.Resources.Where(item => item.Level == ResourceLevel.Site))
|
||||||
|
{
|
||||||
|
if (!serverState.Scripts.Contains(resource))
|
||||||
|
{
|
||||||
|
serverState.Scripts.Add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (permissions.Count == 0)
|
if (permissions.Count == 0)
|
||||||
{
|
{
|
||||||
// no module definition permissions exist for this site
|
// no module definition permissions exist for this site
|
||||||
@ -216,6 +251,7 @@ namespace Oqtane.Repository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_serverState.SetServerState(siteId, serverState);
|
||||||
|
|
||||||
// clean up any orphaned permissions
|
// clean up any orphaned permissions
|
||||||
var ids = new HashSet<int>(ModuleDefinitions.Select(item => item.ModuleDefinitionId));
|
var ids = new HashSet<int>(ModuleDefinitions.Select(item => item.ModuleDefinitionId));
|
||||||
@ -295,6 +331,16 @@ namespace Oqtane.Repository
|
|||||||
moduledefinition.ModuleDefinitionName = qualifiedModuleType;
|
moduledefinition.ModuleDefinitionName = qualifiedModuleType;
|
||||||
moduledefinition.ControlTypeTemplate = modulecontroltype.Namespace + "." + Constants.ActionToken + ", " + modulecontroltype.Assembly.GetName().Name;
|
moduledefinition.ControlTypeTemplate = modulecontroltype.Namespace + "." + Constants.ActionToken + ", " + modulecontroltype.Assembly.GetName().Name;
|
||||||
moduledefinition.AssemblyName = assembly.GetName().Name;
|
moduledefinition.AssemblyName = assembly.GetName().Name;
|
||||||
|
if (moduledefinition.Resources != null)
|
||||||
|
{
|
||||||
|
foreach (var resource in moduledefinition.Resources)
|
||||||
|
{
|
||||||
|
if (resource.Url.StartsWith("~"))
|
||||||
|
{
|
||||||
|
resource.Url = resource.Url.Replace("~", "/Modules/" + Utilities.GetTypeName(moduledefinition.ModuleDefinitionName) + "/").Replace("//", "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
moduledefinition.IsPortable = false;
|
moduledefinition.IsPortable = false;
|
||||||
if (!string.IsNullOrEmpty(moduledefinition.ServerManagerType))
|
if (!string.IsNullOrEmpty(moduledefinition.ServerManagerType))
|
||||||
@ -313,10 +359,21 @@ namespace Oqtane.Repository
|
|||||||
|
|
||||||
if (moduledefinition.Categories == "Admin")
|
if (moduledefinition.Categories == "Admin")
|
||||||
{
|
{
|
||||||
moduledefinition.PermissionList = new List<Permission>
|
var shortName = moduledefinition.ModuleDefinitionName.Replace("Oqtane.Modules.Admin.", "").Replace(", Oqtane.Client", "");
|
||||||
|
if (Constants.DefaultHostModuleTypes.Contains(shortName))
|
||||||
{
|
{
|
||||||
new Permission(PermissionNames.Utilize, RoleNames.Admin, true)
|
moduledefinition.PermissionList = new List<Permission>
|
||||||
};
|
{
|
||||||
|
new Permission(PermissionNames.Utilize, RoleNames.Host, true)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
moduledefinition.PermissionList = new List<Permission>
|
||||||
|
{
|
||||||
|
new Permission(PermissionNames.Utilize, RoleNames.Admin, true)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -24,11 +24,12 @@ namespace Oqtane.Repository
|
|||||||
private readonly IModuleRepository _moduleRepository;
|
private readonly IModuleRepository _moduleRepository;
|
||||||
private readonly IPageModuleRepository _pageModuleRepository;
|
private readonly IPageModuleRepository _pageModuleRepository;
|
||||||
private readonly IModuleDefinitionRepository _moduleDefinitionRepository;
|
private readonly IModuleDefinitionRepository _moduleDefinitionRepository;
|
||||||
|
private readonly IThemeRepository _themeRepository;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IConfigurationRoot _config;
|
private readonly IConfigurationRoot _config;
|
||||||
|
|
||||||
public SiteRepository(TenantDBContext context, IRoleRepository roleRepository, IProfileRepository profileRepository, IFolderRepository folderRepository, IPageRepository pageRepository,
|
public SiteRepository(TenantDBContext context, IRoleRepository roleRepository, IProfileRepository profileRepository, IFolderRepository folderRepository, IPageRepository pageRepository,
|
||||||
IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IServiceProvider serviceProvider,
|
IModuleRepository moduleRepository, IPageModuleRepository pageModuleRepository, IModuleDefinitionRepository moduleDefinitionRepository, IThemeRepository themeRepository, IServiceProvider serviceProvider,
|
||||||
IConfigurationRoot config)
|
IConfigurationRoot config)
|
||||||
{
|
{
|
||||||
_db = context;
|
_db = context;
|
||||||
@ -39,6 +40,7 @@ namespace Oqtane.Repository
|
|||||||
_moduleRepository = moduleRepository;
|
_moduleRepository = moduleRepository;
|
||||||
_pageModuleRepository = pageModuleRepository;
|
_pageModuleRepository = pageModuleRepository;
|
||||||
_moduleDefinitionRepository = moduleDefinitionRepository;
|
_moduleDefinitionRepository = moduleDefinitionRepository;
|
||||||
|
_themeRepository = themeRepository;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
@ -88,6 +90,12 @@ namespace Oqtane.Repository
|
|||||||
_db.SaveChanges();
|
_db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void InitializeSite(int siteId)
|
||||||
|
{
|
||||||
|
_moduleDefinitionRepository.GetModuleDefinitions(siteId);
|
||||||
|
_themeRepository.GetThemes();
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateSite(Site site)
|
private void CreateSite(Site site)
|
||||||
{
|
{
|
||||||
// create default entities for site
|
// create default entities for site
|
||||||
|
@ -12,6 +12,7 @@ using Oqtane.Models;
|
|||||||
using Oqtane.Shared;
|
using Oqtane.Shared;
|
||||||
using Oqtane.Themes;
|
using Oqtane.Themes;
|
||||||
using System.Reflection.Metadata;
|
using System.Reflection.Metadata;
|
||||||
|
using Oqtane.Migrations.Master;
|
||||||
|
|
||||||
namespace Oqtane.Repository
|
namespace Oqtane.Repository
|
||||||
{
|
{
|
||||||
@ -21,14 +22,16 @@ namespace Oqtane.Repository
|
|||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly ITenantManager _tenants;
|
private readonly ITenantManager _tenants;
|
||||||
private readonly ISettingRepository _settings;
|
private readonly ISettingRepository _settings;
|
||||||
|
private readonly ServerStateManager _serverState;
|
||||||
private readonly string settingprefix = "SiteEnabled:";
|
private readonly string settingprefix = "SiteEnabled:";
|
||||||
|
|
||||||
public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings)
|
public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings, ServerStateManager serverState)
|
||||||
{
|
{
|
||||||
_db = context;
|
_db = context;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_tenants = tenants;
|
_tenants = tenants;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
_serverState = serverState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Theme> GetThemes()
|
public IEnumerable<Theme> GetThemes()
|
||||||
@ -123,6 +126,12 @@ namespace Oqtane.Repository
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// override user customizable property values
|
||||||
|
Theme.Name = (!string.IsNullOrEmpty(theme.Name)) ? theme.Name : Theme.Name;
|
||||||
|
foreach (var themecontrol in Theme.Themes)
|
||||||
|
{
|
||||||
|
themecontrol.Name = Theme.Name + " - " + themecontrol.Name;
|
||||||
|
}
|
||||||
// remove theme from list as it is already synced
|
// remove theme from list as it is already synced
|
||||||
themes.Remove(theme);
|
themes.Remove(theme);
|
||||||
}
|
}
|
||||||
@ -147,6 +156,7 @@ namespace Oqtane.Repository
|
|||||||
var settings = _settings.GetSettings(EntityNames.Theme).ToList();
|
var settings = _settings.GetSettings(EntityNames.Theme).ToList();
|
||||||
|
|
||||||
// populate theme site settings
|
// populate theme site settings
|
||||||
|
var serverState = _serverState.GetServerState(siteId);
|
||||||
foreach (Theme theme in Themes)
|
foreach (Theme theme in Themes)
|
||||||
{
|
{
|
||||||
theme.SiteId = siteId;
|
theme.SiteId = siteId;
|
||||||
@ -160,7 +170,38 @@ namespace Oqtane.Repository
|
|||||||
{
|
{
|
||||||
theme.IsEnabled = theme.IsAutoEnabled;
|
theme.IsEnabled = theme.IsAutoEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (theme.IsEnabled)
|
||||||
|
{
|
||||||
|
// build list of assemblies for site
|
||||||
|
if (!serverState.Assemblies.Contains(theme.AssemblyName))
|
||||||
|
{
|
||||||
|
serverState.Assemblies.Add(theme.AssemblyName);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(theme.Dependencies))
|
||||||
|
{
|
||||||
|
foreach (var assembly in theme.Dependencies.Replace(".dll", "").Split(',', StringSplitOptions.RemoveEmptyEntries).Reverse())
|
||||||
|
{
|
||||||
|
if (!serverState.Assemblies.Contains(assembly))
|
||||||
|
{
|
||||||
|
serverState.Assemblies.Insert(0, assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// build list of scripts for site
|
||||||
|
if (theme.Resources != null)
|
||||||
|
{
|
||||||
|
foreach (var resource in theme.Resources.Where(item => item.Level == ResourceLevel.Site))
|
||||||
|
{
|
||||||
|
if (!serverState.Scripts.Contains(resource))
|
||||||
|
{
|
||||||
|
serverState.Scripts.Add(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_serverState.SetServerState(siteId, serverState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Themes;
|
return Themes;
|
||||||
@ -225,12 +266,22 @@ namespace Oqtane.Repository
|
|||||||
Version = new Version(1, 0, 0).ToString()
|
Version = new Version(1, 0, 0).ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// set internal properties
|
// set internal properties
|
||||||
theme.ThemeName = qualifiedThemeType;
|
theme.ThemeName = qualifiedThemeType;
|
||||||
theme.Themes = new List<ThemeControl>();
|
theme.Themes = new List<ThemeControl>();
|
||||||
theme.Containers = new List<ThemeControl>();
|
theme.Containers = new List<ThemeControl>();
|
||||||
theme.AssemblyName = assembly.FullName.Split(",")[0];
|
theme.AssemblyName = assembly.FullName.Split(",")[0];
|
||||||
|
if (theme.Resources != null)
|
||||||
|
{
|
||||||
|
foreach (var resource in theme.Resources)
|
||||||
|
{
|
||||||
|
if (resource.Url.StartsWith("~"))
|
||||||
|
{
|
||||||
|
resource.Url = resource.Url.Replace("~", "/Themes/" + Utilities.GetTypeName(theme.ThemeName) + "/").Replace("//", "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}");
|
Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}");
|
||||||
themes.Add(theme);
|
themes.Add(theme);
|
||||||
index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType);
|
index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType);
|
||||||
@ -242,7 +293,7 @@ namespace Oqtane.Repository
|
|||||||
new ThemeControl
|
new ThemeControl
|
||||||
{
|
{
|
||||||
TypeName = themeControlType.FullName + ", " + themeControlType.Assembly.GetName().Name,
|
TypeName = themeControlType.FullName + ", " + themeControlType.Assembly.GetName().Name,
|
||||||
Name = theme.Name + " - " + ((string.IsNullOrEmpty(themecontrolobject.Name)) ? Utilities.GetTypeNameLastSegment(themeControlType.FullName, 0) : themecontrolobject.Name),
|
Name = ((string.IsNullOrEmpty(themecontrolobject.Name)) ? Utilities.GetTypeNameLastSegment(themeControlType.FullName, 0) : themecontrolobject.Name),
|
||||||
Thumbnail = themecontrolobject.Thumbnail,
|
Thumbnail = themecontrolobject.Thumbnail,
|
||||||
Panes = themecontrolobject.Panes
|
Panes = themecontrolobject.Panes
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ namespace Oqtane.Shared
|
|||||||
public enum ResourceLevel
|
public enum ResourceLevel
|
||||||
{
|
{
|
||||||
App,
|
App,
|
||||||
|
Site,
|
||||||
Page,
|
Page,
|
||||||
Module
|
Module
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ namespace Oqtane.Models
|
|||||||
public string ModuleDefinitionName { get; set; }
|
public string ModuleDefinitionName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nice name to show in admin / edit dialogs.
|
/// Friendly name to show in UI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
@ -68,5 +68,10 @@ namespace Oqtane.Models
|
|||||||
/// This gives possible values for dropdown input fields.
|
/// This gives possible values for dropdown input fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Options { get; set; }
|
public string Options { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional RegExp validation expression
|
||||||
|
/// </summary>
|
||||||
|
public string Validation { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,10 +35,12 @@ namespace Oqtane.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ThemeName { get; set; }
|
public string ThemeName { get; set; }
|
||||||
|
|
||||||
// additional ITheme properties
|
/// <summary>
|
||||||
[NotMapped]
|
/// Friendly name to show in UI
|
||||||
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
// additional ITheme properties
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Oqtane.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Oqtane.Shared
|
namespace Oqtane.Shared
|
||||||
{
|
{
|
||||||
@ -39,6 +42,8 @@ namespace Oqtane.Shared
|
|||||||
|
|
||||||
public const string DefaultSiteTemplate = "Oqtane.SiteTemplates.DefaultSiteTemplate, Oqtane.Server";
|
public const string DefaultSiteTemplate = "Oqtane.SiteTemplates.DefaultSiteTemplate, Oqtane.Server";
|
||||||
|
|
||||||
|
public static readonly string[] DefaultHostModuleTypes = new[] { "Upgrade", "Themes", "SystemInfo", "Sql", "Sites", "ModuleDefinitions", "Logs", "Jobs", "ModuleCreator" };
|
||||||
|
|
||||||
public const string FileUrl = "/files/";
|
public const string FileUrl = "/files/";
|
||||||
public const string ImageUrl = "/api/file/image/";
|
public const string ImageUrl = "/api/file/image/";
|
||||||
public const int UserFolderCapacity = 20; // megabytes
|
public const int UserFolderCapacity = 20; // megabytes
|
||||||
|
@ -48,7 +48,6 @@ This project is open source, and therefore is a work in progress...
|
|||||||
Backlog (TBD)
|
Backlog (TBD)
|
||||||
- [ ] Azure Autoscale support (ie. web farm)
|
- [ ] Azure Autoscale support (ie. web farm)
|
||||||
- [ ] Routable Modules (ie. declarative configuration)
|
- [ ] Routable Modules (ie. declarative configuration)
|
||||||
- [ ] Client Assembly Loading / Site
|
|
||||||
- [ ] Folder Providers
|
- [ ] Folder Providers
|
||||||
- [ ] Generative AI Integration
|
- [ ] Generative AI Integration
|
||||||
|
|
||||||
@ -56,7 +55,9 @@ Backlog (TBD)
|
|||||||
- [ ] Migration to .NET 8
|
- [ ] Migration to .NET 8
|
||||||
|
|
||||||
4.0.0 (Q2 2023)
|
4.0.0 (Q2 2023)
|
||||||
- [ ] Migration to .NET 7
|
- [x] Migration to .NET 7
|
||||||
|
- [x] Improved JavaScript, CSS, and Meta support
|
||||||
|
- [x] Optimized Client Assembly Loading
|
||||||
|
|
||||||
[3.4.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3) ( May 3, 2023 )
|
[3.4.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3) ( May 3, 2023 )
|
||||||
- [x] Stabilization improvements
|
- [x] Stabilization improvements
|
||||||
|
Loading…
x
Reference in New Issue
Block a user