Compare commits

...

151 Commits

Author SHA1 Message Date
Shaun Walker
1fb78a457f Merge pull request #5921 from oqtane/master
10.0.2 Release
2025-12-23 13:21:30 -05:00
Shaun Walker
82cb7a8c02 Merge pull request #5920 from oqtane/dev
10.0.2 Release
2025-12-23 13:21:11 -05:00
Shaun Walker
cae61ab701 Update README.md 2025-12-23 13:20:09 -05:00
Shaun Walker
975c1955f2 Merge pull request #5919 from sbwalker/dev
update azuredeploy version
2025-12-23 13:16:27 -05:00
sbwalker
fc2a8cb9dd update azuredeploy version 2025-12-23 13:16:12 -05:00
Shaun Walker
e2d9aaaa0f Merge pull request #5918 from sbwalker/dev
fix #5916 - PostgreSQL failing to install on .NET 10
2025-12-23 12:09:35 -05:00
sbwalker
aca70dd6c7 fix #5916 - PostgreSQL failing to install on .NET 10 2025-12-23 12:09:16 -05:00
Shaun Walker
5a1b3a7017 Merge pull request #5915 from sbwalker/dev
synchronize static assets with .NET MAUI
2025-12-22 16:04:17 -05:00
sbwalker
6733299290 synchronize static assets with .NET MAUI 2025-12-22 16:04:00 -05:00
Shaun Walker
e11e021750 Merge pull request #5914 from sbwalker/dev
add ability for menu component to support arbitrary attributes
2025-12-22 08:38:06 -05:00
sbwalker
0ad5bd2335 add ability for menu component to support arbitrary attributes 2025-12-22 08:37:45 -05:00
Shaun Walker
255764a4ac Merge pull request #5912 from zyhfish/task/fix-5911
Fix #5911: let redirect to mapped url works in load balance env.
2025-12-22 07:41:16 -05:00
Ben
7330d5b2a7 Fix #5911: let redirect to mapped url works in load balance env. 2025-12-22 15:17:40 +08:00
Shaun Walker
5d3e507672 Merge pull request #5906 from sbwalker/dev
bump version to 10.0.2
2025-12-19 15:51:37 -05:00
sbwalker
15ee2c9bcb bump version to 10.0.2 2025-12-19 15:51:21 -05:00
Shaun Walker
e1163b7ab1 Merge pull request #5905 from sbwalker/dev
allow menu component to be extensible
2025-12-19 15:35:52 -05:00
sbwalker
96bea78424 allow menu component to be extensible 2025-12-19 15:35:35 -05:00
Shaun Walker
8ba67a63ba Merge pull request #5904 from sbwalker/dev
add url mapping referrer
2025-12-19 15:06:22 -05:00
sbwalker
8120db84f4 add url mapping referrer 2025-12-19 15:06:06 -05:00
Shaun Walker
4e6a6afaab Merge pull request #5903 from sbwalker/dev
expand size of page name
2025-12-19 14:26:03 -05:00
sbwalker
417a6bf226 expand size of page name 2025-12-19 14:25:44 -05:00
Shaun Walker
5e5d91bd93 Merge pull request #5902 from sbwalker/dev
handle case sensitivity for entity names and permission names
2025-12-19 10:56:32 -05:00
sbwalker
5c536aafc2 handle case sensitivity for entity names and permission names 2025-12-19 10:56:08 -05:00
Shaun Walker
15efe75aa8 Merge pull request #5901 from sbwalker/dev
fix #5897 - allow SQLite to drop columns, remove deprecated columns, and handle upgrade logic
2025-12-19 09:04:14 -05:00
sbwalker
a10575bfc3 fix #5897 - allow SQLite to drop columns, remove deprecated columns, and handle upgrade logic 2025-12-19 09:03:44 -05:00
Shaun Walker
57e26d6156 Merge pull request #5899 from sbwalker/dev
login improvements
2025-12-18 16:01:05 -05:00
sbwalker
1682a123b4 login improvements 2025-12-18 16:00:46 -05:00
Shaun Walker
13064cdb26 Merge pull request #5895 from sbwalker/dev
enable EnhancedNavigation by default
2025-12-17 15:51:38 -05:00
sbwalker
f74eda274a enable EnhancedNavigation by default 2025-12-17 15:51:21 -05:00
Shaun Walker
7dee55ce30 Merge pull request #5892 from sbwalker/dev
module migration issues should not prevent the framework from starting up
2025-12-16 14:34:03 -05:00
sbwalker
8113c82da8 module migration issues should not prevent the framework from starting up 2025-12-16 14:33:42 -05:00
Shaun Walker
2d684d23d7 Merge pull request #5887 from oqtane/master
Merge pull request #5886 from oqtane/dev
2025-12-15 14:06:20 -05:00
Shaun Walker
33da5809e3 Merge pull request #5886 from oqtane/dev
10.0.1 Release
2025-12-15 14:05:48 -05:00
Shaun Walker
68a6e6862e Update README.md 2025-12-15 14:02:25 -05:00
Shaun Walker
07fb15d5be Merge pull request #5885 from sbwalker/dev
update azuredeploy.json
2025-12-15 13:59:47 -05:00
sbwalker
0f791253ba update azuredeploy.json 2025-12-15 13:59:30 -05:00
Shaun Walker
828d070194 Merge pull request #5884 from sbwalker/dev
increase width of login component
2025-12-15 11:29:09 -05:00
sbwalker
0d5bb3f3b3 increase width of login component 2025-12-15 11:28:51 -05:00
Shaun Walker
32de1ca511 Merge pull request #5883 from sbwalker/dev
limit management of user settings to host users
2025-12-15 11:16:57 -05:00
sbwalker
f5f00c51c1 limit management of user settings to host users 2025-12-15 11:16:37 -05:00
Shaun Walker
1305125390 Merge pull request #5882 from sbwalker/dev
remove unnecessary comment
2025-12-15 10:47:40 -05:00
sbwalker
c539f41ebf remove unnecessary comment 2025-12-15 10:47:01 -05:00
Shaun Walker
075e754830 Merge pull request #5881 from sbwalker/dev
use EmailConfirmationToken (which is valid for 10 minutes)
2025-12-15 10:43:28 -05:00
sbwalker
87fd9dd000 use EmailConfirmationToken (which is valid for 10 minutes) 2025-12-15 10:43:11 -05:00
Shaun Walker
e34321f727 Merge pull request #5880 from sbwalker/dev
improve new API method signatures
2025-12-15 10:29:22 -05:00
sbwalker
a48dff4a85 improve new API method signatures 2025-12-15 10:29:03 -05:00
Shaun Walker
576d3d0b56 Merge pull request #5879 from sbwalker/dev
relocate the GetUser() call in App.razor so that it is not included in the Site cache
2025-12-15 09:10:41 -05:00
sbwalker
640c2cee00 Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2025-12-15 09:02:32 -05:00
sbwalker
1958787185 relocate the GetUser() call in App.razor so that it is not included in the Site cache 2025-12-15 09:02:25 -05:00
Shaun Walker
073e1ac13a Merge pull request #5876 from leigh-pointer/CurrentPageUser
Add pagination state to Pager in Index.razor
2025-12-15 08:25:24 -05:00
Shaun Walker
c0eacd0d6b Merge pull request #5878 from sbwalker/dev
refactor new Forgot Username and Login Link methods
2025-12-15 08:24:36 -05:00
sbwalker
7938eaf123 refactor new Forgot Username and Login Link methods 2025-12-15 08:23:41 -05:00
Leigh Pointer
b4f8896713 Add pagination state to Pager in Index.razor
When clicking the Roles or Edit button, returning would load the first page.
Pager now tracks and updates the current page using a new _page field and the CurrentPage/OnPageChange parameters. This improves pagination handling and user experience by persisting the current page state.
2025-12-15 10:21:32 +01:00
Shaun Walker
c418ddf240 Merge pull request #5875 from sbwalker/dev
use a more complex token for login links
2025-12-14 17:08:45 -05:00
sbwalker
6c6b36f3da use a more complex token for login links 2025-12-14 17:08:19 -05:00
Shaun Walker
c0c71251ab Merge pull request #5873 from leigh-pointer/SupportCustomRole
Enhance tab authorization with role and permission checks #5872
2025-12-14 15:15:19 -05:00
Shaun Walker
2685c18798 Merge pull request #5871 from leigh-pointer/AltText
Add AltText/title support to ActionDialog and ActionLink
2025-12-14 15:15:03 -05:00
Shaun Walker
7f914271ed Merge pull request #5870 from leigh-pointer/RendarBoundry
Add null checks for RenderModeBoundary in ModuleBase methods
2025-12-14 15:14:44 -05:00
Shaun Walker
7b37cc3c82 Merge pull request #5874 from sbwalker/dev
added support for Forgot Username and Use Login Link
2025-12-14 15:14:25 -05:00
sbwalker
ec2afd5f03 added support for Forgot Username and Use Login Link 2025-12-14 15:13:53 -05:00
Leigh Pointer
e62268af2e Update TabStrip.razor
The authorization flow is:
•	Host tabs: Only Host (Admin blocked by Step 1)
•	Everything else: Admin bypasses, others check permissions
2025-12-13 21:56:05 +01:00
Leigh Pointer
01ad99b925 Enhance tab authorization with role and permission checks #5872
Add RoleName and PermissionName parameters to TabPanel for fine-grained tab visibility control. Update IsAuthorized logic in TabStrip to prioritize Host/Admin access, then check SecurityAccessLevel, and additionally require specified roles or permissions if provided. Removes redundant Admin/Host checks from the switch statement for clarity.
2025-12-13 18:13:37 +01:00
Leigh Pointer
a33e9d25cc Add AltText/title support to ActionDialog and ActionLink
Introduce optional AltText parameter to ActionDialog and ActionLink components. AltText is now used as the title attribute on rendered buttons and links, providing tooltips for improved accessibility and user experience. All relevant elements, including those in disabled states, now support this enhancement.
2025-12-13 13:04:30 +01:00
Leigh Pointer
a0e45cbea0 Add null checks for RenderModeBoundary in ModuleBase methods
Add null checks to key ModuleBase methods to ensure RenderModeBoundary is available before use. Throw a detailed InvalidOperationException with guidance if it is missing, improving error handling and developer feedback.
2025-12-13 12:55:24 +01:00
Shaun Walker
171314947c Merge pull request #5869 from sbwalker/dev
add null check for User
2025-12-12 15:57:07 -05:00
sbwalker
6b883b3f94 add null check for User 2025-12-12 15:56:50 -05:00
Shaun Walker
c99348650f Merge pull request #5867 from sbwalker/dev
admin dashboard should always use enhanced navigation
2025-12-11 19:29:10 -05:00
sbwalker
011375a081 admin dashboard should always use enhanced navigation 2025-12-11 19:28:50 -05:00
Shaun Walker
38f43c9988 Merge pull request #5866 from sbwalker/dev
update version in Oqtane Application Template nuspec
2025-12-11 15:51:43 -05:00
sbwalker
f459d0503a update version in Oqtane Application Template nuspec 2025-12-11 15:51:27 -05:00
Shaun Walker
c2912a291e Merge pull request #5865 from sbwalker/dev
bump Oqtane version to 10.0.1
2025-12-11 15:27:00 -05:00
sbwalker
53a88e0c9f bump Oqtane version to 10.0.1 2025-12-11 15:26:42 -05:00
Shaun Walker
9cf670bcad Merge pull request #5864 from sbwalker/dev
update nuspec files to .NET SDK 10.0.1
2025-12-11 15:14:01 -05:00
sbwalker
156e7bd3d4 update nuspec files to .NET SDK 10.0.1 2025-12-11 15:13:45 -05:00
Shaun Walker
009829c8f9 Merge pull request #5863 from sbwalker/dev
update to .NET SDK 10.0.1
2025-12-11 15:09:13 -05:00
sbwalker
d7c0b0aaaf update to .NET SDK 10.0.1 2025-12-11 15:08:52 -05:00
Shaun Walker
06071fb7f9 Merge pull request #5857 from sbwalker/dev
move user workload from siterouter to app component to improve performance and 404 handling
2025-12-05 08:40:57 -05:00
sbwalker
a51f87d743 move user workload from siterouter to app component to improve performance and 404 handling 2025-12-05 08:40:30 -05:00
Shaun Walker
12fa6ff4f0 Merge pull request #5855 from sbwalker/dev
remove unique index of TenantId and Name from Site table as site name does not need to be unique. Remove TenantId column from Site table as it is not necessary and should be obtained from the Alias.
2025-12-03 15:29:07 -05:00
sbwalker
23d14c62a5 remove unique index of TenantId and Name from Site table as site name does not need to be unique. Remove TenantId column from Site table as it is not necessary and should be obtained from the Alias. 2025-12-03 15:28:31 -05:00
Shaun Walker
ad993c6180 Merge pull request #5854 from leigh-pointer/refs
Package updates
2025-12-03 13:03:23 -05:00
Shaun Walker
e99ce3ac7b Merge pull request #5853 from zyhfish/task/fix-5852
Fix #5852: clear the cache after import content.
2025-12-03 13:03:04 -05:00
Leigh Pointer
270b447fbd Package updates
Radzen, Swashbuckle
Added the Bold tool to the Radzen editor.
2025-12-03 11:19:10 +01:00
Leigh Pointer
1c55a74ff1 Merge remote-tracking branch 'upstream/dev' into dev 2025-12-03 11:02:09 +01:00
Ben
47f42747cb clean the usage. 2025-12-03 09:09:43 +08:00
Ben
86a3f67871 Fix #5852: clear the cache after import content. 2025-12-03 09:08:01 +08:00
Shaun Walker
29b87f809f Merge pull request #5848 from W6HBR/dev
Fix SMTPRelay condition for sender email validation
2025-12-02 11:27:44 -05:00
Shaun Walker
8bd63fdc61 Merge pull request #5850 from zyhfish/task/fix-5849
Fix #5849: correct resources key.
2025-12-02 11:25:03 -05:00
Ben
cf88347c3d Fix #5849: correct resources key. 2025-12-02 09:23:43 +08:00
Jon Welfringer
171f9c84a0 Fix SMTPRelay condition for sender email validation
Prior change was leaving sender null and not properly setting "From" address when used in a relay configuration. This caused emails to go to the deleted state and not be delivered.
2025-12-01 16:36:19 -08:00
Shaun Walker
a6069e572d Merge pull request #5782 from zyhfish/task/display-missing-service-error
Display error message when missing injected services.
2025-12-01 15:45:28 -05:00
Shaun Walker
16a13a6c01 Merge pull request #5845 from Amazing-Software-Solutions/dev
Added Style Paramater to RichTextEditor
2025-12-01 15:45:17 -05:00
vnetonline
ac31cd3f41 Merge branch 'oqtane:dev' into dev 2025-11-29 15:12:07 +11:00
vnetonline
321fe2954e Added Style Paramater to RichTextEditor to remove the margin-bottom: 50px; if the developer wishes 2025-11-29 15:10:58 +11:00
Shaun Walker
e2f174e0b5 Merge pull request #5843 from sbwalker/dev
update to .NET 10 PostgreSQL provider
2025-11-25 14:50:00 -05:00
sbwalker
50c085fe65 update to .NET 10 PostgreSQL provider 2025-11-25 14:49:45 -05:00
Shaun Walker
4e63c9ce9d Merge pull request #5842 from sbwalker/dev
add Enhanced Navigation option in Site Settings
2025-11-25 14:44:10 -05:00
sbwalker
fb6e8bb233 add Enhanced Navigation option in Site Settings 2025-11-25 14:43:51 -05:00
Shaun Walker
44103c1311 Merge pull request #5840 from zyhfish/task/fix-5839
Fix #5839: do not send confirmation email  to deleted users.
2025-11-25 08:52:41 -05:00
Ben
6ef6e6aac8 Fix #5839: do not send confirmation email to deleted users. 2025-11-25 11:21:20 +08:00
Shaun Walker
9499012825 Merge pull request #5834 from leigh-pointer/baseNull
Update ReplaceTokens on ModuleBase
2025-11-24 10:18:59 -05:00
Shaun Walker
6af73873d7 Merge pull request #5837 from zyhfish/task/fix-5836
Fix #5836: update the setting by check existing first.
2025-11-24 10:18:47 -05:00
Ben
0a04035b2f Fix #5836: update the setting by check existing first. 2025-11-24 18:14:14 +08:00
Leigh Pointer
1e3c176ddf Update ReplaceTokens on ModuleBase
Check for Contents == null
2025-11-22 13:31:04 +01:00
Leigh Pointer
f5bb9a934c Merge remote-tracking branch 'upstream/dev' into dev 2025-11-22 13:26:18 +01:00
Shaun Walker
476cf7c080 Merge pull request #5832 from zyhfish/task/fix-5649
Fix #5649: handle not found request.
2025-11-21 10:26:59 -05:00
Ben
1279c30fbb Fix #5649: check path by internal api. 2025-11-21 18:34:22 +08:00
Ben
012b7ba6ed Fix #5649: handle not found request. 2025-11-20 19:03:26 +08:00
Shaun Walker
e08c033e76 Merge pull request #5831 from sbwalker/dev
initialize the Owner name when using an Oqtane Application and creating new modules or themes
2025-11-19 10:49:38 -05:00
sbwalker
dafbae7237 initialize the Owner name when using an Oqtane Application and creating new modules or themes 2025-11-19 10:47:38 -05:00
Shaun Walker
31b8080a3d Update README.md 2025-11-17 11:24:49 -05:00
Shaun Walker
28c7617227 Refine instructions for submitting pull requests 2025-11-17 11:22:40 -05:00
Shaun Walker
7f7c53dabe Fix command reference in README for uninstalling template
Corrected the command reference from .NERT to .NET CLI.
2025-11-17 11:20:47 -05:00
Shaun Walker
950a9bf2fa Update README.md 2025-11-17 11:20:08 -05:00
Shaun Walker
f0d4a416be Clarify cloning instructions for Oqtane repository
Updated instructions for cloning Oqtane source code.
2025-11-17 09:33:54 -05:00
Shaun Walker
708d79ffaf Fix filename extension in README for solution file 2025-11-17 09:29:45 -05:00
Shaun Walker
583bd3b511 Update README.md 2025-11-17 09:29:06 -05:00
Leigh Pointer
528cbde7e5 Merge remote-tracking branch 'upstream/dev' into dev 2025-11-13 00:31:56 +01:00
Leigh Pointer
11284f0285 Merge remote-tracking branch 'upstream/dev' into dev 2025-11-12 12:07:30 +01:00
Leigh Pointer
dc9d4a1938 Merge remote-tracking branch 'upstream/dev' into dev 2025-11-07 12:57:35 +01:00
Ben
e58ee4e5b1 Display error message when missing injected services. 2025-11-07 14:40:14 +08:00
Leigh Pointer
4339833aa3 Merge remote-tracking branch 'upstream/dev' into dev 2025-11-01 09:29:35 +01:00
Leigh Pointer
a06b1becc5 Merge remote-tracking branch 'upstream/dev' into dev 2025-10-27 11:24:37 +01:00
Leigh Pointer
a59f2e7ca6 Merge remote-tracking branch 'upstream/dev' into dev 2025-10-06 14:39:25 +02:00
Leigh Pointer
b1cc0ffc13 Fixed the cropped glow on the Oqtane logo 2025-10-03 18:38:07 +02:00
Leigh Pointer
4e49092434 Merge remote-tracking branch 'upstream/dev' into dev 2025-10-03 18:33:52 +02:00
Leigh Pointer
026d716ece Merge remote-tracking branch 'upstream/dev' into dev 2025-09-26 12:33:37 +02:00
Leigh Pointer
a85ae69ed1 Merge remote-tracking branch 'upstream/dev' into dev 2025-09-20 10:49:28 +02:00
Leigh Pointer
33f525dbda Merge remote-tracking branch 'upstream/dev' into dev 2025-09-18 15:07:20 +02:00
Leigh Pointer
4ba7e034b7 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-31 12:56:43 +02:00
Leigh Pointer
74a5fb656e Merge remote-tracking branch 'upstream/dev' into dev 2025-08-22 22:31:19 +02:00
Leigh Pointer
bc5ce74925 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-20 18:30:09 +02:00
Leigh Pointer
338c652635 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-20 12:53:04 +02:00
Leigh Pointer
4834761f64 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-15 18:58:21 +02:00
Leigh Pointer
d0ef5d0fe3 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-13 12:22:41 +02:00
Leigh Pointer
7d9b102ec4 Merge remote-tracking branch 'upstream/dev' into dev 2025-08-06 10:44:26 +02:00
Leigh Pointer
5b2dff254f Merge remote-tracking branch 'upstream/dev' into dev 2025-07-31 17:10:52 +02:00
Leigh Pointer
986c9d9f72 Merge remote-tracking branch 'upstream/dev' into dev 2025-07-30 12:33:50 +02:00
Leigh Pointer
4db37059cd Merge remote-tracking branch 'upstream/dev' into dev 2025-07-23 14:50:24 +02:00
Leigh Pointer
378c68be4b Merge remote-tracking branch 'upstream/dev' into dev 2025-07-22 07:40:38 +02:00
Leigh Pointer
ebca580a0b Merge remote-tracking branch 'upstream/dev' into dev 2025-07-09 01:12:17 +02:00
Leigh Pointer
413df647d3 Merge remote-tracking branch 'upstream/dev' into dev 2025-06-13 19:55:43 +02:00
Leigh Pointer
a1011ed709 Merge remote-tracking branch 'upstream/dev' into dev 2025-06-05 17:03:48 +02:00
Leigh Pointer
71be6d4ded Merge remote-tracking branch 'upstream/dev' into dev 2025-06-05 15:46:15 +02:00
Leigh Pointer
f586401a14 Merge remote-tracking branch 'upstream/dev' into dev 2025-06-03 15:25:00 +02:00
Leigh Pointer
fa384cb6f3 Merge remote-tracking branch 'upstream/dev' into dev 2025-05-28 19:41:13 +02:00
Leigh Pointer
05db1bcbfb Merge remote-tracking branch 'upstream/dev' into dev 2025-05-20 11:24:04 +02:00
Leigh Pointer
bdd6d9781c Merge remote-tracking branch 'upstream/dev' into dev 2025-05-19 11:24:47 +02:00
Leigh Pointer
d23a5ad91b Merge remote-tracking branch 'upstream/dev' into dev 2025-05-16 12:49:44 +02:00
Leigh Pointer
a50e179744 Merge remote-tracking branch 'upstream/dev' into dev 2025-05-15 11:52:15 +02:00
Leigh Pointer
874d9f32a9 Updated fBootstrap for Blazor theme 2025-05-02 12:18:45 +02:00
100 changed files with 2040 additions and 990 deletions

