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",
"Oqtane"
],
"name": "Oqtane Application Template",
"shortName": "oqtane-app",
"defaultName": "MyCompany.MyProject",
"identity": "Oqtane.Application.Template",
"tags": {
"language": "C#",
"type": "project"
"type": "solution",
"editorTreatAs":"solution"
},
"identity": "Oqtane.Application.Template",
"name": "Oqtane Application Template For Blazor",
"shortName": "oqtane-app",
"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">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Client.Oqtane</AssemblyName>
<IsPackable>true</IsPackable>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Client.Oqtane</AssemblyName>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" 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.Http" Version="9.0.9" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="6.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Oqtane.Application.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="6.2.1" />
</ItemGroup>
</Project>

View File

@@ -9,7 +9,7 @@ cd MyCompany.MyProject
dotnet build
cd Server
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.

View File

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

View File

@@ -6,7 +6,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5000",
"applicationUrl": "http://localhost:44358",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -16,7 +16,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"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": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

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

View File

@@ -14,7 +14,7 @@
@if (_initialized)
{
<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>
<div class="container">
<div class="row mb-1 align-items-center">
@@ -236,11 +236,10 @@
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private List<Page> _pagesWithModules;
#pragma warning disable 649
private PermissionGrid _permissionGrid;
#pragma warning restore 649
private List<Page> _pagesWithModules;
private List<Package> _packages;
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)))
{
_themetype = PageState.Site.DefaultThemeType;
_themes = ThemeService.GetThemeControls(PageState.Site.Themes);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
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);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = PageState.Site.DefaultContainerType;
_children = new List<Page>();
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;
}
_themes = ThemeService.GetThemeControls(PageState.Site.Themes);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
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);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = _page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype))
{

View File

@@ -2,6 +2,7 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -56,9 +57,25 @@
</div>
</div>
<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">
<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 class="row mb-1 align-items-center">
@@ -95,7 +112,7 @@
<br />
<button type="button" class="btn btn-success" @onclick="SaveProfile">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (PageState.QueryString.ContainsKey("id"))
@if (PageState.QueryString.ContainsKey("id"))
{
<br />
<br />
@@ -116,6 +133,8 @@
private string _rows = "1";
private string _defaultvalue = string.Empty;
private string _options = string.Empty;
private string _optiontype = "Settings";
private List<string> _entitynames;
private string _validation = string.Empty;
private string _autocomplete = string.Empty;
private string _isrequired = "False";
@@ -133,6 +152,8 @@
{
try
{
_entitynames = await SettingService.GetEntityNamesAsync();
if (PageState.QueryString.ContainsKey("id"))
{
_profileid = Int32.Parse(PageState.QueryString["id"]);
@@ -148,6 +169,11 @@
_rows = profile.Rows.ToString();
_defaultvalue = profile.DefaultValue;
_options = profile.Options;
if (_options.StartsWith("EntityName:"))
{
_optiontype = "Options";
_options = _options.Substring(11);
}
_validation = profile.Validation;
_autocomplete = profile.Autocomplete;
_isrequired = profile.IsRequired.ToString();
@@ -166,6 +192,18 @@
}
}
private void ToggleOptionType()
{
if (_optiontype == "Options")
{
_optiontype = "Settings";
}
else
{
_optiontype = "Options";
}
}
private async Task SaveProfile()
{
validated = true;
@@ -193,7 +231,14 @@
profile.MaxLength = int.Parse(_maxlength);
profile.Rows = int.Parse(_rows);
profile.DefaultValue = _defaultvalue;
profile.Options = _options;
if (_optiontype == "Options" && !string.IsNullOrEmpty(_options))
{
profile.Options = "EntityName:" + _options;
}
else
{
profile.Options = _options;
}
profile.Validation = _validation;
profile.Autocomplete = _autocomplete;
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));

View File

@@ -592,9 +592,17 @@
{
_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;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containers = ThemeService.GetContainerControls(themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);

View File

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

View File

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

View File

@@ -9,84 +9,98 @@
@if (_initialized)
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
<TabStrip>
<TabPanel Name="Theme" ResourceKey="Theme" Heading="Theme">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<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 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>
<br />
<button type="button" class="btn btn-success" @onclick="SaveTheme">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
</TabStrip>
}
@code {
@@ -103,11 +117,14 @@
private string _url = "";
private string _contact = "";
private string _license = "";
private List<Permission> _permissions = null;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private PermissionGrid _permissionGrid;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
@@ -126,6 +143,7 @@
_url = theme.Url;
_contact = theme.Contact;
_license = theme.License;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy;
_createdon = theme.CreatedOn;
_modifiedby = theme.ModifiedBy;
@@ -152,6 +170,7 @@
var theme = await ThemeService.GetThemeAsync(_themeId, ModuleState.SiteId);
theme.Name = _name;
theme.IsEnabled = (_isenabled == null ? true : bool.Parse(_isenabled));
theme.PermissionList = _permissionGrid.GetPermissionList();
await ThemeService.UpdateThemeAsync(theme);
await logger.LogInformation("Theme Saved {Theme}", theme);
NavigationManager.NavigateTo(NavigateUrl());

View File

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

View File

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

View File

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

View File

@@ -157,7 +157,7 @@
<value>The default value for this profile item</value>
</data>
<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 name="Required.HelpText" xml:space="preserve">
<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">
<value>Autocomplete: </value>
</data>
<data name="Options" xml:space="preserve">
<value>Options</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
</root>

View File

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

View File

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

View File

@@ -17,8 +17,9 @@ namespace Oqtane.Services
/// <summary>
/// Returns a list of available themes
/// </summary>
/// <param name="siteId"></param>
/// <returns></returns>
Task<List<Theme>> GetThemesAsync();
Task<List<Theme>> GetThemesAsync(int siteId);
/// <summary>
/// Returns a specific theme
@@ -69,9 +70,10 @@ namespace Oqtane.Services
/// <summary>
/// Deletes a theme
/// </summary>
/// <param name="themeName"></param>
/// <param name="themeId"></param>
/// <param name="siteId"></param>
/// <returns></returns>
Task DeleteThemeAsync(string themeName);
Task DeleteThemeAsync(int themeId, int siteId);
/// <summary>
/// Creates a new theme
@@ -103,9 +105,9 @@ namespace Oqtane.Services
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();
}
public async Task<Theme> GetThemeAsync(int themeId, int siteId)
@@ -139,9 +141,9 @@ namespace Oqtane.Services
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)

View File

@@ -23,7 +23,7 @@
<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.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>
</dependencies>
</metadata>

View File

@@ -27,7 +27,7 @@
<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="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="MailKit" version="4.13.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
{
_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;
}
}

