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

This commit is contained in:
Leigh Pointer
2025-11-01 09:29:35 +01:00
101 changed files with 3480 additions and 706 deletions

View File

@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>6.2.1</Version>
<Version>10.0.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
</PropertyGroup>

View File

@@ -30,12 +30,12 @@
"datatype": "choice",
"choices": [
{
"choice": "net9.0",
"description": "Target net9.0"
"choice": "net10.0",
"description": "Target net10.0"
}
],
"replaces": "net9.0",
"defaultValue": "net9.0"
"replaces": "net10.0",
"defaultValue": "net10.0"
},
"HttpPort": {
"type": "parameter",

View File

@@ -1,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Client.Oqtane</AssemblyName>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<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" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
@@ -24,7 +24,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="6.2.1" />
<PackageReference Include="Oqtane.Client" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>Oqtane.Application.Template</id>
<version>6.2.1</version>
<version>10.0.0</version>
<title>Oqtane Application Template For Blazor</title>
<authors>Shaun Walker</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@@ -1,20 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.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>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<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" />
<Compile Remove="wwwroot\Modules\Templates\**" />
<Compile Remove="wwwroot\Themes\Templates\**" />
<Content Remove="wwwroot\Modules\Templates\**" />
<Content Remove="wwwroot\Themes\Templates\**" />
<EmbeddedResource Remove="wwwroot\Modules\Templates\**" />
<EmbeddedResource Remove="wwwroot\Themes\Templates\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
@@ -23,7 +32,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="6.2.1" />
<PackageReference Include="Oqtane.Server" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,13 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore;
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Oqtane.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Shared;
namespace Oqtane.Application.Server
{
@@ -11,32 +15,41 @@ namespace Oqtane.Application.Server
{
public static void Main(string[] args)
{
// defer server startup to Oqtane - do not modify
var host = BuildWebHost(args);
var databaseManager = host.Services.GetService<IDatabaseManager>();
var builder = WebApplication.CreateBuilder(args);
AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(builder.Environment.ContentRootPath, "Data"));
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(builder.Environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables();
var configuration = configurationBuilder.Build();
builder.Services.AddOqtane(configuration, builder.Environment);
var app = builder.Build();
var corsService = app.Services.GetRequiredService<ICorsService>();
var corsPolicyProvider = app.Services.GetRequiredService<ICorsPolicyProvider>();
var syncManager = app.Services.GetRequiredService<ISyncManager>();
app.UseOqtane(configuration, builder.Environment, corsService, corsPolicyProvider, syncManager);
var databaseManager = app.Services.GetService<IDatabaseManager>();
var install = databaseManager.Install();
if (!string.IsNullOrEmpty(install.Message))
{
var filelogger = host.Services.GetRequiredService<ILogger<Program>>();
var filelogger = app.Services.GetRequiredService<ILogger<Program>>();
if (filelogger != null)
{
filelogger.LogError($"[Oqtane.Application.Server.Program.Main] {install.Message}");
filelogger.LogError($"[Oqtane.Server.Program.Main] {install.Message}");
}
}
else
{
host.Run();
app.Run();
}
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseConfiguration(new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables()
.Build())
.UseStartup<Startup>()
.ConfigureLocalizationSettings()
.Build();
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Shared;
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace Oqtane.Application.Server
{
public class Startup
{
private readonly IConfigurationRoot _configuration;
private readonly IWebHostEnvironment _environment;
public Startup(IWebHostEnvironment environment)
{
AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data"));
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables();
_configuration = builder.Build();
_environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
// defer server startup to Oqtane - do not modify
services.AddOqtane(_configuration, _environment);
}
public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync)
{
// defer server startup to Oqtane - do not modify
app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync);
}
}
}

View File

@@ -0,0 +1,107 @@
@using Oqtane.Modules.Controls
@using [Owner].Module.[Module].Services
@using [Owner].Module.[Module].Models
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
<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="Enter a name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="Save">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<br /><br />
@if (PageState.Action == "Edit")
{
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
}
</form>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit";
public override string Title => "Manage [Module]";
private ElementReference form;
private bool validated = false;
private int _id;
private string _name;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
protected override async Task OnInitializedAsync()
{
try
{
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
if ([Module] != null)
{
_name = [Module].Name;
_createdby = [Module].CreatedBy;
_createdon = [Module].CreatedOn;
_modifiedby = [Module].ModifiedBy;
_modifiedon = [Module].ModifiedOn;
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading [Module] {[Module]Id} {Error}", _id, ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task Save()
{
try
{
validated = true;
var interop = new Oqtane.UI.Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (PageState.Action == "Add")
{
[Module] [Module] = new [Module]();
[Module].ModuleId = ModuleState.ModuleId;
[Module].Name = _name;
[Module] = await [Module]Service.Add[Module]Async([Module]);
await logger.LogInformation("[Module] Added {[Module]}", [Module]);
}
else
{
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module].Name = _name;
await [Module]Service.Update[Module]Async([Module]);
await logger.LogInformation("[Module] Updated {[Module]}", [Module]);
}
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.SaveValidation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving [Module] {Error}", ex.Message);
AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error);
}
}
}

View File

@@ -0,0 +1,79 @@
@using [Owner].Module.[Module].Services
@using [Owner].Module.[Module].Models
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer
@if (_[Module]s == null)
{
<p><em>Loading...</em></p>
}
else
{
<ActionLink Action="Add" Security="SecurityAccessLevel.Edit" Text="Add [Module]" ResourceKey="Add" />
<br />
<br />
@if (@_[Module]s.Count != 0)
{
<Pager Items="@_[Module]s">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.[Module]Id.ToString())" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete [Module]" Message="Are You Sure You Wish To Delete This [Module]?" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" ResourceKey="Delete" Id="@context.[Module]Id.ToString()" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<p>@Localizer["Message.DisplayNone"]</p>
}
}
@code {
public override string RenderMode => RenderModes.Static;
public override List<Resource> Resources => new List<Resource>()
{
new Stylesheet(ModulePath() + "[Owner].Module.[Module]/Module.css"),
new Script(ModulePath() + "[Owner].Module.[Module]/Module.js")
};
List<[Module]> _[Module]s;
protected override async Task OnInitializedAsync()
{
try
{
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading [Module] {Error}", ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task Delete([Module] [Module])
{
try
{
await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
await logger.LogInformation("[Module] Deleted {[Module]}", [Module]);
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting [Module] {[Module]} {Error}", [Module], ex.Message);
AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error);
}
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace [Owner].Module.[Module]
{
public class Interop
{
private readonly IJSRuntime _jsRuntime;
public Interop(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
}
}

View File

@@ -0,0 +1,16 @@
using Oqtane.Models;
using Oqtane.Modules;
namespace [Owner].Module.[Module]
{
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "[Module]",
Description = "[Description]",
Version = "1.0.0",
ServerManagerType = "[ServerManagerType]"
};
}
}

View File

@@ -0,0 +1,47 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject ISettingService SettingService
@inject IStringLocalizer<Settings> Localizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="value" HelpText="Enter a value" ResourceKey="SettingName" ResourceType="@resourceType">Name: </Label>
<div class="col-sm-9">
<input id="value" type="text" class="form-control" @bind="@_value" />
</div>
</div>
</div>
@code {
private string resourceType = "[Owner].Module.[Module].Settings, [Owner].Module.[Module].Client.Oqtane"; // for localization
public override string Title => "[Module] Settings";
string _value;
protected override async Task OnInitializedAsync()
{
try
{
Dictionary<string, string> settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
_value = SettingService.GetSetting(settings, "SettingName", "");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "SettingName", _value);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name.Text" xml:space="preserve">
<value>Name: </value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the name</value>
</data>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Message.LoadError" xml:space="preserve">
<value>Error Loading [Module]</value>
</data>
<data name="Message.SaveValidation" xml:space="preserve">
<value>Please Provide All Required Information</value>
</data>
<data name="Message.SaveError" xml:space="preserve">
<value>Error Saving [Module]</value>
</data>
</root>

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Add.Text" xml:space="preserve">
<value>Add [Module]</value>
</data>
<data name="Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Delete.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Delete.Header" xml:space="preserve">
<value>Delete [Module]</value>
</data>
<data name="Delete.Message" xml:space="preserve">
<value>Are You Sure You Wish To Delete This [Module]?</value>
</data>
<data name="Message.DisplayNone" xml:space="preserve">
<value>No [Module]s To Display</value>
</data>
<data name="Message.LoadError" xml:space="preserve">
<value>Error Loading [Module]</value>
</data>
<data name="Message.DeleteError" xml:space="preserve">
<value>Error Deleting [Module]</value>
</data>
</root>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingName.Text" xml:space="preserve">
<value>Name: </value>
</data>
<data name="SettingName.HelpText" xml:space="preserve">
<value>Enter a value</value>
</data>
</root>

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Services;
using Oqtane.Shared;
namespace [Owner].Module.[Module].Services
{
public interface I[Module]Service
{
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
public class [Module]Service : ServiceBase, I[Module]Service
{
public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("[Module]");
public async Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId)
{
List<Models.[Module]> [Module]s = await GetJsonAsync<List<Models.[Module]>>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty<Models.[Module]>().ToList());
return [Module]s.OrderBy(item => item.Name).ToList();
}
public async Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId)
{
return await GetJsonAsync<Models.[Module]>(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId));
}
public async Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module])
{
return await PostJsonAsync<Models.[Module]>(CreateAuthorizationPolicyUrl($"{Apiurl}", EntityNames.Module, [Module].ModuleId), [Module]);
}
public async Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module])
{
return await PutJsonAsync<Models.[Module]>(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module].[Module]Id}", EntityNames.Module, [Module].ModuleId), [Module]);
}
public async Task Delete[Module]Async(int [Module]Id, int ModuleId)
{
await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId));
}
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using Oqtane.Services;
using [Owner].Module.[Module].Services;
namespace [Owner].Module.[Module].Startup
{
public class ClientStartup : IClientStartup
{
public void ConfigureServices(IServiceCollection services)
{
if (!services.Any(s => s.ServiceType == typeof(I[Module]Service)))
{
services.AddScoped<I[Module]Service, [Module]Service>();
}
}
}
}

View File

