Merge pull request #3044 from oqtane/dev

4.0.1 Release
This commit is contained in:
Shaun Walker
2023-07-18 12:25:41 -04:00
committed by GitHub
122 changed files with 2317 additions and 1045 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
Oqtane.Server/wwwroot/Modules/Templates/External/Package/*.sh eol=lf
Oqtane.Server/wwwroot/Themes/Templates/External/Package/*.sh eol=lf

View File

@ -22,7 +22,7 @@ else
<th>@Localizer["Default"]</th>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<th style="width: 1px;">@Localizer["Translation"]</th>
<th style="width: 1px;">@Localizer["Translation"]</th>
<th style="width: 1px;">&nbsp;</th>
}
</Header>
@ -33,7 +33,7 @@ else
<td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<td>@((string.IsNullOrEmpty(context.Version)) ? "---" : context.Version)</td>
<td>@((string.IsNullOrEmpty(context.Version)) ? "---" : context.Version)</td>
<td>
@{
var translation = TranslationAvailable(context.Code, context.Version);
@ -99,56 +99,64 @@ else
}
@code {
private List<Language> _languages;
private List<Package> _packages;
private Package _package;
private List<Language> _languages;
private List<Package> _packages;
private Package _package;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
await GetLanguages();
protected override async Task OnParametersSetAsync()
{
await GetLanguages();
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_packages = await PackageService.GetPackagesAsync("translation");
}
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_packages = await PackageService.GetPackagesAsync("translation");
}
}
private async Task GetLanguages()
{
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId);
}
private async Task GetLanguages()
{
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId);
}
private async Task DeleteLanguage(Language language)
{
try
{
await LanguageService.DeleteLanguageAsync(language.LanguageId);
await logger.LogInformation("Language Deleted {Language}", language);
await GetLanguages();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Language {Language} {Error}", language, ex.Message);
private async Task DeleteLanguage(Language language)
{
try
{
await LanguageService.DeleteLanguageAsync(language.LanguageId);
await logger.LogInformation("Language Deleted {Language}", language);
await GetLanguages();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Language {Language} {Error}", language, ex.Message);
AddModuleMessage(Localizer["Error.Language.Delete"], MessageType.Error);
}
}
AddModuleMessage(Localizer["Error.Language.Delete"], MessageType.Error);
}
}
private Package TranslationAvailable(string code, string version)
{
return _packages?.FirstOrDefault(item => item.PackageId == (Constants.PackageId + "." + code));
}
private Package TranslationAvailable(string code, string version)
{
return _packages?.FirstOrDefault(item => item.PackageId == (Constants.PackageId + "." + code));
}
private async Task GetPackage(string code, string version)
{
try
{
_package = await PackageService.GetPackageAsync(Constants.PackageId + "." + code, version);
StateHasChanged();
}
private async Task GetPackage(string code, string version)
{
try
{
_package = await PackageService.GetPackageAsync(Constants.PackageId + "." + code, version);
if (_package != null)
{
StateHasChanged();
}
else
{
await logger.LogError("Error Getting Package {PackageId} {Version}", Constants.PackageId + "." + code, Constants.Version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", Constants.PackageId + "." + code, Constants.Version);

View File

@ -37,7 +37,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="category" HelpText="The categories that were affected" ResourceKey="Category">Category: </Label>
<Label Class="col-sm-3" For="category" HelpText="The fully qualified type type that was affected" ResourceKey="Category">Type Name: </Label>
<div class="col-sm-9">
<input id="category" class="form-control" @bind="@_category" readonly />
</div>

View File

@ -217,8 +217,13 @@
_packagelicense = package.License.Replace("\n", "<br />");
}
_packageversion = package.Version;
StateHasChanged();
}
else
{
await logger.LogError("Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
StateHasChanged();
}
catch (Exception ex)
{

View File

@ -319,55 +319,63 @@
AddModuleMessage(Localizer["Message.DuplicateName"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void HideModal()
{
_package = null;
StateHasChanged();
}
private void HideModal()
{
_package = null;
StateHasChanged();
}
private string TranslationAvailable(string packagename, string version)
{
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
if (string.IsNullOrEmpty(version))
{
return "install";
}
else
{
if (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return "upgrade";
}
}
}
}
return "";
}
private string TranslationAvailable(string packagename, string version)
{
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
if (string.IsNullOrEmpty(version))
{
return "install";
}
else
{
if (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return "upgrade";
}
}
}
}
return "";
}
private async Task GetPackage(string packagename)
{
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
try
{
_package = await PackageService.GetPackageAsync(packagename, version);
StateHasChanged();
}
private async Task GetPackage(string packagename)
{
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
try
{
_package = await PackageService.GetPackageAsync(packagename, version);
if (_package != null)
{
StateHasChanged();
}
else
{
await logger.LogError("Error Getting Package {PackageId} {Version}", packagename, version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packagename, version);

View File

@ -45,6 +45,7 @@ else
<th>@SharedLocalizer["Version"]</th>
<th>@Localizer["Enabled"]</th>
<th>@Localizer["InUse"]</th>
<th>@SharedLocalizer["Support"]</th>
<th>@SharedLocalizer["Expires"]</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
@ -78,6 +79,9 @@ else
<span>@SharedLocalizer["No"]</span>
}
</td>
<td>
@((MarkupString)SupportLink(context.PackageName))
</td>
<td>
@((MarkupString)PurchaseLink(context.PackageName))
</td>
@ -130,7 +134,7 @@ else
{
if (package.ExpiryDate != null && package.ExpiryDate.Value.Date != DateTime.MaxValue.Date)
{
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span>";
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span><br />";
if (!string.IsNullOrEmpty(package.PaymentUrl))
{
link += "&nbsp;&nbsp;<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + SharedLocalizer["Extend"] + "</a>";
@ -141,6 +145,20 @@ else
return link;
}
private string SupportLink(string packagename)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && !string.IsNullOrEmpty(package.SupportUrl))
{
link += "<a class=\"btn btn-success\" style=\"text-decoration: none !important\" href=\"" + package.SupportUrl + "\" target=\"_new\">" + SharedLocalizer["Help"] + "</a>";
}
}
return link;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)

View File

@ -14,10 +14,16 @@
@if (_containers != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="The name of the module" ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
<input id="module" type="text" class="form-control" @bind="@_module" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Enter the title of the module" ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" type="text" name="Title" class="form-control" @bind="@_title" required />
<input id="title" type="text" class="form-control" @bind="@_title" required />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -104,6 +110,7 @@
private ElementReference form;
private bool validated = false;
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _module;
private string _title;
private string _containerType;
private string _allPages = "false";
@ -125,6 +132,7 @@
protected override void OnInitialized()
{
_module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;

View File

@ -137,7 +137,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
<select id="theme" class="form-select" @bind="@_themetype" required>
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
@ -246,7 +246,7 @@
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList)))
{
_themetype = PageState.Site.DefaultThemeType;
_themes = ThemeService.GetThemeControls(PageState.Site.Themes, _themetype);
_themes = ThemeService.GetThemeControls(PageState.Site.Themes);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = PageState.Site.DefaultContainerType;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
@ -301,6 +301,19 @@
}
}
private void ThemeChanged(ChangeEventArgs e)
{
_themetype = (string)e.Value;
if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
{
AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
}
}
private void ThemeSettings()
{
_themeSettingsType = null;

View File

@ -148,7 +148,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
<select id="theme" class="form-select" @bind="@_themetype" required>
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
@ -359,7 +359,7 @@
{
_themetype = PageState.Site.DefaultThemeType;
}
_themes = ThemeService.GetThemeControls(PageState.Site.Themes, _themetype);
_themes = ThemeService.GetThemeControls(PageState.Site.Themes);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype))
@ -401,25 +401,6 @@
}
}
private async Task DeleteModule(Module module)
{
try
{
PageModule pagemodule = await PageModuleService.GetPageModuleAsync(_page.PageId, module.ModuleId);
pagemodule.IsDeleted = true;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await logger.LogInformation(LogFunction.Update,"Module Deleted {Title}", module.Title);
_pageModules.RemoveAll(item => item.PageModuleId == pagemodule.PageModuleId);
StateHasChanged();
NavigationManager.NavigateTo(NavigationManager.Uri + "&tab=PageModules");
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {Title} {Error}", module.Title, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private async void ParentChanged(ChangeEventArgs e)
{
try
@ -463,6 +444,19 @@
}
}
private void ThemeChanged(ChangeEventArgs e)
{
_themetype = (string)e.Value;
if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
{
AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
}
}
private void ThemeSettings()
{
_themeSettingsType = null;
@ -603,16 +597,6 @@
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid));
}
// update child paths
if (_parentid != _currentparentid)
{
foreach (Page p in PageState.Pages.Where(item => item.Path.StartsWith(currentPath)))
{
p.Path = p.Path.Replace(currentPath, _page.Path);
await PageService.UpdatePageAsync(p);
}
}
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
@ -656,4 +640,24 @@
NavigationManager.NavigateTo(NavigateUrl());
}
}
private async Task DeleteModule(Module module)
{
try
{
PageModule pagemodule = await PageModuleService.GetPageModuleAsync(_page.PageId, module.ModuleId);
pagemodule.IsDeleted = true;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await logger.LogInformation(LogFunction.Update, "Module Deleted {Title}", module.Title);
_pageModules.RemoveAll(item => item.PageModuleId == pagemodule.PageModuleId);
StateHasChanged();
NavigationManager.NavigateTo(NavigationManager.Uri + "&tab=PageModules");
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {Title} {Error}", module.Title, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
}

View File

@ -234,53 +234,53 @@
{
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The list of aliases for this site" ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteModule" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@context.IsDefault</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultaias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteModule" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@context.IsDefault</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
</div>
</div>
</Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">

View File

@ -29,7 +29,7 @@ else
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="Enter the aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder)." ResourceKey="Aliases">Aliases: </Label>
<Label Class="col-sm-3" For="alias" HelpText="The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label>
<div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
</div>
@ -288,12 +288,13 @@ else
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
}
_containertype = _containers.First().TypeName;
}
else
{
_containers = new List<ThemeControl>();
}
_containertype = "-";
_containertype = "-";
}
_admincontainertype = "";
StateHasChanged();
}

View File

@ -18,7 +18,7 @@ else
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["AliasName"]</th>
</Header>
<Row>
<td><button type="button" class="btn btn-primary" @onclick="@(async () => Edit(context.Name))">@SharedLocalizer["Edit"]</button></td>

View File

@ -217,8 +217,13 @@
}
_packageid = package.PackageId;
_version = package.Version;
StateHasChanged();
}
else
{
await logger.LogError("Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Theme.Download"], MessageType.Error);
}
StateHasChanged();
}
catch (Exception ex)
{

View File

@ -24,6 +24,7 @@ else
<th>@SharedLocalizer["Name"]</th>
<th>@SharedLocalizer["Version"]</th>
<th>@Localizer["Enabled"]</th>
<th>@SharedLocalizer["Support"]</th>
<th>@SharedLocalizer["Expires"]</th>
<th>&nbsp;</th>
</Header>
@ -47,6 +48,9 @@ else
<span>@SharedLocalizer["No"]</span>
}
</td>
<td>
@((MarkupString)SupportLink(context.PackageName))
</td>
<td>
@((MarkupString)PurchaseLink(context.PackageName))
</td>
@ -97,7 +101,7 @@ else
{
if (package.ExpiryDate != null && package.ExpiryDate.Value.Date != DateTime.MaxValue.Date)
{
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span>";
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span><br />";
if (!string.IsNullOrEmpty(package.PaymentUrl))
{
link += "&nbsp;&nbsp;<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + SharedLocalizer["Extend"] + "</a>";
@ -108,6 +112,20 @@ else
return link;
}
private string SupportLink(string packagename)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && !string.IsNullOrEmpty(package.SupportUrl))
{
link += "<a class=\"btn btn-success\" style=\"text-decoration: none !important\" href=\"" + package.SupportUrl + "\" target=\"_new\">" + SharedLocalizer["Help"] + "</a>";
}
}
return link;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)

View File

@ -144,6 +144,11 @@ else
<TabPanel Name="Notifications" ResourceKey="Notifications">
@if (notifications != null)
{
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
<br />
<ActionLink Action="Add" Text="Send Notification" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="SendNotification" />
<br /><br />
@if (filter == "to")
@ -159,22 +164,41 @@ else
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
@if (context.IsRead)
{
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.FromDisplayName</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
}
else
{
<b>@notificationSummary</b>
}
</td>
</Detail>
</Pager>
@ -192,22 +216,42 @@ else
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
@if (context.IsRead)
{
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
}
else
{
<td><b>@context.ToDisplayName</b></td>
<td><b>@context.Subject</b></td>
<td><b>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</b></td>
}
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
}
else
{
<b>@notificationSummary</b>
}
</td>
</Detail>
</Pager>
@ -217,11 +261,6 @@ else
<br />
<ActionDialog Header="Clear Notifications" Message="Are You Sure You Wish To Permanently Delete All Notifications ?" Action="Delete All Notifications" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllNotifications())" ResourceKey="DeleteAllNotifications" />
}
<br /><hr />
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
}
</TabPanel>
</TabStrip>
@ -246,6 +285,7 @@ else
private string category = string.Empty;
private string filter = "to";
private List<Notification> notifications;
private string notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;

View File

@ -118,6 +118,9 @@
Notification notification = await NotificationService.GetNotificationAsync(notificationid);
if (notification != null)
{
notification.IsRead = true;
notification = await NotificationService.UpdateNotificationAsync(notification);
int userid = -1;
if (notification.ToUserId == PageState.User.UserId)
{

View File

@ -35,9 +35,9 @@ else
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Username"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["LastLoginOn"]</th>
<th class="app-sort-th" @onclick="@(() => SortTable("Username"))">@Localizer["Username"]<i class="@(SetSortIcon("Username"))"></i></th>
<th class="app-sort-th" @onclick="@(() => SortTable("DisplayName"))">@Localizer["Name"]<i class="@(SetSortIcon("DisplayName"))"></i></th>
<th class="app-sort-th" @onclick="@(() => SortTable("LastLoginOn"))">@Localizer["LastLoginOn"]<i class="@(SetSortIcon("LastLoginOn"))"></i></th>
</Header>
<Row>
<td>
@ -413,6 +413,9 @@ else
private string _lifetime;
private string _token;
private bool isSortedAscending;
private string activeSortColumn;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync()
@ -654,4 +657,43 @@ else
_togglesecret = SharedLocalizer["ShowPassword"];
}
}
private void SortTable(string columnName)
{
if (columnName != activeSortColumn)
{
users = users.OrderBy(x => x.User.GetType().GetProperty(columnName)?.GetValue(x.User)).ToList();
isSortedAscending = true;
activeSortColumn = columnName;
}
else
{
if (isSortedAscending)
{
users = users.OrderByDescending(x => x.User.GetType().GetProperty(columnName)?.GetValue(x.User)).ToList();
}
else
{
users = users.OrderBy(x => x.User.GetType().GetProperty(columnName)?.GetValue(x.User)).ToList();
}
isSortedAscending = !isSortedAscending;
}
}
private string SetSortIcon(string columnName)
{
if (activeSortColumn != columnName)
{
return "app-fas pe-3 ";
}
if (isSortedAscending)
{
return "app-fas oi oi-sort-ascending";
}
else
{
return "app-fas oi oi-sort-descending";
}
}
}

View File

@ -34,13 +34,13 @@ else
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this role assignment is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input id="effectiveDate" class="form-control" @bind="@effectivedate" />
<input type="date" id="effectiveDate" class="form-control" @bind="@effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this role assignment expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input id="expiryDate" class="form-control" @bind="@expirydate" />
<input type="date" id="expiryDate" class="form-control" @bind="@expirydate" />
</div>
</div>
@ -75,8 +75,8 @@ else
private string name = string.Empty;
private List<Role> roles;
private int roleid = -1;
private string effectivedate = string.Empty;
private string expirydate = string.Empty;
private DateTime? effectivedate = null;
private DateTime? expirydate = null;
private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@ -130,23 +130,8 @@ else
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
{
if (string.IsNullOrEmpty(effectivedate))
{
userrole.EffectiveDate = null;
}
else
{
userrole.EffectiveDate = DateTime.Parse(effectivedate);
}
if (string.IsNullOrEmpty(expirydate))
{
userrole.ExpiryDate = null;
}
else
{
userrole.ExpiryDate = DateTime.Parse(expirydate);
}
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
@ -154,25 +139,8 @@ else
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
if (string.IsNullOrEmpty(effectivedate))
{
userrole.EffectiveDate = null;
}
else
{
userrole.EffectiveDate = DateTime.Parse(effectivedate);
}
if (string.IsNullOrEmpty(expirydate))
{
userrole.ExpiryDate = null;
}
else
{
userrole.ExpiryDate = DateTime.Parse(expirydate);
}
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.AddUserRoleAsync(userrole);
}

View File

@ -55,16 +55,26 @@
</div>
<div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1)
@if (GetFileId() != -1)
{
<button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button>
}
</div>
</div>
<div class="row">
<div class="col mt-1"><span id="@_progressinfoid" style="display: none;"></span></div>
<div class="col text-center mt-1"><progress id="@_progressbarid" class="mt-1" style="display: none;"></progress></div>
</div>
@if (ShowProgress)
{
<div class="row">
<div class="col mt-1"><span id="@_progressinfoid" style="display: none;"></span></div>
<div class="col mt-1"><progress id="@_progressbarid" class="mt-1" style="display: none;"></progress></div>
</div>
}
else
{
if (_uploading)
{
<div class="app-progress-indicator"></div>
}
}
}
</div>
</div>
@ -100,6 +110,7 @@
private string _guid;
private string _message = string.Empty;
private MessageType _messagetype;
private bool _uploading = false;
[Parameter]
public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility
@ -128,6 +139,9 @@
[Parameter]
public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true
[Parameter]
public bool ShowProgress { get; set; } = true; // optional - for indicating whether progress info should be displayed during upload - default is true
[Parameter]
public bool ShowSuccess { get; set; } = false; // optional - for indicating whether a success message should be displayed upon successful upload - default is false
@ -143,7 +157,7 @@
[Parameter]
public EventCallback<int> OnDelete { get; set; } // optional - executes a method in the calling component when a file is deleted
protected override async Task OnInitializedAsync()
protected override async Task OnParametersSetAsync()
{
// packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder)
@ -154,11 +168,6 @@
ShowSuccess = true;
}
if (!ShowFiles)
{
ShowImage = false;
}
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder)
@ -182,7 +191,6 @@
if (file != null)
{
FolderId = file.FolderId;
await OnSelect.InvokeAsync(FileId);
}
else
{
@ -224,7 +232,14 @@
if (folder != null)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.PermissionList);
_files = await FileService.GetFilesAsync(FolderId);
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Browse, folder.PermissionList))
{
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
_files = new List<File>();
}
}
else
{
@ -270,13 +285,10 @@
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
if (FileId != -1)
{
await OnSelect.InvokeAsync(FileId);
}
await SetImage();
await OnSelect.InvokeAsync(FileId);
StateHasChanged();
}
private async Task SetImage()
@ -320,6 +332,12 @@
}
if (restricted == "")
{
if (!ShowProgress)
{
_uploading = true;
StateHasChanged();
}
try
{
// upload the files
@ -327,32 +345,42 @@
var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString();
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken);
// uploading is asynchronous so we need to wait for the uploads to complete
// note that this will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
bool success = false;
int attempts = 0;
while (attempts < 5 && !success)
// uploading is asynchronous so we need to poll to determine if uploads are completed
var success = true;
int upload = 0;
while (upload < uploads.Length && success)
{
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
success = true;
List<File> files = await FileService.GetFilesAsync(folder);
if (files.Count > 0)
success = false;
// note that progressive retry will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
int attempts = 0;
while (attempts < 5 && !success)
{
foreach (string upload in uploads)
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[upload]);
if (file != null)
{
if (!files.Exists(item => item.Name == upload))
{
success = false;
}
success = true;
}
}
if (success)
{
upload++;
}
}
// reset progress indicators
await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
if (ShowProgress)
{
await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
}
else
{
_uploading = false;
StateHasChanged();
}
if (success)
{
@ -377,48 +405,53 @@
else
{
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == uploads[0]).FirstOrDefault();
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0]);
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
await GetFiles();
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
}
else
{
_message = Localizer["Message.File.NotSelected"];
_messagetype = MessageType.Warning;
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
_uploading = false;
}
private async Task DeleteFile()
{
_message = string.Empty;
try
{
await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId);
await OnDelete.InvokeAsync(FileId);
}
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
}
else
{
_message = Localizer["Message.File.NotSelected"];
_messagetype = MessageType.Warning;
}
}
_message = Localizer["Success.File.Delete"];
_messagetype = MessageType.Success;
private async Task DeleteFile()
{
_message = string.Empty;
try
{
await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId);
await OnDelete.InvokeAsync(FileId);
if (ShowSuccess)
{
_message = Localizer["Success.File.Delete"];
_messagetype = MessageType.Success;
}
await GetFiles();
FileId = -1;

View File

@ -17,7 +17,10 @@
<hr class="app-rule" />
</div>
<div class="collapse @_show" id="@Name">
@ChildContent
@if (ChildContent != null)
{
@ChildContent
}
</div>
@code {
@ -26,7 +29,7 @@
private string _show = string.Empty;
[Parameter]
public RenderFragment ChildContent { get; set; }
public RenderFragment ChildContent { get; set; } = null;
[Parameter]
public string Name { get; set; } // required - the name of the section
@ -37,19 +40,10 @@
[Parameter]
public string Expanded { get; set; } // optional - will default to false if not provided
protected override void OnInitialized()
{
_heading = (!string.IsNullOrEmpty(Heading)) ? Heading : Name;
_expanded = (!string.IsNullOrEmpty(Expanded)) ? Expanded : "false";
if (_expanded == "true") { _show = "show"; }
}
protected override void OnParametersSet()
{
base.OnParametersSet();
_heading = !string.IsNullOrEmpty(Heading)
? Localize(nameof(Heading), Heading)
: Localize(nameof(Name), Name);
_heading = !string.IsNullOrEmpty(Heading) ? Localize(nameof(Heading), Heading) : Localize(nameof(Name), Name);
_expanded = (!string.IsNullOrEmpty(Expanded)) ? Expanded.ToLower() : "false";
if (_expanded == "true") { _show = "show"; }
}
}

View File

@ -8,7 +8,7 @@
@foreach (TabPanel tabPanel in _tabPanels)
{
<li class="nav-item" @key="tabPanel.Name">
@if (tabPanel.Name == ActiveTab)
@if (tabPanel.Name.ToLower() == ActiveTab.ToLower())
{
<a class="nav-link active" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()

View File

@ -9,7 +9,6 @@ using Oqtane.UI;
using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using Oqtane.Themes;
namespace Oqtane.Modules
{

View File

@ -3,9 +3,8 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -13,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -136,7 +136,7 @@
<value>The function that was performed</value>
</data>
<data name="Category.HelpText" xml:space="preserve">
<value>The categories that were affected</value>
<value>The fully qualified type type that was affected</value>
</data>
<data name="Page.HelpText" xml:space="preserve">
<value>The page that was affected</value>
@ -178,7 +178,7 @@
<value>Function: </value>
</data>
<data name="Category.Text" xml:space="preserve">
<value>Category: </value>
<value>Type Name: </value>
</data>
<data name="Page.Text" xml:space="preserve">
<value>Page: </value>

View File

@ -150,4 +150,10 @@
<data name="Error.Module.Load" xml:space="preserve">
<value>A Problem Was Encountered Loading Module {0}. The Module Is Either Invalid Or Does Not Exist.</value>
</data>
<data name="Module.HelpText" xml:space="preserve">
<value>The name of the module</value>
</data>
<data name="Module.Text" xml:space="preserve">
<value>Module:</value>
</data>
</root>

View File

@ -246,4 +246,7 @@
<data name="PageContent.Heading" xml:space="preserve">
<value>Page Content</value>
</data>
<data name="ThemeChanged.Message" xml:space="preserve">
<value>Please Note That Overriding The Default Site Theme With An Unrelated Page Theme May Result In Compatibility Issues For Your Site</value>
</data>
</root>

View File

@ -282,4 +282,7 @@
<data name="PageContent.Heading" xml:space="preserve">
<value>Page Content</value>
</data>
<data name="ThemeChanged.Message" xml:space="preserve">
<value>Please Note That Overriding The Default Site Theme With An Unrelated Page Theme May Result In Compatibility Issues For Your Site</value>
</data>
</root>

View File

@ -166,7 +166,7 @@
<value>The name of the database used for the site</value>
</data>
<data name="Aliases.HelpText" xml:space="preserve">
<value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder).</value>
<value>The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder).</value>
</data>
<data name="IsDeleted.HelpText" xml:space="preserve">
<value>Is this site deleted?</value>
@ -217,7 +217,7 @@
<value>Database: </value>
</data>
<data name="Aliases.Text" xml:space="preserve">
<value>Aliases: </value>
<value>Urls:</value>
</data>
<data name="IsDeleted.Text" xml:space="preserve">
<value>Deleted? </value>
@ -322,10 +322,10 @@
<value>Default Alias: </value>
</data>
<data name="Aliases.Heading" xml:space="preserve">
<value>Aliases</value>
<value>Urls</value>
</data>
<data name="AliasName" xml:space="preserve">
<value>Name</value>
<value>Url</value>
</data>
<data name="AliasDefault" xml:space="preserve">
<value>Default?</value>

View File

@ -136,7 +136,7 @@
<value>Default Admin Container</value>
</data>
<data name="Aliases.HelpText" xml:space="preserve">
<value>Enter the alias for the server</value>
<value>The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value>
</data>
<data name="DefaultContainer.HelpText" xml:space="preserve">
<value>Select the default container for the site</value>
@ -145,7 +145,7 @@
<value>Database: </value>
</data>
<data name="Aliases.Text" xml:space="preserve">
<value>Aliases: </value>
<value>Urls: </value>
</data>
<data name="DefaultTheme.Text" xml:space="preserve">
<value>Default Theme: </value>

View File

@ -135,4 +135,7 @@
<data name="Browse" xml:space="preserve">
<value>Browse</value>
</data>
<data name="AliasName" xml:space="preserve">
<value>Url</value>
</data>
</root>

View File

@ -343,7 +343,7 @@
<value>Please note that third party extensions are registered in the &lt;a href="https://www.oqtane.net" target="_new"&gt;Oqtane Marketplace&lt;/a&gt; which enables them to be seamlessly downloaded and installed into the framework.</value>
</data>
<data name="Home" xml:space="preserve">
<value>Home</value>
<value>Home</value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
@ -384,4 +384,10 @@
<data name="Confirm" xml:space="preserve">
<value>Confirm</value>
</data>
</root>
<data name="Help" xml:space="preserve">
<value>Help</value>
</data>
<data name="Support" xml:space="preserve">
<value>Support</value>
</data>
</root>

View File

@ -46,6 +46,11 @@ namespace Oqtane.Services
return await GetJsonAsync<File>($"{Apiurl}/{fileId}");
}
public async Task<File> GetFileAsync(int folderId, string name)
{
return await GetJsonAsync<File>($"{Apiurl}/name/{name}/{folderId}");
}
public async Task<File> AddFileAsync(File file)
{
return await PostJsonAsync<File>(Apiurl, file);

View File

@ -1,5 +1,6 @@
using Oqtane.Models;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace Oqtane.Services
@ -33,6 +34,15 @@ namespace Oqtane.Services
/// <returns></returns>
Task<File> GetFileAsync(int fileId);
/// <summary>
/// Get a <see cref="File"/> based on the <see cref="Folder"/> and file name.
/// </summary>
/// <param name="folderId">Reference to the <see cref="Folder"/></param>
/// <param name="name">name of the file
/// </param>
/// <returns></returns>
Task<File> GetFileAsync(int folderId, string name);
/// <summary>
/// Add / store a <see cref="File"/> record.
/// This does not contain the file contents.

View File

@ -18,6 +18,27 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId);
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="direction"></param>
/// <param name="userId"></param>
/// <param name="count"></param>
/// <param name="isRead"></param>
/// <returns></returns>
Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId, int count, bool isRead);
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="direction"></param>
/// <param name="userId"></param>
/// <param name="isRead"></param>
/// <returns></returns>
Task<int> GetNotificationCountAsync(int siteId, string direction, int userId, bool isRead);
/// <summary>
/// Returns a specific notifications
/// </summary>

View File

@ -27,6 +27,17 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package);
/// <summary>
/// Returns a list of packages matching the given parameters
/// </summary>
/// <param name="type"></param>
/// <param name="search"></param>
/// <param name="price"></param>
/// <param name="package"></param>
/// <param name="sort"></param>
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort);
/// <summary>
/// Returns a specific package
/// </summary>

View File

@ -22,6 +22,20 @@ namespace Oqtane.Services
return notifications.OrderByDescending(item => item.CreatedOn).ToList();
}
public async Task<List<Notification>> GetNotificationsAsync(int siteId, string direction, int userId, int count, bool isRead)
{
var notifications = await GetJsonAsync<List<Notification>>($"{Apiurl}/read?siteid={siteId}&direction={direction.ToLower()}&userid={userId}&count={count}&isread={isRead}");
return notifications.OrderByDescending(item => item.CreatedOn).ToList();
}
public async Task<int> GetNotificationCountAsync(int siteId, string direction, int userId, bool isRead)
{
var notificationCount = await GetJsonAsync<int>($"{Apiurl}/read-count?siteid={siteId}&direction={direction.ToLower()}&userid={userId}&isread={isRead}");
return notificationCount;
}
public async Task<Notification> GetNotificationAsync(int notificationId)
{
return await GetJsonAsync<Notification>($"{Apiurl}/{notificationId}");

View File

@ -23,7 +23,12 @@ namespace Oqtane.Services
public async Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}");
return await GetPackagesAsync(type, search, price, package, "");
}
public async Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}");
}
public async Task<Package> GetPackageAsync(string packageId, string version)

View File

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Shared;
@ -21,7 +23,7 @@ namespace Oqtane.Services
_siteState = siteState;
}
private HttpClient GetHttpClient()
public HttpClient GetHttpClient()
{
if (!_httpClient.DefaultRequestHeaders.Contains(Constants.AntiForgeryTokenHeaderName) && _siteState != null && !string.IsNullOrEmpty(_siteState.AntiForgeryToken))
{
@ -64,7 +66,7 @@ namespace Oqtane.Services
// legacy support for ControllerRoutes.Default
if (alias != null)
{
// include the alias for multi-tenant context
// include the alias id for multi-tenant context
apiurl += $"{alias.AliasId}/";
}
else
@ -105,7 +107,7 @@ namespace Oqtane.Services
protected async Task GetAsync(string uri)
{
var response = await GetHttpClient().GetAsync(uri);
CheckResponse(response);
await CheckResponse(response, uri);
}
protected async Task<string> GetStringAsync(string uri)
@ -139,7 +141,7 @@ namespace Oqtane.Services
protected async Task<T> GetJsonAsync<T>(string uri)
{
var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
if (await CheckResponse(response, uri) && ValidateJsonContent(response.Content))
{
return await response.Content.ReadFromJsonAsync<T>();
}
@ -150,7 +152,7 @@ namespace Oqtane.Services
protected async Task PutAsync(string uri)
{
var response = await GetHttpClient().PutAsync(uri, null);
CheckResponse(response);
await CheckResponse(response, uri);
}
protected async Task<T> PutJsonAsync<T>(string uri, T value)
@ -161,7 +163,7 @@ namespace Oqtane.Services
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
{
var response = await GetHttpClient().PutAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
if (await CheckResponse(response, uri) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadFromJsonAsync<TResult>();
return result;
@ -172,7 +174,7 @@ namespace Oqtane.Services
protected async Task PostAsync(string uri)
{
var response = await GetHttpClient().PostAsync(uri, null);
CheckResponse(response);
await CheckResponse(response, uri);
}
protected async Task<T> PostJsonAsync<T>(string uri, T value)
@ -183,7 +185,7 @@ namespace Oqtane.Services
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
{
var response = await GetHttpClient().PostAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
if (await CheckResponse(response, uri) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadFromJsonAsync<TResult>();
return result;
@ -195,18 +197,21 @@ namespace Oqtane.Services
protected async Task DeleteAsync(string uri)
{
var response = await GetHttpClient().DeleteAsync(uri);
CheckResponse(response);
await CheckResponse(response, uri);
}
private bool CheckResponse(HttpResponseMessage response)
private async Task<bool> CheckResponse(HttpResponseMessage response, string uri)
{
if (response.IsSuccessStatusCode && uri.Contains("/api/") && !response.RequestMessage.RequestUri.AbsolutePath.Contains("/api/"))
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Not Mapped To An API Controller Method", uri);
return false;
}
if (response.IsSuccessStatusCode) return true;
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
{
Console.WriteLine($"Request: {response.RequestMessage.RequestUri}");
Console.WriteLine($"Response status: {response.StatusCode} {response.ReasonPhrase}");
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Failed With Status {StatusCode} - {ReasonPhrase}", uri, response.StatusCode, response.ReasonPhrase);
}
return false;
}
@ -216,6 +221,51 @@ namespace Oqtane.Services
return mediaType != null && mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase);
}
private async Task Log(string uri, string method, string status, string message, params object[] args)
{
if (_siteState.Alias != null && !uri.StartsWith(CreateApiUrl("Log")))
{
var log = new Log();
log.SiteId = _siteState.Alias.SiteId;
log.PageId = null;
log.ModuleId = null;
log.UserId = null;
log.Url = uri;
log.Category = GetType().AssemblyQualifiedName;
log.Feature = Utilities.GetTypeNameLastSegment(log.Category, 0);
switch (method)
{
case "GET":
log.Function = LogFunction.Read.ToString();
break;
case "POST":
log.Function = LogFunction.Create.ToString();
break;
case "PUT":
log.Function = LogFunction.Update.ToString();
break;
case "DELETE":
log.Function = LogFunction.Delete.ToString();
break;
default:
log.Function = LogFunction.Other.ToString();
break;
}
if (status == "500")
{
log.Level = LogLevel.Error.ToString();
}
else
{
log.Level = LogLevel.Warning.ToString();
}
log.Message = message;
log.MessageTemplate = "";
log.Properties = JsonSerializer.Serialize(args);
await PostJsonAsync(CreateApiUrl("Log"), log);
}
}
//[Obsolete("This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead.", false)]
// This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead.
protected ServiceBase(HttpClient client)

View File

@ -6,7 +6,7 @@
{
<div class="app-moduleactions py-2 px-3">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"></a>
<ul class="dropdown-menu" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 37px, 0px);">
<ul class="dropdown-menu" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 37px, 0px);" role="button">
@foreach (var action in Actions.Where(item => !item.Name.Contains("Pane")))
{
if (string.IsNullOrEmpty(action.Name))

View File

@ -139,11 +139,11 @@ namespace Oqtane.Themes.Controls
var permissions = pagemodule.Module.PermissionList;
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
{
permissions.Add(new Permission(ModuleState.SiteId, EntityNames.Page, pagemodule.PageId, PermissionNames.View, RoleNames.Everyone, null, true));
permissions.Add(new Permission(ModuleState.SiteId, EntityNames.Module, pagemodule.ModuleId, PermissionNames.View, RoleNames.Everyone, null, true));
}
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{
permissions.Add(new Permission(ModuleState.SiteId, EntityNames.Page, pagemodule.PageId, PermissionNames.View, RoleNames.Registered, null, true));
permissions.Add(new Permission(ModuleState.SiteId, EntityNames.Module, pagemodule.ModuleId, PermissionNames.View, RoleNames.Registered, null, true));
}
pagemodule.Module.PermissionList = permissions;
await ModuleService.UpdateModuleAsync(pagemodule.Module);

View File

@ -36,7 +36,7 @@
@if (_canViewAdminDashboard || UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{
<button type="button" class="btn @ButtonClass" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel">
<button type="button" class="btn @ButtonClass ms-1" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel">
<span class="oi oi-cog"></span>
</button>
@ -124,7 +124,7 @@
{
@if (_moduleDefinitions != null)
{
<select class="form-select" @onchange="(e => CategoryChanged(e))">
<select class="form-select mt-1" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == Category)
@ -137,7 +137,7 @@
}
}
</select>
<select class="form-select" @onchange="(e => ModuleChanged(e))">
<select class="form-select mt-1" @onchange="(e => ModuleChanged(e))">
@if (ModuleDefinitionName == "-")
{
<option value="-" selected>&lt;@Localizer["Module.Select"]&gt;</option>
@ -161,14 +161,14 @@
}
else
{
<select class="form-select" @onchange="(e => PageChanged(e))">
<select class="form-select mt-1" @onchange="(e => PageChanged(e))">
<option value="-">&lt;@Localizer["Page.Select"]&gt;</option>
@foreach (Page p in _pages)
{
<option value="@p.PageId">@p.Name</option>
}
</select>
<select class="form-select" @bind="@ModuleId">
<select class="form-select mt-1" @bind="@ModuleId">
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
@foreach (Module module in _modules)
{
@ -471,6 +471,12 @@
private async Task ToggleEditMode(bool EditMode)
{
Page page = null;
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
}
if (_showEditMode)
{
if (EditMode)
@ -490,9 +496,8 @@
}
else
{
if (PageState.Page.IsPersonalizable && PageState.User != null)
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
var page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
PageState.EditMode = true;
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + ((PageState.EditMode) ? "true" : "false")));
}
@ -545,13 +550,29 @@
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{
var permissions = PageState.Page.PermissionList;
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
switch (action)
{
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Everyone, null, true));
}
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Registered, null, true));
case "publish":
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
{
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Everyone, null, true));
}
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Registered, null, true));
}
break;
case "unpublish":
if (permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
{
permissions.RemoveAll(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone);
}
if (permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{
permissions.RemoveAll(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered);
}
break;
}
PageState.Page.PermissionList = permissions;
await PageService.UpdatePageAsync(PageState.Page);

View File

@ -19,7 +19,6 @@ namespace Oqtane.Themes.Controls
[Inject] public IUserService UserService { get; set; }
[Inject] public IJSRuntime jsRuntime { get; set; }
[Inject] public IServiceProvider ServiceProvider { get; set; }
[Inject] public ILogService LoggingService { get; set; }
protected void LoginUser()
{

View File

@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
using System;
@ -13,6 +16,9 @@ namespace Oqtane.Themes
{
public abstract class ThemeBase : ComponentBase, IThemeControl
{
[Inject]
protected ILogService LoggingService { get; set; }
[Inject]
protected IJSRuntime JSRuntime { get; set; }
@ -186,6 +192,148 @@ namespace Oqtane.Themes
await interop.ScrollTo(0, 0, "smooth");
}
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{
LogFunction logFunction;
if (string.IsNullOrEmpty(function))
{
// try to infer from page action
function = PageState.Action;
}
if (!Enum.TryParse(function, out logFunction))
{
switch (function.ToLower())
{
case "add":
logFunction = LogFunction.Create;
break;
case "edit":
logFunction = LogFunction.Update;
break;
case "delete":
logFunction = LogFunction.Delete;
break;
case "":
logFunction = LogFunction.Read;
break;
default:
logFunction = LogFunction.Other;
break;
}
}
await Log(alias, level, logFunction, exception, message, args);
}
public async Task Log(Alias alias, LogLevel level, LogFunction function, Exception exception, string message, params object[] args)
{
int pageId = PageState.Page.PageId;
string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(alias, pageId, null, PageState.User?.UserId, category, feature, function, level, exception, message, args);
}
public class Logger
{
private readonly ModuleBase _moduleBase;
public Logger(ModuleBase moduleBase)
{
_moduleBase = moduleBase;
}
public async Task LogTrace(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, "", null, message, args);
}
public async Task LogTrace(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, function, null, message, args);
}
public async Task LogTrace(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, "", exception, message, args);
}
public async Task LogDebug(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, "", null, message, args);
}
public async Task LogDebug(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, function, null, message, args);
}
public async Task LogDebug(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, "", exception, message, args);
}
public async Task LogInformation(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, "", null, message, args);
}
public async Task LogInformation(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, function, null, message, args);
}
public async Task LogInformation(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, "", exception, message, args);
}
public async Task LogWarning(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, "", null, message, args);
}
public async Task LogWarning(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, function, null, message, args);
}
public async Task LogWarning(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, "", exception, message, args);
}
public async Task LogError(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, "", null, message, args);
}
public async Task LogError(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, function, null, message, args);
}
public async Task LogError(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, "", exception, message, args);
}
public async Task LogCritical(string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, "", null, message, args);
}
public async Task LogCritical(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, function, null, message, args);
}
public async Task LogCritical(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, "", exception, message, args);
}
}
[Obsolete("ContentUrl(int fileId) is deprecated. Use FileUrl(int fileId) instead.", false)]
public string ContentUrl(int fileid)
{

View File

@ -9,12 +9,12 @@
@if (_useadminborder)
{
<div class="app-pane-admin-border">
@DynamicComponent
<DynamicComponent Type="@ComponentType"></DynamicComponent>
</div>
}
else
{
@DynamicComponent
<DynamicComponent Type="@ComponentType"></DynamicComponent>
}
</CascadingValue>
@ -23,6 +23,7 @@
@code {
private bool _visible = true;
private bool _useadminborder = false;
public Type ComponentType { get; set; }
[CascadingParameter]
protected PageState PageState { get; set; }
@ -30,8 +31,6 @@
[Parameter]
public Module ModuleState { get; set; }
RenderFragment DynamicComponent { get; set; }
protected override void OnInitialized()
{
((INotifyPropertyChanged)SiteState.Properties).PropertyChanged += PropertyChanged;
@ -54,17 +53,7 @@
_useadminborder = false;
}
DynamicComponent = builder =>
{
Type containerType = Type.GetType(container);
if (containerType == null)
{
// fallback
containerType = Type.GetType(Constants.DefaultContainer);
}
builder.OpenComponent(0, containerType);
builder.CloseComponent();
};
ComponentType = Type.GetType(container) ?? Type.GetType(Constants.DefaultContainer);
}
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
@ -83,4 +72,6 @@
{
((INotifyPropertyChanged)SiteState.Properties).PropertyChanged -= PropertyChanged;
}
}

View File

@ -223,12 +223,12 @@
}
if (page == null)
{
// look for personalized page
page = await PageService.GetPageAsync(route.PagePath, site.SiteId);
}
else
{
if (user != null && page.IsPersonalizable)
// look for personalized page
if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))
{
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId);
if (personalized != null)

View File

@ -26,7 +26,7 @@
}
else
{
SiteState.Properties.PageTitle = PageState.Site.Name + " - " + PageState.Page.Name;
SiteState.Properties.PageTitle = PageState.Page.Name + " - " + PageState.Site.Name;
}
// set page head content

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.MySQL</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane MySQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.PostgreSQL</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane PostgreSQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.SqlServer</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQL Server Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.Sqlite</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQLite Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>4.0.0</ApplicationDisplayVersion>
<ApplicationDisplayVersion>4.0.1</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Client</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v4.0.0/Oqtane.Framework.4.0.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v4.0.1/Oqtane.Framework.4.0.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane framework</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Server</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Shared</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Updater</id>
<version>4.0.0</version>
<version>4.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.0.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.1.Install.zip" -Force

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.0.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.1.Upgrade.zip" -Force

View File

@ -123,8 +123,38 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (file != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
[HttpGet("name/{name}/{folderId}")]
public Models.File Get(string name, int folderId)
{
Models.File file = _files.GetFile(folderId, name);
if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
return file;
}
else
{
if (file != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {Name} For Folder {FolderId}", name, folderId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -43,7 +43,7 @@ namespace Oqtane.Controllers
{
foreach (Folder folder in _folders.GetFolders(SiteId))
{
if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
if (_userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
{
folders.Add(folder);
}
@ -64,14 +64,21 @@ namespace Oqtane.Controllers
public Folder Get(int id)
{
Folder folder = _folders.GetFolder(id);
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
{
return folder;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {FolderId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (folder != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {FolderId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -80,19 +87,26 @@ namespace Oqtane.Controllers
public Folder GetByPath(int siteId, string path)
{
var folderPath = WebUtility.UrlDecode(path).Replace("\\", "/");
if (!folderPath.EndsWith("/"))
if (!folderPath.EndsWith("/") && folderPath != "")
{
folderPath += "/";
}
Folder folder = _folders.GetFolder(siteId, folderPath);
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
{
return folder;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {Path} For Site {SiteId}", path, siteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (folder != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {Path} For Site {SiteId}", path, siteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -124,7 +138,10 @@ namespace Oqtane.Controllers
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.UrlCombine(parent.Path, folder.Name);
}
folder.Path = folder.Path + "/";
if (!folder.Path.EndsWith("/"))
{
folder.Path = folder.Path + "/";
}
folder = _folders.AddFolder(folder);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Folder, folder.FolderId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder);
@ -166,7 +183,10 @@ namespace Oqtane.Controllers
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.UrlCombine(parent.Path, folder.Name);
}
folder.Path = folder.Path + "/";
if (!folder.Path.EndsWith("/"))
{
folder.Path = folder.Path + "/";
}
Folder _folder = _folders.GetFolder(id, false);
if (_folder.Path != folder.Path && Directory.Exists(_folders.GetFolderPath(_folder)))

View File

@ -89,8 +89,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Get Attempt {LanguageId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (language != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Get Attempt {LanguageId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -113,8 +113,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Module Get Attempt {ModuleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (module != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Module Get Attempt {ModuleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -89,15 +89,22 @@ namespace Oqtane.Controllers
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
ModuleDefinition moduledefinition = _moduleDefinitions.GetModuleDefinition(id, SiteId);
if (_userPermissions.IsAuthorized(User, PermissionNames.Utilize, moduledefinition.PermissionList))
if (moduledefinition != null && _userPermissions.IsAuthorized(User, PermissionNames.Utilize, moduledefinition.PermissionList))
{
if (string.IsNullOrEmpty(moduledefinition.Version)) moduledefinition.Version = new Version(1, 0, 0).ToString();
moduledefinition.Version = (string.IsNullOrEmpty(moduledefinition.Version)) ? new Version(1, 0, 0).ToString() : moduledefinition.Version;
return moduledefinition;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Get Attempt {ModuleDefinitionId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (moduledefinition != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Get Attempt {ModuleDefinitionId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -9,6 +9,9 @@ using Oqtane.Repository;
using Oqtane.Security;
using System.Net;
using System.Reflection.Metadata;
using Microsoft.Extensions.Localization;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System.Linq;
namespace Oqtane.Controllers
{
@ -30,6 +33,72 @@ namespace Oqtane.Controllers
_alias = tenantManager.GetAlias();
}
// GET: api/<controller>/read?siteid=x&direction=to&userid=1&count=5&isread=false
[HttpGet("read")]
[Authorize(Roles = RoleNames.Registered)]
public IEnumerable<Notification> Get(string siteid, string direction, string userid, string count, string isread)
{
IEnumerable<Notification> notifications = null;
int SiteId;
int UserId;
int Count;
bool IsRead;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && int.TryParse(userid, out UserId) && int.TryParse(count, out Count) && bool.TryParse(isread, out IsRead) && IsAuthorized(UserId))
{
if (direction == "to")
{
notifications = _notifications.GetNotifications(SiteId, -1, UserId, Count, IsRead);
}
else
{
notifications = _notifications.GetNotifications(SiteId, UserId, -1, Count, IsRead);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {SiteId} {Direction} {UserId} {Count} {isRead}", siteid, direction, userid, count, isread);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
notifications = null;
}
return notifications;
}
// GET: api/<controller>/read?siteid=x&direction=to&userid=1&count=5&isread=false
[HttpGet("read-count")]
[Authorize(Roles = RoleNames.Registered)]
public int Get(string siteid, string direction, string userid, string isread)
{
int notificationsCount = 0;
int SiteId;
int UserId;
bool IsRead;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && int.TryParse(userid, out UserId) && bool.TryParse(isread, out IsRead) && IsAuthorized(UserId))
{
if (direction == "to")
{
notificationsCount = _notifications.GetNotificationCount(SiteId, -1, UserId, IsRead);
}
else
{
notificationsCount = _notifications.GetNotificationCount(SiteId, UserId, -1, IsRead);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {SiteId} {Direction} {UserId} {isRead}", siteid, direction, userid, isread);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
notificationsCount = 0;
}
return notificationsCount;
}
// GET: api/<controller>?siteid=x&type=y&userid=z
[HttpGet]
[Authorize(Roles = RoleNames.Registered)]
@ -72,8 +141,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {NotificationId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (notification != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {NotificationId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -34,7 +34,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>?type=x&search=y&price=z&package=a
[HttpGet]
public async Task<IEnumerable<Package>> Get(string type, string search, string price, string package)
public async Task<IEnumerable<Package>> Get(string type, string search, string price, string package, string sort)
{
// get packages
List<Package> packages = new List<Package>();
@ -44,8 +44,8 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, Constants.PackageRegistryUrl + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}");
}
packages = await GetJson<List<Package>>(client, Constants.PackageRegistryUrl + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}&sort={sort}");
}
}
return packages;
}

View File

@ -7,10 +7,8 @@ using System.Linq;
using Oqtane.Security;
using System.Net;
using Oqtane.Enums;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Modules.Admin.Users;
using System.IO;
namespace Oqtane.Controllers
@ -89,8 +87,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Page Get Attempt {PageId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (page != null)
{
_logger.Log(LogLevel.Warning, this, LogFunction.Security, "Unauthorized Page Get Attempt {PageId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -109,8 +114,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Page Get Attempt {SiteId} {Path}", siteid, path);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (page != null)
{
_logger.Log(LogLevel.Warning, this, LogFunction.Security, "Unauthorized Page Get Attempt {SiteId} {Path}", siteid, path);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -177,64 +189,68 @@ namespace Oqtane.Controllers
User user = _userPermissions.GetUser(User);
if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid))
{
page = new Page();
page.SiteId = parent.SiteId;
page.ParentId = parent.PageId;
page.Name = user.Username;
page.Path = parent.Path + "/" + page.Name;
page.Title = parent.Name + " - " + page.Name;
page.Order = 0;
page.IsNavigation = false;
page.Url = "";
page.ThemeType = parent.ThemeType;
page.DefaultContainerType = parent.DefaultContainerType;
page.Icon = parent.Icon;
page.PermissionList = new List<Permission>()
page = _pages.GetPage(parent.Path + "/" + user.Username, parent.SiteId);
if (page == null)
{
new Permission(PermissionNames.View, int.Parse(userid), true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, int.Parse(userid), true)
};
page.IsPersonalizable = false;
page.UserId = int.Parse(userid);
page = _pages.AddPage(page);
// copy modules
List<PageModule> pagemodules = _pageModules.GetPageModules(page.SiteId).ToList();
foreach (PageModule pm in pagemodules.Where(item => item.PageId == parent.PageId && !item.IsDeleted))
{
Module module = new Module();
module.SiteId = page.SiteId;
module.PageId = page.PageId;
module.ModuleDefinitionName = pm.Module.ModuleDefinitionName;
module.AllPages = false;
module.PermissionList = new List<Permission>()
page = new Page();
page.SiteId = parent.SiteId;
page.ParentId = parent.PageId;
page.Name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username;
page.Path = parent.Path + "/" + user.Username;
page.Title = page.Name + " - " + parent.Name;
page.Order = 0;
page.IsNavigation = false;
page.Url = "";
page.ThemeType = parent.ThemeType;
page.DefaultContainerType = parent.DefaultContainerType;
page.Icon = parent.Icon;
page.PermissionList = new List<Permission>()
{
new Permission(PermissionNames.View, int.Parse(userid), true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, int.Parse(userid), true)
};
module = _modules.AddModule(module);
page.IsPersonalizable = false;
page.UserId = int.Parse(userid);
page = _pages.AddPage(page);
string content = _modules.ExportModule(pm.ModuleId);
if (content != "")
// copy modules
List<PageModule> pagemodules = _pageModules.GetPageModules(page.SiteId).ToList();
foreach (PageModule pm in pagemodules.Where(item => item.PageId == parent.PageId && !item.IsDeleted))
{
_modules.ImportModule(module.ModuleId, content);
Module module = new Module();
module.SiteId = page.SiteId;
module.PageId = page.PageId;
module.ModuleDefinitionName = pm.Module.ModuleDefinitionName;
module.AllPages = false;
module.PermissionList = new List<Permission>()
{
new Permission(PermissionNames.View, int.Parse(userid), true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, int.Parse(userid), true)
};
module = _modules.AddModule(module);
string content = _modules.ExportModule(pm.ModuleId);
if (content != "")
{
_modules.ImportModule(module.ModuleId, content);
}
PageModule pagemodule = new PageModule();
pagemodule.PageId = page.PageId;
pagemodule.ModuleId = module.ModuleId;
pagemodule.Title = pm.Title;
pagemodule.Pane = pm.Pane;
pagemodule.Order = pm.Order;
pagemodule.ContainerType = pm.ContainerType;
_pageModules.AddPageModule(pagemodule);
}
PageModule pagemodule = new PageModule();
pagemodule.PageId = page.PageId;
pagemodule.ModuleId = module.ModuleId;
pagemodule.Title = pm.Title;
pagemodule.Pane = pm.Pane;
pagemodule.Order = pm.Order;
pagemodule.ContainerType = pm.ContainerType;
_pageModules.AddPageModule(pagemodule);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Page, page.PageId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
}
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Page, page.PageId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
}
else
{
@ -263,14 +279,18 @@ namespace Oqtane.Controllers
// save url mapping if page path changed
if (currentPage.Path != page.Path)
{
var urlMapping = new UrlMapping();
urlMapping.SiteId = page.SiteId;
urlMapping.Url = currentPage.Path;
urlMapping.MappedUrl = page.Path;
urlMapping.Requests = 0;
urlMapping.CreatedOn = System.DateTime.UtcNow;
urlMapping.RequestedOn = System.DateTime.UtcNow;
_urlMappings.AddUrlMapping(urlMapping);
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path);
if (urlMapping == null)
{
urlMapping = new UrlMapping();
urlMapping.SiteId = page.SiteId;
urlMapping.Url = currentPage.Path;
urlMapping.MappedUrl = page.Path;
urlMapping.Requests = 0;
urlMapping.CreatedOn = System.DateTime.UtcNow;
urlMapping.RequestedOn = System.DateTime.UtcNow;
_urlMappings.AddUrlMapping(urlMapping);
}
}
// get differences between current and new page permissions
@ -314,6 +334,16 @@ namespace Oqtane.Controllers
}
}
// update child paths
if (page.ParentId != currentPage.ParentId)
{
foreach (Page _page in _pages.GetPages(page.SiteId).Where(item => item.Path.StartsWith(currentPage.Path)).ToList())
{
_page.Path = _page.Path.Replace(currentPage.Path, page.Path);
_pages.UpdatePage(_page);
}
}
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Page, page.PageId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page);

View File

@ -44,8 +44,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageModuleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (pagemodule != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageModuleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -61,8 +68,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageId} {ModuleId}", pageid, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (pagemodule != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageId} {ModuleId}", pageid, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -56,8 +56,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Profile Get Attempt {ProfileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (profile != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Profile Get Attempt {ProfileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -59,9 +59,16 @@ namespace Oqtane.Controllers
return role;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Role Get Attempt {RoleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
{
if (role != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Role Get Attempt {RoleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -89,11 +89,15 @@ namespace Oqtane.Controllers
}
else
{
if (entityName != EntityNames.Visitor)
if (setting != null && entityName != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Setting {EntityName} {SettingId}", entityName, id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -79,7 +79,7 @@ namespace Oqtane.Controllers
private Site GetSite(int siteid)
{
var site = _sites.GetSite(siteid);
if (site.SiteId == _alias.SiteId)
if (site != null && site.SiteId == _alias.SiteId)
{
// site settings
site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId)
@ -153,8 +153,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (site != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -246,7 +253,7 @@ namespace Oqtane.Controllers
foreach (Page child in children)
{
child.Level = level + 1;
child.HasChildren = pages.Any(item => item.ParentId == child.PageId);
child.HasChildren = pages.Any(item => item.ParentId == child.PageId && !item.IsDeleted && item.IsNavigation);
hierarchy.Add(child);
getPath(pageList, child);
}

View File

@ -55,9 +55,16 @@ namespace Oqtane.Controllers
return urlMapping;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {UrlMappingId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
{
if (urlMapping != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {UrlMappingId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}
@ -73,8 +80,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {SiteId} {Url}", siteid, url);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (urlMapping != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {SiteId} {Url}", siteid, url);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@ -9,13 +8,13 @@ using System.Linq;
using System.Security.Claims;
using Oqtane.Shared;
using System;
using System.IO;
using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Extensions;
using Oqtane.Managers;
namespace Oqtane.Controllers
{
@ -23,31 +22,27 @@ namespace Oqtane.Controllers
public class UserController : Controller
{
private readonly IUserRepository _users;
private readonly IUserRoleRepository _userRoles;
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders;
private readonly IUserManager _userManager;
private readonly ISiteRepository _sites;
private readonly IUserPermissions _userPermissions;
private readonly IJwtManager _jwtManager;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
public UserController(IUserRepository users, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ISyncManager syncManager, ILogManager logger)
public UserController(IUserRepository users, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ILogManager logger)
{
_users = users;
_userRoles = userRoles;
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_notifications = notifications;
_folders = folders;
_userManager = userManager;
_sites = sites;
_userPermissions = userPermissions;
_jwtManager = jwtManager;
_syncManager = syncManager;
_logger = logger;
}
@ -56,14 +51,12 @@ namespace Oqtane.Controllers
[Authorize]
public User Get(int id, string siteid)
{
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
User user = _users.GetUser(id);
if (user != null)
User user = _userManager.GetUser(id, SiteId);
if (user == null)
{
user.SiteId = int.Parse(siteid);
user.Roles = GetUserRoles(user.UserId, user.SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return Filter(user);
}
@ -79,14 +72,12 @@ namespace Oqtane.Controllers
[HttpGet("name/{name}")]
public User Get(string name, string siteid)
{
int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
User user = _users.GetUser(name);
if (user != null)
User user = _userManager.GetUser(name, SiteId);
if (user == null)
{
user.SiteId = int.Parse(siteid);
user.Roles = GetUserRoles(user.UserId, user.SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return Filter(user);
}
@ -133,8 +124,28 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId)
{
var User = await CreateUser(user);
return User;
bool allowregistration;
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{
user.EmailConfirmed = true;
allowregistration = true;
}
else
{
user.EmailConfirmed = false;
allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
}
if (allowregistration)
{
user = await _userManager.AddUser(user);
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "User Registration Is Not Enabled For Site. User Was Not Added {User}", user);
}
return user;
}
else
{
@ -145,99 +156,6 @@ namespace Oqtane.Controllers
}
}
private async Task<User> CreateUser(User user)
{
User newUser = null;
bool verified;
bool allowregistration;
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{
verified = true;
allowregistration = true;
}
else
{
verified = false;
allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
}
if (allowregistration)
{
bool succeeded;
string errors = "";
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser == null)
{
identityuser = new IdentityUser();
identityuser.UserName = user.Username;
identityuser.Email = user.Email;
identityuser.EmailConfirmed = verified;
var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
succeeded = result.Succeeded;
if (!succeeded)
{
errors = string.Join(", ", result.Errors.Select(e => e.Description));
}
}
else
{
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
succeeded = result.Succeeded;
if (!succeeded)
{
errors = "Password Not Valid For User";
}
verified = succeeded;
}
if (succeeded)
{
user.LastLoginOn = null;
user.LastIPAddress = "";
newUser = _users.AddUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, newUser.UserId, SyncEventActions.Create);
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {Username} - {Errors}", user.Username, errors);
}
if (newUser != null)
{
if (!verified)
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, newUser, "User Account Verification", body);
_notifications.AddNotification(notification);
}
else
{
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name;
string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully Created For You. Please Use The Following Link To Access The Site:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, newUser, "User Account Notification", body);
_notifications.AddNotification(notification);
}
newUser.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", newUser);
}
else
{
user.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {User}", user);
}
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "User Registration Is Not Enabled For Site. User Was Not Added {User}", user);
}
return newUser;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize]
@ -246,37 +164,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
identityuser.Email = user.Email;
var valid = true;
if (user.Password != "")
{
var validator = new PasswordValidator<IdentityUser>();
var result = await validator.ValidateAsync(_identityUserManager, null, user.Password);
valid = result.Succeeded;
if (valid)
{
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
}
}
if (valid)
{
await _identityUserManager.UpdateAsync(identityuser);
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Refresh);
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. Password Does Not Meet Complexity Requirements.", user.Username);
user = null;
}
}
user = await _userManager.UpdateUser(user);
}
else
{
@ -293,51 +181,10 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")]
public async Task Delete(int id, string siteid)
{
int SiteId;
User user = _users.GetUser(id);
if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
User user = _users.GetUser(id, false);
if (user != null && int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
// remove user roles for site
foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, SiteId).ToList())
{
_userRoles.DeleteUserRole(userrole.UserRoleId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userrole);
}
// remove user folder for site
var folder = _folders.GetFolder(SiteId, $"Users{user.UserId}/");
if (folder != null)
{
if (Directory.Exists(_folders.GetFolderPath(folder)))
{
Directory.Delete(_folders.GetFolderPath(folder), true);
}
_folders.DeleteFolder(folder.FolderId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Folder Deleted {Folder}", folder);
}
// delete user if they are not a member of any other sites
if (!_userRoles.GetUserRoles(user.UserId, -1).Any())
{
// get identity user
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
// delete identity user
var result = await _identityUserManager.DeleteAsync(identityuser);
if (result != null)
{
// delete user
_users.DeleteUser(user.UserId);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", user.UserId, result.ToString());
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Error Deleting User {UserId}", user.UserId);
}
}
}
await _userManager.DeleteUser(id, SiteId);
}
else
{
@ -350,83 +197,15 @@ namespace Oqtane.Controllers
[HttpPost("login")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent)
{
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
if (result.Succeeded)
{
var LastIPAddress = user.LastIPAddress ?? "";
user = _users.GetUser(user.Username);
if (user.TwoFactorRequired)
{
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
user.TwoFactorCode = token;
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
_users.UpdateUser(user);
string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token +
"\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." +
"\n\nThank You!";
var notification = new Notification(loginUser.SiteId, user, "User Verification Code", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username);
loginUser.TwoFactorRequired = true;
}
else
{
loginUser = _users.GetUser(identityuser.UserName);
if (loginUser != null)
{
if (identityuser.EmailConfirmed)
{
loginUser.IsAuthenticated = true;
loginUser.LastLoginOn = DateTime.UtcNow;
loginUser.LastIPAddress = LastIPAddress;
_users.UpdateUser(loginUser);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
}
}
}
}
else
{
if (result.IsLockedOut)
{
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nThank You!";
var notification = new Notification(loginUser.SiteId, user, "User Lockout", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
}
}
}
user = await _userManager.LoginUser(user, setCookie, isPersistent);
}
return loginUser;
else
{
user = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
}
return user;
}
// POST api/<controller>/logout
@ -444,25 +223,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Email Verified For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}And Token {Token}", user.Username, token);
user = null;
}
user = await _userManager.VerifyEmail(user, token);
}
return user;
}
@ -473,25 +234,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nIf you did not request to reset your password you can safely ignore this message." +
"\n\nThank You!";
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, "User Password Reset", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
}
await _userManager.ForgotPassword(user);
}
}
@ -501,26 +244,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
if (result.Succeeded)
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username);
user.Password = "";
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
user = await _userManager.ResetPassword(user, token);
}
return user;
}
@ -529,21 +253,16 @@ namespace Oqtane.Controllers
[HttpPost("twofactor")]
public User TwoFactor([FromBody] User user, string token)
{
User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
if (ModelState.IsValid && !string.IsNullOrEmpty(token))
{
user = _users.GetUser(user.Username);
if (user != null)
{
if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{
loginUser.IsAuthenticated = true;
}
}
user = _userManager.VerifyTwoFactor(user, token);
}
else
{
user.IsAuthenticated = false;
}
return loginUser;
return user;
}
// POST api/<controller>/link
@ -552,23 +271,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
user = await _userManager.LinkExternalAccount(user, token, type, key, name);
}
else
{
@ -582,9 +285,7 @@ namespace Oqtane.Controllers
[HttpGet("validate/{password}")]
public async Task<bool> Validate(string password)
{
var validator = new PasswordValidator<IdentityUser>();
var result = await validator.ValidateAsync(_identityUserManager, null, password);
return result.Succeeded;
return await _userManager.ValidatePassword(password);
}
// GET api/<controller>/token
@ -640,21 +341,5 @@ namespace Oqtane.Controllers
}
return user;
}
private string GetUserRoles(int userId, int siteId)
{
string roles = "";
List<UserRole> userroles = _userRoles.GetUserRoles(userId, siteId).ToList();
foreach (UserRole userrole in userroles)
{
roles += userrole.Role.Name + ";";
if (userrole.Role.Name == RoleNames.Host && userroles.Where(item => item.Role.Name == RoleNames.Admin).FirstOrDefault() == null)
{
roles += RoleNames.Admin + ";";
}
}
if (roles != "") roles = ";" + roles;
return roles;
}
}
}

View File

@ -79,8 +79,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Role Get Attempt {UserRoleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (userrole != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Role Get Attempt {UserRoleId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -64,8 +64,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Visitor Get Attempt {VisitorId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (visitor != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Visitor Get Attempt {VisitorId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
}

View File

@ -19,6 +19,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Repository;
@ -73,13 +74,7 @@ namespace Microsoft.Extensions.DependencyInjection
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{
services.AddTransient<IDBContextDependencies, DBContextDependencies>();
services.AddTransient<ITenantManager, TenantManager>();
services.AddTransient<IAliasAccessor, AliasAccessor>();
services.AddTransient<IUserPermissions, UserPermissions>();
services.AddTransient<ITenantResolver, TenantResolver>();
services.AddTransient<IJwtManager, JwtManager>();
// repositories
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IAliasRepository, AliasRepository>();
@ -95,7 +90,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<IPermissionRepository, PermissionRepository>();
services.AddTransient<ISettingRepository, SettingRepository>();
services.AddTransient<ILogRepository, LogRepository>();
services.AddTransient<ILogManager, LogManager>();
services.AddTransient<ILocalizationManager, LocalizationManager>();
services.AddTransient<IJobRepository, JobRepository>();
services.AddTransient<IJobLogRepository, JobLogRepository>();
@ -104,11 +98,21 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<IFileRepository, FileRepository>();
services.AddTransient<ISiteTemplateRepository, SiteTemplateRepository>();
services.AddTransient<ISqlRepository, SqlRepository>();
services.AddTransient<IUpgradeManager, UpgradeManager>();
services.AddTransient<ILanguageRepository, LanguageRepository>();
services.AddTransient<IVisitorRepository, VisitorRepository>();
services.AddTransient<IUrlMappingRepository, UrlMappingRepository>();
// managers
services.AddTransient<IDBContextDependencies, DBContextDependencies>();
services.AddTransient<ITenantManager, TenantManager>();
services.AddTransient<IAliasAccessor, AliasAccessor>();
services.AddTransient<IUserPermissions, UserPermissions>();
services.AddTransient<ITenantResolver, TenantResolver>();
services.AddTransient<IJwtManager, JwtManager>();
services.AddTransient<ILogManager, LogManager>();
services.AddTransient<IUpgradeManager, UpgradeManager>();
services.AddTransient<IUserManager, UserManager>();
// obsolete - replaced by ITenantManager
services.AddTransient<ITenantResolver, TenantResolver>();

View File

@ -201,7 +201,24 @@ namespace Oqtane.Infrastructure
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
entry.ExtractToFile(filename, true);
if (Path.Exists(filename) && Path.GetExtension(filename).ToLower() == ".dll")
{
// ensure assembly version is equal to or greater than existing assembly
var assembly = filename.Replace(Path.GetFileName(filename), "temp.dll");
entry.ExtractToFile(assembly, true);
if (Version.Parse(FileVersionInfo.GetVersionInfo(assembly).FileVersion).CompareTo(Version.Parse(FileVersionInfo.GetVersionInfo(filename).FileVersion)) >= 0)
{
File.Move(assembly, filename, true);
}
else
{
File.Delete(assembly);
}
}
else
{
entry.ExtractToFile(filename, true);
}
}
catch
{

View File

@ -42,7 +42,7 @@ namespace Oqtane.Infrastructure
// get site settings
List<Setting> sitesettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToList();
Dictionary<string, string> settings = GetSettings(sitesettings);
if (!settings.ContainsKey("SMTPEnabled") || settings["SMTPEnabled"] == "True")
if (!site.IsDeleted && (!settings.ContainsKey("SMTPEnabled") || settings["SMTPEnabled"] == "True"))
{
if (settings.ContainsKey("SMTPHost") && settings["SMTPHost"] != "" &&
settings.ContainsKey("SMTPPort") && settings["SMTPPort"] != "" &&
@ -162,7 +162,7 @@ namespace Oqtane.Infrastructure
}
else
{
log += "SMTP Disabled In Site Settings" + "<br />";
log += "Site Deleted Or SMTP Disabled In Site Settings" + "<br />";
}
}

View File

@ -210,7 +210,7 @@ namespace Oqtane.Infrastructure
{
var subject = $"{alias.Name} Site {log.Level} Notification";
var url = $"{_accessor.HttpContext.Request.Scheme}://{alias.Name}/admin/log?id={log.LogId}";
string body = $"Log Message: {log.Message}\n\nPlease visit {url} for more information";
string body = $"Log Message: {log.Message}<br /><br />Please visit {url} for more information";
var notification = new Notification(log.SiteId.Value, userrole.User, subject, body);
_notifications.AddNotification(notification);
}

View File

@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Managers
{
public interface IUserManager
{
User GetUser(int userid, int siteid);
User GetUser(string username, int siteid);
Task<User> AddUser(User user);
Task<User> UpdateUser(User user);
Task DeleteUser(int userid, int siteid);
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
Task<User> VerifyEmail(User user, string token);
Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
Task<bool> ValidatePassword(string password);
}
}

View File

@ -0,0 +1,434 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Managers
{
public class UserManager : IUserManager
{
private readonly IUserRepository _users;
private readonly IUserRoleRepository _userRoles;
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
public UserManager(IUserRepository users, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ILogManager logger)
{
_users = users;
_userRoles = userRoles;
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_notifications = notifications;
_folders = folders;
_syncManager = syncManager;
_logger = logger;
}
public User GetUser(int userid, int siteid)
{
User user = _users.GetUser(userid);
if (user != null)
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
}
return user;
}
public User GetUser(string username, int siteid)
{
User user = _users.GetUser(username);
if (user != null)
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
}
return user;
}
private string GetUserRoles(int userId, int siteId)
{
string roles = "";
List<UserRole> userroles = _userRoles.GetUserRoles(userId, siteId).ToList();
foreach (UserRole userrole in userroles)
{
roles += userrole.Role.Name + ";";
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Admin))
{
roles += RoleNames.Admin + ";";
}
if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Registered))
{
roles += RoleNames.Registered + ";";
}
}
if (roles != "") roles = ";" + roles;
return roles;
}
public async Task<User> AddUser(User user)
{
User User = null;
var alias = _tenantManager.GetAlias();
bool succeeded = false;
string errors = "";
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser == null)
{
identityuser = new IdentityUser();
identityuser.UserName = user.Username;
identityuser.Email = user.Email;
identityuser.EmailConfirmed = user.EmailConfirmed;
var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
succeeded = result.Succeeded;
if (!succeeded)
{
errors = string.Join(", ", result.Errors.Select(e => e.Description));
}
}
else
{
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
succeeded = result.Succeeded;
if (!succeeded)
{
errors = "Password Not Valid For User";
}
user.EmailConfirmed = succeeded;
}
if (succeeded)
{
user.DisplayName = (user.DisplayName == null) ? user.Username : user.DisplayName;
user.LastLoginOn = null;
user.LastIPAddress = "";
User = _users.AddUser(user);
_syncManager.AddSyncEvent(alias.TenantId, EntityNames.User, User.UserId, SyncEventActions.Create);
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {Username} - {Errors}", user.Username, errors);
}
if (User != null)
{
if (!user.EmailConfirmed)
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + "://" + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Verification", body);
_notifications.AddNotification(notification);
}
else
{
string url = alias.Protocol + "://" + alias.Name;
string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully Created For You. Please Use The Following Link To Access The Site:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Notification", body);
_notifications.AddNotification(notification);
}
User.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", User);
}
else
{
user.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {User}", user);
}
return User;
}
public async Task<User> UpdateUser(User user)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
identityuser.Email = user.Email;
var valid = true;
if (user.Password != "")
{
var validator = new PasswordValidator<IdentityUser>();
var result = await validator.ValidateAsync(_identityUserManager, null, user.Password);
valid = result.Succeeded;
if (valid)
{
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
}
}
if (valid)
{
await _identityUserManager.UpdateAsync(identityuser);
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Refresh);
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
}
else
{
_logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. Password Does Not Meet Complexity Requirements.", user.Username);
user = null;
}
}
return user;
}
public async Task DeleteUser(int userid, int siteid)
{
// remove user roles for site
foreach (UserRole userrole in _userRoles.GetUserRoles(userid, siteid).ToList())
{
_userRoles.DeleteUserRole(userrole.UserRoleId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userrole);
}
// remove user folder for site
var folder = _folders.GetFolder(siteid, $"Users/{userid}/");
if (folder != null)
{
if (Directory.Exists(_folders.GetFolderPath(folder)))
{
Directory.Delete(_folders.GetFolderPath(folder), true);
}
_folders.DeleteFolder(folder.FolderId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Folder Deleted {Folder}", folder);
}
// delete user if they are not a member of any other sites
if (!_userRoles.GetUserRoles(userid, -1).Any())
{
// get identity user
var user = _users.GetUser(userid, false);
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
// delete identity user
var result = await _identityUserManager.DeleteAsync(identityuser);
if (result != null)
{
// delete user
_users.DeleteUser(userid);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, userid, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", userid, result.ToString());
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Error Deleting User {UserId}", userid);
}
}
}
}
public async Task<User> LoginUser(User user, bool setCookie, bool isPersistent)
{
user.IsAuthenticated = false;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
if (result.Succeeded)
{
var LastIPAddress = user.LastIPAddress ?? "";
user = _users.GetUser(user.Username);
if (user.TwoFactorRequired)
{
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
user.TwoFactorCode = token;
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
_users.UpdateUser(user);
string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token +
"\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." +
"\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Verification Code", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username);
user.TwoFactorRequired = true;
}
else
{
user = _users.GetUser(identityuser.UserName);
if (user != null)
{
if (identityuser.EmailConfirmed)
{
user.IsAuthenticated = true;
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = LastIPAddress;
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
}
}
}
}
else
{
if (result.IsLockedOut)
{
var alias = _tenantManager.GetAlias();
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + "://" + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Lockout", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
}
}
}
return user;
}
public async Task<User> VerifyEmail(User user, string token)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Email Verified For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}And Token {Token}", user.Username, token);
user = null;
}
return user;
}
public async Task ForgotPassword(User user)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var alias = _tenantManager.GetAlias();
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + "://" + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nIf you did not request to reset your password you can safely ignore this message." +
"\n\nThank You!";
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, "User Password Reset", body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
}
}
public async Task<User> ResetPassword(User user, string token)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
if (result.Succeeded)
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username);
user.Password = "";
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
return user;
}
public User VerifyTwoFactor(User user, string token)
{
user = _users.GetUser(user.Username);
if (user != null)
{
if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{
user.IsAuthenticated = true;
}
}
return user;
}
public async Task<User> LinkExternalAccount(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task<bool> ValidatePassword(string password)
{
var validator = new PasswordValidator<IdentityUser>();
var result = await validator.ValidateAsync(_identityUserManager, null, password);
return result.Succeeded;
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.04.00.01.01")]
public class AddNotificationIsRead : MultiDatabaseMigration
{
public AddNotificationIsRead(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var notificationEntityBuilder = new NotificationEntityBuilder(migrationBuilder, ActiveDatabase);
notificationEntityBuilder.AddBooleanColumn("IsRead", true);
notificationEntityBuilder.UpdateColumn("IsRead", "1", "bool", "");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var notificationEntityBuilder = new NotificationEntityBuilder(migrationBuilder, ActiveDatabase);
notificationEntityBuilder.DropColumn("IsRead");
}
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -114,7 +114,7 @@ namespace Oqtane.Pages
}
var site = _sites.InitializeSite(alias);
if (site != null && !site.IsDeleted && site.Runtime != "Hybrid")
if (site != null && (!site.IsDeleted || url.Contains("admin/site")) && site.Runtime != "Hybrid")
{
Route route = new Route(url, alias.Path);

View File

@ -6,6 +6,8 @@ namespace Oqtane.Repository
public interface INotificationRepository
{
IEnumerable<Notification> GetNotifications(int siteId, int fromUserId, int toUserId);
IEnumerable<Notification> GetNotifications(int siteId, int fromUserId, int toUserId, int count, bool isRead);
int GetNotificationCount(int siteId, int fromUserId, int toUserId, bool isRead);
Notification AddNotification(Notification notification);
Notification UpdateNotification(Notification notification);
Notification GetNotification(int notificationId);

View File

@ -101,6 +101,7 @@ namespace Oqtane.Repository
ModuleDefinition.IsPortable = moduleDefinition.IsPortable;
ModuleDefinition.Resources = moduleDefinition.Resources;
ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled;
ModuleDefinition.PackageName = moduleDefinition.PackageName;
}
return ModuleDefinition;

View File

@ -33,6 +33,54 @@ namespace Oqtane.Repository
.ToList();
}
public IEnumerable<Notification> GetNotifications(int siteId, int fromUserId, int toUserId, int count, bool isRead)
{
if (toUserId == -1 && fromUserId == -1)
{
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.IsDelivered == false && item.IsDeleted == false)
.Where(item => item.SendOn == null || item.SendOn < System.DateTime.UtcNow)
.Where(item => item.IsRead == isRead)
.OrderByDescending(item => item.CreatedOn)
.ToList()
.Take(count);
}
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.ToUserId == toUserId || toUserId == -1)
.Where(item => item.FromUserId == fromUserId || fromUserId == -1)
.Where(item => item.IsRead == isRead)
.OrderByDescending(item => item.CreatedOn)
.ToList()
.Take(count);
}
public int GetNotificationCount(int siteId, int fromUserId, int toUserId, bool isRead)
{
if (toUserId == -1 && fromUserId == -1)
{
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.IsDelivered == false && item.IsDeleted == false)
.Where(item => item.SendOn == null || item.SendOn < System.DateTime.UtcNow)
.Where(item => item.IsRead == isRead)
.ToList()
.Count();
}
return _db.Notification
.Where(item => item.SiteId == siteId)
.Where(item => item.ToUserId == toUserId || toUserId == -1)
.Where(item => item.FromUserId == fromUserId || fromUserId == -1)
.Where(item => item.IsRead == isRead)
.ToList()
.Count();
}
public Notification AddNotification(Notification notification)
{
_db.Notification.Add(notification);

View File

@ -89,6 +89,7 @@ namespace Oqtane.Repository
Theme.Containers = theme.Containers;
Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName;
Themes.Add(Theme);
}

View File

@ -13,7 +13,7 @@
</div>
@code {
private string resourceType = "[Owner].[Module].Settings, [Owner].[Module].Client.Oqtane"; // for localization
private string resourceType = "[Owner].Module.[Module].Settings, [Owner].Module.[Module].Client.Oqtane"; // for localization
public override string Title => "[Module] Settings";
string _value;

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<Version>1.0.0</Version>
<Authors>[Owner]</Authors>
<Company>[Owner]</Company>

View File

@ -1,7 +1,7 @@
cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/"

View File

@ -1,2 +1,2 @@
"..\..\oqtane.framework\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec
"..\..\oqtane.framework\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec
cp -f "*.nupkg" "..\..\oqtane.framework\Oqtane.Server\Packages\"

View File

@ -20,9 +20,7 @@ Global
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.Build.0 = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU

Some files were not shown because too many files have changed in this diff Show More