View File

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

View File

@@ -12,10 +12,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.0" />
<PackageReference Include="Oqtane.Client" Version="10.0.2" />
</ItemGroup>
</Project>

View File

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

View File

@@ -22,9 +22,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
@@ -33,7 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.0" />
<PackageReference Include="Oqtane.Server" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="10.0.0" />
<PackageReference Include="Oqtane.Shared" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@@ -13,7 +13,7 @@
{
string url = NavigateUrl(p.Path);
<div class="col-md-2 mx-auto text-center my-3">
<NavLink class="nav-link text-body" href="@url" Match="NavLinkMatch.All">
<NavLink class="nav-link text-body" href="@url" Match="NavLinkMatch.All" @attributes="_attributes">
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>
<div class="lead">@((MarkupString)SharedLocalizer[p.Name].ToString().Replace(" ", "<br />"))</div>
</NavLink>
@@ -24,13 +24,19 @@
}
@code {
private List<Page> _pages;
Dictionary<string, object> _attributes { get; set; } = new();
private List<Page> _pages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string RenderMode => RenderModes.Static;
protected override void OnInitialized()
{
if (PageState.RenderMode == RenderModes.Static && !PageState.Site.EnhancedNavigation)
{
_attributes.Add("data-enhance-nav", "true"); // Admin Dashboard utilizes enhanced navigation
}
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
if (admin != null)
{

View File

@@ -14,93 +14,134 @@
}
else
{
@if (!twofactor)
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<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 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 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>
@if (!_alwaysremember)
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@switch (_action)
{
case "Login":
@if (_allowexternallogin)
{
<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 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 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>
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" @ref="password" 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>
@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">Stay Signed In?</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="CancelLogin">@SharedLocalizer["Cancel"]</button>
</div>
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="@(() => SetAction("ForgotPassword"))">@Localizer["ForgotPassword"]</button>
}
<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-secondary col-12 mt-4" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (_allowloginlink)
{
<hr class="app-rule mt-3" />
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="@(() => SetAction("LoginLink"))">@Localizer["UseLoginLink"]</button>
}
@if (_allowpasskeys)
{
<hr class="app-rule mt-3" />
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="Passkey">@Localizer["Passkey"]</button>
<button type="button" class="btn btn-primary col-12 mt-2" @onclick="PasskeyLogin">@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>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
break;
case "ForgotPassword":
<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" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
</div>
<div class="btn-group mt-4 col-12" role="group">
<button type="button" class="btn btn-primary col-6" @onclick="ForgotPassword">@SharedLocalizer["Send"]</button>
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
</div>
<button type="button" class="btn btn-secondary col-12 mt-4" @onclick="@(() => SetAction("ForgotUsername"))">@Localizer["ForgotUsername"]</button>
break;
case "ForgotUsername":
<div class="form-group text-center">
<Label Class="control-label" For="email" HelpText="Please enter your Email" ResourceKey="Email">Email:</Label>
<input id="email" type="text" class="form-control" placeholder="@Localizer["Email.Placeholder"]" @bind="@_email" required />
</div>
<div class="btn-group mt-4 col-12" role="group">
<button type="button" class="btn btn-primary col-6" @onclick="ForgotUsername">@SharedLocalizer["Send"]</button>
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
</div>
break;
case "LoginLink":
<div class="form-group text-center">
<Label Class="control-label" For="email" HelpText="Please enter your Email" ResourceKey="Email">Email:</Label>
<input id="email" type="text" class="form-control" placeholder="@Localizer["Email.Placeholder"]" @bind="@_email" required />
</div>
<div class="btn-group mt-4 col-12" role="group">
<button type="button" class="btn btn-primary col-6" @onclick="LoginLink">@SharedLocalizer["Send"]</button>
<button type="button" class="btn btn-secondary col-6" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
</div>
break;
case "TwoFactor":
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<div class="btn-group mt-4 col-12" role="group">
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="@(() => SetAction("Login"))">@SharedLocalizer["Cancel"]</button>
</div>
break;
}
</div>
</form>
}
@code {
private bool _allowsitelogin = true;
private string _action = "Login";
private bool _allowexternallogin = false;
private bool _allowsitelogin = true;
private bool _allowloginlink = false;
private bool _allowpasskeys = false;
private string _returnurl = string.Empty;
private ElementReference login;
private bool validated = false;
private bool twofactor = false;
private string _username = string.Empty;
private ElementReference username;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private ElementReference password;
private bool _remember = false;
private bool _alwaysremember = false;
private string _code = string.Empty;
private string _registerurl = string.Empty;
private string _email = string.Empty;
private string _code = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override bool? Prerender => true;
@@ -116,6 +157,7 @@ else
{
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_allowloginlink = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LoginLink", "false"));
_allowpasskeys = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:Passkeys", "false"));
_alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
@@ -128,6 +170,9 @@ else
_registerurl = NavigateUrl("register");
}
// PageState.ReturnUrl is not specified if user navigated directly to login page
_returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name"))
@@ -175,7 +220,7 @@ else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning);
}
}
}
@@ -186,6 +231,48 @@ else
}
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
switch (_action)
{
case "Login":
await Login();
break;
}
}
}
private void SetAction(string action)
{
_action = action;
_username = "";
_password = "";
_email = "";
ClearModuleMessage();
StateHasChanged();
}
private void ExternalLogin()
{
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(_returnurl)), true);
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
private async Task Login()
{
try
@@ -197,7 +284,7 @@ else
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
if (!twofactor)
if (_action == "Login")
{
_remember = _alwaysremember || _remember;
user = await UserService.LoginUserAsync(user, hybrid, _remember);
@@ -211,20 +298,17 @@ else
{
await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress);
// return url is not specified if user navigated directly to login page
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(returnurl, true));
NavigationManager.NavigateTo(NavigateUrl(_returnurl, true));
}
else
{
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(returnurl) };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(_returnurl) };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
@@ -233,13 +317,13 @@ else
{
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
{
twofactor = true;
_action = "TwoFactor";
validated = false;
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
}
else
{
if (!twofactor)
if (_action != "TwoFactor")
{
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
@@ -264,23 +348,30 @@ else
}
}
private void Cancel()
private void CancelLogin()
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
NavigationManager.NavigateTo(_returnurl);
}
private async Task Forgot()
private async Task PasskeyLogin()
{
// 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 = _returnurl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
private async Task ForgotPassword()
{
try
{
if (_username != string.Empty)
if (!string.IsNullOrEmpty(_username))
{
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
if (user != null)
if (await UserService.ForgotPasswordAsync(_username))
{
await UserService.ForgotPasswordAsync(user);
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info);
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
}
else
{
@@ -289,10 +380,8 @@ else
}
else
{
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
StateHasChanged();
}
catch (Exception ex)
{
@@ -301,51 +390,62 @@ else
}
}
private void Reset()
private async Task ForgotUsername()
{
twofactor = false;
_username = "";
_password = "";
ClearModuleMessage();
StateHasChanged();
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
try
{
await Login();
if (!string.IsNullOrEmpty(_email))
{
if (await UserService.ForgotUsernameAsync(_email))
{
AddModuleMessage(Localizer["Message.ForgotUsername"], MessageType.Info);
await logger.LogInformation(LogFunction.Security, "Username Reminder Notification Sent For Email {Email}", _email);
}
else
{
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Sending Username Reminder {Error}", ex.Message);
AddModuleMessage(Localizer["Error.ForgotUsername"], MessageType.Error);
}
}
private void TogglePassword()
private async Task LoginLink()
{
if (_passwordtype == "password")
try
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
if (!string.IsNullOrEmpty(_email))
{
if (await UserService.SendLoginLinkAsync(_email, _returnurl))
{
AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info);
await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email);
}
else
{
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
await logger.LogError(ex, "Error Sending Login Link {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SendLoginLink"], MessageType.Error);
}
}
private void ExternalLogin()
{
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"))
@@ -358,8 +458,7 @@ else
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 };
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);
}
@@ -377,18 +476,28 @@ else
return;
}
if (firstRender && PageState.User == null && _allowsitelogin)
if (firstRender && PageState.User == null && _allowsitelogin && _action == "Login")
{
if (!string.IsNullOrEmpty(username.Id)) // ensure username is visible in UI
if (string.IsNullOrEmpty(_username))
{
await username.FocusAsync();
if (!string.IsNullOrEmpty(username.Id)) // ensure username is visible in UI
{
await username.FocusAsync();
}
}
else
{
if (!string.IsNullOrEmpty(password.Id)) // ensure password is visible in UI
{
await password.FocusAsync();
}
}
}
// redirect logged in user to specified page
if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
NavigationManager.NavigateTo(_returnurl);
}
}
}

