Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
Leigh Pointer
2025-09-26 12:33:37 +02:00
35 changed files with 516 additions and 219 deletions

View File

@@ -7,13 +7,80 @@
"Blazor", "Blazor",
"Oqtane" "Oqtane"
], ],
"name": "Oqtane Application Template",
"shortName": "oqtane-app",
"defaultName": "MyCompany.MyProject",
"identity": "Oqtane.Application.Template",
"tags": { "tags": {
"language": "C#", "language": "C#",
"type": "project" "type": "solution",
"editorTreatAs":"solution"
}, },
"identity": "Oqtane.Application.Template",
"name": "Oqtane Application Template For Blazor",
"shortName": "oqtane-app",
"sourceName": "Oqtane.Application", "sourceName": "Oqtane.Application",
"preferNameDirectory": true "preferNameDirectory": true,
"guids": [
"04B05448-788F-433D-92C0-FED35122D45A",
"AA8E58A1-CD09-4208-BF66-A8BB341FD669",
"18D73F73-D7BE-4388-85BA-FBD9AC96FCA2"
],
"symbols": {
"Framework": {
"type": "parameter",
"description": "The target framework for the project",
"datatype": "choice",
"choices": [
{
"choice": "net9.0",
"description": "Target net9.0"
}
],
"replaces": "net9.0",
"defaultValue": "net9.0"
},
"HttpPort": {
"type": "parameter",
"datatype": "integer",
"description": "Port number to use for the HTTP endpoint in launchSettings.json."
},
"HttpPortGenerated": {
"type": "generated",
"generator": "port"
},
"HttpPortReplacer": {
"type": "generated",
"generator": "coalesce",
"parameters": {
"sourceVariableName": "HttpPort",
"fallbackVariableName": "HttpPortGenerated"
},
"replaces": "44358"
},
"HttpsPort": {
"type": "parameter",
"datatype": "integer",
"description": "Port number to use for the HTTPS endpoint in launchSettings.json."
},
"HttpsPortGenerated": {
"type": "generated",
"generator": "port",
"parameters": {
"low": 44300,
"high": 44399
}
},
"HttpsPortReplacer": {
"type": "generated",
"generator": "coalesce",
"parameters": {
"sourceVariableName": "HttpsPort",
"fallbackVariableName": "HttpsPortGenerated"
},
"replaces": "44359"
}
},
"primaryOutputs": [
{
"path": "Oqtane.Application.sln"
}
]
} }

View File

@@ -1,23 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Client.Oqtane</AssemblyName> <AssemblyName>Oqtane.Application.Client.Oqtane</AssemblyName>
<IsPackable>true</IsPackable> <StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode> <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData> <PublishTrimmed>false</PublishTrimmed>
<PublishTrimmed>false</PublishTrimmed> <BlazorEnableCompression>false</BlazorEnableCompression>
<BlazorEnableCompression>false</BlazorEnableCompression> </PropertyGroup>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
</ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.9" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Client" Version="6.2.1" /> <ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="6.2.1" />
</ItemGroup>
</Project> </Project>

View File

@@ -9,7 +9,7 @@ cd MyCompany.MyProject
dotnet build dotnet build
cd Server cd Server
dotnet run dotnet run
browse to http://localhost:5001 browse to Url
``` ```
When using this approach you do not need to have a local copy of the oqtane.framework source code - you simply utilize Oqtane as a standard application dependency. When using this approach you do not need to have a local copy of the oqtane.framework source code - you simply utilize Oqtane as a standard application dependency.

View File

@@ -1,23 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Server.Oqtane</AssemblyName> <AssemblyName>Oqtane.Application.Server.Oqtane</AssemblyName>
<IsPackable>true</IsPackable> <PreserveCompilationContext>true</PreserveCompilationContext>
<PreserveCompilationContext>true</PreserveCompilationContext> <SatelliteResourceLanguages>none</SatelliteResourceLanguages>
<SatelliteResourceLanguages>none</SatelliteResourceLanguages> <CompressionEnabled>false</CompressionEnabled>
<CompressionEnabled>false</CompressionEnabled> <StaticWebAssetsFingerprintContent>false</StaticWebAssetsFingerprintContent>
<StaticWebAssetsFingerprintContent>false</StaticWebAssetsFingerprintContent> </PropertyGroup>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Client\Oqtane.Application.Client.csproj" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.9" />
<ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.9" />
</ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Server" Version="6.2.1" /> <ProjectReference Include="..\Client\Oqtane.Application.Client.csproj" />
</ItemGroup> <ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="6.2.1" />
</ItemGroup>
</Project> </Project>