@@ -0,0 +1,114 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Oqtane.Shared;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using [Owner].Module.[Module].Services;
using Oqtane.Controllers;
using System.Net;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class [Module]Controller : ModuleControllerBase
{
private readonly I[Module]Service _[Module]Service;
public [Module]Controller(I[Module]Service [Module]Service, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
{
_[Module]Service = [Module]Service;
}
// GET: api/<controller>?moduleid=x
[HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IEnumerable<Models.[Module]>> Get(string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _[Module]Service.Get[Module]sAsync(ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {ModuleId}", moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<Models.[Module]> Get(int id, int moduleid)
{
Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid);
if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{
return [Module];
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<Models.[Module]> Post([FromBody] Models.[Module] [Module])
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{
[Module] = await _[Module]Service.Add[Module]Async([Module]);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Post Attempt {[Module]}", [Module]);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
[Module] = null;
}
return [Module];
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<Models.[Module]> Put(int id, [FromBody] Models.[Module] [Module])
{
if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{
[Module] = await _[Module]Service.Update[Module]Async([Module]);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Put Attempt {[Module]}", [Module]);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
[Module] = null;
}
return [Module];
}
// DELETE api/<controller>/5
[HttpDelete("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task Delete(int id, int moduleid)
{
Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid);
if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{
await _[Module]Service.Delete[Module]Async(id, [Module].ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Oqtane.Modules;
using Oqtane.Models;
using Oqtane.Infrastructure;
using Oqtane.Interfaces;
using Oqtane.Enums;
using Oqtane.Repository;
using [Owner].Module.[Module].Repository;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Manager
{
public class [Module]Manager : MigratableModuleBase, IInstallable, IPortable, ISearchable
{
private readonly I[Module]Repository _[Module]Repository;
private readonly IDBContextDependencies _DBContextDependencies;
public [Module]Manager(I[Module]Repository [Module]Repository, IDBContextDependencies DBContextDependencies)
{
_[Module]Repository = [Module]Repository;
_DBContextDependencies = DBContextDependencies;
}
public bool Install(Tenant tenant, string version)
{
return Migrate(new [Module]Context(_DBContextDependencies), tenant, MigrationType.Up);
}
public bool Uninstall(Tenant tenant)
{
return Migrate(new [Module]Context(_DBContextDependencies), tenant, MigrationType.Down);
}
public string ExportModule(Oqtane.Models.Module module)
{
string content = "";
List<Models.[Module]> [Module]s = _[Module]Repository.Get[Module]s(module.ModuleId).ToList();
if ([Module]s != null)
{
content = JsonSerializer.Serialize([Module]s);
}
return content;
}
public void ImportModule(Oqtane.Models.Module module, string content, string version)
{
List<Models.[Module]> [Module]s = null;
if (!string.IsNullOrEmpty(content))
{
[Module]s = JsonSerializer.Deserialize<List<Models.[Module]>>(content);
}
if ([Module]s != null)
{
foreach(var [Module] in [Module]s)
{
_[Module]Repository.Add[Module](new Models.[Module] { ModuleId = module.ModuleId, Name = [Module].Name });
}
}
}
public Task<List<SearchContent>> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn)
{
var searchContentList = new List<SearchContent>();
foreach (var [Module] in _[Module]Repository.Get[Module]s(pageModule.ModuleId))
{
if ([Module].ModifiedOn >= lastIndexedOn)
{
searchContentList.Add(new SearchContent
{
EntityName = "[Owner][Module]",
EntityId = [Module].[Module]Id.ToString(),
Title = [Module].Name,
Body = [Module].Name,
ContentModifiedBy = [Module].ModifiedBy,
ContentModifiedOn = [Module].ModifiedOn
});
}
}
return Task.FromResult(searchContentList);
}
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using [Owner].Module.[Module].Migrations.EntityBuilders;
using [Owner].Module.[Module].Repository;
namespace [Owner].Module.[Module].Migrations
{
[DbContext(typeof([Module]Context))]
[Migration("[Owner].Module.[Module].01.00.00.00")]
public class [Module]Initialize : MultiDatabaseMigration
{
public [Module]Initialize(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var entityBuilder = new [Module]EntityBuilder(migrationBuilder, ActiveDatabase);
entityBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var entityBuilder = new [Module]EntityBuilder(migrationBuilder, ActiveDatabase);
entityBuilder.Drop();
}
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using Oqtane.Migrations.EntityBuilders;
namespace [Owner].Module.[Module].Migrations.EntityBuilders
{
public class [Module]EntityBuilder : AuditableBaseEntityBuilder<[Module]EntityBuilder>
{
private const string _entityTableName = "[Owner][Module]";
private readonly PrimaryKey<[Module]EntityBuilder> _primaryKey = new("PK_[Owner][Module]", x => x.[Module]Id);
private readonly ForeignKey<[Module]EntityBuilder> _moduleForeignKey = new("FK_[Owner][Module]_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade);
public [Module]EntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_moduleForeignKey);
}
protected override [Module]EntityBuilder BuildTable(ColumnsBuilder table)
{
[Module]Id = AddAutoIncrementColumn(table,"[Module]Id");
ModuleId = AddIntegerColumn(table,"ModuleId");
Name = AddMaxStringColumn(table,"Name");
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> [Module]Id { get; set; }
public OperationBuilder<AddColumnOperation> ModuleId { get; set; }
public OperationBuilder<AddColumnOperation> Name { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
using Oqtane.Modules;
using Oqtane.Repository;
using Oqtane.Infrastructure;
using Oqtane.Repository.Databases.Interfaces;
namespace [Owner].Module.[Module].Repository
{
public class [Module]Context : DBContextBase, ITransientService, IMultiDatabase
{
public virtual DbSet<Models.[Module]> [Module] { get; set; }
public [Module]Context(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies)
{
// ContextBase handles multi-tenant database connections
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Models.[Module]>().ToTable(ActiveDatabase.RewriteName("[Owner][Module]"));
}
}
}

View File

@@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Modules;
namespace [Owner].Module.[Module].Repository
{
public interface I[Module]Repository
{
IEnumerable<Models.[Module]> Get[Module]s(int ModuleId);
Models.[Module] Get[Module](int [Module]Id);
Models.[Module] Get[Module](int [Module]Id, bool tracking);
Models.[Module] Add[Module](Models.[Module] [Module]);
Models.[Module] Update[Module](Models.[Module] [Module]);
void Delete[Module](int [Module]Id);
}
public class [Module]Repository : I[Module]Repository, ITransientService
{
private readonly IDbContextFactory<[Module]Context> _factory;
public [Module]Repository(IDbContextFactory<[Module]Context> factory)
{
_factory = factory;
}
public IEnumerable<Models.[Module]> Get[Module]s(int ModuleId)
{
using var db = _factory.CreateDbContext();
return db.[Module].Where(item => item.ModuleId == ModuleId).ToList();
}
public Models.[Module] Get[Module](int [Module]Id)
{
return Get[Module]([Module]Id, true);
}
public Models.[Module] Get[Module](int [Module]Id, bool tracking)
{
using var db = _factory.CreateDbContext();
if (tracking)
{
return db.[Module].Find([Module]Id);
}
else
{
return db.[Module].AsNoTracking().FirstOrDefault(item => item.[Module]Id == [Module]Id);
}
}
public Models.[Module] Add[Module](Models.[Module] [Module])
{
using var db = _factory.CreateDbContext();
db.[Module].Add([Module]);
db.SaveChanges();
return [Module];
}
public Models.[Module] Update[Module](Models.[Module] [Module])
{
using var db = _factory.CreateDbContext();
db.Entry([Module]).State = EntityState.Modified;
db.SaveChanges();
return [Module];
}
public void Delete[Module](int [Module]Id)
{
using var db = _factory.CreateDbContext();
Models.[Module] [Module] = db.[Module].Find([Module]Id);
db.[Module].Remove([Module]);
db.SaveChanges();
}
}
}

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Security;
using Oqtane.Shared;
using [Owner].Module.[Module].Repository;
namespace [Owner].Module.[Module].Services
{
public class Server[Module]Service : I[Module]Service
{
private readonly I[Module]Repository _[Module]Repository;
private readonly IUserPermissions _userPermissions;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
private readonly Alias _alias;
public Server[Module]Service(I[Module]Repository [Module]Repository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
{
_[Module]Repository = [Module]Repository;
_userPermissions = userPermissions;
_logger = logger;
_accessor = accessor;
_alias = tenantManager.GetAlias();
}
public Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return Task.FromResult(_[Module]Repository.Get[Module]s(ModuleId).ToList());
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {ModuleId}", ModuleId);
return null;
}
}
public Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return Task.FromResult(_[Module]Repository.Get[Module]([Module]Id));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id} {ModuleId}", [Module]Id, ModuleId);
return null;
}
}
public Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module])
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, [Module].ModuleId, PermissionNames.Edit))
{
[Module] = _[Module]Repository.Add[Module]([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "[Module] Added {[Module]}", [Module]);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Add Attempt {[Module]}", [Module]);
[Module] = null;
}
return Task.FromResult([Module]);
}
public Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module])
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, [Module].ModuleId, PermissionNames.Edit))
{
[Module] = _[Module]Repository.Update[Module]([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Update Attempt {[Module]}", [Module]);
[Module] = null;
}
return Task.FromResult([Module]);
}
public Task Delete[Module]Async(int [Module]Id, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
_[Module]Repository.Delete[Module]([Module]Id);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "[Module] Deleted {[Module]Id}", [Module]Id);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", [Module]Id, ModuleId);
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using [Owner].Module.[Module].Repository;
using [Owner].Module.[Module].Services;
namespace [Owner].Module.[Module].Startup
{
public class [Module]ServerStartup : IServerStartup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// not implemented
}
public void ConfigureMvc(IMvcBuilder mvcBuilder)
{
// not implemented
}
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<I[Module]Service, Server[Module]Service>();
services.AddDbContextFactory<[Module]Context>(opt => { }, ServiceLifetime.Transient);
}
}
}

View File

@@ -0,0 +1,5 @@
/* Module Script */
var [Owner] = [Owner] || {};
[Owner].[Module] = {
};

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Oqtane.Models;
namespace [Owner].Module.[Module].Models
{
[Table("[Owner][Module]")]
public class [Module] : ModelBase
{
[Key]
public int [Module]Id { get; set; }
public int ModuleId { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
{
"Title": "Default Module Template",
"Type": "Internal",
"Version": "10.0.0",
"Namespace": "[Owner].Module.[Module]"
}

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title.HelpText" xml:space="preserve">
<value>Specify If The Module Title Should Be Displayed</value>
</data>
<data name="Title.Text" xml:space="preserve">
<value>Display Title</value>
</data>
</root>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Login.HelpText" xml:space="preserve">
<value>Specify if a Login option should be displayed. Note that this option does not prevent the login page from being accessible via a direct url.</value>
</data>
<data name="Login.Text" xml:space="preserve">
<value>Show Login?</value>
</data>
<data name="Register.HelpText" xml:space="preserve">
<value>Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.</value>
</data>
<data name="Register.Text" xml:space="preserve">
<value>Show Register?</value>
</data>
<data name="Scope.HelpText" xml:space="preserve">
<value>Specify if the settings are applicable to this page or the entire site.</value>
</data>
<data name="Scope.Text" xml:space="preserve">
<value>Setting Scope:</value>
</data>
</root>

View File

@@ -0,0 +1,46 @@
@namespace [Owner].Theme.[Theme]
@inherits ContainerBase
@inject ISettingService SettingService
<div class="@_classes">
@if (_title && ModuleState.Title != "-")
{
<div class="row px-4">
<div class="d-flex flex-nowrap">
<ModuleActions /><h2><ModuleTitle /></h2>
</div>
<hr class="app-rule" />
</div>
}
else
{
<ModuleActions />
}
<div class="row px-4">
<div class="container-fluid">
<ModuleInstance />
</div>
</div>
</div>
@code {
public override string Name => "[Owner] [Theme] - Container1";
private bool _title = true;
private string _classes = "container-fluid";
protected override void OnParametersSet()
{
try
{
_title = bool.Parse(SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true"));
}
catch
{
// error loading container settings
}
}
}

View File

@@ -0,0 +1,50 @@
@namespace [Owner].Theme.[Theme]
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@attribute [OqtaneIgnore]
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" ResourceKey="Title" ResourceType="@resourceType" HelpText="Specify If The Module Title Should Be Displayed">Display Title?</Label>
<div class="col-sm-9">
<select id="title" class="form-select" @bind="@_title">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
</div>
</div>
@code {
private string resourceType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane"; // for localization
private string _title = "true";
protected override void OnInitialized()
{
try
{
_title = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Title", _title);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@@ -0,0 +1,118 @@
@namespace [Owner].Theme.[Theme]
@inherits ThemeBase
@inject ISettingService SettingService
<main role="main">
<nav class="navbar navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" />
<div class="controls ms-auto">
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel ButtonClass="btn-outline-light" /></div>
</div>
</nav>
<div class="content">
<div class="container">
<div class="row">
<div class="col-md-12">
<Pane Name="@PaneNames.Admin" />
</div>
</div>
</div>
<Pane Name="Top Full Width" />
<div class="container">
<div class="row">
<div class="col-md-12">
<Pane Name="Top 100%" />
</div>
</div>
<div class="row">
<div class="col-md-6">
<Pane Name="Left 50%" />
</div>
<div class="col-md-6">
<Pane Name="Right 50%" />
</div>
</div>
<div class="row">
<div class="col-md-4">
<Pane Name="Left 33%" />
</div>
<div class="col-md-4">
<Pane Name="Center 33%" />
</div>
<div class="col-md-4">
<Pane Name="Right 33%" />
</div>
</div>
<div class="row">
<div class="col-md-3">
<Pane Name="Left Outer 25%" />
</div>
<div class="col-md-3">
<Pane Name="Left Inner 25%" />
</div>
<div class="col-md-3">
<Pane Name="Right Inner 25%" />
</div>
<div class="col-md-3">
<Pane Name="Right Outer 25%" />
</div>
</div>
<div class="row">
<div class="col-md-3">
<Pane Name="Left 25%" />
</div>
<div class="col-md-6">
<Pane Name="Center 50%" />
</div>
<div class="col-md-3">
<Pane Name="Right 25%" />
</div>
</div>
<div class="row">
<div class="col-md-8">
<Pane Name="Left Sidebar 66%" />
</div>
<div class="col-md-4">
<Pane Name="Right Sidebar 33%" />
</div>
</div>
<div class="row">
<div class="col-md-4">
<Pane Name="Left Sidebar 33%" />
</div>
<div class="col-md-8">
<Pane Name="Right Sidebar 66%" />
</div>
</div>
<div class="row">
<div class="col-md-12">
<Pane Name="Bottom 100%" />
</div>
</div>
</div>
<Pane Name="Bottom Full Width" />
</div>
</main>
@code {
public override string Name => "Theme1";
public override string Panes => PaneNames.Admin + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width";
private bool _login = true;
private bool _register = true;
protected override void OnParametersSet()
{
try
{
var settings = SettingService.MergeSettings(PageState.Site.Settings, PageState.Page.Settings);
_login = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true"));
_register = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true"));
}
catch
{
// error loading theme settings
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using Oqtane.Models;
using Oqtane.Themes;
using Oqtane.Shared;
namespace [Owner].Theme.[Theme]
{
public class ThemeInfo : ITheme
{
public Oqtane.Models.Theme Theme => new Oqtane.Models.Theme
{
Name = "[Owner] [Theme]",
Version = "1.0.0",
PackageName = "[Owner].Theme.[Theme]",
ThemeSettingsType = "[Owner].Theme.[Theme].ThemeSettings, [Owner].Theme.[Theme].Client.Oqtane",
ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane",
Resources = new List<Resource>()
{
// obtained from https://cdnjs.com/libraries
new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"),
new Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}
};
}
}

View File

@@ -0,0 +1,140 @@
@namespace [Owner].Theme.[Theme]
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@inject IStringLocalizer<ThemeSettings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@attribute [OqtaneIgnore]
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scope" ResourceKey="Scope" ResourceType="@resourceType" HelpText="Specify if the settings are applicable to this page or the entire site.">Setting Scope:</Label>
<div class="col-sm-9">
<select id="scope" class="form-select" value="@_scope" @onchange="(e => ScopeChanged(e))">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<option value="site">@Localizer["Site"]</option>
}
<option value="page">@Localizer["Page"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="login" ResourceKey="Login" ResourceType="@resourceType" HelpText="Specify if a Login option should be displayed. Note that this option does not prevent the login page from being accessible via a direct url.">Show Login?</Label>
<div class="col-sm-9">
<select id="login" class="form-select" @bind="@_login">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="register" ResourceKey="Register" ResourceType="@resourceType" HelpText="Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.">Show Register?</Label>
<div class="col-sm-9">
<select id="register" class="form-select" @bind="@_register">
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
@code {
private int pageId = -1;
private string resourceType = "[Owner].Theme.[Theme].ThemeSettings, [Owner].Theme.[Theme].Client.Oqtane"; // for localization
private string _scope = "page";
private string _login = "-";
private string _register = "-";
protected override async Task OnInitializedAsync()
{
if (PageState.QueryString.ContainsKey("id"))
{
pageId = int.Parse(PageState.QueryString["id"]);
}
try
{
await LoadSettings();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
AddModuleMessage("Error Loading Settings", MessageType.Error);
}
}
private async Task LoadSettings()
{
if (_scope == "site")
{
var settings = PageState.Site.Settings;
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true");
}
else
{
var settings = await SettingService.GetPageSettingsAsync(pageId);
settings = SettingService.MergeSettings(PageState.Site.Settings, settings);
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "-");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "-");
}
await Task.Yield();
}
private async Task ScopeChanged(ChangeEventArgs eventArgs)
{
try
{
_scope = (string)eventArgs.Value;
await LoadSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
AddModuleMessage("Error Loading Settings", MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
if (_scope == "site")
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
if (_login != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
}
if (_register != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
}
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
}
else
{
var settings = await SettingService.GetPageSettingsAsync(pageId);
if (_login != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
}
if (_register != "-")
{
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
}
await SettingService.UpdatePageSettingsAsync(settings, pageId);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Settings {Error}", ex.Message);
AddModuleMessage("Error Saving Settings", MessageType.Error);
}
}
}

View File

@@ -0,0 +1,123 @@
/* Oqtane Styles */
body {
padding-top: 7rem;
}
/* App Logo */
.app-logo .img-fluid {
max-height: 90px;
padding: 0 5px 0 5px;
}
.table > :not(caption) > * > * {
box-shadow: none;
}
.table .form-control {
background-color: #ffffff !important;
border-width: 0.5px !important;
border-bottom-color: #ccc !important;
}
.table .form-select {
background-color: #ffffff !important;
border-width: 0.5px !important;
border-bottom-color: #ccc !important;
}
.table .btn-primary {
background-color: var(--bs-primary);
}
.table .btn-secondary {
background-color: var(--bs-secondary);
}
.alert-dismissible .btn-close {
z-index: 1;
}
.controls {
z-index: 2000;
padding-top: 15px;
padding-bottom: 15px;
margin-right: 10px;
}
.app-menu .nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
white-space: nowrap;
}
.app-menu .nav-item a {
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
padding-left: 1rem;
}
.app-menu .nav-item a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}
.app-menu .nav-item a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.app-menu .nav-link .oi {
width: 1.5rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
margin: .5rem;
}
div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu {
color: #000000;
}
.dropdown-menu span {
mix-blend-mode: difference;
}
@media (max-width: 767.98px) {
.app-menu {
width: 100%;
}
.navbar {
position: fixed;
top: 60px;
width: 100%;
}
.controls {
height: 60px;
top: 15px;
position: fixed;
top: 0px;
width: 100%;
background-color: rgb(0, 0, 0);
}
.controls-group {
float: right;
margin-right: 25px;
}
.content {
position: relative;
top: 60px;
}
}

View File

@@ -0,0 +1,6 @@
{
"Title": "Default Theme Template",
"Type": "Internal",
"Version": "10.0.0",
"Namespace": "[Owner].Theme.[Theme]"
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<AssemblyName>Oqtane.Application.Shared.Oqtane</AssemblyName>
</PropertyGroup>
@@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="6.2.1" />
<PackageReference Include="Oqtane.Shared" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -42,29 +42,32 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<select id="reference" class="form-select" @bind="@_reference" required>
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
@if (_type == "External")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the module will be created" ResourceKey="Location">Location: </Label>
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
<select id="reference" class="form-select" @bind="@_reference" required>
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the module will be created" ResourceKey="Location">Location: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
</div>
</div>
}
}
</div>
<button type="button" class="btn btn-success" @onclick="CreateModule">@Localizer["CreateModule"]</button>
@@ -80,9 +83,10 @@
private string _description = string.Empty;
private List<Template> _templates;
private string _template = "-";
private string _minversion = "2.0.0";
private string _type = "External";
private string[] _versions;
private string _reference = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -153,7 +157,7 @@
private bool IsValid(string name)
{
// must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_.]*$");
}
private bool IsValidXML(string description)
@@ -165,11 +169,16 @@
private void TemplateChanged(ChangeEventArgs e)
{
_template = (string)e.Value;
_minversion = "2.0.0";
if (_template != "-")
{
var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version;
_type = template.Type;
}
else
{
_minversion = "2.0.0";
_type = "External";
}
GetLocation();
}

View File

@@ -36,30 +36,33 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<select id="reference" class="form-select" @bind="@_reference">
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the theme will be created" ResourceKey="Location">Location: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
</div>
</div>
}
@if (_type == "External")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<select id="reference" class="form-select" @bind="@_reference">
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the theme will be created" ResourceKey="Location">Location: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
</div>
</div>
}
}
</div>
<br />
<button type="button" class="btn btn-success" @onclick="CreateTheme">@Localizer["Theme.Create"]</button>
@@ -71,9 +74,10 @@
private string _theme = string.Empty;
private List<Template> _templates;
private string _template = "-";
private string _minversion = "2.0.0";
private string _type = "External";
private string[] _versions;
private string _reference = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -125,17 +129,22 @@
private bool IsValid(string name)
{
// must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && name.ToLower() != "theme" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
return !string.IsNullOrEmpty(name) && name.ToLower() != "theme" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_.]*$");
}
private void TemplateChanged(ChangeEventArgs e)
{
_template = (string)e.Value;
_minversion = "2.0.0";
if (_template != "-")
{
var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version;
_type = template.Type;
}
else
{
_minversion = "2.0.0";
_type = "External";
}
GetLocation();
}

View File

@@ -27,7 +27,6 @@
}
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label>
@@ -35,36 +34,6 @@
<input id="username" class="form-control" @bind="@_username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
@if (_allowtwofactor)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
@@ -99,7 +68,80 @@
<br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<button type="button" class="btn btn-danger" @onclick="Logout">@Localizer["Logout Everywhere"]</button>
</TabPanel>
<TabPanel Name="Security" ResourceKey="Security">
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
@if (_allowtwofactor)
{
<Section Name="MFA" Heading="Multi-Factor Authentication" ResourceKey="MFA">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</Section>
}
<Section Name="External" Heading="External Login" ResourceKey="External">
</Section>
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
<Pager Items="@_passkeys">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Passkey"]</th>
</Header>
<Row>
@if (context.CredentialId != _passkeyId)
{
<td><button type="button" class="btn btn-primary" @onclick="@(() => EditPasskey(context))">@SharedLocalizer["Edit"]</button></td>
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeletePasskey(context))" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Passkey.Delete", context.CredentialId])" /></td>
<td>@context.Name</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SavePasskey())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelPasskey())">@SharedLocalizer["Cancel"]</button></td>
<td><input id="aliasname" class="form-control" @bind="@_passkeyName" /></td>
}
</Row>
</Pager>
<br /><br />
</Section>
<Section Name="Logout" Heading="Logout" ResourceKey="Logout">
<button type="button" class="btn btn-danger" @onclick="Logout">@Localizer["Logout Everywhere"]</button>
</Section>
<br />
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
@@ -392,6 +434,10 @@
private File _photo = null;
private string _imagefiles = string.Empty;
private List<Passkey> _passkeys;
private byte[] _passkeyId;
private string _passkeyName = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _userSettings;
private string _category = string.Empty;
@@ -436,6 +482,9 @@
_folderid = folder.FolderId;
}
_imagefiles = SettingService.GetSetting(PageState.Site.Settings, "ImageFiles", Constants.ImageFiles);
_imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
if (PageState.User.PhotoFileId != null)
{
_photofileid = PageState.User.PhotoFileId.Value;
@@ -448,9 +497,6 @@
}
_userSettings = PageState.User.Settings;
var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles);
_imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
await LoadNotificationsAsync();
@@ -584,6 +630,51 @@
}
}
private async Task GetPasskeys()
{
_passkeys = await UserService.GetPasskeysAsync();
}
private async Task AddPasskey()
{
_passkeyName = $"{PageState.User.DisplayName}{_passkeys.Count + 1}"; // set default name
await UserService.AddPasskeyAsync(new Passkey { Name = _passkeyName, CredentialJson = "" });
await GetPasskeys();
StateHasChanged();
}
private void EditPasskey(Passkey passkey)
{
_passkeyId = passkey.CredentialId;
_passkeyName = passkey.Name;
StateHasChanged();
}
private async Task DeletePasskey(Passkey passkey)
{
await UserService.DeletePasskeyAsync(passkey.CredentialId);
await GetPasskeys();
StateHasChanged();
}
private async Task SavePasskey()
{
if (!string.IsNullOrEmpty(_passkeyName))
{
await UserService.UpdatePasskeyAsync(new Passkey { CredentialId = _passkeyId, Name = _passkeyName });
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
}
}
private async Task CancelPasskey()
{
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
}
private bool ValidateProfiles()
{
foreach (Profile profile in _profiles)

View File

@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>Oqtane</RootNamespace>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<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="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Radzen.Blazor" Version="7.4.3" />
</ItemGroup>

View File

@@ -434,11 +434,26 @@ namespace Oqtane.Services
foreach (KeyValuePair<string, string> kvp in settings)
{
var setting = new Setting();
setting.SettingId = 0;
setting.EntityName = entityName;
setting.EntityId = entityId;
setting.SettingName = kvp.Key;
setting.SettingValue = kvp.Value;
setting.IsPrivate = true;
// manage settings modified with SetSetting method
if (setting.SettingValue.StartsWith("[Private]"))
{
setting.SettingValue = setting.SettingValue.Substring(9);
setting.IsPrivate = true;
setting.SettingId = -1; // indicates IsPrivate was explicitly set
}
if (setting.SettingValue.StartsWith("[Public]"))
{
setting.SettingValue = setting.SettingValue.Substring(8);
setting.IsPrivate = false;
setting.SettingId = -1; // indicates IsPrivate was explicitly set
}
settingsList.Add(setting);
}

View File

@@ -172,6 +172,33 @@ namespace Oqtane.Services
/// <param name="notify">Indicates if new users should be notified by email</param>
/// <returns></returns>
Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify);
/// <summary>
/// Get passkeys for a user
/// </summary>
/// <returns></returns>
Task<List<Passkey>> GetPasskeysAsync();
/// <summary>
/// Add a user passkey
/// </summary>
/// <param name="passkey"></param>
/// <returns></returns>
Task<Passkey> AddPasskeyAsync(Passkey passkey);
/// <summary>
/// Update a user passkey
/// </summary>
/// <param name="passkey"></param>
/// <returns></returns>
Task<Passkey> UpdatePasskeyAsync(Passkey passkey);
/// <summary>
/// Delete a user passkey
/// </summary>
/// <param name="credentialId"></param>
/// <returns></returns>
Task DeletePasskeyAsync(byte[] credentialId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -302,5 +329,25 @@ namespace Oqtane.Services
{
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
public async Task<List<Passkey>> GetPasskeysAsync()
{
return await GetJsonAsync<List<Passkey>>($"{Apiurl}/passkey");
}
public async Task<Passkey> AddPasskeyAsync(Passkey passkey)
{
return await PostJsonAsync<Passkey>($"{Apiurl}/passkey", passkey);
}
public async Task<Passkey> UpdatePasskeyAsync(Passkey passkey)
{
return await PutJsonAsync<Passkey>($"{Apiurl}/passkey", passkey);
}
public async Task DeletePasskeyAsync(byte[] credentialId)
{
await DeleteAsync($"{Apiurl}/passkey?id={credentialId}");
}
}
}

