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

This commit is contained in:
Leigh Pointer
2025-11-07 12:57:35 +01:00
76 changed files with 1151 additions and 2700 deletions

View File

@@ -18,11 +18,6 @@
},
"sourceName": "Oqtane.Application",
"preferNameDirectory": true,
"guids": [
"04B05448-788F-433D-92C0-FED35122D45A",
"AA8E58A1-CD09-4208-BF66-A8BB341FD669",
"18D73F73-D7BE-4388-85BA-FBD9AC96FCA2"
],
"symbols": {
"Framework": {
"type": "parameter",
@@ -80,7 +75,7 @@
},
"primaryOutputs": [
{
"path": "Oqtane.Application.sln"
"path": "Oqtane.Application.slnx"
}
]
}

View File

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

View File

@@ -1,112 +0,0 @@
@using Oqtane.Modules.Controls
@using Oqtane.Application.Services
@using Oqtane.Application.Models
@namespace Oqtane.Application.MyModule
@inherits ModuleBase
@inject IMyModuleService MyModuleService
@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 MyModule";
public override List<Resource> Resources => new List<Resource>()
{
new Stylesheet(ModulePath() + "Module.css")
};
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"]);
MyModule MyModule = await MyModuleService.GetMyModuleAsync(_id, ModuleState.ModuleId);
if (MyModule != null)
{
_name = MyModule.Name;
_createdby = MyModule.CreatedBy;
_createdon = MyModule.CreatedOn;
_modifiedby = MyModule.ModifiedBy;
_modifiedon = MyModule.ModifiedOn;
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading MyModule {MyModuleId} {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")
{
MyModule MyModule = new MyModule();
MyModule.ModuleId = ModuleState.ModuleId;
MyModule.Name = _name;
MyModule = await MyModuleService.AddMyModuleAsync(MyModule);
await logger.LogInformation("MyModule Added {MyModule}", MyModule);
}
else
{
MyModule MyModule = await MyModuleService.GetMyModuleAsync(_id, ModuleState.ModuleId);
MyModule.Name = _name;
await MyModuleService.UpdateMyModuleAsync(MyModule);
await logger.LogInformation("MyModule Updated {MyModule}", MyModule);
}
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.SaveValidation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving MyModule {Error}", ex.Message);
AddModuleMessage(Localizer["Message.SaveError"], MessageType.Error);
}
}
}

View File

@@ -1,77 +0,0 @@
@using Oqtane.Application.Services
@using Oqtane.Application.Models
@namespace Oqtane.Application.MyModule
@inherits ModuleBase
@inject IMyModuleService MyModuleService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer
@if (_MyModules == null)
{
<p><em>Loading...</em></p>
}
else
{
<ActionLink Action="Add" Security="SecurityAccessLevel.Edit" Text="Add MyModule" ResourceKey="Add" />
<br />
<br />
@if (@_MyModules.Count != 0)
{
<Pager Items="@_MyModules">
<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.MyModuleId.ToString())" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete MyModule" Message="Are You Sure You Wish To Delete This MyModule?" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" ResourceKey="Delete" Id="@context.MyModuleId.ToString()" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<p>@Localizer["Message.DisplayNone"]</p>
}
}
@code {
public override List<Resource> Resources => new List<Resource>()
{
new Stylesheet(ModulePath() + "Module.css"),
new Script(ModulePath() + "Module.js")
};
List<Models.MyModule> _MyModules;
protected override async Task OnInitializedAsync()
{
try
{
_MyModules = await MyModuleService.GetMyModulesAsync(ModuleState.ModuleId);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading MyModule {Error}", ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task Delete(MyModule MyModule)
{
try
{
await MyModuleService.DeleteMyModuleAsync(MyModule.MyModuleId, ModuleState.ModuleId);
await logger.LogInformation("MyModule Deleted {MyModule}", MyModule);
_MyModules = await MyModuleService.GetMyModulesAsync(ModuleState.ModuleId);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting MyModule {MyModule} {Error}", MyModule, ex.Message);
AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error);
}
}
}

View File

@@ -1,19 +0,0 @@
using Oqtane.Models;
using Oqtane.Modules;
namespace Oqtane.Application.MyModule
{
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "MyModule",
Description = "Example module",
Version = "1.0.0",
ServerManagerType = "Oqtane.Application.Manager.MyModuleManager, Oqtane.Application.Server.Oqtane",
ReleaseVersions = "1.0.0",
Dependencies = "Oqtane.Application.Shared.Oqtane",
PackageName = "Oqtane.Application"
};
}
}

View File

@@ -1,47 +0,0 @@
@namespace Oqtane.Application.MyModule
@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 = "Oqtane.Application.MyModule.Settings, Oqtane.Application.Client.Oqtane"; // for localization
public override string Title => "MyModdule 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
{
Dictionary<string, string> settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "SettingName", _value);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@@ -1,141 +0,0 @@
<?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="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.SaveValidation" xml:space="preserve">
<value>Please Provide All Required Information</value>
</data>
<data name="Message.SaveError" xml:space="preserve">
<value>Error Saving MyModule</value>
</data>
<data name="Message.LoadError" xml:space="preserve">
<value>Error Loading MyModule</value>
</data>
</root>

View File

@@ -1,147 +0,0 @@
<?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="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Add.Text" xml:space="preserve">
<value>Add MyModule</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 MyModule</value>
</data>
<data name="Delete.Message" xml:space="preserve">
<value>Are You Sure You Wish To Delete This MyModule?</value>
</data>
<data name="Message.DisplayNone" xml:space="preserve">
<value>No MyModules To Display</value>
</data>
<data name="Message.LoadError" xml:space="preserve">
<value>Error Loading MyModule</value>
</data>
<data name="Message.DeleteError" xml:space="preserve">
<value>Error Deleting MyModule</value>
</data>
</root>

View File

@@ -1,126 +0,0 @@
<?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

@@ -1,126 +0,0 @@
<?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

@@ -1,138 +0,0 @@
<?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

@@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Application.Services
{
public interface IMyModuleService
{
Task<List<Models.MyModule>> GetMyModulesAsync(int ModuleId);
Task<Models.MyModule> GetMyModuleAsync(int MyModuleId, int ModuleId);
Task<Models.MyModule> AddMyModuleAsync(Models.MyModule MyModule);
Task<Models.MyModule> UpdateMyModuleAsync(Models.MyModule MyModule);
Task DeleteMyModuleAsync(int MyModuleId, int ModuleId);
}
public class MyModuleService : ServiceBase, IMyModuleService
{
public MyModuleService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("MyModule");
public async Task<List<Models.MyModule>> GetMyModulesAsync(int ModuleId)
{
List<Models.MyModule> Tasks = await GetJsonAsync<List<Models.MyModule>>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty<Models.MyModule>().ToList());
return Tasks.OrderBy(item => item.Name).ToList();
}
public async Task<Models.MyModule> GetMyModuleAsync(int MyModuleId, int ModuleId)
{
return await GetJsonAsync<Models.MyModule>(CreateAuthorizationPolicyUrl($"{Apiurl}/{MyModuleId}/{ModuleId}", EntityNames.Module, ModuleId));
}
public async Task<Models.MyModule> AddMyModuleAsync(Models.MyModule MyModule)
{
return await PostJsonAsync<Models.MyModule>(CreateAuthorizationPolicyUrl($"{Apiurl}", EntityNames.Module, MyModule.ModuleId), MyModule);
}
public async Task<Models.MyModule> UpdateMyModuleAsync(Models.MyModule MyModule)
{
return await PutJsonAsync<Models.MyModule>(CreateAuthorizationPolicyUrl($"{Apiurl}/{MyModule.MyModuleId}", EntityNames.Module, MyModule.ModuleId), MyModule);
}
public async Task DeleteMyModuleAsync(int MyModuleId, int ModuleId)
{
await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{MyModuleId}/{ModuleId}", EntityNames.Module, ModuleId));
}
}
}

View File

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

View File

@@ -1,46 +0,0 @@
@namespace Oqtane.Application.MyTheme
@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 => "Container";
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

@@ -1,50 +0,0 @@
@namespace Oqtane.Application.MyTheme
@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 = "Oqtane.Application.MyTheme.ContainerSettings, Oqtane.Application.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

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

View File

@@ -1,118 +0,0 @@
@namespace Oqtane.Application.MyTheme
@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 => "MyTheme";
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

@@ -1,140 +0,0 @@
@namespace Oqtane.Application.MyTheme
@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 = "Oqtane.Application.MyTheme.ThemeSettings, Oqtane.Application.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