View File

@@ -6,7 +6,7 @@
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:44358",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@@ -16,7 +16,7 @@
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000", "applicationUrl": "https://localhost:44359;http://localhost:44358",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -1,14 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Shared.Oqtane</AssemblyName> <AssemblyName>Oqtane.Application.Shared.Oqtane</AssemblyName>
<IsPackable>true</IsPackable> </PropertyGroup>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="6.2.1" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="6.2.1" />
</ItemGroup>
</Project> </Project>

View File

@@ -14,7 +14,7 @@
@if (_initialized) @if (_initialized)
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Definition" ResourceKey="Definition" Heading="Definition"> <TabPanel Name="Module" ResourceKey="Module" Heading="Module">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -236,11 +236,10 @@
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private List<Page> _pagesWithModules;
#pragma warning disable 649
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
#pragma warning restore 649
private List<Page> _pagesWithModules;
private List<Package> _packages; private List<Package> _packages;
private List<Language> _languages; private List<Language> _languages;

View File

@@ -269,8 +269,16 @@
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList))) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || (_parent != null && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _parent.PermissionList)))
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_children = new List<Page>(); _children = new List<Page>();
foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))

View File

@@ -443,8 +443,16 @@
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
} }
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = _page.DefaultContainerType; _containertype = _page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype)) if (string.IsNullOrEmpty(_containertype))
{ {

View File

@@ -2,6 +2,7 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -56,9 +57,25 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="options" HelpText="A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from custom Settings (ie. 'EntityName:Countries')." ResourceKey="Options">Options: </Label> <Label Class="col-sm-3" For="options" HelpText="A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from Settings." ResourceKey="Options">Options: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="options" class="form-control" @bind="@_options" maxlength="2000" /> <div class="input-group">
@if (_optiontype == "Settings")
{
<input id="options" class="form-control" @bind="@_options" maxlength="2000" />
}
else
{
<select id="entityName" class="form-select" @bind="@_options">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var entityname in _entitynames)
{
<option value="@entityname">@entityname</option>
}
</select>
}
<button type="button" class="btn btn-secondary" @onclick="ToggleOptionType">@Localizer[_optiontype]</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -95,7 +112,7 @@
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveProfile">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveProfile">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (PageState.QueryString.ContainsKey("id")) @if (PageState.QueryString.ContainsKey("id"))
{ {
<br /> <br />
<br /> <br />
@@ -116,6 +133,8 @@
private string _rows = "1"; private string _rows = "1";
private string _defaultvalue = string.Empty; private string _defaultvalue = string.Empty;
private string _options = string.Empty; private string _options = string.Empty;
private string _optiontype = "Settings";
private List<string> _entitynames;
private string _validation = string.Empty; private string _validation = string.Empty;
private string _autocomplete = string.Empty; private string _autocomplete = string.Empty;
private string _isrequired = "False"; private string _isrequired = "False";
@@ -133,6 +152,8 @@
{ {
try try
{ {
_entitynames = await SettingService.GetEntityNamesAsync();
if (PageState.QueryString.ContainsKey("id")) if (PageState.QueryString.ContainsKey("id"))
{ {
_profileid = Int32.Parse(PageState.QueryString["id"]); _profileid = Int32.Parse(PageState.QueryString["id"]);
@@ -148,6 +169,11 @@
_rows = profile.Rows.ToString(); _rows = profile.Rows.ToString();
_defaultvalue = profile.DefaultValue; _defaultvalue = profile.DefaultValue;
_options = profile.Options; _options = profile.Options;
if (_options.StartsWith("EntityName:"))
{
_optiontype = "Options";
_options = _options.Substring(11);
}
_validation = profile.Validation; _validation = profile.Validation;
_autocomplete = profile.Autocomplete; _autocomplete = profile.Autocomplete;
_isrequired = profile.IsRequired.ToString(); _isrequired = profile.IsRequired.ToString();
@@ -166,6 +192,18 @@
} }
} }
private void ToggleOptionType()
{
if (_optiontype == "Options")
{
_optiontype = "Settings";
}
else
{
_optiontype = "Options";
}
}
private async Task SaveProfile() private async Task SaveProfile()
{ {
validated = true; validated = true;
@@ -193,7 +231,14 @@
profile.MaxLength = int.Parse(_maxlength); profile.MaxLength = int.Parse(_maxlength);
profile.Rows = int.Parse(_rows); profile.Rows = int.Parse(_rows);
profile.DefaultValue = _defaultvalue; profile.DefaultValue = _defaultvalue;
profile.Options = _options; if (_optiontype == "Options" && !string.IsNullOrEmpty(_options))
{
profile.Options = "EntityName:" + _options;
}
else
{
profile.Options = _options;
}
profile.Validation = _validation; profile.Validation = _validation;
profile.Autocomplete = _autocomplete; profile.Autocomplete = _autocomplete;
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired)); profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));

View File

@@ -592,9 +592,17 @@
{ {
_faviconfileid = site.FaviconFileId.Value; _faviconfileid = site.FaviconFileId.Value;
} }
_themes = ThemeService.GetThemeControls(PageState.Site.Themes); var themes = new List<Theme>();
foreach (var theme in PageState.Site.Themes)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
_themes = ThemeService.GetThemeControls(themes);
_themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme; _themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty); _cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);

View File

@@ -216,7 +216,7 @@ else
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
} }
_urls = PageState.Alias.Name; _urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList); _themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme)) if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{ {

View File

@@ -195,7 +195,7 @@
{ {
try try
{ {
_themes = await ThemeService.GetThemesAsync(); _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
await LoadPackages(); await LoadPackages();
_initialized = true; _initialized = true;
} }

View File

@@ -9,84 +9,98 @@
@if (_initialized) @if (_initialized)
{ {
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <TabStrip>
<div class="container"> <TabPanel Name="Theme" ResourceKey="Theme" Heading="Theme">
<div class="row mb-1 align-items-center"> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label> <div class="container">
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<input id="name" class="form-control" @bind="@_name" /> <Label Class="col-sm-3" For="name" HelpText="The name of the theme" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isenabled" HelpText="Is theme enabled for this site?" ResourceKey="IsEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="isenabled" class="form-select" @bind="@_isenabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
@if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~"))
{
<a href="@_license.Replace("~", PageState?.Alias.BaseUrl + "/Themes/" + Utilities.GetTypeName(_themeName))" class="btn btn-info" style="text-decoration: none !important" target="_new">@Localizer["View License"]</a>
}
else
{
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
}
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Theme" PermissionNames="@PermissionNames.Utilize" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <br />
<Label Class="col-sm-3" For="isenabled" HelpText="Is theme enabled for this site?" ResourceKey="IsEnabled">Enabled? </Label> <button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<div class="col-sm-9"> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<select id="isenabled" class="form-select" @bind="@_isenabled" required> </TabPanel>
<option value="True">@SharedLocalizer["Yes"]</option> </TabStrip>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
@if (_license.StartsWith("http") || _license.StartsWith("/") || _license.StartsWith("~"))
{
<a href="@_license.Replace("~", PageState?.Alias.BaseUrl + "/Themes/" + Utilities.GetTypeName(_themeName))" class="btn btn-info" style="text-decoration: none !important" target="_new">@Localizer["View License"]</a>
}
else
{
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
}
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
} }
@code { @code {
@@ -103,11 +117,14 @@
private string _url = ""; private string _url = "";
private string _contact = ""; private string _contact = "";
private string _license = ""; private string _license = "";
private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
private PermissionGrid _permissionGrid;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@@ -126,6 +143,7 @@
_url = theme.Url; _url = theme.Url;
_contact = theme.Contact; _contact = theme.Contact;
_license = theme.License; _license = theme.License;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy; _createdby = theme.CreatedBy;
_createdon = theme.CreatedOn; _createdon = theme.CreatedOn;
_modifiedby = theme.ModifiedBy; _modifiedby = theme.ModifiedBy;
@@ -152,6 +170,7 @@
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId); var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
theme.Name = _name; theme.Name = _name;
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled)); theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
theme.PermissionList = _permissionGrid.GetPermissionList();
await ThemeService.UpdateThemeAsync(theme); await ThemeService.UpdateThemeAsync(theme);
await logger.LogInformation("Theme Saved {Theme}", theme); await logger.LogInformation("Theme Saved {Theme}", theme);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@@ -78,7 +78,7 @@ else
{ {
try try
{ {
_themes = await ThemeService.GetThemesAsync(); _themes = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackageUpdatesAsync("theme"); _packages = await PackageService.GetPackageUpdatesAsync("theme");
} }
catch (Exception ex) catch (Exception ex)
@@ -161,7 +161,7 @@ else
{ {
try try
{ {
await ThemeService.DeleteThemeAsync(Theme.ThemeName); await ThemeService.DeleteThemeAsync(Theme.ThemeId, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success); AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
} }

View File

@@ -345,11 +345,11 @@
try try
{ {
FolderId = int.Parse((string)e.Value); FolderId = int.Parse((string)e.Value);
await OnSelectFolder.InvokeAsync(FolderId);
FileId = -1; FileId = -1;
GetFolderPermission(); GetFolderPermission();
await SetImage(); await SetImage();
await GetFiles(); await GetFiles();
await OnSelectFolder.InvokeAsync(FolderId);
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@@ -364,11 +364,11 @@
{ {
_message = string.Empty; _message = string.Empty;
FileId = int.Parse((string)e.Value); FileId = int.Parse((string)e.Value);
await SetImage();
#pragma warning disable CS0618 #pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId); await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618 #pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId); await OnSelectFile.InvokeAsync(FileId);
await SetImage();
StateHasChanged(); StateHasChanged();
} }
@@ -460,13 +460,14 @@
} }
} }
await SetImage();
await OnUpload.InvokeAsync(FileId); await OnUpload.InvokeAsync(FileId);
#pragma warning disable CS0618 #pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId); await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618 #pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId); await OnSelectFile.InvokeAsync(FileId);
await SetImage();
await GetFiles(); await GetFiles();
StateHasChanged(); StateHasChanged();
} }
@@ -518,12 +519,13 @@
} }
FileId = -1; FileId = -1;
await SetImage();
#pragma warning disable CS0618 #pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId); await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618 #pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId); await OnSelectFile.InvokeAsync(FileId);
await SetImage();
await GetFiles(); await GetFiles();
StateHasChanged(); StateHasChanged();
} }