View File

@@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase
@using System.Text.RegularExpressions
@using System.Reflection
@inject NavigationManager NavigationManager
@inject IModuleDefinitionService ModuleDefinitionService
@inject IModuleService ModuleService
@@ -97,6 +98,16 @@
{
AddModuleMessage(Localizer["Info.Module.Development"], MessageType.Info);
}
else
{
var entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name;
if (entryAssemblyName.EndsWith(".Oqtane"))
{
// Oqtane Application assemblies end with .Server.Oqtane or .Client.Oqtane
string[] segments = entryAssemblyName.Split('.');
_owner = string.Join(".", segments, 0, segments.Length - 2);
}
}
}
protected override async Task OnParametersSetAsync()

View File

@@ -16,7 +16,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
<input id="name" class="form-control" @bind="@_name" maxlength="100" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@@ -81,21 +81,9 @@
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -110,27 +98,6 @@
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="personalizable" HelpText="Select whether you would like users to be able to personalize this page with their own content" ResourceKey="Personalizable">Personalizable? </Label>
<div class="col-sm-9">
@@ -141,15 +108,8 @@
</div>
</div>
</div>
<Section Name="Appearance" ResourceKey="Appearance" Heading=@Localizer["Appearance.Name"]>
<Section Name="Theme" Heading="Theme" ResourceKey="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
@@ -181,6 +141,49 @@
</div>
</div>
</Section>
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" ResourceKey="PageContent" Heading=@Localizer["PageContent.Heading"]>
<div class="container">
<div class="row mb-1 align-items-center">

View File

@@ -22,7 +22,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
<input id="name" class="form-control" @bind="@_name" maxlength="100" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@@ -98,21 +98,9 @@
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -127,27 +115,6 @@
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="personalizable" HelpText="Select whether you would like users to be able to personalize this page with their own content" ResourceKey="Personalizable">Personalizable? </Label>
<div class="col-sm-9">
@@ -158,14 +125,8 @@
</div>
</div>
</div>
<Section Name="Appearance" ResourceKey="Appearance" Heading="Appearance">
<Section Name="Theme" ResourceKey="Theme" Heading="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
@@ -200,6 +161,49 @@
</div>
</div>
</Section>
<Section Name="Appearance" ResourceKey="Appearance" Heading="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
<div class="container">
<div class="row mb-1 align-items-center">

View File

@@ -411,6 +411,18 @@
</select>
</div>
</div>
@if (_rendermode == RenderModes.Static)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enhancednavigation" HelpText="Indicates if enhanced navigation should be used with static rendering" ResourceKey="EnhancedNavigation">Enhanced Navigation: </Label>
<div class="col-sm-9">
<select id="enhancednavigation" class="form-select" @bind="@_enhancednavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The render mode for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<div class="col-sm-9">
@@ -537,6 +549,7 @@
private string _defaultalias;
private string _rendermode = RenderModes.Interactive;
private string _enhancednavigation = "True";
private string _runtime = Runtimes.Server;
private string _prerender = "True";
private string _hybrid = "False";
@@ -660,6 +673,7 @@
// hosting model
_rendermode = site.RenderMode;
_enhancednavigation = site.EnhancedNavigation.ToString();
_runtime = site.Runtime;
_prerender = site.Prerender.ToString();
_hybrid = site.Hybrid.ToString();
@@ -669,7 +683,7 @@
{
var tenants = await TenantService.GetTenantsAsync();
var _databases = await DatabaseService.GetDatabasesAsync();
var tenant = tenants.Find(item => item.TenantId == site.TenantId);
var tenant = tenants.Find(item => item.TenantId == PageState.Alias.TenantId);
if (tenant != null)
{
_tenant = tenant.Name;
@@ -807,13 +821,11 @@
// hosting model
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
if (site.RenderMode != _rendermode || site.Runtime != _runtime || site.Prerender != bool.Parse(_prerender) || site.Hybrid != bool.Parse(_hybrid))
{
site.RenderMode = _rendermode;
site.Runtime = _runtime;
site.Prerender = bool.Parse(_prerender);
site.Hybrid = bool.Parse(_hybrid);
}
site.RenderMode = _rendermode;
site.EnhancedNavigation = bool.Parse(_enhancednavigation);
site.Runtime = _runtime;
site.Prerender = bool.Parse(_prerender);
site.Hybrid = bool.Parse(_hybrid);
}
site = await SiteService.UpdateSiteAsync(site);
@@ -874,17 +886,17 @@
try
{
var aliases = await AliasService.GetAliasesAsync();
if (aliases.Any(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId))
if (aliases.Any(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Alias.TenantId))
{
await SiteService.DeleteSiteAsync(PageState.Site.SiteId);
await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId);
foreach (Alias alias in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId))
foreach (Alias alias in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
}
var redirect = aliases.First(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId);
var redirect = aliases.First(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Alias.TenantId);
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + redirect.Name, true);
}
else
@@ -981,7 +993,7 @@
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList();
}
}
@@ -1034,7 +1046,7 @@
{
if (_aliasid == 0)
{
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
else

View File

@@ -271,7 +271,7 @@
}
var tenants = await TenantService.GetTenantsAsync();
_tenant = tenants.Find(item => item.TenantId == PageState.Site.TenantId).Name;
_tenant = tenants.Find(item => item.TenantId == PageState.Alias.TenantId).Name;
_history = await MigrationHistoryService.GetMigrationHistoryAsync();
_initialized = true;

View File

@@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.Themes
@inherits ModuleBase
@using System.Text.RegularExpressions
@using System.Reflection
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IModuleService ModuleService
@@ -88,6 +89,16 @@
{
AddModuleMessage(Localizer["Info.Theme.CreatorIntent"], MessageType.Info);
}
else
{
var entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name;
if (entryAssemblyName.EndsWith(".Oqtane"))
{
// Oqtane Application assemblies end with .Server.Oqtane or .Client.Oqtane
string[] segments = entryAssemblyName.Split('.');
_owner = string.Join(".", segments, 0, segments.Length - 2);
}
}
}
protected override async Task OnParametersSetAsync()

View File

@@ -36,6 +36,7 @@ else
<th>@Localizer["Url"]</th>
<th>@Localizer["Requests"]</th>
<th>@Localizer["Requested"]</th>
<th>@Localizer["Referrer"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UrlMappingId.ToString())" ResourceKey="Edit" /></td>
@@ -49,7 +50,8 @@ else
</td>
<td>@context.Requests</td>
<td>@UtcToLocal(context.RequestedOn)</td>
</Row>
<td>@context.Referrer</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">

View File