@@ -1,33 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35506.116 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Server", "Server\Oqtane.Application.Server.csproj", "{04B05448-788F-433D-92C0-FED35122D45A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Client", "Client\Oqtane.Application.Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Shared", "Shared\Oqtane.Application.Shared.csproj", "{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.Build.0 = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="Server\Oqtane.Application.Server.csproj" DefaultStartup="true" />
<Project Path="Client\Oqtane.Application.Client.csproj" />
<Project Path="Shared\Oqtane.Application.Shared.csproj" />
</Solution>

View File

@@ -1,114 +0,0 @@
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 Oqtane.Application.Services;
using Oqtane.Controllers;
using System.Net;
using System.Threading.Tasks;
namespace Oqtane.Application.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class MyModuleController : ModuleControllerBase
{
private readonly IMyModuleService _MyModuleService;
public MyModuleController(IMyModuleService MyModuleService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
{
_MyModuleService = MyModuleService;
}
// GET: api/<controller>?moduleid=x
[HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IEnumerable<Models.MyModule>> Get(string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _MyModuleService.GetMyModulesAsync(ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule 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.MyModule> Get(int id, int moduleid)
{
Models.MyModule MyModule = await _MyModuleService.GetMyModuleAsync(id, moduleid);
if (MyModule != null && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
return MyModule;
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {MyModuleId} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<Models.MyModule> Post([FromBody] Models.MyModule MyModule)
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
MyModule = await _MyModuleService.AddMyModuleAsync(MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Post Attempt {MyModule}", MyModule);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
MyModule = null;
}
return MyModule;
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task<Models.MyModule> Put(int id, [FromBody] Models.MyModule MyModule)
{
if (ModelState.IsValid && MyModule.MyModuleId == id && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
MyModule = await _MyModuleService.UpdateMyModuleAsync(MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Put Attempt {MyModule}", MyModule);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
MyModule = null;
}
return MyModule;
}
// DELETE api/<controller>/5
[HttpDelete("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task Delete(int id, int moduleid)
{
Models.MyModule MyModule = await _MyModuleService.GetMyModuleAsync(id, moduleid);
if (MyModule != null && IsAuthorizedEntityId(EntityNames.Module, MyModule.ModuleId))
{
await _MyModuleService.DeleteMyModuleAsync(id, MyModule.ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized v Delete Attempt {MyModuleId} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -1,87 +0,0 @@
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 Oqtane.Application.Repository;
using System.Threading.Tasks;
namespace Oqtane.Application.Manager
{
public class MyModuleManager : MigratableModuleBase, IInstallable, IPortable, ISearchable
{
private readonly IMyModuleRepository _MyModuleRepository;
private readonly IDBContextDependencies _DBContextDependencies;
public MyModuleManager(IMyModuleRepository MyModuleRepository, IDBContextDependencies DBContextDependencies)
{
_MyModuleRepository = MyModuleRepository;
_DBContextDependencies = DBContextDependencies;
}
public bool Install(Tenant tenant, string version)
{
return Migrate(new Context(_DBContextDependencies), tenant, MigrationType.Up);
}
public bool Uninstall(Tenant tenant)
{
return Migrate(new Context(_DBContextDependencies), tenant, MigrationType.Down);
}
public string ExportModule(Module module)
{
string content = "";
List<Models.MyModule> MyModules = _MyModuleRepository.GetMyModules(module.ModuleId).ToList();
if (MyModules != null)
{
content = JsonSerializer.Serialize(MyModules);
}
return content;
}
public void ImportModule(Module module, string content, string version)
{
List<Models.MyModule> MyModules = null;
if (!string.IsNullOrEmpty(content))
{
MyModules = JsonSerializer.Deserialize<List<Models.MyModule>>(content);
}
if (MyModules != null)
{
foreach(var Task in MyModules)
{
_MyModuleRepository.AddMyModule(new Models.MyModule { ModuleId = module.ModuleId, Name = Task.Name });
}
}
}
public Task<List<SearchContent>> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn)
{
var searchContentList = new List<SearchContent>();
foreach (var MyModule in _MyModuleRepository.GetMyModules(pageModule.ModuleId))
{
if (MyModule.ModifiedOn >= lastIndexedOn)
{
searchContentList.Add(new SearchContent
{
EntityName = "MyModule",
EntityId = MyModule.MyModuleId.ToString(),
Title = MyModule.Name,
Body = MyModule.Name,
ContentModifiedBy = MyModule.ModifiedBy,
ContentModifiedOn = MyModule.ModifiedOn
});
}
}
return Task.FromResult(searchContentList);
}
}
}

View File

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

View File

@@ -1,36 +0,0 @@
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 Oqtane.Application.Migrations.EntityBuilders
{
public class MyModuleEntityBuilder : AuditableBaseEntityBuilder<MyModuleEntityBuilder>
{
private const string _entityTableName = "MyModule";
private readonly PrimaryKey<MyModuleEntityBuilder> _primaryKey = new("PK_MyModule", x => x.MyModuleId);
private readonly ForeignKey<MyModuleEntityBuilder> _moduleForeignKey = new("FK_MyModule_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade);
public MyModuleEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_moduleForeignKey);
}
protected override MyModuleEntityBuilder BuildTable(ColumnsBuilder table)
{
MyModuleId = AddAutoIncrementColumn(table, "MyModuleId");
ModuleId = AddIntegerColumn(table,"ModuleId");
Name = AddMaxStringColumn(table,"Name");
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> MyModuleId { get; set; }
public OperationBuilder<AddColumnOperation> ModuleId { get; set; }
public OperationBuilder<AddColumnOperation> Name { get; set; }
}
}

View File

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

View File

@@ -1,75 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Modules;
namespace Oqtane.Application.Repository
{
public interface IMyModuleRepository
{
IEnumerable<Models.MyModule> GetMyModules(int ModuleId);
Models.MyModule GetMyModule(int MyModuleId);
Models.MyModule GetMyModule(int MyModuleId, bool tracking);
Models.MyModule AddMyModule(Models.MyModule MyModule);
Models.MyModule UpdateMyModule(Models.MyModule MyModule);
void DeleteMyModule(int MyModuleId);
}
public class MyModuleRepository : IMyModuleRepository, ITransientService
{
private readonly IDbContextFactory<Context> _factory;
public MyModuleRepository(IDbContextFactory<Context> factory)
{
_factory = factory;
}
public IEnumerable<Models.MyModule> GetMyModules(int ModuleId)
{
using var db = _factory.CreateDbContext();
return db.MyModule.Where(item => item.ModuleId == ModuleId).ToList();
}
public Models.MyModule GetMyModule(int MyModuleId)
{
return GetMyModule(MyModuleId, true);
}
public Models.MyModule GetMyModule(int MyModuleId, bool tracking)
{
using var db = _factory.CreateDbContext();
if (tracking)
{
return db.MyModule.Find(MyModuleId);
}
else
{
return db.MyModule.AsNoTracking().FirstOrDefault(item => item.MyModuleId == MyModuleId);
}
}
public Models.MyModule AddMyModule(Models.MyModule MyModule)
{
using var db = _factory.CreateDbContext();
db.MyModule.Add(MyModule);
db.SaveChanges();
return MyModule;
}
public Models.MyModule UpdateMyModule(Models.MyModule MyModule)
{
using var db = _factory.CreateDbContext();
db.Entry(MyModule).State = EntityState.Modified;
db.SaveChanges();
return MyModule;
}
public void DeleteMyModule(int MyModuleId)
{
using var db = _factory.CreateDbContext();
Models.MyModule MyModule = db.MyModule.Find(MyModuleId);
db.MyModule.Remove(MyModule);
db.SaveChanges();
}
}
}

View File

@@ -1,101 +0,0 @@
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 Oqtane.Application.Repository;
namespace Oqtane.Application.Services
{
public class ServerMyModuleService : IMyModuleService
{
private readonly IMyModuleRepository _MyModuleRepository;
private readonly IUserPermissions _userPermissions;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
private readonly Alias _alias;
public ServerMyModuleService(IMyModuleRepository MyModuleRepository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
{
_MyModuleRepository = MyModuleRepository;
_userPermissions = userPermissions;
_logger = logger;
_accessor = accessor;
_alias = tenantManager.GetAlias();
}
public Task<List<Models.MyModule>> GetMyModulesAsync(int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return Task.FromResult(_MyModuleRepository.GetMyModules(ModuleId).ToList());
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {ModuleId}", ModuleId);
return null;
}
}
public Task<Models.MyModule> GetMyModuleAsync(int MyModuleId, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
{
return Task.FromResult(_MyModuleRepository.GetMyModule(MyModuleId));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Get Attempt {TaskId} {ModuleId}", MyModuleId, ModuleId);
return null;
}
}
public Task<Models.MyModule> AddMyModuleAsync(Models.MyModule MyModule)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, MyModule.ModuleId, PermissionNames.Edit))
{
MyModule = _MyModuleRepository.AddMyModule(MyModule);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "MyModule Added {MyModule}", MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Add Attempt {MyModule}", MyModule);
MyModule = null;
}
return Task.FromResult(MyModule);
}
public Task<Models.MyModule> UpdateMyModuleAsync(Models.MyModule MyModule)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, MyModule.ModuleId, PermissionNames.Edit))
{
MyModule = _MyModuleRepository.UpdateMyModule(MyModule);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "MyModule Updated {MyModule}", MyModule);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Update Attempt {MyModule}", MyModule);
MyModule = null;
}
return Task.FromResult(MyModule);
}
public Task DeleteMyModuleAsync(int MyModuleId, int ModuleId)
{
if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
_MyModuleRepository.DeleteMyModule(MyModuleId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "MyModule Deleted {MyModuleId}", MyModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized MyModule Delete Attempt {MyModuleId} {ModuleId}", MyModuleId, ModuleId);
}
return Task.CompletedTask;
}
}
}

View File

@@ -1,28 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using Oqtane.Application.Repository;
using Oqtane.Application.Services;
namespace Oqtane.Application.Startup
{
public class 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<IMyModuleService, ServerMyModuleService>();
services.AddDbContextFactory<Context>(opt => { }, ServiceLifetime.Transient);
}
}
}

View File

@@ -1 +0,0 @@
/* Module Custom Styles */

View File

@@ -1,5 +0,0 @@
/* Module Script */
var App = App || {};
App.MyModule = {
};

View File

@@ -1,123 +0,0 @@
/* 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

@@ -1,19 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
using Oqtane.Models;
namespace Oqtane.Application.Models
{
public class MyModule : IAuditable
{
[Key]
public int MyModuleId { 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

@@ -20,39 +20,50 @@ else
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /><br />
<button type="button" class="btn btn-primary col-12" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<hr class="app-rule mt-3 mb-2" />
}
@if (_allowsitelogin)
{
<div class="form-group">
<div class="form-group text-center">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
</div>
<div class="form-group mt-2">
<div class="form-group text-center mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" @bind:event="oninput" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
<div class="form-group mt-2">
@if (!_alwaysremember)
{
<div class="form-check">
@if (!_alwaysremember)
{
<div class="form-group text-center mt-2">
<div>
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
}
<div class="btn-group mt-2 col-12" role="group">
<button type="button" class="btn btn-primary col-6" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary col-6" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (_allowpasskeys)
{
<br /><br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
<hr class="app-rule mt-3" />
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="Passkey">@Localizer["Passkey"]</button>
}
@if (PageState.Site.AllowRegistration)
{
<hr class="app-rule mt-3" />
<div class="text-center mt-2">
<NavLink href="@_registerurl">@Localizer["Register"]</NavLink>
</div>
}
}
</div>
@@ -77,6 +88,7 @@ else
@code {
private bool _allowsitelogin = true;
private bool _allowexternallogin = false;
private bool _allowpasskeys = false;
private ElementReference login;
private bool validated = false;
private bool twofactor = false;
@@ -88,6 +100,7 @@ else
private bool _remember = false;
private bool _alwaysremember = false;
private string _code = string.Empty;
private string _registerurl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override bool? Prerender => true;
@@ -103,8 +116,18 @@ else
{
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_allowpasskeys = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false"));
_alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
{
_registerurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "");
}
else
{
_registerurl = NavigateUrl("register");
}
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name"))
@@ -120,7 +143,7 @@ else
if (PageState.QueryString.ContainsKey("key"))
{
user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
user = await UserService.AddLoginAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
@@ -163,23 +186,6 @@ else
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && PageState.User == null && _allowsitelogin)
{
if (!string.IsNullOrEmpty(username.Id)) // ensure username is visible in UI
{
await username.FocusAsync();
}
}
// redirect logged in user to specified page
if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
}
private async Task Login()
{
try
@@ -331,4 +337,58 @@ else
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(PageState.ReturnUrl)), true);
}
private async Task Passkey()
{
// post back to the Passkey page so that the cookies are set correctly
var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = NavigateUrl() };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && PageState.QueryString.ContainsKey("options"))
{
// user has initiated a passkey login
try
{
var interop = new Interop(JSRuntime);
var credential = await interop.RequestCredential(WebUtility.UrlDecode(PageState.QueryString["options"]));
if (!string.IsNullOrEmpty(credential))
{
// post back to the Passkey page so that the cookies are set correctly
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path + "/";
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "login", credential = credential, returnurl = returnurl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
else
{
await logger.LogError("Passkey Login Was Not Successful");
AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Passkey Login Was Not Successful");
AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
}
return;
}
if (firstRender && PageState.User == null && _allowsitelogin)
{
if (!string.IsNullOrEmpty(username.Id)) // ensure username is visible in UI
{
await username.FocusAsync();
}
}
// redirect logged in user to specified page
if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
}
}

View File

@@ -26,7 +26,7 @@
<br />
}
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
<TabPanel Name="Identity" Heading="Identity" ResourceKey="Identity">
<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>
@@ -69,7 +69,7 @@
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
<TabPanel Name="Security" ResourceKey="Security">
<TabPanel Name="Security" Heading="Security" ResourceKey="Security">
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
@@ -110,40 +110,71 @@
</div>
</div>
</Section>
<br />
}
<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>
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
@if (_passkeys != null && _passkeys.Count > 0)
{
<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="DeletePasskey" Class="btn btn-danger" Header="Delete Passkey" Message="@string.Format(Localizer["Confirm.Passkey.Delete", context.Name])" /></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="passkeyname" class="form-control" @bind="@_passkeyName" /></td>
}
</Row>
</Pager>
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Login"]</th>
</Header>
<Row>
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeleteLogin(context))" ResourceKey="DeleteLogin" Class="btn btn-danger" Header="Delete Login" Message="@string.Format(Localizer["Confirm.Login.Delete", context.Name])" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />
}
<br />
<button type="button" class="btn btn-danger" @onclick="Logout">@Localizer["Logout Everywhere"]</button>
<br />
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
<TabPanel Name="Profile" Heading="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in _profiles)
@@ -272,11 +303,11 @@
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
<TabPanel Name="Notifications" ResourceKey="Notifications">
<TabPanel Name="Notifications" Heading="Notifications" ResourceKey="Notifications">
<ActionLink Action="Add" Text="Send Notification" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="SendNotification" ReturnUrl="@NavigateUrl(PageState.Page.Path, "tab=Notifications")" />
<br />
<br />
<select class="form-select" @onchange="(e => FilterChanged(e))">
<select class="form-select" @onchange="(e => FilterNotifications(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
@@ -295,7 +326,7 @@
</Header>
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" ReturnUrl="@NavigateUrl(PageState.Page.Path, "tab=Notifications")" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await DeleteNotification(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
@if (context.IsRead)
{
@@ -340,7 +371,7 @@
else
{
<div class="no-notifications-text">
@Localizer["NoNotificationsReceived.Text"]
@Localizer["NoNotificationsReceived"]
</div>
}
}
@@ -358,7 +389,7 @@
</Header>
<Row>
<td><ActionLink Action="View" Parameters="@($"id=" + context.NotificationId.ToString())" Security="SecurityAccessLevel.View" EditMode="false" ResourceKey="ViewNotification" ReturnUrl="@NavigateUrl(PageState.Page.Path, "tab=Notifications")" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await DeleteNotification(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
@if (context.IsRead)
{
@@ -404,7 +435,7 @@
else
{
<div class="no-notifications-text">
@Localizer["NoNotificationsSent.Text"]
@Localizer["NoNotificationsSent"]
</div>
}
}
@@ -415,15 +446,14 @@
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private bool _initialized = false;
private string _passwordrequirements;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private bool _allowtwofactor = false;
private string _twofactor = "False";
private bool _allowpasskeys = false;
private bool _allowexternallogin = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
private FileManager _filemanager;
@@ -434,9 +464,16 @@
private File _photo = null;
private string _imagefiles = string.Empty;
private List<Passkey> _passkeys;
private string _passwordrequirements;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _twofactor = "False";
private List<UserPasskey> _passkeys;
private byte[] _passkeyId;
private string _passkeyName = string.Empty;
private List<UserLogin> _logins;
private List<Profile> _profiles;
private Dictionary<string, string> _userSettings;
@@ -446,45 +483,29 @@
private List<Notification> _notifications;
private string _notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync()
{
try
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
_allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_timezones = TimeZoneService.GetTimeZones();
_allowpasskeys = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false") == "true");
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
if (PageState.User != null)
{
// identity section
_username = PageState.User.Username;
_twofactor = PageState.User.TwoFactorRequired.ToString();
_email = PageState.User.Email;
_displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
// get user folder
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
{
_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;
@@ -496,8 +517,27 @@
_photo = null;
}
// security section
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
_twofactor = PageState.User.TwoFactorRequired.ToString();
await GetPasskeys();
await GetLogins();
// profile section
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_userSettings = PageState.User.Settings;
// notification section
await LoadNotificationsAsync();
_initialized = true;
@@ -514,22 +554,7 @@
}
}
private async Task LoadNotificationsAsync()
{
_notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
_notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
}
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
// identity methods
private async Task Save()
{
try
@@ -604,6 +629,124 @@
}
}
private void Cancel()
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
// security methods
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetPasskeys()
{
if (_allowpasskeys)
{
_passkeys = await UserService.GetPasskeysAsync(PageState.User.UserId);
}
}
private async Task AddPasskey()
{
// post back to the Passkey page so that the cookies are set correctly
var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "create", returnurl = NavigateUrl(PageState.Page.Path, "tab=Security") };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// user has initiated a passkey addition
if (PageState.QueryString.ContainsKey("options"))
{
try
{
var interop = new Interop(JSRuntime);
var credential = await interop.CreateCredential(WebUtility.UrlDecode(PageState.QueryString["options"]));
if (!string.IsNullOrEmpty(credential))
{
// post back to the Passkey page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "validate", credential = credential, returnurl = NavigateUrl(PageState.Page.Path, "tab=Security") };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
else
{
await logger.LogError("Passkey Could Not Be Created");
AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Passkey Could Not Be Created");
AddModuleMessage(Localizer["Error.Passkey.Fail"], MessageType.Warning);
}
}
}
}
private void EditPasskey(UserPasskey passkey)
{
_passkeyId = passkey.CredentialId;
_passkeyName = passkey.Name;
StateHasChanged();
}
private async Task DeletePasskey(UserPasskey passkey)
{
await UserService.DeletePasskeyAsync(PageState.User.UserId, passkey.CredentialId);
await GetPasskeys();
StateHasChanged();
}
private async Task SavePasskey()
{
if (!string.IsNullOrEmpty(_passkeyName))
{
await UserService.UpdatePasskeyAsync(new UserPasskey { CredentialId = _passkeyId, Name = _passkeyName, UserId = PageState.User.UserId });
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
}
}
private async Task CancelPasskey()
{
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
}
private async Task GetLogins()
{
if (_allowexternallogin)
{
_logins = await UserService.GetLoginsAsync(PageState.User.UserId);
}
}
private async Task DeleteLogin(UserLogin login)
{
await UserService.DeleteLoginAsync(PageState.User.UserId, login.Provider, login.Key);
await GetLogins();
StateHasChanged();
}
private async Task Logout()
{
await logger.LogInformation("User Logout Everywhere For Username {Username}", PageState.User?.Username);
@@ -630,51 +773,24 @@
}
}
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();
}
// profile methods
private void EditPasskey(Passkey passkey)
private string GetProfileValue(string SettingName, string DefaultValue)
{
_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))
string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]"))
{
await UserService.UpdatePasskeyAsync(new Passkey { CredentialId = _passkeyId, Name = _passkeyName });
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task CancelPasskey()
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
await GetPasskeys();
_passkeyName = "";
StateHasChanged();
var value = (string)e.Value;
_userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
}
private bool ValidateProfiles()
{
foreach (Profile profile in _profiles)
@@ -706,18 +822,22 @@
return true;
}
private void Cancel()
// notification methods
private async Task LoadNotificationsAsync()
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
_notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
_notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
}
private void ProfileChanged(ChangeEventArgs e, string SettingName)
private async void FilterNotifications(ChangeEventArgs e)
{
var value = (string)e.Value;
_userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
_filter = (string)e.Value;
await LoadNotificationsAsync();
StateHasChanged();
}
private async Task Delete(Notification Notification)
private async Task DeleteNotification(Notification Notification)
{
try
{
@@ -742,13 +862,6 @@
}
}
private async void FilterChanged(ChangeEventArgs e)
{
_filter = (string)e.Value;
await LoadNotificationsAsync();
StateHasChanged();
}
private async Task DeleteAllNotifications()
{
try
@@ -780,18 +893,4 @@
HideProgressIndicator();
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@@ -14,8 +14,7 @@
@if (_initialized)
{
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<TabPanel Name="Identity" Heading="Identity" ResourceKey="Identity">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username">Username:</Label>
@@ -23,24 +22,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="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">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="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</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 class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
@@ -100,7 +81,77 @@
</div>
</div>
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
<TabPanel Name="Security" Heading="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="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">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="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</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 /><br />
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
@if (_passkeys != null && _passkeys.Count > 0)
{
<Pager Items="@_passkeys">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Passkey"]</th>
</Header>
<Row>
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeletePasskey(context))" ResourceKey="DeletePasskey" Class="btn btn-danger" Header="Delete Passkey" Message="@string.Format(Localizer["Confirm.Passkey.Delete", context.Name])" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Login"]</th>
</Header>
<Row>
<td><ActionDialog Action="Delete" OnClick="@(async () => await DeleteLogin(context))" ResourceKey="DeleteLogin" Class="btn btn-danger" Header="Delete Login" Message="@string.Format(Localizer["Confirm.Login.Delete", context.Name])" /></td>
<td>@context.Name</td>
</Row>
</Pager>
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />
}
</TabPanel>
<TabPanel Name="Profile" Heading="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in _profiles)
@@ -169,24 +220,30 @@
}
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private bool _allowpasskeys = false;
private bool _allowexternallogin = false;
private int _userid;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private string _isdeleted;
private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private string _passwordrequirements;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private List<UserPasskey> _passkeys;
private List<UserLogin> _logins;
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
private string _category = string.Empty;
@@ -204,19 +261,8 @@
{
try
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_timezones = TimeZoneService.GetTimeZones();
_allowpasskeys = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false") == "true");
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{
@@ -228,13 +274,30 @@
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
_isdeleted = user.IsDeleted.ToString();
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
_settings = user.Settings;
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
await GetPasskeys();
await GetLogins();
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
foreach (var profile in _profiles)
{
if (profile.Options.ToLower().StartsWith("entityname:"))
{
var options = await SettingService.GetSettingsAsync(profile.Options.Substring(11), -1);
options.Add("", $"<{SharedLocalizer["Not Specified"]}>");
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_settings = user.Settings;
_createdby = user.CreatedBy;
_createdon = user.CreatedOn;
_modifiedby = user.ModifiedBy;
@@ -354,6 +417,35 @@
}
}
private async Task GetPasskeys()
{
if (_allowpasskeys)
{
_passkeys = await UserService.GetPasskeysAsync(_userid);
}
}
private async Task DeletePasskey(UserPasskey passkey)
{
await UserService.DeletePasskeyAsync(_userid, passkey.CredentialId);
await GetPasskeys();
StateHasChanged();
}
private async Task GetLogins()
{
if (_allowexternallogin)
{
_logins = await UserService.GetLoginsAsync(_userid);
}
}
private async Task DeleteLogin(UserLogin login)
{
await UserService.DeleteLoginAsync(_userid, login.Provider, login.Key);
await GetLogins();
StateHasChanged();
}
private bool ValidateProfiles()
{
foreach (Profile profile in _profiles)

View File

@@ -72,32 +72,41 @@ else
</select>
</div>
</div>
@if (_allowregistration == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="registerurl" HelpText="Optionally provide a custom registration url" ResourceKey="RegisterUrl">Register Url:</Label>
<div class="col-sm-9">
<input id="registerurl" class="form-control" @bind="@_registerurl" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileurl" HelpText="Optionally provide a custom profile url" ResourceKey="ProfileUrl">Profile Url:</Label>
<div class="col-sm-9">
<input id="profileurl" class="form-control" @bind="@_profileurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireconfirmedemail" HelpText="Do you want to require registered users to verify their email address before they are allowed to log in?" ResourceKey="RequireConfirmedEmail">Require Verified Email?</Label>
<div class="col-sm-9">
<select id="requireconfirmedemail" class="form-select" @bind="@_requireconfirmedemail">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@if (_allowregistration == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="registerurl" HelpText="Optionally provide a custom registration url" ResourceKey="RegisterUrl">Register Url:</Label>
<div class="col-sm-9">
<input id="registerurl" class="form-control" @bind="@_registerurl" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileurl" HelpText="Optionally provide a custom profile url" ResourceKey="ProfileUrl">Profile Url:</Label>
<div class="col-sm-9">
<input id="profileurl" class="form-control" @bind="@_profileurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireconfirmedemail" HelpText="Do you want to require registered users to verify their email address before they are allowed to log in?" ResourceKey="RequireConfirmedEmail">Require Verified Email?</Label>
<div class="col-sm-9">
<select id="requireconfirmedemail" class="form-select" @bind="@_requireconfirmedemail">
<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="passkeys" HelpText="Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)" ResourceKey="Passkeys">Allow Passkeys?</Label>
<div class="col-sm-9">
<select id="passkeys" class="form-select" @bind="@_passkeys">
<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="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor Authentication?</Label>
<div class="col-sm-9">
@@ -538,6 +547,7 @@ else
private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _passkeys;
private string _twofactor;
private string _cookiename;
private string _cookiedomain;
@@ -609,12 +619,13 @@ else
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
_passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false");
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", "");
@@ -753,6 +764,7 @@ else
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true);

View File

@@ -64,7 +64,7 @@
</span>
}
</div>
<div @ref="@_editorElement"></div>
<div @ref="@_editorElement" class="app-editor-resizable"></div>
</div>
</div>
</TabPanel>

View File

@@ -3,6 +3,7 @@
@using System.Text.RegularExpressions
@using Radzen
@using Radzen.Blazor
@using System.Reflection
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@@ -17,7 +18,7 @@
<RadzenTheme Theme="@RadzenEditorDefinitions.DefaultTheme" />
<RadzenComponents />
<RadzenHtmlEditor @ref="_editor" Visible="_visible" Placeholder="@Placeholder" style="@($"height: {Height}px;")"
@bind-Value="_value" Execute="OnExecute" class="rz-text-editor">
@bind-Value="_value" Execute="OnExecute" class="rz-text-editor app-editor-resizable">
<ChildContent>
@_toolbar
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@@ -93,6 +94,17 @@
}
await _interop.SetBackgroundColor(_editor.Element, backgroundColor);
}
var subscribers = GetEventSubscribers(DialogService, "OnOpen");
var dialogSubscibers = subscribers?.Where(s => s.Method.DeclaringType == typeof(RadzenDialog)) ?? Enumerable.Empty<Delegate>();
if (dialogSubscibers.Count() > 1)
{
//clean the event to avoid multiple RadzenDialog instances subscribing to the event
dialogSubscibers.Skip(1).ToList().ForEach(s =>
{
DialogService.OnOpen -= s as Action<string, Type, Dictionary<string, object>, DialogOptions>;
});
}
}
}
@@ -212,4 +224,23 @@
{
await _interop.UpdateDialogLayout(_editor.Element);
}
private Delegate[] GetEventSubscribers(object target, string eventName)
{
var type = target.GetType();
var eventField = type.GetField(eventName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (eventField == null)
{
return null;
}
var eventDelegate = eventField.GetValue(target) as Delegate;
if (eventDelegate == null)
{
return new Delegate[0];
}
return eventDelegate.GetInvocationList();
}
}

View File

@@ -22,7 +22,7 @@ namespace Oqtane.Modules
private Dictionary<string, string> _urlparameters;
private bool _scriptsloaded = false;
protected Logger logger => _logger ?? (_logger = new Logger(this));
public Logger logger => _logger ?? (_logger = new Logger(this));
[Inject]
protected ILogService LoggingService { get; set; }

View File

@@ -5,6 +5,7 @@
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<WasmFingerprintAssets>false</WasmFingerprintAssets>
</PropertyGroup>
<ItemGroup>

View File

@@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ForgotPassword" xml:space="preserve">
<value>Forgot Password</value>
<value>Forgot Password?</value>
</data>
<data name="Success.Account.Verified" xml:space="preserve">
<value>User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.</value>
@@ -127,7 +127,7 @@
<value>User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value>
<value>External Login Linked Successfully. You Can Now Login.</value>
</data>
<data name="Message.Account.NotLinked" xml:space="preserve">
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
@@ -231,4 +231,10 @@
<data name="Register" xml:space="preserve">
<value>Register as new user?</value>
</data>
<data name="Passkey" xml:space="preserve">
<value>Use Passkey</value>
</data>
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Login Was Not Successful</value>
</data>
</root>

View File

@@ -168,9 +168,6 @@
<data name="DeleteNotification.Message" xml:space="preserve">
<value>Are You Sure You Wish To Delete This Notification?</value>
</data>
<data name="Identity.Name" xml:space="preserve">
<value>Identity</value>
</data>
<data name="Confirm.HelpText" xml:space="preserve">
<value>If you are changing your password you must enter it again to confirm it matches the value entered above</value>
</data>
@@ -211,7 +208,7 @@
<value>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.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value>
<value>Use Two Factor?</value>
</data>
<data name="DeleteAllNotifications.Header" xml:space="preserve">
<value>Clear Notifications</value>
@@ -234,11 +231,11 @@
<data name="DeleteNotification.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="NoNotificationsReceived.Text" xml:space="preserve">
<value>No notifications have been received</value>
<data name="NoNotificationsReceived" xml:space="preserve">
<value>You Have Not Received Any Notifications</value>
</data>
<data name="NoNotificationsSent.Text" xml:space="preserve">
<value>No notifications have been sent</value>
<data name="NoNotificationsSent" xml:space="preserve">
<value>You Have Not Sent Any Notifications</value>
</data>
<data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value>
@@ -249,4 +246,46 @@
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
<data name="Identity.Heading" xml:space="preserve">
<value>Identity</value>
</data>
<data name="Security.Heading" xml:space="preserve">
<value>Security</value>
</data>
<data name="MFA.Heading" xml:space="preserve">
<value>Multi-Factor Authenticationxxx</value>
</data>
<data name="Passkeys.Heading" xml:space="preserve">
<value>Passkeys</value>
</data>
<data name="Logins.Heading" xml:space="preserve">
<value>External Logins</value>
</data>
<data name="Passkey" xml:space="preserve">
<value>Passkey</value>
</data>
<data name="DeletePasskey.Header" xml:space="preserve">
<value>Delete Passkey</value>
</data>
<data name="Confirm.Passkey.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="Login" xml:space="preserve">
<value>Login</value>
</data>
<data name="DeleteLogin.Header" xml:space="preserve">
<value>Delete Login</value>
</data>
<data name="Confirm.Login.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="Message.Passkeys.None" xml:space="preserve">
<value>You Have Not Created Any Passkeys</value>
</data>
<data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value>
</data>
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Could Not Be Created</value>
</data>
</root>

View File

@@ -123,9 +123,6 @@
<data name="Message.Password.Complexity" xml:space="preserve">
<value>Password Provided Does Not Meet The Complexity Policy</value>
</data>
<data name="Identity.Name" xml:space="preserve">
<value>Identity</value>
</data>
<data name="Confirm.Delete" xml:space="preserve">
<value>Is Deleted?</value>
</data>
@@ -222,4 +219,37 @@
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
<data name="Security.Heading" xml:space="preserve">
<value>Security</value>
</data>
<data name="Passkeys.Heading" xml:space="preserve">
<value>Passkeys</value>
</data>
<data name="Logins.Heading" xml:space="preserve">
<value>External Logins</value>
</data>
<data name="Passkey" xml:space="preserve">
<value>Passkey</value>
</data>
<data name="Login" xml:space="preserve">
<value>Login</value>
</data>
<data name="DeletePasskey.Header" xml:space="preserve">
<value>Delete Passkey</value>
</data>
<data name="DeleteLogin.Header" xml:space="preserve">
<value>Delete Login</value>
</data>
<data name="Confirm.Passkey.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="Confirm.Login.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="Message.Passkeys.None" xml:space="preserve">
<value>You Have Not Created Any Passkeys</value>
</data>
<data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value>
</data>
</root>

View File

@@ -561,4 +561,10 @@
<data name="SingleLogout.HelpText" xml:space="preserve">
<value>Specify if users should be logged out of both the application and provider (the default is false indicating they will only be logged out of the application)</value>
</data>
<data name="Passkeys.Text" xml:space="preserve">
<value>Allow Passkeys?</value>
</data>
<data name="Passkeys.HelpText" xml:space="preserve">
<value>Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)?</value>
</data>
</root>

View File

@@ -3,12 +3,14 @@ using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Shared;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Oqtane.Services
{
@@ -206,6 +208,17 @@ namespace Oqtane.Services
await CheckResponse(response, uri);
}
protected async Task<string> PostStringAsync(string uri)
{
var response = await GetHttpClient().PostAsync(uri, null);
if (await CheckResponse(response, uri) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadAsStringAsync();
return result;
}
return default;
}
protected async Task<T> PostJsonAsync<T>(string uri, T value)
{
return await PostJsonAsync<T, T>(uri, value);

View File

@@ -1,11 +1,12 @@
using Oqtane.Shared;
using Oqtane.Models;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
using System.Net;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Services
{
@@ -146,17 +147,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<string> GetPersonalAccessTokenAsync();
/// <summary>
/// Link an external login with a local user account
/// </summary>
/// <param name="user">The <see cref="User"/> we're verifying</param>
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
/// <param name="type">External Login provider type</param>
/// <param name="key">External Login provider key</param>
/// <param name="name">External Login provider display name</param>
/// <returns></returns>
Task<User> LinkUserAsync(User user, string token, string type, string key, string name);
/// <summary>
/// Get password requirements for site
/// </summary>
@@ -176,29 +166,51 @@ namespace Oqtane.Services
/// <summary>
/// Get passkeys for a user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<List<Passkey>> GetPasskeysAsync();
/// <summary>
/// Add a user passkey
/// </summary>
/// <param name="passkey"></param>
/// <returns></returns>
Task<Passkey> AddPasskeyAsync(Passkey passkey);
Task<List<UserPasskey>> GetPasskeysAsync(int userId);
/// <summary>
/// Update a user passkey
/// </summary>
/// <param name="passkey"></param>
/// <returns></returns>
Task<Passkey> UpdatePasskeyAsync(Passkey passkey);
Task<UserPasskey> UpdatePasskeyAsync(UserPasskey passkey);
/// <summary>
/// Delete a user passkey
/// </summary>
/// <param name="userId"></param>
/// <param name="credentialId"></param>
/// <returns></returns>
Task DeletePasskeyAsync(byte[] credentialId);
Task DeletePasskeyAsync(int userId, byte[] credentialId);
/// <summary>
/// Get logins for a user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<List<UserLogin>> GetLoginsAsync(int userId);
/// <summary>
/// Link an external login with a local user account
/// </summary>
/// <param name="user">The <see cref="User"/> we're verifying</param>
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
/// <param name="type">External Login provider type</param>
/// <param name="key">External Login provider key</param>
/// <param name="name">External Login provider display name</param>
/// <returns></returns>
Task<User> AddLoginAsync(User user, string token, string type, string key, string name);
/// <summary>
/// Delete a user login
/// </summary>
/// <param name="userId"></param>
/// <param name="provider"></param>
/// <param name="key"></param>
/// <returns></returns>
Task DeleteLoginAsync(int userId, string provider, string key);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -245,7 +257,7 @@ namespace Oqtane.Services
public async Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent)
{
return await PostJsonAsync<User>($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user);
return await PostJsonAsync<User>($"{Apiurl}/signin?setcookie={setCookie}&persistent={isPersistent}", user);
}
public async Task LogoutUserAsync(User user)
@@ -298,11 +310,6 @@ namespace Oqtane.Services
return await GetStringAsync($"{Apiurl}/personalaccesstoken");
}
public async Task<User> LinkUserAsync(User user, string token, string type, string key, string name)
{
return await PostJsonAsync<User>($"{Apiurl}/link?token={token}&type={type}&key={key}&name={name}", user);
}
public async Task<string> GetPasswordRequirementsAsync(int siteId)
{
var requirements = await GetJsonAsync<Dictionary<string, string>>($"{Apiurl}/passwordrequirements/{siteId}");
@@ -330,24 +337,34 @@ namespace Oqtane.Services
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
public async Task<List<Passkey>> GetPasskeysAsync()
public async Task<List<UserPasskey>> GetPasskeysAsync(int userId)
{
return await GetJsonAsync<List<Passkey>>($"{Apiurl}/passkey");
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}");
}
public async Task<Passkey> AddPasskeyAsync(Passkey passkey)
public async Task<UserPasskey> UpdatePasskeyAsync(UserPasskey passkey)
{
return await PostJsonAsync<Passkey>($"{Apiurl}/passkey", passkey);
return await PutJsonAsync<UserPasskey>($"{Apiurl}/passkey", passkey);
}
public async Task<Passkey> UpdatePasskeyAsync(Passkey passkey)
public async Task DeletePasskeyAsync(int userId, byte[] credentialId)
{
return await PutJsonAsync<Passkey>($"{Apiurl}/passkey", passkey);
await DeleteAsync($"{Apiurl}/passkey?id={userId}&credential={Base64Url.EncodeToString(credentialId)}");
}
public async Task DeletePasskeyAsync(byte[] credentialId)
public async Task<List<UserLogin>> GetLoginsAsync(int userId)
{
await DeleteAsync($"{Apiurl}/passkey?id={credentialId}");
return await GetJsonAsync<List<UserLogin>>($"{Apiurl}/login?id={userId}");
}
public async Task<User> AddLoginAsync(User user, string token, string type, string key, string name)
{
return await PostJsonAsync<User>($"{Apiurl}/login?token={token}&type={type}&key={key}&name={name}", user);
}
public async Task DeleteLoginAsync(int userId, string provider, string key)
{
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
@@ -62,7 +63,14 @@ namespace Oqtane.Themes.Controls
// verify anonymous users can access current page
if (UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList) && Utilities.IsEffectiveAndNotExpired(PageState.Page.EffectiveDate, PageState.Page.ExpiryDate))
{
returnurl = PageState.Route.PathAndQuery;
if (PageState.Route.Action != Constants.DefaultAction && PageState.Modules.Any() && PageState.Modules.First().SecurityAccessLevel > SecurityAccessLevel.View)
{
returnurl = PageState.Route.PagePath;
}
else
{
returnurl = PageState.Route.PathAndQuery;
}
}
else
{

View File

@@ -417,5 +417,30 @@ namespace Oqtane.UI
return Task.CompletedTask;
}
}
public ValueTask<string> CreateCredential(string optionsResponse)
{
try
{
return _jsRuntime.InvokeAsync<string>("Oqtane.Interop.createCredential", optionsResponse);
}
catch
{
return new ValueTask<string>(Task.FromResult(string.Empty));
}
}
public ValueTask<string> RequestCredential(string optionsResponse)
{
try
{
return _jsRuntime.InvokeAsync<string>("Oqtane.Interop.requestCredential", optionsResponse);
}
catch
{
return new ValueTask<string>(Task.FromResult(string.Empty));
}
}
}
}

View File

@@ -1,26 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31611.283
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Maui", "Oqtane.Maui\Oqtane.Maui.csproj", "{5EE64148-2152-4908-A3E7-658EB1D87754}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5EE64148-2152-4908-A3E7-658EB1D87754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EE64148-2152-4908-A3E7-658EB1D87754}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EE64148-2152-4908-A3E7-658EB1D87754}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{5EE64148-2152-4908-A3E7-658EB1D87754}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5EE64148-2152-4908-A3E7-658EB1D87754}.Release|Any CPU.Build.0 = Release|Any CPU
{5EE64148-2152-4908-A3E7-658EB1D87754}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572}
EndGlobalSection
EndGlobal

3
Oqtane.Maui.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="Oqtane.Maui\Oqtane.Maui.csproj" DefaultStartup="true" />
</Solution>

View File

@@ -21,6 +21,7 @@
<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.Localization" 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>

View File

@@ -22,10 +22,8 @@
<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="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="Microsoft.EntityFrameworkCore.Relational" 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,11 +31,9 @@
<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="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="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="10.0.0-rc.2.25502.107" exclude="Build,Analyzers" />
</group>
</dependencies>

View File

@@ -19,10 +19,8 @@
<dependencies>
<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" />
</group>
</dependencies>
</metadata>

View File

@@ -1,4 +1,4 @@
dotnet build -c Release ..\Oqtane.sln
dotnet build -c Release ..\Oqtane.slnx
nuget.exe pack Oqtane.Client.nuspec
nuget.exe pack Oqtane.Server.nuspec
nuget.exe pack Oqtane.Shared.nuspec
@@ -23,7 +23,7 @@ C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1"
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 build -c Release ..\Oqtane.Updater.slnx
dotnet publish ..\Oqtane.Updater\Oqtane.Updater.csproj /p:Configuration=Release
nuget.exe pack Oqtane.Updater.nuspec
nuget.exe pack ..\Oqtane.Application\Oqtane.Application.Template.nuspec -NoDefaultExcludes

View File

@@ -221,6 +221,7 @@ namespace Oqtane.Controllers
Setting existing = existingSettings.FirstOrDefault(item => item.SettingName.Equals(setting.SettingName, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
setting.SettingId = 0; // initialize
existing = _settings.AddSetting(setting);
AddSyncEvent(existing.EntityName, existing.EntityId, existing.SettingId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Setting Created {Setting}", existing);
@@ -228,7 +229,7 @@ namespace Oqtane.Controllers
}
else
{
// note that SettingId will pnly be 0 if the Settings were converted from a Dictionary in the SettingService
// note that setting.SettingId will be 0 or -1 if the Settings were converted from a Dictionary in the SettingService
if (existing.SettingValue != setting.SettingValue || (existing.IsPrivate != setting.IsPrivate && setting.SettingId != 0))
{
existing.SettingValue = setting.SettingValue;

View File

@@ -1,19 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using System.Threading.Tasks;
using System.Linq;
using System.Security.Claims;
using Oqtane.Shared;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Enums;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Extensions;
using Oqtane.Managers;
using System.Collections.Generic;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
@@ -238,8 +241,8 @@ namespace Oqtane.Controllers
}
}
// POST api/<controller>/login
[HttpPost("login")]
// POST api/<controller>/signin
[HttpPost("signin")]
public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersistent)
{
if (ModelState.IsValid)
@@ -328,22 +331,6 @@ namespace Oqtane.Controllers
return user;
}
// POST api/<controller>/link
[HttpPost("link")]
public async Task<User> Link([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
user = await _userManager.LinkExternalAccount(user, token, type, key, name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
return user;
}
// GET api/<controller>/validate/x
[HttpGet("validateuser")]
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
@@ -464,40 +451,41 @@ namespace Oqtane.Controllers
}
}
// GET: api/<controller>/passkey
// GET: api/<controller>/passkey?id=x
[HttpGet("passkey")]
[Authorize]
public async Task<IEnumerable<Passkey>> GetPasskeys()
public async Task<IEnumerable<UserPasskey>> GetPasskeys(int id)
{
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)
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
passkey.UserId = _userPermissions.GetUser(User).UserId;
await _userManager.AddPasskey(passkey);
return await _userManager.GetPasskeys(id, _tenantManager.GetAlias().SiteId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Post Attempt {PassKey}", passkey);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Get Attempt {UserId} {SiteId}", id, _tenantManager.GetAlias().SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// PUT api/<controller>/passkey
[HttpPut("passkey")]
[Authorize]
public async Task UpdatePasskey([FromBody] Passkey passkey)
public async Task UpdatePasskey([FromBody] UserPasskey passkey)
{
if (ModelState.IsValid)
{
passkey.UserId = _userPermissions.GetUser(User).UserId;
await _userManager.UpdatePasskey(passkey);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == passkey.UserId)
{
// passkey name is prefixed with SiteId for multi-tenancy
passkey.Name = $"{_tenantManager.GetAlias().SiteId}:" + passkey.Name;
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;
}
}
else
{
@@ -506,12 +494,70 @@ namespace Oqtane.Controllers
}
}
// DELETE api/<controller>/passkey?id=x
// DELETE api/<controller>/passkey?id=x&credential=y
[HttpDelete("passkey")]
[Authorize]
public async Task DeletePasskey(byte[] id)
public async Task DeletePasskey(int id, string credential)
{
await _userManager.DeletePasskey(_userPermissions.GetUser(User).UserId, id);
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
await _userManager.DeletePasskey(id, Base64Url.DecodeFromChars(credential));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Passkey Delete Attempt {UserId} {Credential}", id, credential);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// GET: api/<controller>/login?id=x
[HttpGet("login")]
[Authorize]
public async Task<IEnumerable<UserLogin>> GetLogins(int id)
{
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
return await _userManager.GetLogins(id, _tenantManager.GetAlias().SiteId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized External Login Get Attempt {UserId} {SiteId}", id, _tenantManager.GetAlias().SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// PUT api/<controller>/login
[HttpPost("login")]
public async Task<User> AddLogin([FromBody] User user, string token, string type, string key, string name)
{
if (ModelState.IsValid)
{
user = await _userManager.AddLogin(user, token, type, key, name);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized External Login Post Attempt {Username} {Token}", user.Username, token);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
user = null;
}
return user;
}
// DELETE api/<controller>/login?id=x&provider=y&key=z
[HttpDelete("login")]
[Authorize]
public async Task DeleteLogin(int id, string provider, string key)
{
if (_userPermissions.IsAuthorized(User, _tenantManager.GetAlias().SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == id)
{
await _userManager.DeleteLogin(id, provider, key);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized External Login Delete Attempt {UserId} {Provider} {Key}", id, provider, key);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

View File

@@ -102,7 +102,8 @@ namespace Microsoft.Extensions.DependencyInjection
})
.AddCookie(Constants.AuthenticationScheme)
.AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { })
.AddOAuth(AuthenticationProviderTypes.OAuth2, options => { });
.AddOAuth(AuthenticationProviderTypes.OAuth2, options => { })
.AddTwoFactorUserIdCookie();
services.ConfigureOqtaneCookieOptions();
services.ConfigureOqtaneAuthenticationOptions(configuration);

View File

@@ -676,12 +676,12 @@ namespace Oqtane.Extensions
var _syncManager = httpContext.RequestServices.GetRequiredService<ISyncManager>();
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login");
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName);
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName);
}
else
{
identity.Label = ExternalLoginStatus.AccessDenied;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External User Login Denied For {Username}. User Account Is Deleted Or Not An Active Member Of Site {SiteId}.", user.Username, user.SiteId);
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Denied For {Username}. User Account Is Deleted Or Not An Active Member Of Site {SiteId}.", user.Username, user.SiteId);
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Net;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using Oqtane.Enums;
@@ -32,14 +33,15 @@ namespace Oqtane.Managers
Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
Task<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<List<UserPasskey>> GetPasskeys(int userId, int siteId);
Task UpdatePasskey(UserPasskey passkey);
Task DeletePasskey(int userId, byte[] credentialId);
Task<List<UserLogin>> GetLogins(int userId, int siteId);
Task<User> AddLogin(User user, string token, string type, string key, string name);
Task DeleteLogin(int userId, string provider, string key);
}
public class UserManager : IUserManager
@@ -586,29 +588,6 @@ namespace Oqtane.Managers
}
return user;
}
public async Task<User> LinkExternalAccount(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
var validateResult = new UserValidateResult { Succeeded = true };
@@ -824,9 +803,9 @@ namespace Oqtane.Managers
return result;
}
public async Task<List<Passkey>> GetPasskeys(int userId)
public async Task<List<UserPasskey>> GetPasskeys(int userId, int siteId)
{
var passkeys = new List<Passkey>();
var passkeys = new List<UserPasskey>();
var user = _users.GetUser(userId);
if (user != null)
{
@@ -836,31 +815,18 @@ namespace Oqtane.Managers
var userpasskeys = await _identityUserManager.GetPasskeysAsync(identityuser);
foreach (var userpasskey in userpasskeys)
{
passkeys.Add(new Passkey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name, UserId = userId });
// passkey name is prefixed with SiteId for multi-tenancy
if (userpasskey.Name.StartsWith($"{siteId}:"))
{
passkeys.Add(new UserPasskey { CredentialId = userpasskey.CredentialId, Name = userpasskey.Name.Split(':')[1], 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)
public async Task UpdatePasskey(UserPasskey passkey)
{
var user = _users.GetUser(passkey.UserId);
if (user != null)
@@ -890,5 +856,63 @@ namespace Oqtane.Managers
}
}
}
public async Task<List<UserLogin>> GetLogins(int userId, int siteId)
{
var logins = new List<UserLogin>();
var user = _users.GetUser(userId);
if (user != null)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var userlogins = await _identityUserManager.GetLoginsAsync(identityuser);
foreach (var userlogin in userlogins)
{
if (userlogin.LoginProvider.EndsWith(":" + siteId.ToString()))
{
logins.Add(new UserLogin { Provider = userlogin.LoginProvider, Key = userlogin.ProviderKey, Name = userlogin.ProviderDisplayName });
}
}
}
}
return logins;
}
public async Task<User> AddLogin(User user, string token, string type, string key, string name)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
// make LoginProvider multi-tenant aware
type += ":" + user.SiteId.ToString();
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
_logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
user = null;
}
}
return user;
}
public async Task DeleteLogin(int userId, string provider, string key)
{
var user = _users.GetUser(userId);
if (user != null)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
await _identityUserManager.RemoveLoginAsync(identityuser, provider, key);
}
}
}
}
}

View File

@@ -29,13 +29,8 @@
<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.Data.SqlClient" Version="6.1.1" />
<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.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" 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,12 +43,10 @@
<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="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="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="10.0.0-rc.2.25502.107" />
</ItemGroup>

View File

@@ -0,0 +1,3 @@
@page "/pages/passkey"
@namespace Oqtane.Pages
@model Oqtane.Pages.PasskeyModel

View File

@@ -0,0 +1,154 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Enums;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Security;
using Oqtane.Shared;
namespace Oqtane.Pages
{
[AllowAnonymous]
public class PasskeyModel : PageModel
{
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly IUserManager _userManager;
private readonly ILogManager _logger;
public PasskeyModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager, ILogManager logger)
{
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> OnPostAsync(string operation, string credential, string returnurl)
{
if (HttpContext.GetSiteSettings().GetValue("LoginOptions:Passkeys", "false") == "true")
{
IdentityUser identityuser;
switch (operation.ToLower())
{
case "create":
if (User.Identity.IsAuthenticated)
{
identityuser = await _identityUserManager.FindByNameAsync(User.Identity.Name);
if (identityuser != null)
{
var creationOptionsJson = await _identitySignInManager.MakePasskeyCreationOptionsAsync(new()
{
Id = identityuser.Id,
Name = identityuser.UserName,
DisplayName = identityuser.UserName
});
returnurl += (!returnurl.Contains("?") ? "?" : "&") + $"options={WebUtility.UrlEncode(creationOptionsJson)}";
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Create Attempt - User {User} Does Not Exist", User.Identity.Name);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Create Attempt - User Not Authenticated");
}
break;
case "validate":
if (User.Identity.IsAuthenticated && !string.IsNullOrEmpty(credential))
{
identityuser = await _identityUserManager.FindByNameAsync(User.Identity.Name);
if (identityuser != null)
{
var attestationResult = await _identitySignInManager.PerformPasskeyAttestationAsync(credential);
if (attestationResult.Succeeded)
{
var user = _userManager.GetUser(User.Identity.Name, HttpContext.GetAlias().SiteId);
if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered))
{
// setting a default name and including a SiteId prefix for multi-tenancy
var name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username;
attestationResult.Passkey.Name = HttpContext.GetAlias().SiteId + ":" + name + "'s Passkey";
var addPasskeyResult = await _identityUserManager.AddOrUpdatePasskeyAsync(identityuser, attestationResult.Passkey);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Validation Failed - User {Username} Is Deleted Or Is Not A Registered User For The Site", User.Identity.Name);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Validation Failed For User {Username} - {Message}", User.Identity.Name, attestationResult.Failure.Message);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Validation Attempt - User {User} Does Not Exist", User.Identity.Name);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Validation Attempt - User Not Authenticated Or Credential Not Provided");
}
break;
case "request":
if (!User.Identity.IsAuthenticated)
{
identityuser = null;
var requestOptionsJson = await _identitySignInManager.MakePasskeyRequestOptionsAsync(identityuser);
returnurl += $"?options={WebUtility.UrlEncode(requestOptionsJson)}";
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Request Attempt - User Is Already Authenticated");
}
break;
case "login":
if (!User.Identity.IsAuthenticated && !string.IsNullOrEmpty(credential))
{
var result = await _identitySignInManager.PasskeySignInAsync(credential);
if (result.Succeeded)
{
var user = _userManager.GetUser(User.Identity.Name, HttpContext.GetAlias().SiteId);
if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered))
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Login Successful For User {Username}", User.Identity.Name);
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Passkey Login Failed - User {Username} Is Deleted Or Is Not A Registered User For The Site", User.Identity.Name);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Login Failed - Invalid Credential");
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Login Attempt");
}
break;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Passkey Request - Passkeys Are Not Enabled For Site");
}
if (!returnurl.StartsWith("/"))
{
returnurl = "/" + returnurl;
}
return LocalRedirect(Url.Content("~" + returnurl));
}
}
}

View File

@@ -1,5 +1,5 @@
/* Login Module Custom Styles */
.Oqtane-Modules-Admin-Login {
width: 200px;
width: 220px;
}

View File

@@ -1,47 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28621.142
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "..\[RootFolder]\Oqtane.Server\Oqtane.Server.csproj", "{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Module.[Module].Client", "Client\[Owner].Module.[Module].Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Module.[Module].Server", "Server\[Owner].Module.[Module].Server.csproj", "{04B05448-788F-433D-92C0-FED35122D45A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Module.[Module].Shared", "Shared\[Owner].Module.[Module].Shared.csproj", "{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Module.[Module].Package", "Package\[Owner].Module.[Module].Package.csproj", "{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.Build.0 = Release|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.Build.0 = Release|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D016F15-46FE-4726-8DFD-2E4FD4DC7668}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,7 @@
<Solution>
<Project Path="..\[RootFolder]\Oqtane.Server\Oqtane.Server.csproj" DefaultStartup="true" />
<Project Path="Client\[Owner].Module.[Module].Client.csproj" />
<Project Path="Server\[Owner].Module.[Module].Server.csproj" />
<Project Path="Shared\[Owner].Module.[Module].Shared.csproj" />
<Project Path="Package\[Owner].Module.[Module].Package.csproj" />
</Solution>

View File

@@ -1,35 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28621.142
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "..\[RootFolder]\Oqtane.Server\Oqtane.Server.csproj", "{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Theme.[Theme].Client", "Client\[Owner].Theme.[Theme].Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Theme.[Theme].Package", "Package\[Owner].Theme.[Theme].Package.csproj", "{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D016F15-46FE-4726-8DFD-2E4FD4DC7668}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="..\[RootFolder]\Oqtane.Server\Oqtane.Server.csproj" DefaultStartup="true" />
<Project Path="Client\[Owner].Theme.[Theme].Client.csproj" />
<Project Path="Package\[Owner].Theme.[Theme].Package.csproj" />
</Solution>

View File

@@ -273,6 +273,11 @@ app {
min-height: 250px;
}
.app-editor-resizable {
resize: vertical;
overflow: auto;
}
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}

View File

@@ -516,5 +516,17 @@ Oqtane.Interop = {
}
}
}
},
createCredential: async function (optionsResponse) {
const optionsJson = JSON.parse(optionsResponse);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
const credential = await navigator.credentials.create({ publicKey: options });
return JSON.stringify(credential);
},
requestCredential: async function (optionsResponse) {
const optionsJson = JSON.parse(optionsResponse);
const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
const credential = await navigator.credentials.get({ publicKey: options, undefined });
return JSON.stringify(credential);
}
};