View File

@@ -3,31 +3,29 @@
@inject IStringLocalizer<Login> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<span class="app-login">
@if (PageState.User != null)
@if (PageState.User != null)
{
@if (PageState.Runtime == Runtime.Hybrid)
{
@if (PageState.Runtime == Runtime.Hybrid)
{
<button type="button" class="@CssClass" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<input type="hidden" name="everywhere" value="@everywhere" />
<button type="submit" class="@CssClass">@Localizer["Logout"]</button>
</form>
}
<button type="button" class="@CssClass app-login" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
@if (ShowLogin)
{
<a href="@loginurl" class="@CssClass">@SharedLocalizer["Login"]</a>
}
<form method="post" class="app-form-inline app-login" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<input type="hidden" name="everywhere" value="@everywhere" />
<button type="submit" class="@CssClass">@Localizer["Logout"]</button>
</form>
}
</span>
}
else
{
@if (ShowLogin)
{
<a href="@loginurl" class="@CssClass app-login">@SharedLocalizer["Login"]</a>
}
}
@code
{

View File

@@ -5,19 +5,17 @@
@inject IStringLocalizer<UserProfile> Localizer
@inject NavigationManager NavigationManager
<span class="app-profile">
@if (PageState.User != null)
@if (PageState.User != null)
{
<a href="@_profileurl" class="@CssClass app-profile">@PageState.User.Username</a>
}
else
{
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@_profileurl" class="@CssClass">@PageState.User.Username</a>
<a href="@_registerurl" class="@CssClass app-profile">@Localizer["Register"]</a>
}
else
{
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@_registerurl" class="@CssClass">@Localizer["Register"]</a>
}
}
</span>
}
@code {

View File

@@ -17,7 +17,7 @@ namespace Oqtane.Themes.OqtaneTheme
Resources = new List<Resource>()
{
// obtained from https://cdnjs.com/libraries/bootswatch
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.7/cyborg/bootstrap.min.css", "sha512-/LQFzDeXqysGQ/POl5YOEjgVZH1BmqDHvshhnFIChf50bMGQ470qhUrsecD9MRCUwzwqRoshwAbmA2oTW4I6Yg==", "anonymous"),
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.8/cyborg/bootstrap.min.css", "sha512-Sq+1MhDgkXwshbeZBKh8j5N7bjn56Jg40kyGm27FoBYEBPksAG+GcRwLEHT/UL4F/WdYUCl65IAQiGTANnBzLg==", "anonymous"),
new Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}

View File

@@ -1,12 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build android, ios, and maccatalyst -->
<!-- <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<RootNamespace>Oqtane.Maui</RootNamespace>
<RootNamespace>Oqtane.Maui</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
@@ -19,32 +18,31 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>6.2.1</ApplicationDisplayVersion>
<ApplicationDisplayVersion>10.0.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
@@ -55,24 +53,24 @@
<None Remove="Platforms\Android\Resources\xml\network_security_config.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.9" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.9" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.110" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.110" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.110" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
</ItemGroup>
<ItemGroup>
<Reference Include="Oqtane.Client">
<HintPath>..\Oqtane.Server\bin\Debug\net9.0\Oqtane.Client.dll</HintPath>
</Reference>
<Reference Include="Oqtane.Shared">
<HintPath>..\Oqtane.Server\bin\Debug\net9.0\Oqtane.Shared.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="Oqtane.Client">
<HintPath>..\Oqtane.Server\bin\Debug\net10.0\Oqtane.Client.dll</HintPath>
</Reference>
<Reference Include="Oqtane.Shared">
<HintPath>..\Oqtane.Server\bin\Debug\net10.0\Oqtane.Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

Binary file not shown.

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>6.2.1</version>
<version>10.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,24 +12,23 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net9.0">
<dependency id="Oqtane.Shared" version="6.2.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" 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.Localization" version="9.0.9" exclude="Build,Analyzers" />
<group targetFramework="net10.0">
<dependency id="Oqtane.Shared" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="7.4.3" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
<files>
<file src="..\Oqtane.Client\bin\Release\net9.0\Oqtane.Client.dll" target="lib\net9.0" />
<file src="..\Oqtane.Client\bin\Release\net9.0\Oqtane.Client.pdb" target="lib\net9.0" />
<file src="..\Oqtane.Client\bin\Release\net10.0\Oqtane.Client.dll" target="lib\net10.0" />
<file src="..\Oqtane.Client\bin\Release\net10.0\Oqtane.Client.pdb" target="lib\net10.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Framework</id>
<version>6.2.1</version>
<version>10.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v6.2.1/Oqtane.Framework.6.2.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.0.0/Oqtane.Framework.10.0.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane framework</tags>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Server</id>
<version>6.2.1</version>
<version>10.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,20 +12,20 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net9.0">
<dependency id="Oqtane.Client" version="6.2.1" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="6.2.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="9.0.9" exclude="Build,Analyzers" />
<group targetFramework="net10.0">
<dependency id="Oqtane.Client" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.SqlClient" version="6.1.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore" 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.EntityFrameworkCore" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="SixLabors.ImageSharp" version="3.1.11" exclude="Build,Analyzers" />
<dependency id="HtmlAgilityPack" version="1.12.3" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="9.0.5" exclude="Build,Analyzers" />
@@ -33,28 +33,28 @@
<dependency id="MySql.Data" version="9.4.0" exclude="Build,Analyzers" />
<dependency id="Pomelo.EntityFrameworkCore.MySql" version="9.0.0" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="9.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="9.0.4" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="SQLitePCLRaw.bundle_e_sqlite3" version="3.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
</group>
</dependencies>
<frameworkReferences>
<group targetFramework="net9.0">
<group targetFramework="net10.0">
<frameworkReference name="Microsoft.AspNetCore.App" />
</group>
</frameworkReferences>
</metadata>
<files>
<file src="..\Oqtane.Server\bin\Release\net9.0\Oqtane.Server.dll" target="lib\net9.0" />
<file src="..\Oqtane.Server\bin\Release\net9.0\Oqtane.Server.pdb" target="lib\net9.0" />
<file src="..\Oqtane.Server\obj\Release\net9.0\staticwebassets\msbuild.Oqtane.Server.Microsoft.AspNetCore.StaticWebAssetEndpoints.props" target="build\Microsoft.AspNetCore.StaticWebAssetEndpoints.props" />
<file src="..\Oqtane.Server\obj\Release\net9.0\staticwebassets\msbuild.Oqtane.Server.Microsoft.AspNetCore.StaticWebAssets.props" target="build\Microsoft.AspNetCore.StaticWebAssets.props" />
<file src="..\Oqtane.Server\obj\Release\net9.0\staticwebassets\msbuild.build.Oqtane.Server.props" target="build\Oqtane.Server.props" />
<file src="..\Oqtane.Server\obj\Release\net9.0\staticwebassets\msbuild.buildMultiTargeting.Oqtane.Server.props" target="buildMultiTargeting\Oqtane.Server.props" />
<file src="..\Oqtane.Server\obj\Release\net9.0\staticwebassets\msbuild.buildTransitive.Oqtane.Server.props" target="buildTransitive\Oqtane.Server.props" />
<file src="..\Oqtane.Server\bin\Release\net10.0\Oqtane.Server.dll" target="lib\net10.0" />
<file src="..\Oqtane.Server\bin\Release\net10.0\Oqtane.Server.pdb" target="lib\net10.0" />
<file src="..\Oqtane.Server\obj\Release\net10.0\staticwebassets\msbuild.Oqtane.Server.Microsoft.AspNetCore.StaticWebAssetEndpoints.props" target="build\Microsoft.AspNetCore.StaticWebAssetEndpoints.props" />
<file src="..\Oqtane.Server\obj\Release\net10.0\staticwebassets\msbuild.Oqtane.Server.Microsoft.AspNetCore.StaticWebAssets.props" target="build\Microsoft.AspNetCore.StaticWebAssets.props" />
<file src="..\Oqtane.Server\obj\Release\net10.0\staticwebassets\msbuild.build.Oqtane.Server.props" target="build\Oqtane.Server.props" />
<file src="..\Oqtane.Server\obj\Release\net10.0\staticwebassets\msbuild.buildMultiTargeting.Oqtane.Server.props" target="buildMultiTargeting\Oqtane.Server.props" />
<file src="..\Oqtane.Server\obj\Release\net10.0\staticwebassets\msbuild.buildTransitive.Oqtane.Server.props" target="buildTransitive\Oqtane.Server.props" />
<file src="..\Oqtane.Server\wwwroot\**\*" exclude="..\Oqtane.Server\wwwroot\Modules\Templates\**;..\Oqtane.Server\wwwroot\Themes\Templates\**" target="staticwebassets" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>6.2.1</version>
<version>10.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,24 +12,23 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net9.0">
<dependency id="Microsoft.EntityFrameworkCore" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="9.0.9" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="9.0.9" exclude="Build,Analyzers" />
<group targetFramework="net10.0">
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.2.2" exclude="Build,Analyzers" />
<dependency id="System.ComponentModel.Annotations" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Text.Json" version="9.0.9" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
<files>
<file src="..\Oqtane.Shared\bin\Release\net9.0\Oqtane.Shared.dll" target="lib\net9.0" />
<file src="..\Oqtane.Shared\bin\Release\net9.0\Oqtane.Shared.pdb" target="lib\net9.0" />
<file src="..\Oqtane.Shared\bin\Release\net10.0\Oqtane.Shared.dll" target="lib\net10.0" />
<file src="..\Oqtane.Shared\bin\Release\net10.0\Oqtane.Shared.pdb" target="lib\net10.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>6.2.1</version>
<version>10.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,13 +12,13 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
<files>
<file src="..\Oqtane.Updater\bin\Release\net9.0\publish\*.*" target="lib\net9.0" />
<file src="..\Oqtane.Updater\bin\Release\net10.0\publish\*.*" target="lib\net10.0" />
<file src="icon.png" target="" />
<file src="readme.md" target="" />
</files>

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.2.1.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.0.Install.zip" -Force

View File

@@ -4,24 +4,24 @@ nuget.exe pack Oqtane.Server.nuspec
nuget.exe pack Oqtane.Shared.nuspec
nuget.exe pack Oqtane.Framework.nuspec
dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\Content"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\android-arm"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\android-arm64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\android-x64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\android-x86"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\ios-arm"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\ios-arm64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\iossimulator-arm64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\iossimulator-x64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\runtimes\iossimulator-x86"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Modules\Templates"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Themes\Templates"
del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json"
ren "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.release.json" "appsettings.json"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\Content"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\wwwroot\Content"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\android-arm"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\android-arm64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\android-x64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\android-x86"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\ios-arm"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\ios-arm64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\iossimulator-arm64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\iossimulator-x64"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\runtimes\iossimulator-x86"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\wwwroot\Modules\Templates"
rmdir /Q /S "..\Oqtane.Server\bin\Release\net10.0\publish\wwwroot\Themes\Templates"
del "..\Oqtane.Server\bin\Release\net10.0\publish\appsettings.json"
ren "..\Oqtane.Server\bin\Release\net10.0\publish\appsettings.release.json" "appsettings.json"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1"
del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json"
del "..\Oqtane.Server\bin\Release\net9.0\publish\web.config"
del "..\Oqtane.Server\bin\Release\net10.0\publish\appsettings.json"
del "..\Oqtane.Server\bin\Release\net10.0\publish\web.config"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\upgrade.ps1"
dotnet build -c Release ..\Oqtane.Updater.sln
dotnet publish ..\Oqtane.Updater\Oqtane.Updater.csproj /p:Configuration=Release

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.2.1.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.0.Upgrade.zip" -Force

View File

@@ -197,10 +197,6 @@
{
_scripts += CreatePWAScript(alias, site, route);
}
@if (_renderMode == RenderModes.Static)
{
_scripts += CreateScrollPositionScript();
}
// set culture if not specified
string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
@@ -510,25 +506,6 @@
"</script>" + Environment.NewLine;
}
private string CreateScrollPositionScript()
{
return Environment.NewLine +
"<script>" + Environment.NewLine +
" // Blazor Static Rendering Scroll Position" + Environment.NewLine +
" window.interceptNavigation = () => {" + Environment.NewLine +
" let currentUrl = window.location.pathname;" + Environment.NewLine +
" Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine +
" let newUrl = window.location.pathname;" + Environment.NewLine +
" if (currentUrl !== newUrl || window.location.hash === '#top') {" + Environment.NewLine +
" window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine +
" }" + Environment.NewLine +
" currentUrl = newUrl;" + Environment.NewLine +
" });" + Environment.NewLine +
" };" + Environment.NewLine +
" document.onload += window.interceptNavigation();" + Environment.NewLine +
"</script>" + Environment.NewLine;
}
private void AddScript(Resource resource, Alias alias)
{
var script = CreateScript(resource, alias);

View File

@@ -132,8 +132,9 @@ namespace Oqtane.Controllers
if (moduleDefinition.Template.ToLower().Contains("internal"))
{
rootPath = Utilities.PathCombine(rootFolder.FullName, Path.DirectorySeparatorChar.ToString());
moduleDefinition.ServerManagerType = moduleDefinition.ModuleDefinitionName + ".Manager." + moduleDefinition.Name + "Manager, Oqtane.Server";
moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", Oqtane.Client";
var assemblyName = Assembly.GetExecutingAssembly().GetName().Name;
moduleDefinition.ServerManagerType = moduleDefinition.ModuleDefinitionName + ".Manager." + moduleDefinition.Name + "Manager, " + assemblyName;
moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", " + assemblyName.Replace(".Server", ".Client");
}
else
{
@@ -350,9 +351,9 @@ namespace Oqtane.Controllers
return new Dictionary<string, object>()
{
{ "FrameworkVersion", Constants.Version },
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Client.dll</HintPath></Reference>" },
{ "ServerReference", $"<Reference Include=\"Oqtane.Server\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Server.dll</HintPath></Reference>" },
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Shared.dll</HintPath></Reference>" },
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net10.0\\Oqtane.Client.dll</HintPath></Reference>" },
{ "ServerReference", $"<Reference Include=\"Oqtane.Server\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net10.0\\Oqtane.Server.dll</HintPath></Reference>" },
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net10.0\\Oqtane.Shared.dll</HintPath></Reference>" },
};
});
}