View File

@@ -0,0 +1,12 @@
// This is just a placeholder file
// It is necessary for the documentation to successfully build this project.
// Reason is that docfx will run the .net compiler and find references
// to this class in the project.
// But since the real class is just a .razor file, ATM docfx will fail.
//
// Note added 2025-09-23 by @tvatavuk.
// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
// as well, and this file can be removed.
namespace Oqtane.Modules.Controls;
public partial class RadzenTextEditor;

View File

@@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.9" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="Radzen.Blazor" Version="7.3.4" /> <PackageReference Include="Radzen.Blazor" Version="7.3.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -183,8 +183,8 @@
<data name="Runtimes.Text" xml:space="preserve"> <data name="Runtimes.Text" xml:space="preserve">
<value>Runtimes: </value> <value>Runtimes: </value>
</data> </data>
<data name="Definition.Heading" xml:space="preserve"> <data name="Module.Heading" xml:space="preserve">
<value>Definition</value> <value>Module</value>
</data> </data>
<data name="Information.Heading" xml:space="preserve"> <data name="Information.Heading" xml:space="preserve">
<value>Information</value> <value>Information</value>

View File

@@ -157,7 +157,7 @@
<value>The default value for this profile item</value> <value>The default value for this profile item</value>
</data> </data>
<data name="Options.HelpText" xml:space="preserve"> <data name="Options.HelpText" xml:space="preserve">
<value>A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from custom Settings (ie. 'EntityName:Countries').</value> <value>A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from Settings.</value>
</data> </data>
<data name="Required.HelpText" xml:space="preserve"> <data name="Required.HelpText" xml:space="preserve">
<value>Should a user be required to provide a value for this profile item?</value> <value>Should a user be required to provide a value for this profile item?</value>
@@ -201,4 +201,10 @@
<data name="Autocomplete.Text" xml:space="preserve"> <data name="Autocomplete.Text" xml:space="preserve">
<value>Autocomplete: </value> <value>Autocomplete: </value>
</data> </data>
<data name="Options" xml:space="preserve">
<value>Options</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
</root> </root>