@@ -114,9 +114,9 @@
}
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys" Expanded="@((_passkeys.Count > 0).ToString())">
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
@if (_passkeys != null && _passkeys.Count > 0)
@if (_passkeys.Count > 0)
{
<Pager Items="@_passkeys">
<Header>
@@ -142,15 +142,15 @@
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
<div class="mt-2">@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
<Section Name="Logins" Heading="Logins" ResourceKey="Logins" Expanded="@((_logins.Count > 0).ToString())">
@if (_logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
@@ -165,7 +165,7 @@
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
<div class="mt-2">@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />

View File

@@ -106,8 +106,8 @@
<br /><br />
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
@if (_passkeys != null && _passkeys.Count > 0)
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys" Expanded="@((_passkeys.Count > 0).ToString())">
@if (_passkeys.Count > 0)
{
<Pager Items="@_passkeys">
<Header>
@@ -122,15 +122,15 @@
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
<div class="mt-2">@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
<Section Name="Logins" Heading="Logins" ResourceKey="Logins" Expanded="@((_logins.Count > 0).ToString())">
@if (_logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
@@ -145,7 +145,7 @@
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
<div class="mt-2">@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />

View File

@@ -33,7 +33,7 @@ else
</div>
<br />
<Pager Items="@users" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName">
<Pager Items="@users" CurrentPage="@_page.ToString()" OnPageChange="@((page) => _page = page)" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
@@ -60,9 +60,46 @@ else
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Host">
<div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="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 alternative login method, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Local Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<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">Use 2FA?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="loginlink" HelpText="Do you want to allow users to login using a time sensitive link sent by email" ResourceKey="LoginLink">Allow Login Link?</Label>
<div class="col-sm-9">
<select id="loginlink" class="form-select" @bind="@_loginlink">
<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="allowregistration" HelpText="Do you want anonymous visitors to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<div class="col-sm-9">
@@ -72,466 +109,432 @@ else
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@if (_allowregistration == "true")
{
@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>
<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="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">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiedomain" HelpText="If you would like to share cookies across subdomains you will need to specify a root domain with a leading dot (ie. '.example.com')" ResourceKey="CookieDomain">Cookie Domain:</Label>
<div class="col-sm-9">
<input id="cookiedomain" class="form-control" @bind="@_cookiedomain" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieexpiration" HelpText="You can choose to use a custom authentication cookie expiration timespan for each site (e.g. '08:00:00' for 8 hours). The default is 14 days if not specified." ResourceKey="CookieExpiration">Cookie Expiration Timespan:</Label>
<div class="col-sm-9">
<input id="cookieexpiration" class="form-control" @bind="@_cookieexpiration" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alwaysremember" HelpText="Enabling this option will set a permanent cookie in conjunction with the Cookie Expiration Timespan, which will automatically sign in users the next time they visit the site. By default the site will use session cookies." ResourceKey="AlwaysRemember">Always Remember User?</Label>
<div class="col-sm-9">
<select id="alwaysremember" class="form-select" @bind="@_alwaysremember">
<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="logouteverywhere" HelpText="Do you want users to be logged out of every active session on any device, or only their current session?" ResourceKey="LogoutEverywhere">Logout Everywhere?</Label>
<div class="col-sm-9">
<select id="logouteverywhere" class="form-select" @bind="@_logouteverywhere">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
<input id="registerurl" class="form-control" @bind="@_registerurl" />
</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="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="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiedomain" HelpText="If you would like to share cookies across subdomains you will need to specify a root domain with a leading dot (ie. '.example.com')" ResourceKey="CookieDomain">Cookie Domain:</Label>
<div class="col-sm-9">
<input id="cookiedomain" class="form-control" @bind="@_cookiedomain" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieexpiration" HelpText="You can choose to use a custom authentication cookie expiration timespan for each site (e.g. '08:00:00' for 8 hours). The default is 14 days if not specified." ResourceKey="CookieExpiration">Cookie Expiration Timespan:</Label>
<div class="col-sm-9">
<input id="cookieexpiration" class="form-control" @bind="@_cookieexpiration" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alwaysremember" HelpText="Enabling this option will set a permanent cookie in conjunction with the Cookie Expiration Timespan, which will automatically sign in users the next time they visit the site. By default the site will use session cookies." ResourceKey="AlwaysRemember">Always Remember User?</Label>
<div class="col-sm-9">
<select id="alwaysremember" class="form-select" @bind="@_alwaysremember">
<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="logouteverywhere" HelpText="Do you want users to be logged out of every active session on any device, or only their current session?" ResourceKey="LogoutEverywhere">Logout Everywhere?</Label>
<div class="col-sm-9">
<select id="logouteverywhere" class="form-select" @bind="@_logouteverywhere">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="provider" HelpText="Select the external login provider" ResourceKey="Provider">Provider:</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="provider" class="form-select" value="@_provider" @onchange="(e => ProviderChanged(e))">
@foreach (var provider in Shared.ExternalLoginProviders.Providers)
{
<option value="@provider.Name">@Localizer[provider.Name]</option>
}
</select>
@if (!string.IsNullOrEmpty(_providerurl))
{
<a href="@_providerurl" class="btn btn-secondary" target="_new">@Localizer["Info"]</a>
}
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>&lt;@Localizer["Not Specified"]&gt;</option>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OIDC"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<Label Class="col-sm-3" For="providername" HelpText="Specify a friendly name for the external login provider which will be displayed on the Login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<Label Class="col-sm-3" For="metadataurl" HelpText="The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
</div>
}
@if (_providertype == AuthenticationProviderTypes.OAuth2)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="provider" HelpText="Select the external login provider" ResourceKey="Provider">Provider:</Label>
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="provider" class="form-select" value="@_provider" @onchange="(e => ProviderChanged(e))">
@foreach (var provider in Shared.ExternalLoginProviders.Providers)
{
<option value="@provider.Name">@Localizer[provider.Name]</option>
}
</select>
@if (!string.IsNullOrEmpty(_providerurl))
{
<a href="@_providerurl" class="btn btn-secondary" target="_new">@Localizer["Info"]</a>
}
<input type="@_clientsecrettype" id="clientsecret" class="form-control" @bind="@_clientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleClientSecret">@_toggleclientsecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>&lt;@Localizer["Not Specified"]&gt;</option>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OIDC"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="Specify a friendly name for the external login provider which will be displayed on the Login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<Label Class="col-sm-3" For="authresponsetype" HelpText="Specify the authorization response type. The default is Authorization Code which is considered to be the most secure option based on the latest OAuth specification." ResourceKey="AuthResponseType">Authorization Response Type:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
<select id="authresponsetype" class="form-select" @bind="@_authresponsetype" required>
<option value="code">@Localizer["AuthFlow.Code"]</option>
<option value="code id_token">@Localizer["AuthFlow.CodeIdToken"]</option>
<option value="code id_token token">@Localizer["AuthFlow.CodeIdTokenToken"]</option>
<option value="code token">@Localizer["AuthFlow.CodeToken"]</option>
<option value="id_token">@Localizer["AuthFlow.IdToken"]</option>
<option value="id_token token">@Localizer["AuthFlow.IdTokenToken"]</option>
<option value="token">@Localizer["AuthFlow.Token"]</option>
<option value="none">@Localizer["AuthFlow.None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadataurl" HelpText="The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<Label Class="col-sm-3" For="requirenonce" HelpText="Specify if Nonce validation is required for the ID token (the default is true)" ResourceKey="RequireNonce">Require Nonce?</Label>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
<select id="requirenonce" class="form-select" @bind="@_requirenonce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="singlelogout" HelpText="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)" ResourceKey="SingleLogout">Use Single Logout?</Label>
<div class="col-sm-9">
<select id="singlelogout" class="form-select" @bind="@_singlelogout" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OAuth2)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_clientsecrettype" id="clientsecret" class="form-control" @bind="@_clientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleClientSecret">@_toggleclientsecret</button>
</div>
</div>
</div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authresponsetype" HelpText="Specify the authorization response type. The default is Authorization Code which is considered to be the most secure option based on the latest OAuth specification." ResourceKey="AuthResponseType">Authorization Response Type:</Label>
<div class="col-sm-9">
<select id="authresponsetype" class="form-select" @bind="@_authresponsetype" required>
<option value="code">@Localizer["AuthFlow.Code"]</option>
<option value="code id_token">@Localizer["AuthFlow.CodeIdToken"]</option>
<option value="code id_token token">@Localizer["AuthFlow.CodeIdTokenToken"]</option>
<option value="code token">@Localizer["AuthFlow.CodeToken"]</option>
<option value="id_token">@Localizer["AuthFlow.IdToken"]</option>
<option value="id_token token">@Localizer["AuthFlow.IdTokenToken"]</option>
<option value="token">@Localizer["AuthFlow.Token"]</option>
<option value="none">@Localizer["AuthFlow.None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirenonce" HelpText="Specify if Nonce validation is required for the ID token (the default is true)" ResourceKey="RequireNonce">Require Nonce?</Label>
<div class="col-sm-9">
<select id="requirenonce" class="form-select" @bind="@_requirenonce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="singlelogout" HelpText="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)" ResourceKey="SingleLogout">Use Single Logout?</Label>
<div class="col-sm-9">
<select id="singlelogout" class="form-select" @bind="@_singlelogout" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parameters" HelpText="Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple)." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reviewclaims" HelpText="This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled." ResourceKey="ReviewClaims">Review Claims?</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="reviewclaims" class="form-select" @bind="@_reviewclaims" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
@if (_reviewclaims == "true")
{
<a href="@_externalloginurl" target="_blank" class="btn btn-secondary">@SharedLocalizer["Test"]</a>
}
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="Specify the type name of the unique user identifier claim provided by the provider. The default value is 'sub'." ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="nameclaimtype" HelpText="Optionally specify the type name of the user's name claim provided by the provider. The typical value is 'name'." ResourceKey="NameClaimType">Name Claim:</Label>
<div class="col-sm-9">
<input id="nameclaimtype" class="form-control" @bind="@_nameclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="Optionally specify the type name of the email address claim provided by the provider. The typical value is 'email'," ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the roles claim provided by the provider" ResourceKey="RoleClaimType">Roles Claim:</Label>
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimmappings" HelpText="Optionally provide a comma delimited list of role names provided by the identity provider, as well as mappings to your site roles." ResourceKey="RoleClaimMappings">Role Claim Mappings:</Label>
<div class="col-sm-9">
<input id="roleclaimmappings" class="form-control" @bind="@_roleclaimmappings" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronizeroles" HelpText="This option will add or remove role assignments so that the site roles exactly match the roles provided by the identity provider" ResourceKey="SynchronizeRoles">Synchronize Roles?</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="synchronizeroles" class="form-select" @bind="@_synchronizeroles" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="savetokens" HelpText="Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie." ResourceKey="SaveTokens">Save Tokens?</Label>
<div class="col-sm-9">
<select id="savetokens" class="form-select" @bind="@_savetokens" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<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="verifyusers" HelpText="Do you want existing users to perform an additional email verification step to link their external login? If you disable this option, existing users will be linked automatically." ResourceKey="VerifyUsers">Verify Existing Users?</Label>
<div class="col-sm-9">
<select id="verifyusers" class="form-select" @bind="@_verifyusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowhostrole" HelpText="Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation." ResourceKey="AllowHostRole">Allow Host Role?</Label>
<div class="col-sm-9">
<select id="allowhostrole" class="form-select" @bind="@_allowhostrole" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="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." ResourceKey="AllowSiteLogin">Allow Local Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="jwtsecret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parameters" HelpText="Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple)." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reviewclaims" HelpText="This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled." ResourceKey="ReviewClaims">Review Claims?</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="jwtsecret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
<select id="reviewclaims" class="form-select" @bind="@_reviewclaims" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
@if (_reviewclaims == "true")
{
<a href="@_externalloginurl" target="_blank" class="btn btn-secondary">@SharedLocalizer["Test"]</a>
}
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="Specify the type name of the unique user identifier claim provided by the provider. The default value is 'sub'." ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" />
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
<Label Class="col-sm-3" For="nameclaimtype" HelpText="Optionally specify the type name of the user's name claim provided by the provider. The typical value is 'name'." ResourceKey="NameClaimType">Name Claim:</Label>
<div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" />
<input id="nameclaimtype" class="form-control" @bind="@_nameclaimtype" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
<Label Class="col-sm-3" For="emailclaimtype" HelpText="Optionally specify the type name of the email address claim provided by the provider. The typical value is 'email'," ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" />
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the roles claim provided by the provider" ResourceKey="RoleClaimType">Roles Claim:</Label>
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimmappings" HelpText="Optionally provide a comma delimited list of role names provided by the identity provider, as well as mappings to your site roles." ResourceKey="RoleClaimMappings">Role Claim Mappings:</Label>
<div class="col-sm-9">
<input id="roleclaimmappings" class="form-control" @bind="@_roleclaimmappings" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronizeroles" HelpText="This option will add or remove role assignments so that the site roles exactly match the roles provided by the identity provider" ResourceKey="SynchronizeRoles">Synchronize Roles?</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
<select id="synchronizeroles" class="form-select" @bind="@_synchronizeroles" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</Section>
}
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="savetokens" HelpText="Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie." ResourceKey="SaveTokens">Save Tokens?</Label>
<div class="col-sm-9">
<select id="savetokens" class="form-select" @bind="@_savetokens" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<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="verifyusers" HelpText="Do you want existing users to perform an additional email verification step to link their external login? If you disable this option, existing users will be linked automatically." ResourceKey="VerifyUsers">Verify Existing Users?</Label>
<div class="col-sm-9">
<select id="verifyusers" class="form-select" @bind="@_verifyusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowhostrole" HelpText="Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation." ResourceKey="AllowHostRole">Allow Host Role?</Label>
<div class="col-sm-9">
<select id="allowhostrole" class="form-select" @bind="@_allowhostrole" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="jwtsecret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="jwtsecret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
<div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
<div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
<div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
</div>
</div>
</div>
</Section>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
@@ -542,13 +545,16 @@ else
@code {
private List<UserRole> users;
private string _deleted = "false";
private int _page = 1;
private string _allowsitelogin;
private string _twofactor;
private string _loginlink;
private string _passkeys;
private string _allowregistration;
private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _passkeys;
private string _twofactor;
private string _profileurl;
private string _cookiename;
private string _cookiedomain;
private string _cookieexpiration;
@@ -598,7 +604,6 @@ else
private string _createusers;
private string _verifyusers;
private string _allowhostrole;
private string _allowsitelogin;
private string _secret;
private string _secrettype = "password";
@@ -622,11 +627,13 @@ else
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");
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_loginlink = SettingService.GetSetting(settings, "LoginOptions:LoginLink", "false");
_passkeys = SettingService.GetSetting(settings, "LoginOptions:Passkeys", "false");
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", "");
_cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", "");
@@ -688,7 +695,6 @@ else
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
_allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false");
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
}
private async Task LoadUsersAsync()
@@ -753,19 +759,21 @@ else
{
try
{
var site = PageState.Site;
site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
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);
var site = PageState.Site;
site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:LoginLink", _loginlink, false);
settings = SettingService.SetSetting(settings, "LoginOptions:Passkeys", _passkeys, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
@@ -811,16 +819,15 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true);
}
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync();
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync();
}
if (!string.IsNullOrEmpty(_secret))
{

View File

@@ -35,11 +35,11 @@
{
if (Disabled)
{
<button type="button" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
<button type="button" class="@Class" title="@AltText" disabled>@((MarkupString)_openIconSpan) @_openText</button>
}
else
{
<button type="button" class="@Class" @onclick="DisplayModal">@((MarkupString)_openIconSpan) @_openText</button>
<button type="button" class="@Class" title="@AltText" @onclick="DisplayModal">@((MarkupString)_openIconSpan) @_openText</button>
}
}
}
@@ -83,13 +83,13 @@ else
{
if (Disabled)
{
<button type="button" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
<button type="button" title="@AltText" class="@Class" disabled>@((MarkupString)_openIconSpan) @_openText</button>
}
else
{
<form method="post" class="app-form-inline" @formname="@($"ActionDialogActionForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_openIconSpan) @_openText</button>
<button type="submit" title="@AltText" class="@Class">@((MarkupString)_openIconSpan) @_openText</button>
</form>
}
}
@@ -112,6 +112,9 @@ else
[Parameter]
public string Text { get; set; } // optional - defaults to Action if not specified
[Parameter]
public string AltText { get; set; } // optional
[Parameter]
public string Action { get; set; } // optional

View File

@@ -8,17 +8,17 @@
{
if (Disabled)
{
<NavLink class="@($"{_classname} disabled")" href="@_url" style="@_style">@((MarkupString)_iconSpan) @_text</NavLink>
<NavLink class="@($"{_classname} disabled")" title="@AltText" href="@_url" style="@_style">@((MarkupString)_iconSpan) @_text</NavLink>
}
else
{
if (OnClick == null)
{
<NavLink class="@_classname" href="@_url" style="@_style">@((MarkupString)_iconSpan) @_text</NavLink>
<NavLink class="@_classname" title="@AltText" href="@_url" style="@_style">@((MarkupString)_iconSpan) @_text</NavLink>
}
else
{
<button type="button" class="@_classname" style="@_style" onclick="@OnClick">@((MarkupString)_iconSpan) @_text</button>
<button type="button" class="@_classname" title="@AltText" style="@_style" onclick="@OnClick">@((MarkupString)_iconSpan) @_text</button>
}
}
}
@@ -42,6 +42,9 @@
[Parameter]
public string Text { get; set; } // optional - defaults to Action if not specified
[Parameter]
public string AltText { get; set; } // optional
[Parameter]
public int ModuleId { get; set; } = -1; // optional - allows the link to target a specific moduleid

View File

@@ -7,7 +7,7 @@
@inject ISettingService SettingService
@inject IStringLocalizer<RichTextEditor> Localizer
<div class="row" style="margin-bottom: 50px;">
<div class="row" style="@_style">
<div class="col">
@_textEditorComponent
</div>
@@ -18,6 +18,8 @@
private RenderFragment _textEditorComponent;
private ITextEditor _textEditor;
private string _style = "margin-bottom: 50px;";
[Parameter]
public string Content { get; set; }
@@ -30,6 +32,9 @@
[Parameter]
public string Provider { get; set; }
[Parameter]
public string Style { get; set; } // optional
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, object>();
@@ -40,6 +45,12 @@
protected override void OnParametersSet()
{
if (!string.IsNullOrEmpty(Style))
{
_style = Style;
}
_textEditorComponent = (builder) =>
{
CreateTextEditor(builder);

View File

@@ -30,6 +30,12 @@ else
[Parameter]
public SecurityAccessLevel? Security { get; set; } // optional - can be used to specify SecurityAccessLevel
[Parameter]
public string RoleName { get; set; } // optional - can be used to specify Role allowed to view this tab
[Parameter]
public string PermissionName { get; set; } // optional - can be used to specify Permission allowed to view this tab
protected override void OnParametersSet()
{
base.OnParametersSet();

View File

@@ -84,12 +84,37 @@
}
}
/// <summary>
/// Determines if a tab should be visible based on user permissions.
/// Authorization hierarchy:
/// 1. Host and Admin roles ALWAYS have access (bypass all checks)
/// 2. Check standard SecurityAccessLevel (View, Edit, etc.)
/// 3. If RoleName specified AND user is not Admin/Host, check RoleName
/// 4. If PermissionName specified AND user is not Admin/Host, check PermissionName
/// </summary>
/// <param name="tabPanel">The tab panel to check authorization for</param>
/// <returns>True if user is authorized to see this tab, false otherwise</returns>
private bool IsAuthorized(TabPanel tabPanel)
{
// Step 1: Check for Host-only restriction
if (tabPanel.Security == SecurityAccessLevel.Host)
{
// Only Host users can access Host-level security tabs (Admin users are excluded)
return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
}
// Step 2: Admin bypass all other restrictions
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
var authorized = false;
// Step 3: Check standard SecurityAccessLevel
switch (tabPanel.Security)
{
case null: // security not specified - assume SecurityAccessLevel.Anonymous
case null:
authorized = true;
break;
case SecurityAccessLevel.Anonymous:
@@ -101,13 +126,23 @@
case SecurityAccessLevel.Edit:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList);
break;
case SecurityAccessLevel.Admin:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin);
break;
case SecurityAccessLevel.Host:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
break;
}
// Step 4: Check RoleName if provided (additional requirement)
if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName))
{
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Step 5: Check PermissionName if provided (additional requirement)
if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);
}
return authorized;
}
}

View File

@@ -31,6 +31,7 @@ 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, "InsertLink", "RadzenHtmlEditorCustomTool", "InsertLink", "insert_link") },

View File

@@ -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: " +
"<ChildComponent RenderModeBoundary=\"RenderModeBoundary\" />";
public void AddModuleMessage(string message, MessageType type)
{
AddModuleMessage(message, type, "top");
@@ -389,21 +394,37 @@ namespace Oqtane.Modules
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();
}
@@ -460,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();

View File