View File

@@ -216,6 +216,13 @@ namespace Oqtane.Controllers
page.UserId = int.Parse(userid);
page = _pages.AddPage(page);
// copy parent page settings
var settings = _settings.GetSettings(EntityNames.Page, parent.PageId);
foreach (var setting in settings)
{
_settings.AddSetting(new Setting { EntityName = EntityNames.Page, EntityId = page.PageId, SettingName = setting.SettingName, SettingValue = setting.SettingValue, IsPrivate = setting.IsPrivate });
}
// copy modules
List<PageModule> pagemodules = _pageModules.GetPageModules(page.SiteId).ToList();
foreach (PageModule pm in pagemodules.Where(item => item.PageId == parent.PageId && !item.IsDeleted))
@@ -258,8 +265,7 @@ namespace Oqtane.Controllers
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// set user personalized page path
var setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false };
_settings.AddSetting(setting);
_settings.AddSetting(new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false });
_syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update);
}
}

View File

@@ -16,8 +16,6 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using System.IO;
using System.Text.RegularExpressions;
using Oqtane.Migrations.Tenant;
using Google.Protobuf.WellKnownTypes;
using System;
namespace Oqtane.Controllers
@@ -220,39 +218,24 @@ namespace Oqtane.Controllers
var existingSettings = _settings.GetSettings(entityName, entityId).ToList();
foreach (Setting setting in settings)
{
bool modified = false;
// manage settings modified with SetSetting method
if (setting.SettingValue.StartsWith("[Private]"))
Setting existing = existingSettings.FirstOrDefault(item => item.SettingName.Equals(setting.SettingName, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
modified = true;
setting.IsPrivate = true;
setting.SettingValue = setting.SettingValue.Substring(9);
}
if (setting.SettingValue.StartsWith("[Public]"))
{
modified = true;
setting.IsPrivate = false;
setting.SettingValue = setting.SettingValue.Substring(8);
}
Setting existingSetting = existingSettings.FirstOrDefault(item => item.SettingName.Equals(setting.SettingName, StringComparison.OrdinalIgnoreCase));
if (existingSetting == null)
{
_settings.AddSetting(setting);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Created {Setting}", setting);
existing = _settings.AddSetting(setting);
AddSyncEvent(existing.EntityName, existing.EntityId, existing.SettingId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Created {Setting}", existing);
}
else
{
if (existingSetting.SettingValue != setting.SettingValue || (modified && existingSetting.IsPrivate != setting.IsPrivate))
// note that SettingId will pnly be 0 if the Settings were converted from a Dictionary in the SettingService
if (existing.SettingValue != setting.SettingValue || (existing.IsPrivate != setting.IsPrivate && setting.SettingId != 0))
{
existingSetting.SettingValue = setting.SettingValue;
existingSetting.IsPrivate = setting.IsPrivate;
_settings.UpdateSetting(existingSetting);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Updated {Setting}", setting);
existing.SettingValue = setting.SettingValue;
existing.IsPrivate = setting.IsPrivate;
existing = _settings.UpdateSetting(existing);
AddSyncEvent(existing.EntityName, existing.EntityId, existing.SettingId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Updated {Setting}", existing);
}
}
}
@@ -366,13 +349,16 @@ namespace Oqtane.Controllers
var setting = _settings.GetSetting(cols[0], entityId, cols[2]);
if (setting == null)
{
_settings.AddSetting(new Setting { EntityName = cols[0], EntityId = entityId, SettingName = cols[2], SettingValue = cols[3], IsPrivate = isPrivate });
setting = new Setting { EntityName = cols[0], EntityId = entityId, SettingName = cols[2], SettingValue = cols[3], IsPrivate = isPrivate };
_settings.AddSetting(setting);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Create);
}
else
{
setting.SettingValue = cols[3];
setting.IsPrivate = isPrivate;
_settings.UpdateSetting(setting);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Update);
}
rows++;
}