View File

@@ -180,4 +180,10 @@
<data name="View License" xml:space="preserve"> <data name="View License" xml:space="preserve">
<value>View License</value> <value>View License</value>
</data> </data>
<data name="Theme.Heading" xml:space="preserve">
<value>Themex</value>
</data>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissionsx</value>
</data>
</root> </root>

View File

@@ -101,7 +101,6 @@ namespace Oqtane.Services
/// Unzips the contents of a zip file /// Unzips the contents of a zip file
/// </summary> /// </summary>
/// <param name="fileId">Reference to the <see cref="File"/></param> /// <param name="fileId">Reference to the <see cref="File"/></param>
/// </param>
/// <returns></returns> /// <returns></returns>
Task UnzipFileAsync(int fileId); Task UnzipFileAsync(int fileId);
} }

View File

@@ -17,8 +17,9 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Returns a list of available themes /// Returns a list of available themes
/// </summary> /// </summary>
/// <param name="siteId"></param>
/// <returns></returns> /// <returns></returns>
Task<List<Theme>> GetThemesAsync(); Task<List<Theme>> GetThemesAsync(int siteId);
/// <summary> /// <summary>
/// Returns a specific theme /// Returns a specific theme
@@ -69,9 +70,10 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Deletes a theme /// Deletes a theme
/// </summary> /// </summary>
/// <param name="themeName"></param> /// <param name="themeId"></param>
/// <param name="siteId"></param>
/// <returns></returns> /// <returns></returns>
Task DeleteThemeAsync(string themeName); Task DeleteThemeAsync(int themeId, int siteId);
/// <summary> /// <summary>
/// Creates a new theme /// Creates a new theme
@@ -103,9 +105,9 @@ namespace Oqtane.Services
private string ApiUrl => CreateApiUrl("Theme"); private string ApiUrl => CreateApiUrl("Theme");
public async Task<List<Theme>> GetThemesAsync() public async Task<List<Theme>> GetThemesAsync(int siteId)
{ {
List<Theme> themes = await GetJsonAsync<List<Theme>>(ApiUrl); List<Theme> themes = await GetJsonAsync<List<Theme>>($"{ApiUrl}?siteid={siteId}");
return themes.OrderBy(item => item.Name).ToList(); return themes.OrderBy(item => item.Name).ToList();
} }
public async Task<Theme> GetThemeAsync(int themeId, int siteId) public async Task<Theme> GetThemeAsync(int themeId, int siteId)
@@ -139,9 +141,9 @@ namespace Oqtane.Services
await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme); await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme);
} }
public async Task DeleteThemeAsync(string themeName) public async Task DeleteThemeAsync(int themeId, int siteId)
{ {
await DeleteAsync($"{ApiUrl}/{themeName}"); await DeleteAsync($"{ApiUrl}/{themeId}?siteid={siteId}");
} }
public async Task<Theme> CreateThemeAsync(Theme theme) public async Task<Theme> CreateThemeAsync(Theme theme)