@@ -8,11 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Radzen.Blazor" Version="8.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Radzen.Blazor" Version="8.4.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -120,6 +120,12 @@
<data name="ForgotPassword" xml:space="preserve">
<value>Forgot Password?</value>
</data>
<data name="ForgotUsername" xml:space="preserve">
<value>Forgot Username?</value>
</data>
<data name="UseLoginLink" xml:space="preserve">
<value>Use Login Link</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>
</data>
@@ -142,16 +148,19 @@
<value>You Are Already Signed In</value>
</data>
<data name="Message.ForgotPassword" xml:space="preserve">
<value>Please Enter The Username Related To Your Account And Then Select The Forgot Password Option Again</value>
</data>
<data name="Message.ForgotUser" xml:space="preserve">
<value>Please Check The Email Address Associated To Your User Account For A Password Reset Notification</value>
</data>
<data name="Message.ForgotUsername" xml:space="preserve">
<value>Please Check Your Email For A Username Reminder Notification</value>
</data>
<data name="Message.SendLoginLink" xml:space="preserve">
<value>A Login Link Has Been Sent To Your Email Address. The Link Is Only Valid For A Limited Amount Of Time.</value>
</data>
<data name="Message.UserDoesNotExist" xml:space="preserve">
<value>User Does Not Exist</value>
<value>User Does Not Exist For Criteria Specified</value>
</data>
<data name="Code.HelpText" xml:space="preserve">
<value>Please Enter The Secure Verification Code Which Was Sent To You By Email.</value>
<value>Please enter the secure verification code which was sent to you by email</value>
</data>
<data name="Code.Placeholder" xml:space="preserve">
<value>Verification Code</value>
@@ -166,7 +175,7 @@
<value>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.</value>
</data>
<data name="Password.HelpText" xml:space="preserve">
<value>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.</value>
<value>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.</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
@@ -175,13 +184,13 @@
<value>Password:</value>
</data>
<data name="Remember.HelpText" xml:space="preserve">
<value>Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site</value>
<value>Specify if you would like to be signed back in automatically the next time you visit this site</value>
</data>
<data name="Remember.Text" xml:space="preserve">
<value>Remember Me?</value>
<value>Stay Signed In?</value>
</data>
<data name="Username.HelpText" xml:space="preserve">
<value>Please Enter The Username Related To Your Account</value>
<value>Please enter the username related to your account</value>
</data>
<data name="Username.Placeholder" xml:space="preserve">
<value>Username</value>
@@ -201,7 +210,13 @@
<data name="Error.ResetPassword" xml:space="preserve">
<value>Error Resetting Password</value>
</data>
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
<data name="Error.ForgotUsername" xml:space="preserve">
<value>Error Sending Username Reminder</value>
</data>
<data name="Error.SendLoginLink" xml:space="preserve">
<value>Error Sending Login Link</value>
</data>
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.MissingClaims" xml:space="preserve">
@@ -228,6 +243,12 @@
<data name="ExternalLoginStatus.ReviewClaims" xml:space="preserve">
<value>The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider.</value>
</data>
<data name="ExternalLoginStatus.LoginLinkFailed" xml:space="preserve">
<value>Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process.</value>
</data>
<data name="ExternalLoginStatus.PasskeyFailed" xml:space="preserve">
<value>Passkey Login Was Unsuccessful. Please Ensure You Selected The Correct Passkey For This Site.</value>
</data>
<data name="Register" xml:space="preserve">
<value>Register as new user?</value>
</data>
@@ -237,4 +258,13 @@
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Login Was Not Successful</value>
</data>
<data name="Email.HelpText" xml:space="preserve">
<value>Please enter the email address related to your account</value>
</data>
<data name="Email.Placeholder" xml:space="preserve">
<value>Email Address</value>
</data>
<data name="Email.Text" xml:space="preserve">
<value>Email:</value>
</data>
</root>

View File

@@ -225,9 +225,6 @@
<data name="Personalizable.Text" xml:space="preserve">
<value>Personalizable? </value>
</data>
<data name="Appearance.Name" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="HeadContent.HelpText" xml:space="preserve">
<value>Optionally enter content to be included in the page head (ie. meta, link, or script tags)</value>
</data>
@@ -253,7 +250,7 @@
<value>Permissions</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme Settings</value>
<value>Theme</value>
</data>
<data name="EffectiveDate.HelpText" xml:space="preserve">
<value>The date that this page is active</value>
@@ -267,4 +264,7 @@
<data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value>
</data>
</root>
<data name="Appearance.Heading" xml:space="preserve">
<value>Appearance</value>
</data>
</root>

View File

@@ -309,4 +309,7 @@
<data name="UpdateModulePermissions.HelpText" xml:space="preserve">
<value>Specify if changes made to page permissions should be propagated to the modules on this page</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme</value>
</data>
</root>

View File

@@ -204,7 +204,7 @@
<data name="Success.Page.Delete" xml:space="preserve">
<value>Page Deleted Successfully</value>
</data>
<data name="Success.Pages.Deleted" xml:space="preserve">
<data name="Success.Pages.Delete" xml:space="preserve">
<value>All Pages Deleted Successfully</value>
</data>
<data name="Success.Module.Restore" xml:space="preserve">

View File

@@ -217,7 +217,7 @@
<value>Unique Characters:</value>
</data>
<data name="AllowSiteLogin.HelpText" xml:space="preserve">
<value>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.</value>
<value>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.</value>
</data>
<data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Local Login?</value>
@@ -370,7 +370,7 @@
<value>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.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor Authentication?</value>
<value>Use 2FA?</value>
</data>
<data name="RequireConfirmedEmail.HelpText" xml:space="preserve">
<value>Do you want to require registered users to verify their email address before they are allowed to log in?</value>
@@ -567,4 +567,10 @@
<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>
<data name="LoginLink.Text" xml:space="preserve">
<value>Allow Login Link?</value>
</data>
<data name="LoginLink.HelpText" xml:space="preserve">
<value>Do you want to allow users to login using a time sensitive link sent by email?</value>
</data>
</root>

View File

@@ -124,6 +124,9 @@
<value>Module Type Is Invalid For {0}</value>
</data>
<data name="Error.Module.Exception" xml:space="preserve">
<value>An Unexpected Error Has Occurred</value>
<value>An Unexpected Error Has Occurred</value>
</data>
<data name="Error.Module.InvalidInjectedServices" xml:space="preserve">
<value>Missing service(s): {0}. Please make sure they have been registered correctly.</value>
</data>
</root>

View File

@@ -97,11 +97,18 @@ namespace Oqtane.Services
Task<User> VerifyEmailAsync(User user, string token);
/// <summary>
/// Trigger a forgot-password e-mail for this <see cref="User"/>.
/// Trigger a forgot-password e-mail.
/// </summary>
/// <param name="user"></param>
/// <param name="username"></param>
/// <returns></returns>
Task ForgotPasswordAsync(User user);
Task<bool> ForgotPasswordAsync(string username);
/// <summary>
/// Trigger a username reminder e-mail.
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
Task<bool> ForgotUsernameAsync(string email);
/// <summary>
/// Reset the password of this <see cref="User"/>
@@ -211,6 +218,13 @@ namespace Oqtane.Services
/// <param name="key"></param>
/// <returns></returns>
Task DeleteLoginAsync(int userId, string provider, string key);
/// <summary>
/// Send a login link
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
Task<bool> SendLoginLinkAsync(string email, string returnurl);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -275,9 +289,14 @@ namespace Oqtane.Services
return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user);
}
public async Task ForgotPasswordAsync(User user)
public async Task<bool> ForgotPasswordAsync(string username)
{
await PostJsonAsync($"{Apiurl}/forgot", user);
return await GetJsonAsync<bool>($"{Apiurl}/forgotpassword/{WebUtility.UrlEncode(username)}");
}
public async Task<bool> ForgotUsernameAsync(string email)
{
return await GetJsonAsync<bool>($"{Apiurl}/forgotusername/{WebUtility.UrlEncode(email)}");
}
public async Task<User> ResetPasswordAsync(User user, string token)
@@ -366,5 +385,10 @@ namespace Oqtane.Services
{
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
}
public async Task<bool> SendLoginLinkAsync(string email, string returnurl)
{
return await GetJsonAsync<bool>($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}/{WebUtility.UrlEncode(returnurl)}");
}
}
}

View File

@@ -1,20 +1,36 @@
@namespace Oqtane.Themes.Controls
@namespace Oqtane.Themes.Controls
@switch (Orientation)
@if (_menuType != null)
{
case "Horizontal":
<MenuHorizontal/>
break;
default: // Vertical
{
<MenuVertical/>
break;
}
<DynamicComponent Type="@_menuType" Parameters="@Attributes"></DynamicComponent>
}
@code{
[Parameter]
public string Orientation { get; set; }
[Parameter]
public string MenuType { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();
private Type _menuType;
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(MenuType) && !string.IsNullOrEmpty(Orientation))
{
if (Orientation == "Horizontal")
{
MenuType = "Oqtane.Themes.Controls.MenuHorizontal, Oqtane.Client";
}
else
{
MenuType = "Oqtane.Themes.Controls.MenuVertical, Oqtane.Client";
}
}
_menuType = Type.GetType(MenuType);
}
}

View File

@@ -4,7 +4,8 @@
<main role="main">
<nav class="navbar navbar-dark bg-primary fixed-top">
<Logo UseSiteNameAsFallback="true" /><Menu Orientation="Horizontal" />
<Logo UseSiteNameAsFallback="true" />
<Menu MenuType="Oqtane.Themes.Controls.MenuHorizontal, Oqtane.Client" />
<div class="controls ms-auto">
<div class="controls-group">
<Search CssClass="me-3 text-center bg-primary" />

View File

@@ -1,7 +1,11 @@
@namespace Oqtane.UI
@using System.Reflection
@using Module = Oqtane.Models.Module
@inject IServiceProvider ServiceProvider
@inject SiteState ComponentSiteState
@inject IStringLocalizer<ModuleInstance> Localizer
@inject ILogService LoggingService
@inject NavigationManager NavigationManager
@inherits ErrorBoundary
<CascadingValue Value="@PageState" IsFixed="true">
@@ -67,37 +71,50 @@
{
if (ShouldRender())
{
if (!string.IsNullOrEmpty(ModuleState.ModuleType))
{
ModuleType = Type.GetType(ModuleState.ModuleType);
if (ModuleType != null)
{
// repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary)
ComponentSiteState.Hydrate(SiteState);
DynamicComponent = builder =>
{
builder.OpenComponent(0, ModuleType);
builder.AddAttribute(1, "RenderModeBoundary", this);
builder.CloseComponent();
};
}
else
{
// module does not exist with typename specified
_messageContent = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0));
_messageType = MessageType.Error;
_messagePosition = "top";
_messageStyle = MessageStyle.Alert;
}
}
else
if (string.IsNullOrEmpty(ModuleState.ModuleType))
{
_messageContent = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName);
_messageType = MessageType.Error;
_messagePosition = "top";
_messageStyle = MessageStyle.Alert;
return;
}
ModuleType = Type.GetType(ModuleState.ModuleType);
var moduleName = Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0);
if (ModuleType == null)
{
// module does not exist with typename specified
_messageContent = string.Format(Localizer["Error.Module.InvalidName"], moduleName);
_messageType = MessageType.Error;
_messagePosition = "top";
_messageStyle = MessageStyle.Alert;
return;
}
//only validate the services injection in development environment
if (NavigationManager.BaseUri.Contains("localhost:") && !ValidateModuleTypeInjectedServices(ModuleType, out IList<string> missingServices))
{
// module type is not valid for instantiation
_messageContent = string.Format(Localizer["Error.Module.InvalidInjectedServices"], string.Join(",", missingServices));
_messageType = MessageType.Error;
_messagePosition = "top";
_messageStyle = MessageStyle.Alert;
return;
}
// repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary)
ComponentSiteState.Hydrate(SiteState);
DynamicComponent = builder =>
{
builder.OpenComponent(0, ModuleType);
builder.AddAttribute(1, "RenderModeBoundary", this);
builder.CloseComponent();
};
}
}
@@ -165,4 +182,26 @@
_error = "";
base.Recover();
}
private bool ValidateModuleTypeInjectedServices(Type moduleType, out IList<string> missingServices)
{
missingServices = new List<string>();
var properties = Utilities.GetPropertiesIncludingInherited(moduleType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach(var property in properties)
{
var injectAttribute = property.GetCustomAttribute(typeof(InjectAttribute));
if (injectAttribute != null)
{
var serviceType = property.PropertyType;
var service = ServiceProvider.GetService(serviceType);
if (serviceType != null && service == null)
{
missingServices.Add(Utilities.GetTypeNameLastSegment(serviceType.FullName, 0));
}
}
}
return !missingServices.Any();
}
}

View File

@@ -158,11 +158,17 @@
// verify user is authenticated for current site
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey))
if (authState.User.IsAuthenticated() && authState.User.SiteKey() == SiteState.Alias.SiteKey)
{
// get user
var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
user = await UserService.GetUserAsync(userid, SiteState.Alias.SiteId);
if (PageState == null || PageState.User == null || PageState.User.UserId != authState.User.UserId())
{
// get user
user = await UserService.GetUserAsync(authState.User.UserId(), SiteState.Alias.SiteId);
}
else
{
user = PageState.User;
}
if (user != null)
{
user.IsAuthenticated = authState.User.Identity.IsAuthenticated;

View File

@@ -27,3 +27,4 @@
@using Oqtane.Enums
@using Oqtane.Installer
@using Oqtane.Interfaces
@using Oqtane.Extensions

View File

@@ -18,7 +18,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>10.0.0</ApplicationDisplayVersion>
<ApplicationDisplayVersion>10.0.2</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@@ -54,11 +54,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />

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

@@ -124,7 +124,7 @@ Oqtane.Interop = {
}
},
includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) {
var script;
var script = null;
if (src !== "") {
script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]");
}
@@ -140,7 +140,7 @@ Oqtane.Interop = {
}
}
}
if (script !== null) {
if (script instanceof HTMLScriptElement) {
script.remove();
script = null;
}
@@ -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

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>10.0.0</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,18 +12,18 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net10.0">
<dependency id="Oqtane.Shared" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="8.3.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="8.4.0" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>

View File

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

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Server</id>
<version>10.0.0</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,29 +12,29 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net10.0">
<dependency id="Oqtane.Client" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Oqtane.Client" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="SixLabors.ImageSharp" version="3.1.12" exclude="Build,Analyzers" />
<dependency id="HtmlAgilityPack" version="1.12.4" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" />
<dependency id="MySql.Data" version="9.5.0" exclude="Build,Analyzers" />
<dependency id="Pomelo.EntityFrameworkCore.MySql" version="9.0.0" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="10.0.0-rc.2" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0-rc.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.1" exclude="Build,Analyzers" />
</group>
</dependencies>
<frameworkReferences>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>10.0.0</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,15 +12,15 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>
<dependencies>
<group targetFramework="net10.0">
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.2.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.1" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.2.3" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>10.0.0</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

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

View File

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

View File

@@ -60,7 +60,7 @@
}
@((MarkupString)_headResources)
</head>
<body>
<body @attributes="_bodyAttributes">
@if (string.IsNullOrEmpty(_message))
{
@if (_renderMode == RenderModes.Static)
@@ -97,6 +97,7 @@
private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server;
private bool _prerender = true;
Dictionary<string, object> _bodyAttributes { get; set; } = new();
private string _fingerprint = "";
private int _visitorId = -1;
private string _antiForgeryToken = "";
@@ -141,6 +142,10 @@
_renderMode = site.RenderMode;
_runtime = site.Runtime;
_prerender = site.Prerender;
if (_renderMode == RenderModes.Static && !site.EnhancedNavigation)
{
_bodyAttributes.Add("data-enhance-nav", "false");
}
_fingerprint = site.Fingerprint;
var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
@@ -170,6 +175,7 @@
if (page == null || page.IsDeleted)
{
HandlePageNotFound(site, page, route);
return;
}
else
{
@@ -231,7 +237,7 @@
Site = site,
Page = page,
Modules = modules,
User = null,
User = site.User,
Uri = new Uri(url, UriKind.Absolute),
Route = route,
QueryString = Utilities.ParseQueryString(route.Query),
@@ -288,8 +294,11 @@
private void HandlePageNotFound(Site site, Page page, Route route)
{
// referrer will only be set if the link originated externally
string referrer = (Context.Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.Referer] : "";
// page not found - look for url mapping
var urlMapping = UrlMappingRepository.GetUrlMapping(site.SiteId, route.PagePath);
var urlMapping = UrlMappingRepository.GetUrlMapping(site.SiteId, route.PagePath, referrer);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{
// redirect to mapped url
@@ -300,8 +309,16 @@
{
if (route.PagePath != "404")
{
// redirect to 404 page
NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
// handle not found request in static mode
if(_renderMode == RenderModes.Static)
{
NavigationManager.NotFound();
}
else
{
// redirect to 404 page
NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
}
}
}
}

View File

@@ -265,7 +265,19 @@ namespace Oqtane.Controllers
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// set user personalized page path
_settings.AddSetting(new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false });
var settingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}";
var pathSetting = _settings.GetSetting(EntityNames.User, page.UserId.Value, settingName);
if(pathSetting == null)
{
pathSetting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = settingName, SettingValue = path, IsPrivate = false };
_settings.AddSetting(pathSetting);
}
else
{
pathSetting.SettingValue = path;
_settings.UpdateSetting(pathSetting);
}
_syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update);
}
}