View File

@@ -226,7 +226,8 @@ namespace Oqtane.Controllers
if (theme.Template.ToLower().Contains("internal"))
{
rootPath = Utilities.PathCombine(rootFolder.FullName, Path.DirectorySeparatorChar.ToString());
theme.ThemeName = theme.ThemeName + ", Oqtane.Client";
var assemblyName = Assembly.GetExecutingAssembly().GetName().Name;
theme.ThemeName = theme.ThemeName + ", " + assemblyName.Replace(".Server", ".Client");
}
else
{
@@ -304,8 +305,8 @@ namespace Oqtane.Controllers
return new Dictionary<string, object>()
{
{ "FrameworkVersion", Constants.Version },
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Client.dll</HintPath></Reference>" },
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Shared.dll</HintPath></Reference>" },
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net10.0\\Oqtane.Client.dll</HintPath></Reference>" },
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net10.0\\Oqtane.Shared.dll</HintPath></Reference>" },
};
});
}

View File

@@ -463,5 +463,55 @@ namespace Oqtane.Controllers
return null;
}
}
// GET: api/<controller>/passkey
[HttpGet("passkey")]
[Authorize]
public async Task<IEnumerable<Passkey>> GetPasskeys()
{
return await _userManager.GetPasskeys(_userPermissions.GetUser(User).UserId);
}
// POST api/<controller>/passkey
[HttpPost("passkey")]
[Authorize]
public async Task AddPasskey([FromBody] Passkey passkey)
{
if (ModelState.IsValid)
{
passkey.UserId = _userPermissions.GetUser(User).UserId;
await _userManager.AddPasskey(passkey);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Post Attempt {PassKey}", passkey);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// PUT api/<controller>/passkey
[HttpPut("passkey")]
[Authorize]
public async Task UpdatePasskey([FromBody] Passkey passkey)
{
if (ModelState.IsValid)
{
passkey.UserId = _userPermissions.GetUser(User).UserId;
await _userManager.UpdatePasskey(passkey);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Put Attempt {PassKey}", passkey);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// DELETE api/<controller>/passkey?id=x
[HttpDelete("passkey")]
[Authorize]
public async Task DeletePasskey(byte[] id)
{
await _userManager.DeletePasskey(_userPermissions.GetUser(User).UserId, id);
}
}
}

View File

@@ -395,7 +395,7 @@ namespace Oqtane.Infrastructure
var connectionString = _configManager.GetSetting($"{SettingKeys.ConnectionStringsSection}:{tenant.DBConnectionString}", "");
if (!string.IsNullOrEmpty(connectionString))
{
using (var tenantDbContext = new TenantDBContext(DBContextDependencies))
using (var tenantDbContext = new TenantDBContext(new DbContextOptions<TenantDBContext>(), DBContextDependencies))
{
AddEFMigrationsHistory(sql, connectionString, tenant.DBType, tenant.Version, false);
// push latest model into database
@@ -507,6 +507,10 @@ namespace Oqtane.Infrastructure
}
}
}
else
{
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " - ServerManagerType " + moduleDefinition.ServerManagerType + " Does Not Exist";
}
}
if (string.IsNullOrEmpty(result.Message) && moduleDefinition.Version != versions[versions.Length - 1])