View File

@@ -0,0 +1,23 @@
namespace Oqtane.Models
{
/// <summary>
/// Passkey properties
/// </summary>
public class UserLogin
{
/// <summary>
/// the login provider for this login
/// </summary>
public string Provider { get; set; }
/// <summary>
/// The key for this login
/// </summary>
public string Key { get; set; }
/// <summary>
/// The friendly name for the login provider
/// </summary>
public string Name { get; set; }
}
}

View File

@@ -3,7 +3,7 @@ namespace Oqtane.Models
/// <summary>
/// Passkey properties
/// </summary>
public class Passkey
public class UserPasskey
{
/// <summary>
/// the credential ID for this passkey

View File

@@ -6,10 +6,8 @@
<ItemGroup>
<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" />
</ItemGroup>
</Project>

View File

@@ -1,38 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28822.285
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{77EECA8C-B58E-469E-B8C5-D543AFC9A654}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Updater", "Oqtane.Updater\Oqtane.Updater.csproj", "{2E8C6889-37CF-4C8D-88B1-505547F25098}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Shared", "Oqtane.Shared\Oqtane.Shared.csproj", "{E2512C17-291F-460A-A6D1-741C301DA184}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.Build.0 = Release|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2512C17-291F-460A-A6D1-741C301DA184}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1FB11796-35DE-4AED-9A52-17733557FCC4}
EndGlobalSection
EndGlobal