View File

@@ -90,7 +90,7 @@ namespace Oqtane.Controllers
else
{
// suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies or private browsing sessions
if (entityName != EntityNames.Visitor)
if (FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Settings For EntityName {EntityName} And EntityId {EntityId}", entityName, entityId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -114,7 +114,7 @@ namespace Oqtane.Controllers
}
else
{
if (setting != null && entityName != EntityNames.Visitor)
if (setting != null && FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access SettingId {SettingId} For EntityName {EntityName} ", id, entityName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -139,7 +139,7 @@ namespace Oqtane.Controllers
}
else
{
if (setting.EntityName != EntityNames.Visitor)
if (FormatName(setting.EntityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Add Setting {Setting}", setting);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -161,7 +161,7 @@ namespace Oqtane.Controllers
}
else
{
if (setting.EntityName != EntityNames.Visitor)
if (FormatName(setting.EntityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, "User Not Authorized To Update Setting {Setting}", setting);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -261,7 +261,7 @@ namespace Oqtane.Controllers
}
else
{
if (entityName != EntityNames.Visitor)
if (FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For EntityName {EntityName} EntityId {EntityId} SettingName {SettingName}", entityName, entityId, settingName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -282,7 +282,7 @@ namespace Oqtane.Controllers
}
else
{
if (entityName != EntityNames.Visitor)
if (FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For SettingId {SettingId} For EntityName {EntityName} ", id, entityName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -408,19 +408,21 @@ namespace Oqtane.Controllers
private bool IsAuthorized(string entityName, int entityId, string permissionName)
{
bool authorized = false;
if (entityName == EntityNames.PageModule)
{
entityName = EntityNames.Module;
entityId = _pageModules.GetPageModule(entityId).ModuleId;
}
switch (entityName)
switch (FormatName(entityName))
{
case EntityNames.Tenant:
case EntityNames.ModuleDefinition:
case EntityNames.Host:
case EntityNames.Job:
case EntityNames.Theme:
if (permissionName == PermissionNames.Edit)
if (FormatName(permissionName) == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Host);
}
@@ -431,7 +433,7 @@ namespace Oqtane.Controllers
break;
case EntityNames.Site:
case EntityNames.Role:
if (permissionName == PermissionNames.Edit)
if (FormatName(permissionName) == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Admin);
}
@@ -458,7 +460,7 @@ namespace Oqtane.Controllers
break;
default: // custom entity
authorized = true;
if (permissionName == PermissionNames.Edit)
if (FormatName(permissionName) == PermissionNames.Edit)
{
if (entityId == -1)
{
@@ -477,7 +479,7 @@ namespace Oqtane.Controllers
private bool FilterPrivate(string entityName, int entityId)
{
bool filter = false;
switch (entityName)
switch (FormatName(entityName))
{
case EntityNames.Tenant:
case EntityNames.ModuleDefinition:
@@ -526,9 +528,9 @@ namespace Oqtane.Controllers
private void AddSyncEvent(string EntityName, int EntityId, int SettingId, string Action)
{
_syncManager.AddSyncEvent(_alias, EntityName + "Setting", SettingId, Action);
_syncManager.AddSyncEvent(_alias, FormatName(EntityName) + "Setting", SettingId, Action);
switch (EntityName)
switch (FormatName(EntityName))
{
case EntityNames.Module:
case EntityNames.Page:
@@ -540,5 +542,15 @@ namespace Oqtane.Controllers
break;
}
}
private string FormatName(string name)
{
if (!string.IsNullOrEmpty(name))
{
// entity names and permission names are case sensitive
name = name.Substring(0, 1).ToUpper() + name.Substring(1).ToLower();
}
return name;
}
}
}

View File

@@ -3,7 +3,6 @@ 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;
@@ -294,14 +293,18 @@ namespace Oqtane.Controllers
return user;
}
// POST api/<controller>/forgot
[HttpPost("forgot")]
public async Task Forgot([FromBody] User user)
// GET api/<controller>/forgotpassword/x
[HttpGet("forgotpassword/{username}")]
public async Task<bool> ForgotPassword(string username)
{
if (ModelState.IsValid)
{
await _userManager.ForgotPassword(user);
}
return await _userManager.ForgotPassword(username);
}
// GET api/<controller>/forgotusername/x
[HttpGet("forgotusername/{email}")]
public async Task<bool> ForgotUsername(string email)
{
return await _userManager.ForgotUsername(email);
}
// POST api/<controller>/reset
@@ -559,5 +562,12 @@ namespace Oqtane.Controllers
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// GET api/<controller>/loginlink/x/y
[HttpGet("loginlink/{email}/{returnurl}")]
public async Task<bool> SendLoginLink(string email, string returnurl)
{
return await _userManager.SendLoginLink(email, returnurl);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Data;
using System.Globalization;
using System.Linq;
using EFCore.NamingConventions.Internal;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -108,36 +109,40 @@ namespace Oqtane.Database.PostgreSQL
public override void UpdateIdentityStoreTableNames(ModelBuilder builder)
{
foreach(var entity in builder.Model.GetEntityTypes())
foreach (var entity in builder.Model.GetEntityTypes())
{
var tableName = entity.GetTableName();
if (tableName.StartsWith("AspNetUser"))
// the IdentityPasskeyData entity was introduced in .NET 10 and is not mapped to a database table so should be ignored
if (entity.ClrType.Name != "IdentityPasskeyData")
{
// replace table name
entity.SetTableName(RewriteName(entity.GetTableName()));
// replace column names
foreach(var property in entity.GetProperties())
var tableName = entity.GetTableName();
if (tableName.StartsWith("AspNetUser"))
{
property.SetColumnName(RewriteName(property.Name));
}
// replace table name
entity.SetTableName(RewriteName(entity.GetTableName()));
// replace key names
foreach(var key in entity.GetKeys())
{
key.SetName(RewriteName(key.GetName()));
}
// replace column names
foreach (var property in entity.GetProperties())
{
property.SetColumnName(RewriteName(property.Name));
}
// replace foreign key names
foreach (var key in entity.GetForeignKeys())
{
key.PrincipalKey.SetName(RewriteName(key.PrincipalKey.GetName()));
}
// replace key names
foreach (var key in entity.GetKeys())
{
key.SetName(RewriteName(key.GetName()));
}
// replace index names
foreach (var index in entity.GetIndexes())
{
index.SetDatabaseName(RewriteName(index.GetDatabaseName()));
// replace foreign key names
foreach (var key in entity.GetForeignKeys())
{
key.PrincipalKey.SetName(RewriteName(key.PrincipalKey.GetName()));
}
// replace index names
foreach (var index in entity.GetIndexes())
{
index.SetDatabaseName(RewriteName(index.GetDatabaseName()));
}
}
}
}

View File

@@ -31,12 +31,15 @@ namespace Oqtane.Database.Sqlite
public override void DropColumn(MigrationBuilder builder, string name, string table)
{
// not implemented as SQLite does not support dropping columns
// SQLite supports dropping columns starting with version 3.35.0 but EF Core does not implement it yet
// note that a column cannot be dropped if it has a UNIQUE constraint, is part of a PRIMARY KEY, is indexed, or is referenced by other parts of the schema
builder.Sql($"ALTER TABLE {table} DROP COLUMN {name};");
}
public override void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode, string index)
{
// not implemented as SQLite does not support altering columns
// note that column length does not need to be modified as SQLite uses a TEXT type which utilizes variable length strings
}
public override string ConcatenateSql(params string[] values)

View File

@@ -1,8 +1,11 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
@@ -65,6 +68,7 @@ namespace Oqtane.Extensions
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.UseNotFoundResponse();
// execute any IServerStartup logic
app.ConfigureOqtaneAssemblies(environment);
@@ -146,5 +150,66 @@ namespace Oqtane.Extensions
public static IApplicationBuilder UseExceptionMiddleWare(this IApplicationBuilder builder)
=> builder.UseMiddleware<ExceptionMiddleware>();
public static IApplicationBuilder UseNotFoundResponse(this IApplicationBuilder app)
{
const string notFoundRoute = "/404";
app.UseStatusCodePagesWithReExecute(notFoundRoute, createScopeForStatusCodePages: true);
app.Use(async (context, next) =>
{
var path = context.Request.Path.Value ?? string.Empty;
if (string.IsNullOrEmpty(path) || ShouldSkipStatusCodeReExecution(path))
{
var feature = context.Features.Get<IStatusCodePagesFeature>();
feature?.Enabled = false;
}
await next();
});
app.Use(async (context, next) =>
{
var feature = context.Features.Get<IStatusCodeReExecuteFeature>();
var handled = false;
if (feature != null
&& context.Response.StatusCode == (int)HttpStatusCode.NotFound
&& notFoundRoute.Equals(context.Request.Path.Value, StringComparison.OrdinalIgnoreCase))
{
var alias = context.GetAlias();
if (!string.IsNullOrEmpty(alias?.Path))
{
var originalPath = context.Request.Path;
context.Request.Path = new PathString($"/{alias.Path}{notFoundRoute}");
try
{
handled = true;
await next();
}
finally
{
context.Request.Path = originalPath;
}
}
}
if (!handled)
{
await next();
}
});
return app;
}
static bool ShouldSkipStatusCodeReExecution(string path)
{
return Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) || HasStaticFileExtension(path);
}
static bool HasStaticFileExtension(string path)
{
return !string.IsNullOrEmpty(Path.GetExtension(path));
}
}
}

View File

@@ -44,7 +44,10 @@ namespace Microsoft.Extensions.DependencyInjection
// process forwarded headers on load balancers and proxy servers
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
// register localization services

View File

@@ -449,8 +449,6 @@ namespace Oqtane.Infrastructure
private Installation MigrateModules(InstallConfig install)
{
var result = new Installation { Success = false, Message = string.Empty };
using (var scope = _serviceScopeFactory.CreateScope())
{
var moduleDefinitions = scope.ServiceProvider.GetRequiredService<IModuleDefinitionRepository>();
@@ -464,6 +462,8 @@ namespace Oqtane.Infrastructure
var versions = moduleDefinition.ReleaseVersions.Split(',', StringSplitOptions.RemoveEmptyEntries);
using (var db = GetInstallationContext())
{
var message = "";
if (!string.IsNullOrEmpty(moduleDefinition.ServerManagerType))
{
var moduleType = Type.GetType(moduleDefinition.ServerManagerType);
@@ -488,20 +488,23 @@ namespace Oqtane.Infrastructure
var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable;
if (moduleObject == null || !moduleObject.Install(tenant, versions[i]))
{
result.Message = "An Error Occurred Executing IInstallable Interface For " + moduleDefinition.ServerManagerType;
message = "An Error Occurred Executing IInstallable Interface For " + moduleDefinition.ServerManagerType + " On Tenant " + tenant.Name;
_filelogger.LogError(Utilities.LogMessage(this, message));
}
}
else
{
if (!sql.ExecuteScript(tenant, moduleType.Assembly, Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql"))
{
result.Message = "An Error Occurred Executing Database Script " + Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql";
message = "An Error Occurred Executing Database Script " + Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql On Tenant " + tenant.Name;
_filelogger.LogError(Utilities.LogMessage(this, message));
}
}
}
catch (Exception ex)
{
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, message));
}
}
}
@@ -509,11 +512,13 @@ namespace Oqtane.Infrastructure
}
else
{
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " - ServerManagerType " + moduleDefinition.ServerManagerType + " Does Not Exist";
message = "An Error Occurred Installing " + moduleDefinition.Name + " - ServerManagerType " + moduleDefinition.ServerManagerType + " Does Not Exist";
_filelogger.LogError(Utilities.LogMessage(this, message));
}
}
if (string.IsNullOrEmpty(result.Message) && moduleDefinition.Version != versions[versions.Length - 1])
// update module if all migrations were successful and version is not current
if (string.IsNullOrEmpty(message) && moduleDefinition.Version != versions[versions.Length - 1])
{
// get module definition from database to retain user customizable property values
var moduledef = db.ModuleDefinition.AsNoTracking().FirstOrDefault(item => item.ModuleDefinitionId == moduleDefinition.ModuleDefinitionId);
@@ -531,16 +536,8 @@ namespace Oqtane.Infrastructure
}
}
if (string.IsNullOrEmpty(result.Message))
{
result.Success = true;
}
else
{
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
return result;
// module migration issues are logged and should not prevent the framework from starting up
return new Installation { Success = true, Message = string.Empty };
}
private Installation CreateSite(InstallConfig install)
@@ -579,7 +576,6 @@ namespace Oqtane.Infrastructure
site = new Site
{
TenantId = tenant.TenantId,
Name = install.SiteName,
LogoFileId = null,
FaviconFileId = null,
@@ -596,7 +592,9 @@ namespace Oqtane.Infrastructure
RenderMode = rendermode,
Runtime = runtime,
Prerender = (rendermode == RenderModes.Interactive),
Hybrid = false
Hybrid = false,
EnhancedNavigation = true,
TenantId = tenant.TenantId
};
site = sites.AddSite(site);

View File

@@ -186,7 +186,7 @@ namespace Oqtane.Infrastructure
var mailboxAddressValidationError = "";
// sender
if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") != "True")
if ((settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True") && string.IsNullOrEmpty(fromEmail))
{
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", "");
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;

View File

@@ -1,11 +0,0 @@
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteNamedOptions<TOptions>
where TOptions : class, new()
{
void Configure(string name, TOptions options, Alias alias, Dictionary<string, string> sitesettings);
}
}

View File

@@ -1,11 +0,0 @@
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteOptions<TOptions>
where TOptions : class, new()
{
void Configure(TOptions options, Alias alias, Dictionary<string, string> sitesettings);
}
}

View File

@@ -4,6 +4,12 @@ using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteNamedOptions<TOptions>
where TOptions : class, new()
{
void Configure(string name, TOptions options, Alias alias, Dictionary<string, string> sitesettings);
}
public class SiteNamedOptions<TOptions> : ISiteNamedOptions<TOptions>
where TOptions : class, new()
{

View File

@@ -4,6 +4,12 @@ using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteOptions<TOptions>
where TOptions : class, new()
{
void Configure(TOptions options, Alias alias, Dictionary<string, string> sitesettings);
}
public class SiteOptions<TOptions> : ISiteOptions<TOptions>
where TOptions : class, new()
{

View File

@@ -4,10 +4,8 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Policy;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using Oqtane.Enums;
@@ -30,7 +28,8 @@ namespace Oqtane.Managers
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
Task LogoutUserEverywhere(User user);
Task<User> VerifyEmail(User user, string token);
Task ForgotPassword(User user);
Task<bool> ForgotPassword(string username);
Task<bool> ForgotUsername(string email);
Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token);
Task<UserValidateResult> ValidateUser(string username, string email, string password);
@@ -42,6 +41,7 @@ namespace Oqtane.Managers
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);
Task<bool> SendLoginLink(string email, string returnurl);
}
public class UserManager : IUserManager
@@ -279,7 +279,7 @@ namespace Oqtane.Managers
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
}
if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")))
if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")) && !user.IsDeleted)
{
if (user.EmailConfirmed)
{
@@ -519,29 +519,73 @@ namespace Oqtane.Managers
}
return user;
}
public async Task ForgotPassword(User user)
public async Task<bool> ForgotPassword(string username)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
if (!string.IsNullOrEmpty(username))
{
var alias = _tenantManager.GetAlias();
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotPasswordEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["ForgotPasswordEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username);
if (identityuser != null)
{
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
var alias = _tenantManager.GetAlias();
var user = GetUser(username, alias.SiteId);
string url = alias.Protocol + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotPasswordEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["ForgotPasswordEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
return true;
}
}
else
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", username);
return false;
}
public async Task<bool> ForgotUsername(string email)
{
try
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
if (!string.IsNullOrEmpty(email))
{
IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(email);
if (identityuser != null)
{
var alias = _tenantManager.GetAlias();
var user = GetUser(identityuser.UserName, alias.SiteId);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username;
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["ForgotUsernameEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["ForgotUsernameEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Forgot Username Notification Sent For {Email}", user.Email);
return true;
}
}
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Forgot Username Notification Failed For {Email}", email);
return false;
}
catch (Exception ex)
{
// email may not be unique
_logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Forgot Username Notification Failed For {Email}", email);
return false;
}
}
@@ -588,6 +632,7 @@ namespace Oqtane.Managers
}
return user;
}
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
var validateResult = new UserValidateResult { Succeeded = true };
@@ -914,5 +959,45 @@ namespace Oqtane.Managers
}
}
}
public async Task<bool> SendLoginLink(string email, string returnurl)
{
try
{
if (!string.IsNullOrEmpty(email))
{
IdentityUser identityuser = await _identityUserManager.FindByEmailAsync(email);
if (identityuser != null)
{
var token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
var alias = _tenantManager.GetAlias();
var user = GetUser(identityuser.UserName, alias.SiteId);
string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token) + "&returnurl=" + WebUtility.UrlEncode(returnurl);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["LoginLinkEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["LoginLinkEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(_tenantManager.GetAlias().SiteId, user, subject, body);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Notification Sent To {Email}", user.Email);
return true;
}
}
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Notification Failed For {Email}", email);
return false;
}
catch (Exception ex)
{
// email may not be unique
_logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Login Link Notification Failed For {Email}", email);
return false;
}
}
}
}