View File

@@ -42,220 +42,237 @@ namespace Oqtane.Infrastructure
{
log += "Processing Notifications For Site: " + site.Name + "<br />";
// get site settings
var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1);
if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True")
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
if (notifications.Count > 0)
{
bool valid = true;
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
// get site settings
var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1);
if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True")
{
// basic
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "<br />";
valid = false;
}
}
else
{
// oauth
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPAuthority", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientId", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientSecret", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPScopes", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, Authority, Client ID, Client Secret, Scopes, And Sender Are All Required" + "<br />";
valid = false;
}
}
if (valid)
{
// construct SMTP Client
using var client = new SmtpClient();
var secureSocketOptions = SecureSocketOptions.Auto;
switch (settingRepository.GetSettingValue(settings, "SMTPSSL", "Auto"))
{
case "None":
secureSocketOptions = SecureSocketOptions.None;
break;
case "Auto":
secureSocketOptions = SecureSocketOptions.Auto;
break;
case "StartTls":
secureSocketOptions = SecureSocketOptions.StartTls;
break;
case "SslOnConnect":
case "True": // legacy setting value
secureSocketOptions = SecureSocketOptions.SslOnConnect;
break;
case "StartTlsWhenAvailable":
case "False": // legacy setting value
secureSocketOptions = SecureSocketOptions.StartTlsWhenAvailable;
break;
}
await client.ConnectAsync(settingRepository.GetSettingValue(settings, "SMTPHost", ""),
int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")),
secureSocketOptions);
bool valid = true;
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
{
// it is possible to use basic without any authentication (not recommended)
if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "")
// basic
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""),
settingRepository.GetSettingValue(settings, "SMTPPassword", ""));
log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "<br />";
valid = false;
}
}
else
{
// oauth authentication
var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(settingRepository.GetSettingValue(settings, "SMTPClientId", ""))
.WithAuthority(settingRepository.GetSettingValue(settings, "SMTPAuthority", ""))
.WithClientSecret(settingRepository.GetSettingValue(settings, "SMTPClientSecret", ""))
.Build();
try
// oauth
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPAuthority", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientId", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientSecret", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPScopes", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
var result = await confidentialClientApplication.AcquireTokenForClient(settingRepository.GetSettingValue(settings, "SMTPScopes", "").Split(',')).ExecuteAsync();
var oauth2 = new SaslMechanismOAuth2(settingRepository.GetSettingValue(settings, "SMTPSender", ""), result.AccessToken);
await client.AuthenticateAsync(oauth2);
}
catch (Exception ex)
{
log += "SMTP Not Configured Properly In Site Settings - OAuth Token Could Not Be Retrieved From Authority - " + ex.Message + "<br />";
log += "SMTP Not Configured Properly In Site Settings - Host, Port, Authority, Client ID, Client Secret, Scopes, And Sender Are All Required" + "<br />";
valid = false;
}
}
if (valid)
{
// iterate through undelivered notifications
int sent = 0;
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
foreach (Notification notification in notifications)
// construct SMTP Client
using var client = new SmtpClient();
try
{
var fromEmail = notification.FromEmail ?? "";
var fromName = notification.FromDisplayName ?? "";
var toEmail = notification.ToEmail ?? "";
var toName = notification.ToDisplayName ?? "";
// get sender and receiver information from user information if available
if ((string.IsNullOrEmpty(fromEmail) || string.IsNullOrEmpty(fromName)) && notification.FromUserId != null)
var secureSocketOptions = SecureSocketOptions.Auto;
switch (settingRepository.GetSettingValue(settings, "SMTPSSL", "Auto"))
{
var user = userRepository.GetUser(notification.FromUserId.Value);
if (user != null)
case "None":
secureSocketOptions = SecureSocketOptions.None;
break;
case "Auto":
secureSocketOptions = SecureSocketOptions.Auto;
break;
case "StartTls":
secureSocketOptions = SecureSocketOptions.StartTls;
break;
case "SslOnConnect":
case "True": // legacy setting value
secureSocketOptions = SecureSocketOptions.SslOnConnect;
break;
case "StartTlsWhenAvailable":
case "False": // legacy setting value
secureSocketOptions = SecureSocketOptions.StartTlsWhenAvailable;
break;
}
await client.ConnectAsync(settingRepository.GetSettingValue(settings, "SMTPHost", ""),
int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")),
secureSocketOptions);
}
catch (Exception ex)
{
log += "SMTP Not Configured Properly In Site Settings - Could Not Connect To SMTP Server - " + ex.Message + "<br />";
valid = false;
}
if (valid)
{
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
{
// it is possible to use basic without any authentication (not recommended)
if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "")
{
fromEmail = string.IsNullOrEmpty(fromEmail) ? user.Email ?? "" : fromEmail;
fromName = string.IsNullOrEmpty(fromName) ? user.DisplayName ?? "" : fromName;
await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""),
settingRepository.GetSettingValue(settings, "SMTPPassword", ""));
}
}
if ((string.IsNullOrEmpty(toEmail) || string.IsNullOrEmpty(toName)) && notification.ToUserId != null)
{
var user = userRepository.GetUser(notification.ToUserId.Value);
if (user != null)
{
toEmail = string.IsNullOrEmpty(toEmail) ? user.Email ?? "" : toEmail;
toName = string.IsNullOrEmpty(toName) ? user.DisplayName ?? "" : toName;
}
}
// create mailbox addresses
MailboxAddress to = null;
MailboxAddress from = null;
var mailboxAddressValidationError = "";
// sender
if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") != "True")
{
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", "");
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
}
if (MailboxAddress.TryParse(fromEmail, out from))
{
from.Name = fromName;
}
else
{
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;";
}
// recipient
if (MailboxAddress.TryParse(toEmail, out to))
{
to.Name = toName;
}
else
{
mailboxAddressValidationError += $" Invalid Recipient: {toName} &lt;{toEmail}&gt;";
}
// if mailbox addresses are valid
if (from != null && to != null)
{
// create mail message
MimeMessage mailMessage = new MimeMessage();
mailMessage.From.Add(from);
mailMessage.To.Add(to);
// subject
mailMessage.Subject = notification.Subject;
// body
var bodyText = notification.Body;
if (!bodyText.Contains('<') || !bodyText.Contains('>'))
{
// plain text messages should convert line breaks to HTML tags to preserve formatting
bodyText = bodyText.Replace("\n", "<br />");
}
mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8)
{
Text = bodyText
};
// send mail
// oauth authentication
var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(settingRepository.GetSettingValue(settings, "SMTPClientId", ""))
.WithAuthority(settingRepository.GetSettingValue(settings, "SMTPAuthority", ""))
.WithClientSecret(settingRepository.GetSettingValue(settings, "SMTPClientSecret", ""))
.Build();
try
{
await client.SendAsync(mailMessage);
sent++;
notification.IsDelivered = true;
notification.DeliveredOn = DateTime.UtcNow;
notificationRepository.UpdateNotification(notification);
var result = await confidentialClientApplication.AcquireTokenForClient(settingRepository.GetSettingValue(settings, "SMTPScopes", "").Split(',')).ExecuteAsync();
var oauth2 = new SaslMechanismOAuth2(settingRepository.GetSettingValue(settings, "SMTPSender", ""), result.AccessToken);
await client.AuthenticateAsync(oauth2);
}
catch (Exception ex)
{
log += $"Error Sending Notification Id: {notification.NotificationId} - {ex.Message}<br />";
log += "SMTP Not Configured Properly In Site Settings - OAuth Token Could Not Be Retrieved From Authority - " + ex.Message + "<br />";
valid = false;
}
}
else
{
// invalid mailbox address
log += $"Notification Id: {notification.NotificationId} Has An {mailboxAddressValidationError} And Has Been Deleted<br />";
notification.IsDeleted = true;
notificationRepository.UpdateNotification(notification);
}
}
log += "Notifications Delivered: " + sent + "<br />";
}
if (valid)
{
// iterate through undelivered notifications
int sent = 0;
foreach (Notification notification in notifications)
{
var fromEmail = notification.FromEmail ?? "";
var fromName = notification.FromDisplayName ?? "";
var toEmail = notification.ToEmail ?? "";
var toName = notification.ToDisplayName ?? "";
await client.DisconnectAsync(true);
// get sender and receiver information from user information if available
if ((string.IsNullOrEmpty(fromEmail) || string.IsNullOrEmpty(fromName)) && notification.FromUserId != null)
{
var user = userRepository.GetUser(notification.FromUserId.Value);
if (user != null)
{
fromEmail = string.IsNullOrEmpty(fromEmail) ? user.Email ?? "" : fromEmail;
fromName = string.IsNullOrEmpty(fromName) ? user.DisplayName ?? "" : fromName;
}
}
if ((string.IsNullOrEmpty(toEmail) || string.IsNullOrEmpty(toName)) && notification.ToUserId != null)
{
var user = userRepository.GetUser(notification.ToUserId.Value);
if (user != null)
{
toEmail = string.IsNullOrEmpty(toEmail) ? user.Email ?? "" : toEmail;
toName = string.IsNullOrEmpty(toName) ? user.DisplayName ?? "" : toName;
}
}
// create mailbox addresses
MailboxAddress to = null;
MailboxAddress from = null;
var mailboxAddressValidationError = "";
// sender
if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") != "True")
{
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", "");
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
}
if (MailboxAddress.TryParse(fromEmail, out from))
{
from.Name = fromName;
}
else
{
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;";
}
// recipient
if (MailboxAddress.TryParse(toEmail, out to))
{
to.Name = toName;
}
else
{
mailboxAddressValidationError += $" Invalid Recipient: {toName} &lt;{toEmail}&gt;";
}
// if mailbox addresses are valid
if (from != null && to != null)
{
// create mail message
MimeMessage mailMessage = new MimeMessage();
mailMessage.From.Add(from);
mailMessage.To.Add(to);
// subject
mailMessage.Subject = notification.Subject;
// body
var bodyText = notification.Body;
if (!bodyText.Contains('<') || !bodyText.Contains('>'))
{
// plain text messages should convert line breaks to HTML tags to preserve formatting
bodyText = bodyText.Replace("\n", "<br />");
}
mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8)
{
Text = bodyText
};
// send mail
try
{
await client.SendAsync(mailMessage);
sent++;
notification.IsDelivered = true;
notification.DeliveredOn = DateTime.UtcNow;
notificationRepository.UpdateNotification(notification);
}
catch (Exception ex)
{
log += $"Error Sending Notification Id: {notification.NotificationId} - {ex.Message}<br />";
}
}
else
{
// invalid mailbox address
log += $"Notification Id: {notification.NotificationId} Has An {mailboxAddressValidationError} And Has Been Deleted<br />";
notification.IsDeleted = true;
notificationRepository.UpdateNotification(notification);
}
}
log += "Notifications Delivered: " + sent + "<br />";
}
await client.DisconnectAsync(true);
}
}
else
{
log += "Site Deleted Or SMTP Disabled In Site Settings<br />";
}
}
else
{
log += "Site Deleted Or SMTP Disabled In Site Settings" + "<br />";
log += "No Notifications To Deliver<br />";
}
}

View File

