+
+@code {
+ class LinkAttributes
+ {
+ public string InnerText { get; set; }
+ public string InnerHtml { get; set; }
+ public string Href { get; set; }
+ public string Target { get; set; }
+ }
+
+ [Parameter]
+ public RadzenHtmlEditor Editor { get; set; }
+
+ private IDictionary _linkTypes;
+ private IDictionary _linkTargets;
+ private LinkAttributes _linkAttributes;
+ private bool _blank;
+ private int _linkType;
+ private string _message;
+ private bool _linkTextEditable;
+ private FileManager _fileManager;
+ private File _previousFile;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await base.OnInitializedAsync();
+ _linkAttributes = await Editor.GetSelectionAttributes("a", new[] { "innerText", "href", "target" });
+ if (_linkAttributes.Target == "_blank")
+ {
+ _blank = true;
+ }
+
+ _linkTextEditable = string.IsNullOrWhiteSpace(_linkAttributes.InnerHtml) || _linkAttributes.InnerHtml == " ";
+
+ _linkTypes = new Dictionary
+ {
+ { 0, Localizer["WebLink"] },
+ { 1, Localizer["FileLink"] }
+ };
+
+ _linkTargets = new Dictionary
+ {
+ { false, Localizer["OpenInCurrentWindow"] },
+ { true, Localizer["OpenInNewWindow"] }
+ };
+ }
+
+ private void SelectFile()
+ {
+ var file = _fileManager.GetFile();
+ if(file != null)
+ {
+ _linkAttributes.Href = file.Url;
+ if ((string.IsNullOrWhiteSpace(_linkAttributes.InnerText) || _linkAttributes.InnerText == _previousFile?.Name) && _linkTextEditable)
+ {
+ _linkAttributes.InnerText = file.Name;
+ }
+ }
+ else
+ {
+ _linkAttributes.Href = string.Empty;
+ if (_linkAttributes.InnerText == _previousFile?.Name)
+ {
+ _linkAttributes.InnerText = string.Empty;
+ }
+ }
+ _previousFile = file;
+
+ StateHasChanged();
+ }
+
+ private void InsertLink()
+ {
+ _message = string.Empty;
+ if (string.IsNullOrWhiteSpace(_linkAttributes.Href))
+ {
+ _message = _linkType == 1 ? Localizer["Message.Require.File"] : Localizer["Message.Require.WebAddress"];
+ }
+ else if (string.IsNullOrWhiteSpace(_linkAttributes.InnerText) && _linkTextEditable)
+ {
+ _message = Localizer["Message.Require.LinkText"];
+ }
+
+ if (string.IsNullOrWhiteSpace(_message))
+ {
+ var html = new StringBuilder();
+ html.AppendFormat("{0}", string.IsNullOrWhiteSpace(_linkAttributes.InnerText) ? _linkAttributes.InnerHtml : _linkAttributes.InnerText);
+
+ DialogService.Close(html.ToString());
+ }
+ else
+ {
+ StateHasChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs
new file mode 100644
index 00000000..b8623e51
--- /dev/null
+++ b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.placeholder.cs
@@ -0,0 +1,12 @@
+// This is just a placeholder file
+// It is necessary for the documentation to successfully build this project.
+// Reason is that docfx will run the .net compiler and find references
+// to this class in the project.
+// But since the real class is just a .razor file, ATM docfx will fail.
+//
+// Note added 2025-09-23 by @tvatavuk.
+// We hope that as .net and docfx improve, the razor-compiler will work in that scenario
+// as well, and this file can be removed.
+
+namespace Oqtane.Modules.Controls;
+public partial class RadzenTextEditor;
diff --git a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.razor b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.razor
index 3a16bc45..8e0d347e 100644
--- a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.razor
+++ b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditor.razor
@@ -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 @@
+ @bind-Value="_value" Execute="OnExecute" class="rz-text-editor app-editor-resizable">
@_toolbar
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@@ -51,8 +52,8 @@
public override List Resources { get; set; } = new List()
{
- new Resource { ResourceType = ResourceType.Script, Url = "_content/Radzen.Blazor/Radzen.Blazor.js", Location = ResourceLocation.Body },
- new Resource { ResourceType = ResourceType.Script, Url = "js/texteditors/radzen/radzen-interop.js", Location = ResourceLocation.Body }
+ new Script("_content/Radzen.Blazor/Radzen.Blazor.js"),
+ new Script("js/texteditors/radzen/radzen-interop.js")
};
protected override void OnInitialized()
@@ -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();
+ 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, DialogOptions>;
+ });
+ }
}
}
@@ -147,13 +159,17 @@
private async Task OnExecute(HtmlEditorExecuteEventArgs args)
{
- if (args.CommandName == "InsertImage")
+ switch(args.CommandName)
{
- await InsertImage(args.Editor);
- }
- else if (args.CommandName == "Settings")
- {
- await UpdateSettings(args.Editor);
+ case "InsertImage":
+ await InsertImage(args.Editor);
+ break;
+ case "InsertLink":
+ await InsertLink(args.Editor);
+ break;
+ case "Settings":
+ await UpdateSettings(args.Editor);
+ break;
}
}
@@ -161,10 +177,27 @@
{
await editor.SaveSelectionAsync();
- var result = await DialogService.OpenAsync(Localizer["DialogTitle.SelectImage"], new Dictionary
+ var result = await DialogService.OpenAsync(Localizer["DialogTitle.SelectImage"], new Dictionary
{
{ "Filters", PageState.Site.ImageFiles }
- });
+ }, new DialogOptions { CssClass = "rz-text-editor-dialog" });
+
+ await editor.RestoreSelectionAsync();
+
+ if (result != null)
+ {
+ await editor.ExecuteCommandAsync(HtmlEditorCommands.InsertHtml, result);
+ }
+ }
+
+ private async Task InsertLink(RadzenHtmlEditor editor)
+ {
+ await editor.SaveSelectionAsync();
+
+ var result = await DialogService.OpenAsync(Localizer["DialogTitle.InsertLink"], new Dictionary
+ {
+ { "Editor", editor }
+ }, new DialogOptions { CssClass = "rz-text-editor-dialog" });
await editor.RestoreSelectionAsync();
@@ -178,7 +211,7 @@
{
await editor.SaveSelectionAsync();
- var result = await DialogService.OpenAsync(Localizer["Settings"], null, new DialogOptions { Width = "650px" });
+ var result = await DialogService.OpenAsync(Localizer["Settings"], null, new DialogOptions { Width = "650px" });
if (result == true)
{
NavigationManager.NavigateTo(NavigationManager.Uri);
@@ -191,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();
+ }
}
\ No newline at end of file
diff --git a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs
index 9fdd778d..0f483562 100644
--- a/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs
+++ b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorDefinitions.cs
@@ -31,9 +31,10 @@ namespace Oqtane.Modules.Controls
{ "FormatBlock", (builder, sequence) => CreateFragment(builder, sequence, "FormatBlock", "RadzenHtmlEditorFormatBlock") },
{ "Indent", (builder, sequence) => CreateFragment(builder, sequence, "Indent", "RadzenHtmlEditorIndent") },
{ "InsertImage", (builder, sequence) => CreateFragment(builder, sequence, "InsertImage", "RadzenHtmlEditorCustomTool", "InsertImage", "image") },
+ { "Bold", (builder, sequence) => CreateFragment(builder, sequence, "Italic", "RadzenHtmlEditorBold") },
{ "Italic", (builder, sequence) => CreateFragment(builder, sequence, "Italic", "RadzenHtmlEditorItalic") },
{ "Justify", (builder, sequence) => CreateFragment(builder, sequence, "Justify", "RadzenHtmlEditorJustify") },
- { "Link", (builder, sequence) => CreateFragment(builder, sequence, "Link", "RadzenHtmlEditorLink") },
+ { "Link", (builder, sequence) => CreateFragment(builder, sequence, "InsertLink", "RadzenHtmlEditorCustomTool", "InsertLink", "insert_link") },
{ "OrderedList", (builder, sequence) => CreateFragment(builder, sequence, "OrderedList", "RadzenHtmlEditorOrderedList") },
{ "Outdent", (builder, sequence) => CreateFragment(builder, sequence, "Outdent", "RadzenHtmlEditorOutdent") },
{ "Redo", (builder, sequence) => CreateFragment(builder, sequence, "Redo", "RadzenHtmlEditorRedo") },
diff --git a/Oqtane.Client/Modules/Controls/SettingsDialog.razor b/Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorSettingsDialog.razor
similarity index 100%
rename from Oqtane.Client/Modules/Controls/SettingsDialog.razor
rename to Oqtane.Client/Modules/Controls/TextEditors/Radzen/RadzenTextEditorSettingsDialog.razor
diff --git a/Oqtane.Client/Modules/Enums/MessageStyle.cs b/Oqtane.Client/Modules/Enums/MessageStyle.cs
new file mode 100644
index 00000000..dfabe13b
--- /dev/null
+++ b/Oqtane.Client/Modules/Enums/MessageStyle.cs
@@ -0,0 +1,8 @@
+namespace Oqtane.Modules
+{
+ public enum MessageStyle
+ {
+ Alert,
+ Toast
+ }
+}
diff --git a/Oqtane.Client/Modules/MessageType.cs b/Oqtane.Client/Modules/Enums/MessageType.cs
similarity index 100%
rename from Oqtane.Client/Modules/MessageType.cs
rename to Oqtane.Client/Modules/Enums/MessageType.cs
diff --git a/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs b/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs
index 59a473c4..3cba0000 100644
--- a/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs
+++ b/Oqtane.Client/Modules/HtmlText/ModuleInfo.cs
@@ -18,7 +18,7 @@ namespace Oqtane.Modules.HtmlText
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client",
Resources = new List()
{
- new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }
+ new Stylesheet("~/Module.css")
}
};
}
diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs
index 782a4b0a..43851818 100644
--- a/Oqtane.Client/Modules/ModuleBase.cs
+++ b/Oqtane.Client/Modules/ModuleBase.cs
@@ -22,7 +22,7 @@ namespace Oqtane.Modules
private Dictionary _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; }
@@ -372,6 +372,11 @@ namespace Oqtane.Modules
}
// UI methods
+ private static readonly string RenderModeBoundaryErrorMessage =
+ "RenderModeBoundary is not available. This method requires a RenderModeBoundary parameter. " +
+ "If you are using child components, ensure you pass the RenderModeBoundary property to the child component: " +
+ "";
+
public void AddModuleMessage(string message, MessageType type)
{
AddModuleMessage(message, type, "top");
@@ -379,21 +384,47 @@ namespace Oqtane.Modules
public void AddModuleMessage(string message, MessageType type, string position)
{
- RenderModeBoundary.AddModuleMessage(message, type, position);
+ AddModuleMessage(message, type, position, MessageStyle.Alert);
+ }
+
+ public void AddModuleMessage(string message, MessageType type, MessageStyle style)
+ {
+ AddModuleMessage(message, type, "top", style);
+ }
+
+ public void AddModuleMessage(string message, MessageType type, string position, MessageStyle style)
+ {
+ if (RenderModeBoundary == null)
+ {
+ throw new InvalidOperationException(RenderModeBoundaryErrorMessage);
+ }
+ RenderModeBoundary.AddModuleMessage(message, type, position, style);
}
public void ClearModuleMessage()
{
+ if (RenderModeBoundary == null)
+ {
+ throw new InvalidOperationException(RenderModeBoundaryErrorMessage);
+ }
RenderModeBoundary.AddModuleMessage("", MessageType.Undefined);
}
public void ShowProgressIndicator()
{
+ if (RenderModeBoundary == null)
+ {
+ throw new InvalidOperationException(RenderModeBoundaryErrorMessage);
+ }
RenderModeBoundary.ShowProgressIndicator();
}
public void HideProgressIndicator()
{
+ if (RenderModeBoundary == null)
+ {
+ throw new InvalidOperationException(RenderModeBoundaryErrorMessage);
+ }
RenderModeBoundary.HideProgressIndicator();
}
@@ -450,6 +481,11 @@ namespace Oqtane.Modules
public string ReplaceTokens(string content, object obj)
{
+ // check for null or empty content
+ if (string.IsNullOrEmpty(content))
+ {
+ return content;
+ }
// Using StringBuilder avoids the performance penalty of repeated string allocations
// that occur with string.Replace or string concatenation inside loops.
var sb = new StringBuilder();
diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj
index 5ff0e3e1..aca8f7ae 100644
--- a/Oqtane.Client/Oqtane.Client.csproj
+++ b/Oqtane.Client/Oqtane.Client.csproj
@@ -1,32 +1,18 @@
- net9.0
- Exe
- Debug;Release
- 6.2.0
- Oqtane
- Shaun Walker
- .NET Foundation
- CMS and Application Framework for Blazor and .NET MAUI
- .NET Foundation
- https://www.oqtane.org
- https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE
- https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.0
- https://github.com/oqtane/oqtane.framework
- GitOqtane
- truetrueDefault
+ true
-
-
-
-
-
+
+
+
+
+
diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx
index a60a2044..bbb7bc5d 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx
@@ -118,7 +118,13 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- Forgot Password
+ Forgot Password?
+
+
+ Forgot Username?
+
+
+ Use Login LinkUser Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.
@@ -127,7 +133,7 @@
User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions.
- User Account Linked Successfully. You Can Now Login With Your External Login Below.
+ External Login Linked Successfully. You Can Now Login.External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.
@@ -142,16 +148,19 @@
You Are Already Signed In
- Please Enter The Username Related To Your Account And Then Select The Forgot Password Option Again
-
-
Please Check The Email Address Associated To Your User Account For A Password Reset Notification
+
+ Please Check Your Email For A Username Reminder Notification
+
+
+ A Login Link Has Been Sent To Your Email Address. The Link Is Only Valid For A Limited Amount Of Time.
+
- User Does Not Exist
+ User Does Not Exist For Criteria Specified
- Please Enter The Secure Verification Code Which Was Sent To You By Email.
+ Please enter the secure verification code which was sent to you by emailVerification Code
@@ -166,7 +175,7 @@
A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator.
- Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time.
+ Please enter the password related to your account. Remember that passwords are sase sensitive. If you attempt to login to your account multiple times unsuccessfully, you will be locked out for a period of time.Password
@@ -175,13 +184,13 @@
Password:
- Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site
+ Specify if you would like to be signed back in automatically the next time you visit this site
- Remember Me?
+ Stay Signed In?
- Please Enter The Username Related To Your Account
+ Please enter the username related to your accountUsername
@@ -201,7 +210,13 @@
Error Resetting Password
-
+
+ Error Sending Username Reminder
+
+
+ Error Sending Login Link
+
+
Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.
@@ -228,7 +243,28 @@
The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider.
+
+ Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process.
+
+
+ Passkey Login Was Unsuccessful. Please Ensure You Selected The Correct Passkey For This Site.
+
Register as new user?
+
+ Use Passkey
+
+
+ Passkey Login Was Not Successful
+
+
+ Please enter the email address related to your account
+
+
+ Email Address
+
+
+ Email:
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx
index 94942a50..2f7ed37d 100644
--- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx
@@ -136,7 +136,7 @@
You Must Provide A Valid Description (ie. No Punctuation)
- Enter the name of the organization who is developing this module. It should not contain spaces or punctuation or contain the word "oqtane".
+ Enter the name of the organization who is developing this module. It should not contain spaces or punctuation or contain the word "oqtane". If you are using an Internal template then make sure the owner matches the name of the project.Enter a name for this module. It should not contain spaces or punctuation or contain the word "oqtane".
@@ -168,7 +168,10 @@
Location:
-
+
+ The Source Code For Your Module Has Been Created In Your Solution And Must Be Compiled In Order To Make It Functional
+
+
The Source Code For Your Module Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Module.
diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx
index 4421b90a..eb32861b 100644
--- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx
@@ -183,8 +183,8 @@
Runtimes:
-
- Definition
+
+ ModuleInformation
diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx
index b818eacd..e16db76a 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Add.resx
@@ -225,9 +225,6 @@
Personalizable?
-
- Appearance
-
Optionally enter content to be included in the page head (ie. meta, link, or script tags)
@@ -253,7 +250,7 @@
Permissions
- Theme Settings
+ ThemeThe date that this page is active
@@ -267,4 +264,7 @@
Expiry Date:
-
+
+ Appearance
+
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx
index ded50502..0f6df7e5 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx
@@ -309,4 +309,7 @@
Specify if changes made to page permissions should be propagated to the modules on this page
+
+ Theme
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx
index d67a4f7a..4810d519 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Profiles/Edit.resx
@@ -157,7 +157,7 @@
The default value for this profile item
- A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from custom Settings (ie. 'EntityName:Countries').
+ A comma delimited list of options. Options can contain a key and value if they are seperated by a colon (ie. key:value). You can also dynamically load your options from Settings.Should a user be required to provide a value for this profile item?
@@ -201,4 +201,10 @@
Autocomplete:
+
+ Options
+
+
+ Settings
+
diff --git a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx
index 0e212ab8..fbb57778 100644
--- a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx
@@ -204,7 +204,7 @@
Page Deleted Successfully
-
+
All Pages Deleted Successfully
diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx
index 19bdf93a..a4a292d4 100644
--- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx
@@ -309,4 +309,19 @@
API Endpoints
+
+ Migration
+
+
+ Date
+
+
+ Framework Version
+
+
+ Database:
+
+
+ The name of the current database. Note that this is not the physical database name but rather the tenant name which is used within the framework to identify a database.
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx
index 96bfd1c4..fe956c14 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx
@@ -141,14 +141,17 @@
Please Note That The Theme Creator Is Only Intended To Be Used In A Development Environment
-
- The Source Code For Your Theme Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Module.
+
+ The Source Code For Your Theme Has Been Created In Your Solution And Must Be Compiled In Order To Make It Functional
+
+
+ The Source Code For Your Theme Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Theme.You Must Provide A Valid Owner Name And Theme Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template
- Enter the name of the organization who is developing this theme. It should not contain spaces or punctuation.
+ Enter the name of the organization who is developing this theme. It should not contain spaces or punctuation or contain the word "oqtane". If you are using an Internal template then make sure the owner matches the name of the project.Enter a name for this theme. It should not contain spaces or punctuation.
diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx
index 69f9bdd8..27b70ddd 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx
@@ -180,4 +180,10 @@
View License
+
+ Theme
+
+
+ Permissions
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx
index 2a6862bb..b725fd7f 100644
--- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx
@@ -168,9 +168,6 @@
Are You Sure You Wish To Delete This Notification?
-
- Identity
-
If you are changing your password you must enter it again to confirm it matches the value entered above
@@ -211,7 +208,7 @@
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.
- Two Factor?
+ Use Two Factor?Clear Notifications
@@ -234,11 +231,11 @@
Delete
-
- No notifications have been received
+
+ You Have Not Received Any Notifications
-
- No notifications have been sent
+
+ You Have Not Sent Any NotificationsLogout Everywhere
@@ -249,4 +246,46 @@
Your time zone
+
+ Identity
+
+
+ Security
+
+
+ Multi-Factor Authenticationxxx
+
+
+ Passkeys
+
+
+ External Logins
+
+
+ Passkey
+
+
+ Delete Passkey
+
+
+ Are You Sure You Wish To Delete {0}?
+
+
+ Login
+
+
+ Delete Login
+
+
+ Are You Sure You Wish To Delete {0}?
+
+
+ You Have Not Created Any Passkeys
+
+
+ You Do Not Have Any External Logins For This Site
+
+
+ Passkey Could Not Be Created
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx
index 5e578cd5..807b932d 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx
@@ -123,9 +123,6 @@
Password Provided Does Not Meet The Complexity Policy
-
- Identity
-
Is Deleted?
@@ -222,4 +219,37 @@
Indicates if the user's email is verified
+
+ Security
+
+
+ Passkeys
+
+
+ External Logins
+
+
+ Passkey
+
+
+ Login
+
+
+ Delete Passkey
+
+
+ Delete Login
+
+
+ Are You Sure You Wish To Delete {0}?
+
+
+ Are You Sure You Wish To Delete {0}?
+
+
+ You Have Not Created Any Passkeys
+
+
+ You Do Not Have Any External Logins For This Site
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
index e9bc7d13..57eecaca 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
@@ -217,7 +217,7 @@
Unique Characters:
- Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site.
+ Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already successfully configured an alternate login method, or else you may lock yourself out of the site.Allow Local Login?
@@ -370,7 +370,7 @@
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.
- Two Factor Authentication?
+ Use 2FA?Do you want to require registered users to verify their email address before they are allowed to log in?
@@ -555,4 +555,22 @@
If you would like to share cookies across subdomains you will need to specify a root domain with a leading dot (ie. '.example.com')
+
+ Allow Single Logout?
+
+
+ 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)
+
+
+ Allow Passkeys?
+
+
+ Do you want to allow users to login using passkeys (ie. passwordless authentication using WebAuthn/FIDO2)?
+
+
+ Allow Login Link?
+
+
+ Do you want to allow users to login using a time sensitive link sent by email?
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/Modules/Controls/RadzenTextEditor.resx b/Oqtane.Client/Resources/Modules/Controls/RadzenTextEditor.resx
index 4c84d4df..40b9cf62 100644
--- a/Oqtane.Client/Resources/Modules/Controls/RadzenTextEditor.resx
+++ b/Oqtane.Client/Resources/Modules/Controls/RadzenTextEditor.resx
@@ -216,4 +216,37 @@
Reset
+
+ Insert Link
+
+
+ Insert Link
+
+
+ Enter Web Address
+
+
+ Enter Link Text
+
+
+ Open In New Window
+
+
+ Web Link
+
+
+ File Link
+
+
+ Open In Current Window
+
+
+ The Web Address is Empty
+
+
+ The Link Text is Empty
+
+
+ You Must Select a File
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx
index a0078ea5..b4b3ccfa 100644
--- a/Oqtane.Client/Resources/SharedResources.resx
+++ b/Oqtane.Client/Resources/SharedResources.resx
@@ -477,4 +477,7 @@
Path
+
+ Installed
+
\ No newline at end of file
diff --git a/Oqtane.Client/Resources/UI/ModuleInstance.resx b/Oqtane.Client/Resources/UI/ModuleInstance.resx
index fc0994f2..4c4a4c63 100644
--- a/Oqtane.Client/Resources/UI/ModuleInstance.resx
+++ b/Oqtane.Client/Resources/UI/ModuleInstance.resx
@@ -124,6 +124,9 @@
Module Type Is Invalid For {0}
- An Unexpected Error Has Occurred
+ An Unexpected Error Has Occurred
+
+
+ Missing service(s): {0}. Please make sure they have been registered correctly.
\ No newline at end of file
diff --git a/Oqtane.Client/Services/FileService.cs b/Oqtane.Client/Services/FileService.cs
index 2b45455a..75256fc8 100644
--- a/Oqtane.Client/Services/FileService.cs
+++ b/Oqtane.Client/Services/FileService.cs
@@ -101,7 +101,6 @@ namespace Oqtane.Services
/// Unzips the contents of a zip file
///
/// Reference to the
- ///
///
Task UnzipFileAsync(int fileId);
}
diff --git a/Oqtane.Client/Services/MigrationHistoryService.cs b/Oqtane.Client/Services/MigrationHistoryService.cs
new file mode 100644
index 00000000..be3ed815
--- /dev/null
+++ b/Oqtane.Client/Services/MigrationHistoryService.cs
@@ -0,0 +1,34 @@
+using Oqtane.Models;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Oqtane.Documentation;
+using Oqtane.Shared;
+
+namespace Oqtane.Services
+{
+ ///
+ /// Service to manage s
+ ///
+ ///
+ Task> GetMigrationHistoryAsync();
+ }
+
+ [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
+ public class MigrationHistoryService : ServiceBase, IMigrationHistoryService
+ {
+ public MigrationHistoryService(HttpClient http, SiteState siteState) : base(http, siteState) { }
+
+ private string Apiurl => CreateApiUrl("MigrationHistory");
+
+ public async Task> GetMigrationHistoryAsync()
+ {
+ return await GetJsonAsync>(Apiurl);
+ }
+ }
+}
diff --git a/Oqtane.Client/Services/ServiceBase.cs b/Oqtane.Client/Services/ServiceBase.cs
index f7302cb7..ec5b4422 100644
--- a/Oqtane.Client/Services/ServiceBase.cs
+++ b/Oqtane.Client/Services/ServiceBase.cs
@@ -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 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 PostJsonAsync(string uri, T value)
{
return await PostJsonAsync(uri, value);
diff --git a/Oqtane.Client/Services/SettingService.cs b/Oqtane.Client/Services/SettingService.cs
index 5bcbf414..469ab171 100644
--- a/Oqtane.Client/Services/SettingService.cs
+++ b/Oqtane.Client/Services/SettingService.cs
@@ -429,49 +429,35 @@ namespace Oqtane.Services
public async Task UpdateSettingsAsync(Dictionary settings, string entityName, int entityId)
{
- var settingsList = await GetSettingsAsync(entityName, entityId, "");
+ var settingsList = new List();
foreach (KeyValuePair kvp in settings)
{
- string value = kvp.Value;
- bool modified = false;
- bool isprivate = false;
+ var setting = new Setting();
+ setting.SettingId = 0;
+ setting.EntityName = entityName;
+ setting.EntityId = entityId;
+ setting.SettingName = kvp.Key;
+ setting.SettingValue = kvp.Value;
// manage settings modified with SetSetting method
- if (value.StartsWith("[Private]"))
+ if (setting.SettingValue.StartsWith("[Private]"))
{
- modified = true;
- isprivate = true;
- value = value.Substring(9);
+ setting.SettingValue = setting.SettingValue.Substring(9);
+ setting.IsPrivate = true;
+ setting.SettingId = -1; // indicates IsPrivate was explicitly set
}
- if (value.StartsWith("[Public]"))
+ if (setting.SettingValue.StartsWith("[Public]"))
{
- modified = true;
- isprivate = false;
- value = value.Substring(8);
+ setting.SettingValue = setting.SettingValue.Substring(8);
+ setting.IsPrivate = false;
+ setting.SettingId = -1; // indicates IsPrivate was explicitly set
}
- Setting setting = settingsList.FirstOrDefault(item => item.SettingName.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase));
- if (setting == null)
- {
- setting = new Setting();
- setting.EntityName = entityName;
- setting.EntityId = entityId;
- setting.SettingName = kvp.Key;
- setting.SettingValue = value;
- setting.IsPrivate = isprivate;
- setting = await AddSettingAsync(setting);
- }
- else
- {
- if (setting.SettingValue != value || (modified && setting.IsPrivate != isprivate))
- {
- setting.SettingValue = value;
- setting.IsPrivate = isprivate;
- setting = await UpdateSettingAsync(setting);
- }
- }
+ settingsList.Add(setting);
}
+
+ await PutJsonAsync>($"{Apiurl}/{entityName}/{entityId}", settingsList);
}
public async Task AddOrUpdateSettingAsync(string entityName, int entityId, string settingName, string settingValue, bool isPrivate)
diff --git a/Oqtane.Client/Services/ThemeService.cs b/Oqtane.Client/Services/ThemeService.cs
index 4fd1d9eb..942ee7e9 100644
--- a/Oqtane.Client/Services/ThemeService.cs
+++ b/Oqtane.Client/Services/ThemeService.cs
@@ -17,8 +17,9 @@ namespace Oqtane.Services
///
/// Returns a list of available themes
///
+ ///
///
- Task> GetThemesAsync();
+ Task> GetThemesAsync(int siteId);
///
/// Returns a specific theme
@@ -69,9 +70,10 @@ namespace Oqtane.Services
///
/// Deletes a theme
///
- ///
+ ///
+ ///
///
- Task DeleteThemeAsync(string themeName);
+ Task DeleteThemeAsync(int themeId, int siteId);
///
/// Creates a new theme
@@ -103,9 +105,9 @@ namespace Oqtane.Services
private string ApiUrl => CreateApiUrl("Theme");
- public async Task> GetThemesAsync()
+ public async Task> GetThemesAsync(int siteId)
{
- List themes = await GetJsonAsync>(ApiUrl);
+ List themes = await GetJsonAsync>($"{ApiUrl}?siteid={siteId}");
return themes.OrderBy(item => item.Name).ToList();
}
public async Task GetThemeAsync(int themeId, int siteId)
@@ -139,9 +141,9 @@ namespace Oqtane.Services
await PutJsonAsync($"{ApiUrl}/{theme.ThemeId}", theme);
}
- public async Task DeleteThemeAsync(string themeName)
+ public async Task DeleteThemeAsync(int themeId, int siteId)
{
- await DeleteAsync($"{ApiUrl}/{themeName}");
+ await DeleteAsync($"{ApiUrl}/{themeId}?siteid={siteId}");
}
public async Task CreateThemeAsync(Theme theme)
diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs
index ca5203e4..049a93d5 100644
--- a/Oqtane.Client/Services/UserService.cs
+++ b/Oqtane.Client/Services/UserService.cs
@@ -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
{
@@ -96,11 +97,18 @@ namespace Oqtane.Services
Task VerifyEmailAsync(User user, string token);
///
- /// Trigger a forgot-password e-mail for this .
+ /// Trigger a forgot-password e-mail.
///
- ///
+ ///
///
- Task ForgotPasswordAsync(User user);
+ Task ForgotPasswordAsync(string username);
+
+ ///
+ /// Trigger a username reminder e-mail.
+ ///
+ ///
+ ///
+ Task ForgotUsernameAsync(string email);
///
/// Reset the password of this
@@ -146,17 +154,6 @@ namespace Oqtane.Services
///
Task GetPersonalAccessTokenAsync();
- ///
- /// Link an external login with a local user account
- ///
- /// The we're verifying
- /// A Hash value in the URL which verifies this user got the e-mail (containing this token)
- /// External Login provider type
- /// External Login provider key
- /// External Login provider display name
- ///
- Task LinkUserAsync(User user, string token, string type, string key, string name);
-
///
/// Get password requirements for site
///
@@ -172,6 +169,62 @@ namespace Oqtane.Services
/// Indicates if new users should be notified by email
///
Task> ImportUsersAsync(int siteId, int fileId, bool notify);
+
+ ///
+ /// Get passkeys for a user
+ ///
+ ///
+ ///
+ Task> GetPasskeysAsync(int userId);
+
+ ///
+ /// Update a user passkey
+ ///
+ ///
+ ///
+ Task UpdatePasskeyAsync(UserPasskey passkey);
+
+ ///
+ /// Delete a user passkey
+ ///
+ ///
+ ///
+ ///
+ Task DeletePasskeyAsync(int userId, byte[] credentialId);
+
+ ///
+ /// Get logins for a user
+ ///
+ ///
+ ///
+ Task> GetLoginsAsync(int userId);
+
+ ///
+ /// Link an external login with a local user account
+ ///
+ /// The we're verifying
+ /// A Hash value in the URL which verifies this user got the e-mail (containing this token)
+ /// External Login provider type
+ /// External Login provider key
+ /// External Login provider display name
+ ///
+ Task AddLoginAsync(User user, string token, string type, string key, string name);
+
+ ///
+ /// Delete a user login
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task DeleteLoginAsync(int userId, string provider, string key);
+
+ ///
+ /// Send a login link
+ ///
+ ///
+ ///
+ Task SendLoginLinkAsync(string email, string returnurl);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -218,7 +271,7 @@ namespace Oqtane.Services
public async Task LoginUserAsync(User user, bool setCookie, bool isPersistent)
{
- return await PostJsonAsync($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user);
+ return await PostJsonAsync($"{Apiurl}/signin?setcookie={setCookie}&persistent={isPersistent}", user);
}
public async Task LogoutUserAsync(User user)
@@ -236,9 +289,14 @@ namespace Oqtane.Services
return await PostJsonAsync($"{Apiurl}/verify?token={token}", user);
}
- public async Task ForgotPasswordAsync(User user)
+ public async Task ForgotPasswordAsync(string username)
{
- await PostJsonAsync($"{Apiurl}/forgot", user);
+ return await GetJsonAsync($"{Apiurl}/forgotpassword/{WebUtility.UrlEncode(username)}");
+ }
+
+ public async Task ForgotUsernameAsync(string email)
+ {
+ return await GetJsonAsync($"{Apiurl}/forgotusername/{WebUtility.UrlEncode(email)}");
}
public async Task ResetPasswordAsync(User user, string token)
@@ -271,11 +329,6 @@ namespace Oqtane.Services
return await GetStringAsync($"{Apiurl}/personalaccesstoken");
}
- public async Task LinkUserAsync(User user, string token, string type, string key, string name)
- {
- return await PostJsonAsync($"{Apiurl}/link?token={token}&type={type}&key={key}&name={name}", user);
- }
-
public async Task GetPasswordRequirementsAsync(int siteId)
{
var requirements = await GetJsonAsync>($"{Apiurl}/passwordrequirements/{siteId}");
@@ -302,5 +355,40 @@ namespace Oqtane.Services
{
return await PostJsonAsync>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}¬ify={notify}", null);
}
+
+ public async Task> GetPasskeysAsync(int userId)
+ {
+ return await GetJsonAsync>($"{Apiurl}/passkey?id={userId}");
+ }
+
+ public async Task UpdatePasskeyAsync(UserPasskey passkey)
+ {
+ return await PutJsonAsync($"{Apiurl}/passkey", passkey);
+ }
+
+ public async Task DeletePasskeyAsync(int userId, byte[] credentialId)
+ {
+ await DeleteAsync($"{Apiurl}/passkey?id={userId}&credential={Base64Url.EncodeToString(credentialId)}");
+ }
+
+ public async Task> GetLoginsAsync(int userId)
+ {
+ return await GetJsonAsync>($"{Apiurl}/login?id={userId}");
+ }
+
+ public async Task AddLoginAsync(User user, string token, string type, string key, string name)
+ {
+ return await PostJsonAsync($"{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}");
+ }
+
+ public async Task SendLoginLinkAsync(string email, string returnurl)
+ {
+ return await GetJsonAsync($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}/{WebUtility.UrlEncode(returnurl)}");
+ }
}
}
diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor
index f38a66c3..762e457a 100644
--- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor
+++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor
@@ -15,13 +15,13 @@
@if (PageState.EditMode)
{
-