View File

@@ -16,15 +16,18 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
folderEntityBuilder.DropColumn("DeletedBy");
folderEntityBuilder.DropColumn("DeletedOn");
folderEntityBuilder.DropColumn("IsDeleted");
if (ActiveDatabase.Name != "Sqlite")
{
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
folderEntityBuilder.DropColumn("DeletedBy");
folderEntityBuilder.DropColumn("DeletedOn");
folderEntityBuilder.DropColumn("IsDeleted");
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
fileEntityBuilder.DropColumn("DeletedBy");
fileEntityBuilder.DropColumn("DeletedOn");
fileEntityBuilder.DropColumn("IsDeleted");
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
fileEntityBuilder.DropColumn("DeletedBy");
fileEntityBuilder.DropColumn("DeletedOn");
fileEntityBuilder.DropColumn("IsDeleted");
}
}
protected override void Down(MigrationBuilder migrationBuilder)

View File

@@ -16,7 +16,7 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
// IsDeleted columns were removed in 3.2.2 however SQLite does not support column removal so they had to be restored
// IsDeleted columns were removed in 3.2.2 however SQLite did not support column removal so they had to be restored
if (ActiveDatabase.Name != "Sqlite")
{
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);

View File

@@ -16,8 +16,11 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
languageEntityBuilder.DropColumn("Name");
if (ActiveDatabase.Name != "Sqlite")
{
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
languageEntityBuilder.DropColumn("Name");
}
}
protected override void Down(MigrationBuilder migrationBuilder)

View File

@@ -16,7 +16,7 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
// Name column was removed in 5.2.4 however SQLite does not support column removal so it had to be restored
// Name column was removed in 5.2.4 however SQLite did not support column removal so it had to be restored
if (ActiveDatabase.Name != "Sqlite")
{
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.10.00.01.01")]
public class AddSiteEnhancedNavigation : MultiDatabaseMigration
{
public AddSiteEnhancedNavigation(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.AddBooleanColumn("EnhancedNavigation", true);
siteEntityBuilder.UpdateData("EnhancedNavigation", true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.10.00.01.02")]
public class RemoveSiteTenantId : MultiDatabaseMigration
{
public RemoveSiteTenantId(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.DropIndex("IX_Site"); // TenantId, Name
if (ActiveDatabase.Name != "Sqlite")
{
siteEntityBuilder.DropColumn("TenantId");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.10.00.02.01")]
public class RemoveDeprecatedColumns : MultiDatabaseMigration
{
public RemoveDeprecatedColumns(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
// Oqtane 10.0.2 includes support for column removal in SQLite, so we can now clean up deprecated columns
// Folder columns were deprecated in Oqtane 3.2.2
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
folderEntityBuilder.DropColumn("IsDeleted");
if (ActiveDatabase.Name == "Sqlite")
{
/// the following columns were not added back in 3.2.3 but they still exist in SQLite databases
folderEntityBuilder.DropColumn("DeletedBy");
folderEntityBuilder.DropColumn("DeletedOn");
}
// File columns were deprecated in Oqtane 3.2.2
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
// IsDeleted was added back in 3.2.3 for non-SQLLite databases
fileEntityBuilder.DropColumn("IsDeleted");
if (ActiveDatabase.Name == "Sqlite")
{
/// the following columns were not added back in 3.2.3 but they still exist in SQLite databases
fileEntityBuilder.DropColumn("DeletedBy");
fileEntityBuilder.DropColumn("DeletedOn");
}
// Language columns were deprecated in Oqtane 5.2.4
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
languageEntityBuilder.DropColumn("Name");
// Site columns were deprecated in Oqtane 10.0.1
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
if (ActiveDatabase.Name == "Sqlite")
{
/// the following column was removed for non-SQLite databases in 10.0.1
siteEntityBuilder.DropColumn("TenantId");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

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

View File

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

View File

@@ -34,7 +34,6 @@ namespace Oqtane.Modules.Admin.Files.Manager
if (folder.ModifiedOn >= lastIndexedOn)
{
changed = true;
removed = folder.IsDeleted.Value;
}
var files = _fileRepository.GetFiles(folder.FolderId);
@@ -78,7 +77,7 @@ namespace Oqtane.Modules.Admin.Files.Manager
Permissions = $"{EntityNames.Folder}:{folder.FolderId}",
ContentModifiedBy = file.ModifiedBy,
ContentModifiedOn = file.ModifiedOn,
IsDeleted = (removed || file.IsDeleted.Value)
IsDeleted = (removed)
};
searchContents.Add(searchContent);
}

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules.HtmlText.Repository;
@@ -7,11 +10,9 @@ using Oqtane.Repository;
using Oqtane.Shared;
using Oqtane.Migrations.Framework;
using Oqtane.Documentation;
using System.Linq;
using Oqtane.Interfaces;
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
// ReSharper disable ConvertToUsingDeclaration
@@ -23,15 +24,21 @@ namespace Oqtane.Modules.HtmlText.Manager
private readonly IHtmlTextRepository _htmlText;
private readonly IDBContextDependencies _DBContextDependencies;
private readonly ISqlRepository _sqlRepository;
private readonly ITenantManager _tenantManager;
private readonly IMemoryCache _cache;
public HtmlTextManager(
IHtmlTextRepository htmlText,
IDBContextDependencies DBContextDependencies,
ISqlRepository sqlRepository)
ISqlRepository sqlRepository,
ITenantManager tenantManager,
IMemoryCache cache)
{
_htmlText = htmlText;
_DBContextDependencies = DBContextDependencies;
_sqlRepository = sqlRepository;
_tenantManager = tenantManager;
_cache = cache;
}
public string ExportModule(Module module)
@@ -71,6 +78,13 @@ namespace Oqtane.Modules.HtmlText.Manager
htmlText.ModuleId = module.ModuleId;
htmlText.Content = content;
_htmlText.AddHtmlText(htmlText);
//clear the cache for the module
var alias = _tenantManager.GetAlias();
if(alias != null)
{
_cache.Remove($"HtmlText:{alias.SiteKey}:{module.ModuleId}");
}
}
public bool Install(Tenant tenant, string version)

View File

@@ -27,13 +27,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="MailKit" Version="4.14.1" />
</ItemGroup>
@@ -43,12 +43,12 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<!-- PostgreSQL Database Provider Dependencies -->
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0-rc.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0-rc.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<!-- SQLite Database Provider Dependencies -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.1" />
<!-- SQL Server Database Provider Dependencies -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
</ItemGroup>
<!-- Suppress EF Core internal warnings for Database Providers -->

View File

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

View File

@@ -0,0 +1,74 @@
using System;
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.Shared;
namespace Oqtane.Pages
{
[AllowAnonymous]
public class LoginLinkModel : PageModel
{
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly ILogManager _logger;
public LoginLinkModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ILogManager logger)
{
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_logger = logger;
}
public async Task<IActionResult> OnGetAsync(string name, string token, string returnurl)
{
returnurl = (returnurl == null) ? "" : WebUtility.UrlDecode(returnurl);
if (bool.Parse(HttpContext.GetSiteSettings().GetValue("LoginOptions:LoginLink", "false")) &&
!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token))
{
var validuser = false;
if (!User.Identity.IsAuthenticated)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name);
if (identityuser != null)
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
{
await _identitySignInManager.SignInAsync(identityuser, false);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name);
validuser = true;
}
}
}
if (!validuser)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name);
returnurl = HttpContext.GetAlias().Path + $"/login?status={ExternalLoginStatus.LoginLinkFailed}";
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name);
returnurl = HttpContext.GetAlias().Path;
}
if (!returnurl.StartsWith("/"))
{
returnurl = "/" + returnurl;
}
return LocalRedirect(Url.Content("~" + returnurl));
}
}
}

View File