@@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Repository;
using Oqtane.Shared;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace Oqtane.Infrastructure
{

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Memory;
@@ -35,6 +36,10 @@ namespace Oqtane.Managers
Task<UserValidateResult> ValidateUser(string username, string email, string password);
Task<bool> ValidatePassword(string password);
Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify);
Task<List<Passkey>> GetPasskeys(int userId);
Task AddPasskey(Passkey passkey);
Task UpdatePasskey(Passkey passkey);
Task DeletePasskey(int userId, byte[] credentialId);
}
public class UserManager : IUserManager
@@ -818,5 +823,72 @@ namespace Oqtane.Managers
return result;
}
public async Task<List<Passkey>> GetPasskeys(int userId)
{
var passkeys = new List<Passkey>();
var user = _users.GetUser(userId);
if (user != null)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var userpasskeys = await _identityUserManager.GetPasskeysAsync(identityuser);
foreach (var userpasskey in userpasskeys)
{
passkeys.Add(new Passkey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name, UserId = userId });
}
}
}
return passkeys;
}
public async Task AddPasskey(Passkey passkey)
{
var user = _users.GetUser(passkey.UserId);
if (user != null)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(passkey.CredentialJson);
if (attestationResult.Succeeded)
{
var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey);
}
}
}
}
public async Task UpdatePasskey(Passkey passkey)
{
var user = _users.GetUser(passkey.UserId);
if (user != null)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var userPasskeyInfo = await _identityUserManager.GetPasskeyAsync(identityuser, passkey.CredentialId);
if (userPasskeyInfo != null)
{
userPasskeyInfo.Name = passkey.Name;
await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, userPasskeyInfo);
}
}
}
}
public async Task DeletePasskey(int userId, byte[] credentialId)
{
var user = _users.GetUser(userId);
if (user != null)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
await _identityUserManager.RemovePasskeyAsync(identityuser, credentialId);
}
}
}
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Migrations.EntityBuilders
{
public class AspNetUserPasskeysEntityBuilder : BaseEntityBuilder<AspNetUserPasskeysEntityBuilder>
{
private const string _entityTableName = "AspNetUserPasskeys";
private readonly PrimaryKey<AspNetUserPasskeysEntityBuilder> _primaryKey = new("PK_AspNetUserPasskeys", x => x.CredentialId);
private readonly ForeignKey<AspNetUserPasskeysEntityBuilder> _aspNetUsersForeignKey = new("FK_AspNetUserPasskeys_AspNetUsers_UserId", x => x.UserId, "AspNetUsers", "Id", ReferentialAction.Cascade);
public AspNetUserPasskeysEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_aspNetUsersForeignKey);
}
protected override AspNetUserPasskeysEntityBuilder BuildTable(ColumnsBuilder table)
{
CredentialId = AddBinaryColumn(table, "CredentialId", 1024);
UserId = AddStringColumn(table, "UserId", 450);
Data = AddMaxStringColumn(table, "Data");
return this;
}
public OperationBuilder<AddColumnOperation> CredentialId { get; set; }
public OperationBuilder<AddColumnOperation> UserId { get; set; }
public OperationBuilder<AddColumnOperation> Data { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Oqtane.Migrations.EntityBuilders
{
public class AspNetUserTokensEntityBuilder : BaseEntityBuilder<AspNetUserTokensEntityBuilder>
{
private const string _entityTableName = "AspNetUserTokens";
private readonly PrimaryKey<AspNetUserTokensEntityBuilder> _primaryKey = new("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
private readonly ForeignKey<AspNetUserTokensEntityBuilder> _aspNetUsersForeignKey = new("FK_AspNetUserTokens_AspNetUsers_UserId", x => x.UserId, "AspNetUsers", "Id", ReferentialAction.Cascade);
public AspNetUserTokensEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_aspNetUsersForeignKey);
}
protected override AspNetUserTokensEntityBuilder BuildTable(ColumnsBuilder table)
{
UserId = AddStringColumn(table, "UserId", 450);
LoginProvider = AddStringColumn(table, "LoginProvider", 128);
Name = AddStringColumn(table, "Name", 128);
Value = AddMaxStringColumn(table, "Value", true);
return this;
}
public OperationBuilder<AddColumnOperation> UserId { get; set; }
public OperationBuilder<AddColumnOperation> LoginProvider { get; set; }
public OperationBuilder<AddColumnOperation> Name { get; set; }
public OperationBuilder<AddColumnOperation> Value { get; set; }
}
}

View File