View File

@@ -23,7 +23,7 @@
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="9.0.9" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="9.0.9" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Http" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="9.0.9" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Localization" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="7.3.4" exclude="Build,Analyzers" /> <dependency id="Radzen.Blazor" version="7.3.5" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@@ -27,7 +27,7 @@
<dependency id="Microsoft.Extensions.Localization" version="9.0.9" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Localization" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="9.0.9" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="SixLabors.ImageSharp" version="3.1.11" exclude="Build,Analyzers" /> <dependency id="SixLabors.ImageSharp" version="3.1.11" exclude="Build,Analyzers" />
<dependency id="HtmlAgilityPack" version="1.12.2" exclude="Build,Analyzers" /> <dependency id="HtmlAgilityPack" version="1.12.3" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="9.0.4" exclude="Build,Analyzers" /> <dependency id="Swashbuckle.AspNetCore" version="9.0.4" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.13.0" exclude="Build,Analyzers" /> <dependency id="MailKit" version="4.13.0" exclude="Build,Analyzers" />
<dependency id="MySql.Data" version="9.4.0" exclude="Build,Analyzers" /> <dependency id="MySql.Data" version="9.4.0" exclude="Build,Analyzers" />

View File

@@ -252,7 +252,7 @@ namespace Oqtane.Controllers
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Delete Attempt {ModuleDefinitionId}", id); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Delete Attempt {ModuleDefinitionId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }

View File