View File

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

View File

@@ -14,6 +14,9 @@ using System.Text.Json;
using System.Net;
using System;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection.Metadata;
using Oqtane.Security;
using System.Security.Policy;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@@ -26,30 +29,50 @@ namespace Oqtane.Controllers
private readonly IInstallationManager _installationManager;
private readonly IWebHostEnvironment _environment;
private readonly ITenantManager _tenantManager;
private readonly IUserPermissions _userPermissions;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
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;
_installationManager = installationManager;
_environment = environment;
_tenantManager = tenantManager;
_userPermissions = userPermissions;
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
_serviceProvider = serviceProvider;
}
// GET: api/<controller>
// GET: api/<controller>?siteid=x
[HttpGet]
[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
[HttpGet("{id}")]
@@ -58,7 +81,24 @@ namespace Oqtane.Controllers
int 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
{
@@ -86,14 +126,13 @@ namespace Oqtane.Controllers
}
}
// DELETE api/<controller>/xxx
// DELETE api/<controller>/5?siteid=x
[HttpDelete("{themename}")]
[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.Where(item => item.ThemeName == themename).FirstOrDefault();
if (theme != null && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
Theme theme = _themes.GetTheme(id, siteid);
if (theme != null && theme.SiteId == _alias.SiteId && Utilities.GetAssemblyName(theme.ThemeName) != Constants.ClientId)
{
// remove theme assets
if (_installationManager.UninstallPackage(theme.PackageName))
@@ -126,7 +165,7 @@ namespace Oqtane.Controllers
}
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;
}
}

View File

@@ -179,38 +179,22 @@ namespace Oqtane.Infrastructure
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", "");
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
if (MailboxAddress.TryParse(fromEmail, out _))
{
from = new MailboxAddress(fromName, fromEmail);
}
from.Name = fromName;
}
catch
{
// parse error creating sender mailbox address
}
if (from == null)
else
{
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;";
}
// recipient
try
if (MailboxAddress.TryParse(toEmail, out to))
{
// exception handler is necessary because of https://github.com/jstedfast/MimeKit/issues/1186
if (MailboxAddress.TryParse(toEmail, out _))
{
to = new MailboxAddress(toName, toEmail);
}
to.Name = toName;
}
catch
{
// parse error creating recipient mailbox address
}
if (to == null)
else
{
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.AspNetCore.Authentication.OpenIdConnect" Version="9.0.9" />
<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="MailKit" Version="4.13.0" />
</ItemGroup>

View File

@@ -386,6 +386,7 @@ namespace Oqtane.Repository
moduledefinition.Categories = "Common";
}
// default permissions
if (moduledefinition.Categories == "Admin")
{
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)
{
var permissions = new List<Permission>();
foreach (var p in permissionList)
if (permissionList != null)
{
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);
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

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

View File

@@ -15,7 +15,7 @@ namespace Oqtane.Repository
{
public interface IThemeRepository
{
IEnumerable<Theme> GetThemes();
IEnumerable<Theme> GetThemes(int siteId);
Theme GetTheme(int themeId, int siteId);
void UpdateTheme(Theme theme);
void DeleteTheme(int themeId);
@@ -26,24 +26,25 @@ namespace Oqtane.Repository
{
private MasterDBContext _db;
private readonly IMemoryCache _cache;
private readonly IPermissionRepository _permissions;
private readonly ITenantManager _tenants;
private readonly ISettingRepository _settings;
private readonly IServerStateManager _serverState;
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;
_cache = cache;
_permissions = permissions;
_tenants = tenants;
_settings = settings;
_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(_tenants.GetAlias().SiteId);
return LoadThemes(siteId);
}
public Theme GetTheme(int themeId, int siteId)
@@ -56,6 +57,7 @@ namespace Oqtane.Repository
{
_db.Entry(theme).State = EntityState.Modified;
_db.SaveChanges();
_permissions.UpdatePermissions(theme.SiteId, EntityNames.Theme, theme.ThemeId, theme.PermissionList);
var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}";
var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname);
@@ -96,6 +98,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName;
Theme.PermissionList = theme.PermissionList;
Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme);
}
@@ -176,6 +179,9 @@ namespace Oqtane.Repository
var siteKey = _tenants.GetAlias().SiteKey;
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
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
@@ -220,6 +246,20 @@ namespace Oqtane.Repository
{
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;
@@ -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}");
themes.Add(theme);
index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType);
@@ -335,5 +383,27 @@ namespace Oqtane.Repository
}
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
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList());
site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList());
// installation date used for fingerprinting static assets
site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));

View File

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