@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -11,7 +9,6 @@ using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Shared;
using Radzen.Blazor.Markdown;
namespace Oqtane.Pages
{

View File

@@ -10,6 +10,7 @@ using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Security;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Pages
{
@@ -103,7 +104,7 @@ namespace Oqtane.Pages
{
identityuser = null;
var requestOptionsJson = await _identitySignInManager.MakePasskeyRequestOptionsAsync(identityuser);
returnurl += $"?options={WebUtility.UrlEncode(requestOptionsJson)}";
returnurl = HttpContext.GetAlias().Path + $"/login?options={WebUtility.UrlEncode(requestOptionsJson)}&returnurl={WebUtility.UrlEncode(returnurl)}";
}
else
{
@@ -129,6 +130,7 @@ namespace Oqtane.Pages
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Login Failed - Invalid Credential");
returnurl = HttpContext.GetAlias().Path + $"/login?status={ExternalLoginStatus.PasskeyFailed}&returnurl={WebUtility.UrlEncode(returnurl)}";
}
}
else

View File

@@ -72,7 +72,6 @@ namespace Oqtane.Repository
public File AddFile(File file)
{
using var db = _dbContextFactory.CreateDbContext();
file.IsDeleted = false;
db.File.Add(file);
db.SaveChanges();
file.Folder = _folderRepository.GetFolder(file.FolderId);

View File

@@ -51,7 +51,6 @@ namespace Oqtane.Repository
public Folder AddFolder(Folder folder)
{
using var db = _dbContextFactory.CreateDbContext();
folder.IsDeleted = false;
db.Folder.Add(folder);
db.SaveChanges();
_permissions.UpdatePermissions(folder.SiteId, EntityNames.Folder, folder.FolderId, folder.PermissionList);

View File

@@ -14,6 +14,7 @@ namespace Oqtane.Repository
UrlMapping GetUrlMapping(int urlMappingId);
UrlMapping GetUrlMapping(int urlMappingId, bool tracking);
UrlMapping GetUrlMapping(int siteId, string url);
UrlMapping GetUrlMapping(int siteId, string url, string referrer);
void DeleteUrlMapping(int urlMappingId);
int DeleteUrlMappings(int siteId, int age);
}
@@ -78,6 +79,11 @@ namespace Oqtane.Repository
}
public UrlMapping GetUrlMapping(int siteId, string url)
{
return GetUrlMapping(siteId, url, "");
}
public UrlMapping GetUrlMapping(int siteId, string url, string referrer)
{
using var db = _dbContextFactory.CreateDbContext();
url = (url.StartsWith("/")) ? url.Substring(1) : url;
@@ -93,6 +99,7 @@ namespace Oqtane.Repository
urlMapping.Url = url;
urlMapping.MappedUrl = "";
urlMapping.Requests = 1;
urlMapping.Referrer = referrer;
urlMapping.CreatedOn = DateTime.UtcNow;
urlMapping.RequestedOn = DateTime.UtcNow;
try
@@ -109,6 +116,10 @@ namespace Oqtane.Repository
{
urlMapping.Requests += 1;
urlMapping.RequestedOn = DateTime.UtcNow;
if (!string.IsNullOrEmpty(referrer))
{
urlMapping.Referrer = referrer;
}
urlMapping = UpdateUrlMapping(urlMapping);
}
return urlMapping;

View File

@@ -121,7 +121,19 @@
<value>Dear [UserDisplayName]&lt;br&gt;&lt;br&gt;You recently requested to reset your password. Please use the link below to complete the process: &lt;b&gt;&lt;a href="[URL]"&gt;&lt;br&gt;&lt;br&gt;Click here to Reset Password&lt;/a&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;Please note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site.&lt;br&gt;&lt;br&gt;If you did not request to reset your password you can safely ignore this message.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>
</data>
<data name="ForgotPasswordEmailSubject" xml:space="preserve">
<value>Password Reset Notification Sent For [SiteName]</value>
<value>Password Reset Notification For [SiteName]</value>
</data>
<data name="ForgotUsernameEmailBody" xml:space="preserve">
<value>Dear [UserDisplayName]&lt;br&gt;&lt;br&gt;You recently requested a username reminder. Please use the link below to complete the process: &lt;b&gt;&lt;a href="[URL]"&gt;&lt;br&gt;&lt;br&gt;Click here to Login&lt;/a&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;If you did not request a username reminder you can safely ignore this message.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>
</data>
<data name="ForgotUsernameEmailSubject" xml:space="preserve">
<value>Forgotten Username Reminder For [SiteName]</value>
</data>
<data name="LoginLinkEmailBody" xml:space="preserve">
<value>Dear [UserDisplayName]&lt;br&gt;&lt;br&gt;You recently requested a login link. Please use the link below to complete the process: &lt;b&gt;&lt;a href="[URL]"&gt;&lt;br&gt;&lt;br&gt;Click here to Login&lt;/a&gt;&lt;/b&gt;&lt;br&gt;&lt;br&gt;Please note that the link is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate another login link request on the site.&lt;br&gt;&lt;br&gt;If you did not request a login link you can safely ignore this message.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>
</data>
<data name="LoginLinkEmailSubject" xml:space="preserve">
<value>Login Link Notification For [SiteName]</value>
</data>
<data name="NoVerificationEmailBody" xml:space="preserve">
<value>Dear [UserDisplayName],&lt;br&gt;&lt;br&gt;A user account has been successfully created for you with the username &lt;b&gt;[Username]&lt;/b&gt;. Please &lt;b&gt;&lt;a href="[URL]"&gt;click here to login&lt;/a&gt;&lt;/b&gt;. If you do not know your password, use the forgot password option on the login page to reset your account.&lt;br&gt;&lt;br&gt;Thank You!&lt;br&gt;[SiteName] Team</value>

View File

@@ -13,6 +13,7 @@ using Oqtane.Enums;
using Oqtane.Shared;
using System.Globalization;
using Oqtane.Extensions;
using Oqtane.Managers;
namespace Oqtane.Services
{
@@ -25,6 +26,7 @@ namespace Oqtane.Services
private readonly IPageModuleRepository _pageModules;
private readonly IModuleDefinitionRepository _moduleDefinitions;
private readonly ILanguageRepository _languages;
private readonly IUserManager _userManager;
private readonly IUserPermissions _userPermissions;
private readonly ISettingRepository _settings;
private readonly ITenantManager _tenantManager;
@@ -35,7 +37,7 @@ namespace Oqtane.Services
private readonly IHttpContextAccessor _accessor;
private readonly string _private = "[PRIVATE]";
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
{
_sites = sites;
_pages = pages;
@@ -43,6 +45,7 @@ namespace Oqtane.Services
_pageModules = pageModules;
_moduleDefinitions = moduleDefinitions;
_languages = languages;
_userManager = userManager;
_userPermissions = userPermissions;
_settings = settings;
_tenantManager = tenantManager;
@@ -101,6 +104,12 @@ namespace Oqtane.Services
}
site.Languages = site.Languages.OrderBy(item => item.Name).ToList();
// get user
if (_accessor.HttpContext.User.IsAuthenticated())
{
site.User = _userManager.GetUser(_accessor.HttpContext.User.UserId(), site.SiteId);
}
return Task.FromResult(site);
}
@@ -148,6 +157,8 @@ namespace Oqtane.Services
// installation date used for fingerprinting static assets
site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));
site.TenantId = alias.TenantId;
}
else
{
@@ -181,7 +192,7 @@ namespace Oqtane.Services
{
var alias = _tenantManager.GetAlias();
var current = _sites.GetSite(site.SiteId, false);
if (site.SiteId == alias.SiteId && site.TenantId == alias.TenantId && current != null)
if (site.SiteId == alias.SiteId && current != null)
{
site = _sites.UpdateSite(site);
_syncManager.AddSyncEvent(alias, EntityNames.Site, site.SiteId, SyncEventActions.Update);

View File

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

View File

@@ -13,11 +13,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -20,10 +20,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -14,9 +14,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,6 +8,18 @@ namespace Oqtane.Extensions
{
// extension methods cannot be properties - the methods below must include a () suffix when referenced
public static bool IsAuthenticated(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.Identity != null)
{
return claimsPrincipal.Identity.IsAuthenticated;
}
else
{
return false;
}
}
public static string Username(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.Name))

View File

@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Oqtane.Shared;
namespace Oqtane.Models
@@ -55,13 +56,6 @@ namespace Oqtane.Models
/// </summary>
public string Description { get; set; }
/// <summary>
/// Deprecated
/// Note that this property still exists in the database because columns cannot be dropped in SQLite
/// Therefore the property must be retained/mapped even though the framework no longer uses it
/// </summary>
public bool? IsDeleted { get; set; }
/// <summary>
/// Object reference to the <see cref="Folder"/> object.
/// Use this if you need to determine what <see cref="Site"/> the file belongs to.
@@ -74,5 +68,16 @@ namespace Oqtane.Models
/// </summary>
[NotMapped]
public string Url { get; set; }
#region Deprecated Properties
[Obsolete("The IsDeleted property is deprecated. Soft delete of files is not supported.", false)]
[NotMapped]
[JsonIgnore] // exclude from API payload
public bool? IsDeleted { get; set; }
#endregion
}
}

View File

@@ -67,13 +67,6 @@ namespace Oqtane.Models
/// </summary>
public string CacheControl { get; set; }
/// <summary>
/// Deprecated
/// Note that this property still exists in the database because columns cannot be dropped in SQLite
/// Therefore the property must be retained/mapped even though the framework no longer uses it
/// </summary>
public bool? IsDeleted { get; set; }
/// <summary>
/// TODO: todoc what would this contain?
/// </summary>
@@ -110,6 +103,11 @@ namespace Oqtane.Models
}
}
[Obsolete("The IsDeleted property is deprecated. Soft delete of folders is not supported.", false)]
[NotMapped]
[JsonIgnore] // exclude from API payload
public bool? IsDeleted { get; set; }
#endregion
}
}

View File

@@ -31,9 +31,8 @@ namespace Oqtane.Models
/// <summary>
/// Language Name - corresponds to <see cref="Culture.DisplayName"/>, _not_ <see cref="Culture.Name"/>
/// Note that this property still exists in the database because columns cannot be dropped in SQLite
/// Therefore the property must be retained/mapped even though the framework populates it from the Culture API
/// </summary>
[NotMapped]
public string Name { get; set; }
[NotMapped]

View File

@@ -16,11 +16,6 @@ namespace Oqtane.Models
/// </summary>
public int SiteId { get; set; }
/// <summary>
/// Reference to the <see cref="Tenant"/> the Site is in
/// </summary>
public int TenantId { get; set; }
/// <summary>
/// The site Name
/// </summary>
@@ -115,6 +110,11 @@ namespace Oqtane.Models
/// </summary>
public bool Hybrid { get; set; }
/// <summary>
/// Indicates if enhanced navigation should be used with static rendering
/// </summary>
public bool EnhancedNavigation { get; set; }
/// <summary>
/// Keeps track of site configuration changes and is used by the ISiteMigration interface
/// </summary>
@@ -192,18 +192,29 @@ namespace Oqtane.Models
[NotMapped]
public List<Theme> Themes { get; set; }
/// <summary>
/// Current user
/// </summary>
[NotMapped]
public User User { get; set; }
/// <summary>
/// fingerprint for framework static assets
/// </summary>
[NotMapped]
public string Fingerprint { get; set; }
/// <summary>
/// Reference to the <see cref="Tenant"/> the Site belongs to
/// </summary>
[NotMapped]
public int TenantId { get; set; }
public Site Clone()
{
return new Site
{
SiteId = SiteId,
TenantId = TenantId,
Name = Name,
TimeZoneId = TimeZoneId,
LogoFileId = LogoFileId,
@@ -222,6 +233,7 @@ namespace Oqtane.Models
Runtime = Runtime,
Prerender = Prerender,
Hybrid = Hybrid,
EnhancedNavigation = EnhancedNavigation,
Version = Version,
HomePageId = HomePageId,
HeadContent = HeadContent,
@@ -240,7 +252,9 @@ namespace Oqtane.Models
Pages = Pages.ConvertAll(page => page.Clone()),
Languages = Languages.ConvertAll(language => language.Clone()),
Themes = Themes,
Fingerprint = Fingerprint
User = User?.Clone(),
Fingerprint = Fingerprint,
TenantId = TenantId
};
}

View File

@@ -33,6 +33,11 @@ namespace Oqtane.Models
/// </summary>
public int Requests { get; set; }
/// <summary>
/// Last referrer to the Url (only set if linked to externally)
/// </summary>
public string Referrer { get; set; }
/// <summary>
/// Date when the url was first requested for the site
/// </summary>

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Xml.Linq;
namespace Oqtane.Models
{
@@ -128,5 +130,34 @@ namespace Oqtane.Models
/// </summary>
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
public User Clone()
{
return new User
{
UserId = UserId,
Username = Username,
DisplayName = DisplayName,
Email = Email,
TimeZoneId = TimeZoneId,
PhotoFileId = PhotoFileId,
LastLoginOn = LastLoginOn,
LastIPAddress = LastIPAddress,
TwoFactorRequired = TwoFactorRequired,
TwoFactorCode = TwoFactorCode,
TwoFactorExpiry = TwoFactorExpiry,
SecurityStamp = SecurityStamp,
SiteId = SiteId,
Roles = Roles,
DeletedBy = DeletedBy,
DeletedOn = DeletedOn,
IsDeleted = IsDeleted,
Password = Password,
IsAuthenticated = IsAuthenticated,
EmailConfirmed = EmailConfirmed,
SuppressNotification = SuppressNotification,
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
};
}
}
}

View File

@@ -5,9 +5,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
<PackageReference Include="NodaTime" Version="3.2.3" />
</ItemGroup>
</Project>

View File

@@ -4,8 +4,8 @@ namespace Oqtane.Shared
{
public class Constants
{
public static readonly string Version = "10.0.0";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0";
public static readonly string Version = "10.0.2";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";

View File

@@ -10,5 +10,7 @@ namespace Oqtane.Shared {
public const string AccessDenied = "AccessDenied";
public const string RemoteFailure = "RemoteFailure";
public const string ReviewClaims = "ReviewClaims";
public const string LoginLinkFailed = "LoginLinkFailed";
public const string PasskeyFailed = "PasskeyFailed";
}
}

View File

@@ -4,6 +4,8 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -750,6 +752,75 @@ namespace Oqtane.Shared
}
}
public static IEnumerable<PropertyInfo> GetPropertiesIncludingInherited(Type type, BindingFlags bindingFlags)
{
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
var currentType = type;
while (currentType != null)
{
var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly);
foreach (var property in properties)
{
if (!dictionary.TryGetValue(property.Name, out var others))
{
dictionary.Add(property.Name, property);
}
else if (!IsInheritedProperty(property, others))
{
List<PropertyInfo> many;
if (others is PropertyInfo single)
{
many = new List<PropertyInfo> { single };
dictionary[property.Name] = many;
}
else
{
many = (List<PropertyInfo>)others;
}
many.Add(property);
}
}
currentType = currentType.BaseType;
}
foreach (var item in dictionary)
{
if (item.Value is PropertyInfo property)
{
yield return property;
continue;
}
var list = (List<PropertyInfo>)item.Value;
var count = list.Count;
for (var i = 0; i < count; i++)
{
yield return list[i];
}
}
}
private static bool IsInheritedProperty(PropertyInfo property, object others)
{
if (others is PropertyInfo single)
{
return single.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition();
}
var many = (List<PropertyInfo>)others;
foreach (var other in CollectionsMarshal.AsSpan(many))
{
if (other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition())
{
return true;
}
}
return false;
}
[Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)]
public static string ContentUrl(Alias alias, int fileId)
{

View File

@@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline
# Latest Release
[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) was released on November 14, 2025 and is a maintenance release including 77 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[10.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2) was released on December 23, 2025 and is a maintenance release including 19 pull requests by 2 different contributors, pushing the total number of project commits all-time to nearly 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
# Try It Now!
@@ -26,7 +26,11 @@ A free ASP.NET hosting account. No hidden fees. No credit card required.
**Installing using the Oqtane Application Template:**
(Note that "MyCompany.MyProject" can be replaced with your own unique company and project name)
If you have an older version of the Oqtane Application Template installed and want to use the latest, use the following .NET CLI command to uninstall the old version:
```
dotnet new uninstall Oqtane.Application.Template
```
To install the Oqtane Application Template and create a new project, use the following .NET CLI commands (note that "MyCompany.MyProject" can be replaced with your own unique company and project name):
```
dotnet new install Oqtane.Application.Template
@@ -38,7 +42,7 @@ dotnet run
```
- Browse to the Url specified to run the application (an Installation Wizard screen will be displayed the first time you run the application)
- To develop/debug the application in an IDE, open the *.sln file in the root folder and hit F5
- To develop/debug the application in an IDE, open the *.slnx file in the root folder and hit F5
**Installing using source code from the Dev/Master branch:**
@@ -46,7 +50,10 @@ dotnet run
- Install the latest edition of [Visual Studio 2026](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
- Clone (or download) the Oqtane Master or Dev branch source code to your local system.
- Clone (or download) the Oqtane source code to your local system:
- Dev Branch: git clone https://github.com/oqtane/oqtane.framework
- Master Branch: git clone --single-branch --branch master https://github.com/oqtane/oqtane.framework
- Open the **Oqtane.slnx** solution file (make sure you specify Oqtane.Server as the Startup Project)
@@ -81,7 +88,7 @@ dotnet run
- If you have already installed a previous version of Oqtane and you wish to do a clean database install, simply reset the DefaultConnection value in the Oqtane.Server\appsettings.json file to "". This will trigger a re-install when you run the application which will execute the database installation.
- If you want to submit pull requests make sure you install the [Github Extension For Visual Studio](https://visualstudio.github.com/). It is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json"
- If you want to submit pull requests it is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json"
**Video Series**
@@ -104,6 +111,12 @@ Connect with other developers, get support, and share ideas by joining the Oqtan
# Roadmap
This project is open source, and therefore is a work in progress...
[10.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2) (Dec 23, 2025)
- [x] Stabilization improvements
[10.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1) (Dec 15, 2025)
- [x] Stabilization improvements
[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) (Nov 14, 2025)
- [x] Migration to .NET 10
- [x] Passkey Authentication
@@ -190,7 +203,7 @@ This project is open source, and therefore is a work in progress...
➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html)
# Background
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and was inspired by his earlier efforts creating the DotNetNuke web application framework for the .NET Framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
# Reference Implementations

View File

@@ -220,7 +220,7 @@
"apiVersion": "2024-04-01",
"name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]",
"properties": {
"packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.0/Oqtane.Framework.10.0.0.Install.zip"
"packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.2/Oqtane.Framework.10.0.2.Install.zip"
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"