@@ -308,7 +308,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>/entitynames // GET: api/<controller>/entitynames
[HttpGet("entitynames")] [HttpGet("entitynames")]
[Authorize(Roles = RoleNames.Host)] [Authorize(Roles = RoleNames.Admin)]
public IEnumerable<string> GetEntityNames() public IEnumerable<string> GetEntityNames()
{ {
return _settings.GetEntityNames(); return _settings.GetEntityNames();
@@ -316,7 +316,7 @@ namespace Oqtane.Controllers
// GET: api/<controller>/entityids?entityname=x // GET: api/<controller>/entityids?entityname=x
[HttpGet("entityids")] [HttpGet("entityids")]
[Authorize(Roles = RoleNames.Host)] [Authorize(Roles = RoleNames.Admin)]
public IEnumerable<int> GetEntityIds(string entityName) public IEnumerable<int> GetEntityIds(string entityName)
{ {
return _settings.GetEntityIds(entityName); return _settings.GetEntityIds(entityName);

View File

@@ -14,6 +14,9 @@ using System.Text.Json;
using System.Net; using System.Net;
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Reflection.Metadata;
using Oqtane.Security;
using System.Security.Policy;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@@ -26,30 +29,50 @@ namespace Oqtane.Controllers
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly IWebHostEnvironment _environment; private readonly IWebHostEnvironment _environment;
private readonly ITenantManager _tenantManager; private readonly ITenantManager _tenantManager;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IServiceProvider serviceProvider) public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, IServiceProvider serviceProvider)
{ {
_themes = themes; _themes = themes;
_installationManager = installationManager; _installationManager = installationManager;
_environment = environment; _environment = environment;
_tenantManager = tenantManager; _tenantManager = tenantManager;
_userPermissions = userPermissions;
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
} }
// GET: api/<controller> // GET: api/<controller>?siteid=x
[HttpGet] [HttpGet]
[Authorize(Roles = RoleNames.Registered)] [Authorize(Roles = RoleNames.Registered)]
public IEnumerable<Theme> Get() public IEnumerable<Theme> Get(string siteid)
{ {
return _themes.GetThemes(); int SiteId;
} if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
List<Theme> themes = new List<Theme>();
foreach (Theme theme in _themes.GetThemes(SiteId))
{
if (_userPermissions.IsAuthorized(User, PermissionNames.Utilize, theme.PermissionList))
{
themes.Add(theme);
}
}
return themes;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {SiteId}", siteid);
HttpContext.Response.StatusCode = (int) HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5?siteid=x // GET api/<controller>/5?siteid=x
[HttpGet("{id}")] [HttpGet("{id}")]
@@ -58,7 +81,24 @@ namespace Oqtane.Controllers
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{ {
return _themes.GetTheme(id, SiteId); Theme theme = _themes.GetTheme(id, SiteId);
if (theme != null && _userPermissions.IsAuthorized(User, PermissionNames.Utilize, theme.PermissionList))
{
return theme;
}
else
{
if (theme != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {ThemeId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return null;
}
} }
else else
{ {
@@ -86,14 +126,13 @@ namespace Oqtane.Controllers
} }
} }
// DELETE api/<controller>/xxx // DELETE api/<controller>/5?siteid=x
[HttpDelete("{themename}")] [HttpDelete("{themename}")]
[Authorize(Roles = RoleNames.Host)] [Authorize(Roles = RoleNames.Host)]
public void Delete(string themename) public void Delete(int id, int siteid)
{ {
List<Theme> themes = _themes.GetThemes().ToList(); Theme theme = _themes.GetTheme(id, siteid);
Theme theme = themes.Where(item => item.ThemeName == themename).FirstOrDefault(); if (theme != null && theme.SiteId == _alias.SiteId && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
if (theme != null && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
{ {
// remove theme assets // remove theme assets
if (_installationManager.UninstallPackage(theme.PackageName)) if (_installationManager.UninstallPackage(theme.PackageName))
@@ -126,7 +165,7 @@ namespace Oqtane.Controllers
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {Themename}", themename); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Delete Attempt {ThemeId} {SiteId}", id, siteid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }

View File

@@ -179,38 +179,22 @@ namespace Oqtane.Infrastructure
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", ""); fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", "");
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName; fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
} }
try if (MailboxAddress.TryParse(fromEmail, out from))
{ {
// exception handler is necessary because of https://github.com/jstedfast/MimeKit/issues/1186 from.Name = fromName;
if (MailboxAddress.TryParse(fromEmail, out _))
{
from = new MailboxAddress(fromName, fromEmail);
}
} }
catch else
{
// parse error creating sender mailbox address
}
if (from == null)
{ {
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;"; mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;";
} }
// recipient // recipient
try if (MailboxAddress.TryParse(toEmail, out to))
{ {
// exception handler is necessary because of https://github.com/jstedfast/MimeKit/issues/1186 to.Name = toName;
if (MailboxAddress.TryParse(toEmail, out _))
{
to = new MailboxAddress(toName, toEmail);
}
} }
catch else
{
// parse error creating recipient mailbox address
}
if (to == null)
{ {
mailboxAddressValidationError += $" Invalid Recipient: {toName} &lt;{toEmail}&gt;"; mailboxAddressValidationError += $" Invalid Recipient: {toName} &lt;{toEmail}&gt;";
} }

View File

@@ -48,7 +48,7 @@
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.9" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.9" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.2" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
<PackageReference Include="MailKit" Version="4.13.0" /> <PackageReference Include="MailKit" Version="4.13.0" />
</ItemGroup> </ItemGroup>

View File

@@ -386,6 +386,7 @@ namespace Oqtane.Repository
moduledefinition.Categories = "Common"; moduledefinition.Categories = "Common";
} }
// default permissions
if (moduledefinition.Categories == "Admin") if (moduledefinition.Categories == "Admin")
{ {
var shortName = moduledefinition.ModuleDefinitionName.Replace("Oqtane.Modules.Admin.", "").Replace(", Oqtane.Client", ""); var shortName = moduledefinition.ModuleDefinitionName.Replace("Oqtane.Modules.Admin.", "").Replace(", Oqtane.Client", "");
@@ -455,18 +456,21 @@ namespace Oqtane.Repository
private List<Permission> ClonePermissions(int siteId, List<Permission> permissionList) private List<Permission> ClonePermissions(int siteId, List<Permission> permissionList)
{ {
var permissions = new List<Permission>(); var permissions = new List<Permission>();
foreach (var p in permissionList) if (permissionList != null)
{ {
var permission = new Permission(); foreach (var p in permissionList)
permission.SiteId = siteId; {
permission.EntityName = p.EntityName; var permission = new Permission();
permission.EntityId = p.EntityId; permission.SiteId = siteId;
permission.PermissionName = p.PermissionName; permission.EntityName = p.EntityName;
permission.RoleId = null; permission.EntityId = p.EntityId;
permission.RoleName = p.RoleName; permission.PermissionName = p.PermissionName;
permission.UserId = p.UserId; permission.RoleId = null;
permission.IsAuthorized = p.IsAuthorized; permission.RoleName = p.RoleName;
permissions.Add(permission); permission.UserId = p.UserId;
permission.IsAuthorized = p.IsAuthorized;
permissions.Add(permission);
}
} }
return permissions; return permissions;
} }

View File

@@ -135,7 +135,7 @@ namespace Oqtane.Repository
if (site != null) if (site != null)
{ {
// initialize theme Assemblies // initialize theme Assemblies
site.Themes = _themeRepository.GetThemes().ToList(); site.Themes = _themeRepository.GetThemes(site.SiteId).ToList();
// initialize module Assemblies // initialize module Assemblies
var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId); var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId);

View File

@@ -15,7 +15,7 @@ namespace Oqtane.Repository
{ {
public interface IThemeRepository public interface IThemeRepository
{ {
IEnumerable<Theme> GetThemes(); IEnumerable<Theme> GetThemes(int siteId);
Theme GetTheme(int themeId, int siteId); Theme GetTheme(int themeId, int siteId);
void UpdateTheme(Theme theme); void UpdateTheme(Theme theme);
void DeleteTheme(int themeId); void DeleteTheme(int themeId);
@@ -26,24 +26,25 @@ namespace Oqtane.Repository
{ {
private MasterDBContext _db; private MasterDBContext _db;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly IPermissionRepository _permissions;
private readonly ITenantManager _tenants; private readonly ITenantManager _tenants;
private readonly ISettingRepository _settings; private readonly ISettingRepository _settings;
private readonly IServerStateManager _serverState; private readonly IServerStateManager _serverState;
private readonly string settingprefix = "SiteEnabled:"; private readonly string settingprefix = "SiteEnabled:";
public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState) public ThemeRepository(MasterDBContext context, IMemoryCache cache, IPermissionRepository permissions, ITenantManager tenants, ISettingRepository settings, IServerStateManager serverState)
{ {
_db = context; _db = context;
_cache = cache; _cache = cache;
_permissions = permissions;
_tenants = tenants; _tenants = tenants;
_settings = settings; _settings = settings;
_serverState = serverState; _serverState = serverState;
} }
public IEnumerable<Theme> GetThemes() public IEnumerable<Theme> GetThemes(int siteId)
{ {
// for consistency siteid should be passed in as parameter, but this would require breaking change return LoadThemes(siteId);
return LoadThemes(_tenants.GetAlias().SiteId);
} }
public Theme GetTheme(int themeId, int siteId) public Theme GetTheme(int themeId, int siteId)
@@ -56,6 +57,7 @@ namespace Oqtane.Repository
{ {
_db.Entry(theme).State = EntityState.Modified; _db.Entry(theme).State = EntityState.Modified;
_db.SaveChanges(); _db.SaveChanges();
_permissions.UpdatePermissions(theme.SiteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}"; var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}";
var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname); var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname);
@@ -96,6 +98,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName; Theme.PackageName = theme.PackageName;
Theme.PermissionList = theme.PermissionList;
Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme); Themes.Add(Theme);
} }
@@ -176,6 +179,9 @@ namespace Oqtane.Repository
var siteKey = _tenants.GetAlias().SiteKey; var siteKey = _tenants.GetAlias().SiteKey;
var assemblies = new List<string>(); var assemblies = new List<string>();
// get all module definition permissions for site
List<Permission> permissions = _permissions.GetPermissions(siteId, EntityNames.Theme).ToList();
// get settings for site // get settings for site
var settings = _settings.GetSettings(EntityNames.Theme).ToList(); var settings = _settings.GetSettings(EntityNames.Theme).ToList();
@@ -212,6 +218,26 @@ namespace Oqtane.Repository
} }
} }
} }
if (permissions.Count == 0)
{
// no module definition permissions exist for this site
theme.PermissionList = ClonePermissions(siteId, theme.PermissionList);
_permissions.UpdatePermissions(siteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
}
else
{
if (permissions.Any(item => item.EntityId == theme.ThemeId))
{
theme.PermissionList = permissions.Where(item => item.EntityId == theme.ThemeId).ToList();
}
else
{
// permissions for theme do not exist for this site
theme.PermissionList = ClonePermissions(siteId, theme.PermissionList);
_permissions.UpdatePermissions(siteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
}
}
} }
// cache site assemblies // cache site assemblies
@@ -220,6 +246,20 @@ namespace Oqtane.Repository
{ {
if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly); if (!serverState.Assemblies.Contains(assembly)) serverState.Assemblies.Add(assembly);
} }
// clean up any orphaned permissions
var ids = new HashSet<int>(Themes.Select(item => item.ThemeId));
foreach (var permission in permissions.Where(item => !ids.Contains(item.EntityId)))
{
try
{
_permissions.DeletePermission(permission.PermissionId);
}
catch
{
// multi-threading can cause a race condition to occur
}
}
} }
return Themes; return Themes;
@@ -295,6 +335,14 @@ namespace Oqtane.Repository
} }
} }
} }
// default permissions
theme.PermissionList = new List<Permission>
{
new Permission(PermissionNames.Utilize, RoleNames.Admin, true),
new Permission(PermissionNames.Utilize, RoleNames.Registered, true)
};
Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}"); Debug.WriteLine($"Oqtane Info: Registering Theme {theme.ThemeName}");
themes.Add(theme); themes.Add(theme);
index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType); index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType);
@@ -335,5 +383,27 @@ namespace Oqtane.Repository
} }
return themes; return themes;
} }
private List<Permission> ClonePermissions(int siteId, List<Permission> permissionList)
{
var permissions = new List<Permission>();
if (permissionList != null)
{
foreach (var p in permissionList)
{
var permission = new Permission();
permission.SiteId = siteId;
permission.EntityName = p.EntityName;
permission.EntityId = p.EntityId;
permission.PermissionName = p.PermissionName;
permission.RoleId = null;
permission.RoleName = p.RoleName;
permission.UserId = p.UserId;
permission.IsAuthorized = p.IsAuthorized;
permissions.Add(permission);
}
}
return permissions;
}
} }
} }

View File

@@ -144,7 +144,7 @@ namespace Oqtane.Services
} }
// themes // themes
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList());
// installation date used for fingerprinting static assets // installation date used for fingerprinting static assets
site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));

View File

@@ -94,6 +94,9 @@ namespace Oqtane.Models
[NotMapped] [NotMapped]
public List<ThemeControl> Containers { get; set; } public List<ThemeControl> Containers { get; set; }
[NotMapped]
public List<Permission> PermissionList { get; set; }
[NotMapped] [NotMapped]
public string Template { get; set; } public string Template { get; set; }