@@ -299,6 +299,27 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<Guid>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
// binary
public void AddBinaryColumn(string name, int length, bool nullable = false, bool unicode = true)
{
_migrationBuilder.AddColumn<byte[]>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, schema: Schema);
}
public void AddBinaryColumn(string name, int length, bool nullable, bool unicode, string defaultValue)
{
_migrationBuilder.AddColumn<byte[]>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue, schema: Schema);
}
protected OperationBuilder<AddColumnOperation> AddBinaryColumn(ColumnsBuilder table, string name, int length, bool nullable = false, bool unicode = true)
{
return table.Column<byte[]>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode);
}
protected OperationBuilder<AddColumnOperation> AddBinaryColumn(ColumnsBuilder table, string name, int length, bool nullable, bool unicode, string defaultValue)
{
return table.Column<byte[]>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue);
}
// alter string
public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true, string index = "")
{

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.10.00.00.01")]
public class AddAspNetUserPasskeys : MultiDatabaseMigration
{
public AddAspNetUserPasskeys(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var aspNetUserPasskeysEntityBuilder = new AspNetUserPasskeysEntityBuilder(migrationBuilder, ActiveDatabase);
aspNetUserPasskeysEntityBuilder.Create();
aspNetUserPasskeysEntityBuilder.AddIndex("IX_AspNetUserPasskeys_UserId", "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.10.00.00.02")]
public class AddAspNetUserTokens : MultiDatabaseMigration
{
public AddAspNetUserTokens(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var aspNetUserTokensEntityBuilder = new AspNetUserTokensEntityBuilder(migrationBuilder, ActiveDatabase);
aspNetUserTokensEntityBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -9,6 +9,7 @@
<CompressionEnabled>false</CompressionEnabled>
<StaticWebAssetsFingerprintContent>false</StaticWebAssetsFingerprintContent>
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<Compile Remove="wwwroot\Modules\Templates\**" />
@@ -26,16 +27,15 @@
</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.AspNetCore.Components.WebAssembly.Server" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0-rc.2.25502.107">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="10.0.0-rc.2.25502.107" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.5" />
@@ -48,14 +48,14 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<!-- PostgreSQL Database Provider Dependencies -->
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<!-- SQLite Database Provider Dependencies -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.2" />
<!-- SQL Server Database Provider Dependencies -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<!-- Suppress EF Core internal warnings for Database Providers -->

View File

@@ -1,25 +1,46 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using Microsoft.Extensions.Logging;
using Oqtane.Documentation;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Shared;
namespace Oqtane.Server
{
[PrivateApi("Mark Entry-Program as private, since it's not very useful in the public docs")]
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
var databaseManager = host.Services.GetService<IDatabaseManager>();
var builder = WebApplication.CreateBuilder(args);
AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(builder.Environment.ContentRootPath, "Data"));
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(builder.Environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables();
var configuration = configurationBuilder.Build();
builder.Services.AddOqtane(configuration, builder.Environment);
var app = builder.Build();
var corsService = app.Services.GetRequiredService<ICorsService>();
var corsPolicyProvider = app.Services.GetRequiredService<ICorsPolicyProvider>();
var syncManager = app.Services.GetRequiredService<ISyncManager>();
app.UseOqtane(configuration, builder.Environment, corsService, corsPolicyProvider, syncManager);
var databaseManager = app.Services.GetService<IDatabaseManager>();
var install = databaseManager.Install();
if (!string.IsNullOrEmpty(install.Message))
{
var filelogger = host.Services.GetRequiredService<ILogger<Program>>();
var filelogger = app.Services.GetRequiredService<ILogger<Program>>();
if (filelogger != null)
{
filelogger.LogError($"[Oqtane.Server.Program.Main] {install.Message}");
@@ -27,18 +48,8 @@ namespace Oqtane.Server
}
else
{
host.Run();
app.Run();
}
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseConfiguration(new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables()
.Build())
.UseStartup<Startup>()
.ConfigureLocalizationSettings()
.Build();
}
}

View File

@@ -4,7 +4,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
@@ -19,7 +18,7 @@ using Oqtane.Shared;
namespace Oqtane.Repository
{
public class DBContextBase : IdentityUserContext<IdentityUser>
public class DBContextBase : DbContext
{
private readonly ITenantManager _tenantManager;
private readonly IHttpContextAccessor _accessor;
@@ -75,8 +74,6 @@ namespace Oqtane.Repository
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
ActiveDatabase.UpdateIdentityStoreTableNames(builder);
}
public override int SaveChanges()

View File

@@ -4,6 +4,13 @@ using Oqtane.Infrastructure;
namespace Oqtane.Repository
{
public interface IDBContextDependencies
{
ITenantManager TenantManager { get; }
IHttpContextAccessor Accessor { get; }
IConfigurationRoot Config { get; }
}
public class DBContextDependencies : IDBContextDependencies
{
public DBContextDependencies(ITenantManager tenantManager, IHttpContextAccessor httpContextAccessor, IConfigurationRoot config)

View File

@@ -1,13 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Oqtane.Infrastructure;
namespace Oqtane.Repository
{
public interface IDBContextDependencies
{
ITenantManager TenantManager { get; }
IHttpContextAccessor Accessor { get; }
IConfigurationRoot Config { get; }
}
}

View File

@@ -1,6 +1,20 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Databases.Interfaces;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Migrations.Framework;
using Oqtane.Models;
using Oqtane.Repository.Databases.Interfaces;
using Oqtane.Shared;
// ReSharper disable CheckNamespace
// ReSharper disable MemberCanBePrivate.Global
@@ -8,9 +22,87 @@ using Oqtane.Repository.Databases.Interfaces;
namespace Oqtane.Repository
{
public class TenantDBContext : DBContextBase, IMultiDatabase
public class TenantDBContext : IdentityUserContext<IdentityUser>, IMultiDatabase
{
public TenantDBContext(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies) { }
private readonly ITenantManager _tenantManager;
private readonly IHttpContextAccessor _accessor;
private readonly IConfigurationRoot _config;
private string _connectionString = "";
private string _databaseType = "";
public TenantDBContext(DbContextOptions<TenantDBContext> options, IDBContextDependencies DBContextDependencies) : base(options)
{
_tenantManager = DBContextDependencies.TenantManager;
_accessor = DBContextDependencies.Accessor;
_config = DBContextDependencies.Config;
}
public IDatabase ActiveDatabase { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();
// specify the SchemaVersion for .NET Identity as it is not being persisted when using AddIdentityCore()
var services = new ServiceCollection();
services.AddIdentityCore<IdentityUser>(options =>
{
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
});
optionsBuilder.UseApplicationServiceProvider(services.BuildServiceProvider());
if (string.IsNullOrEmpty(_connectionString))
{
Tenant tenant = _tenantManager.GetTenant();
if (tenant != null)
{
_connectionString = _config.GetConnectionString(tenant.DBConnectionString);
if (_connectionString != null)
{
_connectionString = _connectionString.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString());
_databaseType = tenant.DBType;
}
else
{
// tenant connection string does not exist in appsettings.json
}
}
}
if (!string.IsNullOrEmpty(_databaseType))
{
var type = Type.GetType(_databaseType);
ActiveDatabase = Activator.CreateInstance(type) as IDatabase;
}
if (!string.IsNullOrEmpty(_connectionString) && ActiveDatabase != null)
{
optionsBuilder.UseOqtaneDatabase(ActiveDatabase, _connectionString);
}
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
ActiveDatabase.UpdateIdentityStoreTableNames(builder);
}
public override int SaveChanges()
{
DbContextUtils.SaveChanges(this, _accessor);
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
DbContextUtils.SaveChanges(this, _accessor);
return base.SaveChangesAsync(cancellationToken);
}
public virtual DbSet<Site> Site { get; set; }
public virtual DbSet<Page> Page { get; set; }

View File

@@ -110,9 +110,12 @@ namespace Oqtane.Repository
ModuleDefinition moduledefinition = moduledefinitions.FirstOrDefault(item => item.ModuleDefinitionName == module.ModuleDefinitionName);
if (moduledefinition != null)
{
var settings = _settings.GetSettings(EntityNames.Module, moduleId);
ModuleContent modulecontent = new ModuleContent();
modulecontent.ModuleDefinitionName = moduledefinition.ModuleDefinitionName;
modulecontent.Version = moduledefinition.Version;
modulecontent.Settings = settings.Where(item => !item.IsPrivate).ToDictionary(x => x.SettingName, x => x.SettingValue);
modulecontent.Content = "";
if (moduledefinition.ServerManagerType != "")
@@ -122,7 +125,7 @@ namespace Oqtane.Repository
{
try
{
module.Settings = _settings.GetSettings(EntityNames.Module, moduleId).ToDictionary(x => x.SettingName, x => x.SettingValue);
module.Settings = settings.ToDictionary(x => x.SettingName, x => x.SettingValue);
var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype);
modulecontent.Content = ((IPortable)moduleobject).ExportModule(module);
}
@@ -160,6 +163,29 @@ namespace Oqtane.Repository
ModuleContent modulecontent = JsonSerializer.Deserialize<ModuleContent>(content.Replace("\n", ""));
if (modulecontent.ModuleDefinitionName == moduledefinition.ModuleDefinitionName)
{
var settings = _settings.GetSettings(EntityNames.Module, moduleId);
if (modulecontent.Settings != null)
{
foreach (var kvp in modulecontent.Settings)
{
var setting = settings.FirstOrDefault(item => item.SettingName == kvp.Key);
if (setting == null)
{
setting = new Setting { EntityName = EntityNames.Module, EntityId = moduleId, SettingName = kvp.Key, SettingValue = kvp.Value, IsPrivate = false };
_settings.AddSetting(setting);
}
else
{
if (setting.SettingValue != kvp.Value)
{
setting.SettingValue = kvp.Value;
_settings.UpdateSetting(setting);
}
}
}
}
if (moduledefinition.ServerManagerType != "")
{
Type moduletype = Type.GetType(moduledefinition.ServerManagerType);

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Repository
{
@@ -19,10 +20,12 @@ namespace Oqtane.Repository
public class RoleRepository : IRoleRepository
{
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
private readonly ISettingRepository _settings;
public RoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory)
public RoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, ISettingRepository settings)
{
_dbContextFactory = dbContextFactory;
_settings = settings;
}
public IEnumerable<Role> GetRoles(int siteId)
@@ -95,6 +98,9 @@ namespace Oqtane.Repository
db.Permission.Remove(permission);
}
//remove settings for role
_settings.DeleteSettings(EntityNames.Role, roleId);
Role role = db.Role.Find(roleId);
db.Role.Remove(role);
db.SaveChanges();

View File

@@ -251,21 +251,21 @@ namespace Oqtane.Repository
_roleRepository.AddRole(new Role {SiteId = site.SiteId, Name = RoleNames.Admin, Description = RoleNames.Admin, IsAutoAssigned = false, IsSystem = true});
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "FirstName", Title = "First Name", Description = "Your First Or Given Name", Category = "Name", ViewOrder = 1, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "FirstName", Title = "First Name", Description = "Your First Or Given Name", Category = "Name", ViewOrder = 10, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "LastName", Title = "Last Name", Description = "Your Last Or Family Name", Category = "Name", ViewOrder = 2, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "LastName", Title = "Last Name", Description = "Your Last Or Family Name", Category = "Name", ViewOrder = 20, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "Street", Title = "Street", Description = "Street Or Building Address", Category = "Address", ViewOrder = 3, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "Street", Title = "Street", Description = "Street Or Building Address", Category = "Address", ViewOrder = 30, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "City", Title = "City", Description = "City", Category = "Address", ViewOrder = 4, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "City", Title = "City", Description = "City", Category = "Address", ViewOrder = 40, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "Region", Title = "Region", Description = "State Or Province", Category = "Address", ViewOrder = 5, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "Region", Title = "Region", Description = "State Or Province", Category = "Address", ViewOrder = 50, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "Country", Title = "Country", Description = "Country", Category = "Address", ViewOrder = 6, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "Country", Title = "Country", Description = "Country", Category = "Address", ViewOrder = 60, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "PostalCode", Title = "Postal Code", Description = "Postal Code Or Zip Code", Category = "Address", ViewOrder = 7, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "PostalCode", Title = "Postal Code", Description = "Postal Code Or Zip Code", Category = "Address", ViewOrder = 70, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
_profileRepository.AddProfile(new Profile
{ SiteId = site.SiteId, Name = "Phone", Title = "Phone Number", Description = "Phone Number", Category = "Contact", ViewOrder = 8, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
{ SiteId = site.SiteId, Name = "Phone", Title = "Phone Number", Description = "Phone Number", Category = "Contact", ViewOrder = 80, MaxLength = 50, DefaultValue = "", IsRequired = false, IsPrivate = false, Options = "", Rows = 1 });
Folder folder = _folderRepository.AddFolder(new Folder
{

View File

@@ -1,43 +0,0 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Shared;
using Microsoft.AspNetCore.Cors.Infrastructure;
namespace Oqtane
{
public class Startup
{
private readonly IConfigurationRoot _configuration;
private readonly IWebHostEnvironment _environment;
public Startup(IWebHostEnvironment environment)
{
AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data"));
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true)
.AddEnvironmentVariables();
_configuration = builder.Build();
_environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddOqtane(_configuration, _environment);
}
public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync)
{
app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync);
}
}
}

View File

@@ -35,8 +35,8 @@
{
try
{
Dictionary<string, string> settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "SettingName", _value);
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "SettingName", _value);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<Authors>[Owner]</Authors>
<Company>[Owner]</Company>
@@ -13,11 +13,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<AccelerateBuildsInVisualStudio>false</AccelerateBuildsInVisualStudio>
</PropertyGroup>

View File

@@ -27,11 +27,11 @@
<file src="..\Server\bin\Release\$targetframework$\$ProjectName$.Server.Oqtane.pdb" target="lib\$targetframework$" />
<file src="..\Shared\bin\Release\$targetframework$\$ProjectName$.Shared.Oqtane.dll" target="lib\$targetframework$" />
<file src="..\Shared\bin\Release\$targetframework$\$ProjectName$.Shared.Oqtane.pdb" target="lib\$targetframework$" />
<file src="..\Server\obj\Release\net9.0\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssetEndpoints.props" target="build\Microsoft.AspNetCore.StaticWebAssetEndpoints.props" />
<file src="..\Server\obj\Release\net9.0\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssets.props" target="build\Microsoft.AspNetCore.StaticWebAssets.props" />
<file src="..\Server\obj\Release\net9.0\staticwebassets\msbuild.build.$ProjectName$.props" target="build\$ProjectName$.props" />
<file src="..\Server\obj\Release\net9.0\staticwebassets\msbuild.buildMultiTargeting.$ProjectName$.props" target="buildMultiTargeting\$ProjectName$.props" />
<file src="..\Server\obj\Release\net9.0\staticwebassets\msbuild.buildTransitive.$ProjectName$.props" target="buildTransitive\$ProjectName$.props" />
<file src="..\Server\obj\Release\$targetframework$\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssetEndpoints.props" target="build\Microsoft.AspNetCore.StaticWebAssetEndpoints.props" />
<file src="..\Server\obj\Release\$targetframework$\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssets.props" target="build\Microsoft.AspNetCore.StaticWebAssets.props" />
<file src="..\Server\obj\Release\$targetframework$\staticwebassets\msbuild.build.$ProjectName$.props" target="build\$ProjectName$.props" />
<file src="..\Server\obj\Release\$targetframework$\staticwebassets\msbuild.buildMultiTargeting.$ProjectName$.props" target="buildMultiTargeting\$ProjectName$.props" />
<file src="..\Server\obj\Release\$targetframework$\staticwebassets\msbuild.buildTransitive.$ProjectName$.props" target="buildTransitive\$ProjectName$.props" />
<file src="..\Server\wwwroot\**\*.*" target="staticwebassets" />
<file src="icon.png" target="" />
</files>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Version>1.0.0</Version>
<Product>[Owner].Module.[Module]</Product>
@@ -20,10 +20,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,3 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Oqtane.Models;
@@ -6,16 +5,11 @@ using Oqtane.Models;
namespace [Owner].Module.[Module].Models
{
[Table("[Owner][Module]")]
public class [Module] : IAuditable
public class [Module] : ModelBase
{
[Key]
public int [Module]Id { get; set; }
public int ModuleId { get; set; }
public string Name { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedOn { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<Product>[Owner].Module.[Module]</Product>
<Authors>[Owner]</Authors>

View File

@@ -1,6 +1,6 @@
{
"Title": "Default Module Template",
"Type": "External",
"Version": "5.2.0",
"Version": "10.0.0",
"Namespace": "[Owner].Module.[Module]"
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<Authors>[Owner]</Authors>
<Company>[Owner]</Company>
@@ -14,9 +14,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<AccelerateBuildsInVisualStudio>false</AccelerateBuildsInVisualStudio>
</PropertyGroup>

View File

@@ -23,11 +23,11 @@
<files>
<file src="..\Client\bin\Release\$targetframework$\$projectname$.Client.Oqtane.dll" target="lib\$targetframework$" />
<file src="..\Client\bin\Release\$targetframework$\$projectname$.Client.Oqtane.pdb" target="lib\$targetframework$" />
<file src="..\Client\obj\Release\net9.0\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssetEndpoints.props" target="build\Microsoft.AspNetCore.StaticWebAssetEndpoints.props" />
<file src="..\Client\obj\Release\net9.0\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssets.props" target="build\Microsoft.AspNetCore.StaticWebAssets.props" />
<file src="..\Client\obj\Release\net9.0\staticwebassets\msbuild.build.$ProjectName$.props" target="build\$ProjectName$.props" />
<file src="..\Client\obj\Release\net9.0\staticwebassets\msbuild.buildMultiTargeting.$ProjectName$.props" target="buildMultiTargeting\$ProjectName$.props" />
<file src="..\Client\obj\Release\net9.0\staticwebassets\msbuild.buildTransitive.$ProjectName$.props" target="buildTransitive\$ProjectName$.props" />
<file src="..\Client\obj\Release\$targetframework$\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssetEndpoints.props" target="build\Microsoft.AspNetCore.StaticWebAssetEndpoints.props" />
<file src="..\Client\obj\Release\$targetframework$\staticwebassets\msbuild.$ProjectName$.Microsoft.AspNetCore.StaticWebAssets.props" target="build\Microsoft.AspNetCore.StaticWebAssets.props" />
<file src="..\Client\obj\Release\$targetframework$\staticwebassets\msbuild.build.$ProjectName$.props" target="build\$ProjectName$.props" />
<file src="..\Client\obj\Release\$targetframework$\staticwebassets\msbuild.buildMultiTargeting.$ProjectName$.props" target="buildMultiTargeting\$ProjectName$.props" />
<file src="..\Client\obj\Release\$targetframework$\staticwebassets\msbuild.buildTransitive.$ProjectName$.props" target="buildTransitive\$ProjectName$.props" />
<file src="..\Client\wwwroot\**\*.*" target="staticwebassets" />
<file src="icon.png" target="" />
</files>

View File

@@ -1,6 +1,6 @@
{
"Title": "Default Theme Template",
"Type": "External",
"Version": "5.2.0",
"Version": "10.0.0",
"Namespace": "[Owner].Theme.[Theme]"
}

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Oqtane.Models
{
/// <summary>
@@ -15,6 +17,11 @@ namespace Oqtane.Models
/// </summary>
public string Version { get; set; }
/// <summary>
/// Serialized Settings of the module for import/export.
/// </summary>
public Dictionary<string, string> Settings { get; set; }
/// <summary>
/// Serialized Content of the module for import/export.
/// </summary>

View File

@@ -0,0 +1,28 @@
namespace Oqtane.Models
{
/// <summary>
/// Passkey properties
/// </summary>
public class Passkey
{
/// <summary>
/// the credential ID for this passkey
/// </summary>
public byte[] CredentialId { get; set; }
/// <summary>
/// The friendly name of the passkey
/// </summary>
public string Name { get; set; }
/// <summary>
/// The User which this passkey belongs to
/// </summary>
public int UserId { get; set; }
/// <summary>
/// A serialized JSON object from the navigator.credentials.create() JavaScript API - only populated during Add
/// </summary>
public string CredentialJson { get; set; }
}
}

View File

@@ -5,12 +5,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.9" />
</ItemGroup>
</Project>

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