3
Oqtane.Updater.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="Oqtane.Updater\Oqtane.Updater.csproj" DefaultStartup="true" />
</Solution>

View File

@@ -1,44 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32611.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "Oqtane.Server\Oqtane.Server.csproj", "{083BB22D-DF24-43A2-95E5-8F385CCB3318}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Client", "Oqtane.Client\Oqtane.Client.csproj", "{FD15B24A-7F6A-4830-9CA2-9C621771C330}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Shared", "Oqtane.Shared\Oqtane.Shared.csproj", "{19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{77EECA8C-B58E-469E-B8C5-D543AFC9A654}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{083BB22D-DF24-43A2-95E5-8F385CCB3318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{083BB22D-DF24-43A2-95E5-8F385CCB3318}.Debug|Any CPU.Build.0 = Debug|Any CPU
{083BB22D-DF24-43A2-95E5-8F385CCB3318}.Release|Any CPU.ActiveCfg = Release|Any CPU
{083BB22D-DF24-43A2-95E5-8F385CCB3318}.Release|Any CPU.Build.0 = Release|Any CPU
{FD15B24A-7F6A-4830-9CA2-9C621771C330}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD15B24A-7F6A-4830-9CA2-9C621771C330}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD15B24A-7F6A-4830-9CA2-9C621771C330}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD15B24A-7F6A-4830-9CA2-9C621771C330}.Release|Any CPU.Build.0 = Release|Any CPU
{19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19D67A9D-3F2E-41BD-80E6-0B50CA83C3AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1FB11796-35DE-4AED-9A52-17733557FCC4}
EndGlobalSection
EndGlobal

5
Oqtane.slnx Normal file
View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="Oqtane.Server\Oqtane.Server.csproj" DefaultStartup="true" />
<Project Path="Oqtane.Client\Oqtane.Client.csproj" />
<Project Path="Oqtane.Shared\Oqtane.Shared.csproj" />
</Solution>