Compare commits

..

452 Commits

Author SHA1 Message Date
9833d37ebe Merge branch 'dev' of https://github.com/oqtane/oqtane.framework into dev
Some checks failed
build-oqtane / Build the oqtane (push) Has been cancelled
2026-04-11 18:05:16 +02:00
Shaun Walker
6299412fa5 Merge pull request #6168 from sbwalker/dev
add validation to cookie consent form
2026-04-08 16:54:58 -04:00
sbwalker
7c4c6f9c02 add validation to cookie consent form 2026-04-08 16:54:40 -04:00
Shaun Walker
2dfa7b6b2f Merge pull request #6167 from sbwalker/dev
fix #6166 - move UseAntiforgery after UseNotFoundResponse and adjust 404 handling logic
2026-04-08 16:40:29 -04:00
sbwalker
0ea27e7147 fix #6166 - move UseAntiforgery after UseNotFoundResponse and adjust 404 handling logic 2026-04-08 16:40:06 -04:00
Shaun Walker
6852b28b71 Merge pull request #6164 from sbwalker/dev
prevent null reference exception
2026-04-06 15:41:11 -04:00
sbwalker
1741029057 prevent null reference exception 2026-04-06 15:40:46 -04:00
Shaun Walker
a47b8942eb Merge pull request #6163 from sbwalker/dev
fix #6157 - support changing language with LanguageSwitcher when not using content localization
2026-04-06 13:40:29 -04:00
sbwalker
a110497967 fix #6157 - support changing language with LanguageSwitcher when not using content localization 2026-04-06 13:40:06 -04:00
Shaun Walker
59b2a34444 Merge pull request #6156 from sbwalker/dev
allow page order to be specified in @page attribute
2026-03-30 08:56:16 -04:00
sbwalker
6814a7bcdb allow page order to be specified in @page attribute 2026-03-30 08:55:55 -04:00
Shaun Walker
4f7f24a725 Merge pull request #6155 from sbwalker/dev
fix #6154 - handle invalid urls
2026-03-30 08:48:58 -04:00
sbwalker
99949bdeb9 fix #6154 - handle invalid urls 2026-03-30 08:48:33 -04:00
Shaun Walker
26eefa336f Merge pull request #6152 from sbwalker/dev
fix #6148 - ensure cache is refreshed when CultureCode is modified
2026-03-30 08:37:55 -04:00
sbwalker
4c39aadff4 fix #6148 - ensure cache is refreshed when CultureCode is modified 2026-03-30 08:29:47 -04:00
Shaun Walker
3364f18341 Merge pull request #6146 from tvatavuk/patch-5
fix #6145 SynchronizationJob is adding Folder records with null Path...
2026-03-28 07:46:54 -07:00
Tonći Vatavuk
7fd4c617db fix #6145 SynchronizationJob is adding Folder records with null Path during site synchronization 2026-03-27 09:53:53 +01:00
Shaun Walker
a801048ad5 Merge pull request #6143 from oqtane/master
10.1.2 Release
2026-03-26 21:38:41 -07:00
Shaun Walker
d114ae488c Merge pull request #6142 from oqtane/dev
10.1.2 Release
2026-03-26 21:38:23 -07:00
Shaun Walker
2d6b05650b Update latest release to version 10.1.2
Updated the latest release information to version 10.1.2.
2026-03-26 21:37:27 -07:00
Shaun Walker
8ed48c6702 Merge pull request #6141 from sbwalker/dev
update azure deploy package version
2026-03-26 21:33:11 -07:00
sbwalker
6f7789ab3b update azure deploy package version 2026-03-26 21:32:56 -07:00
Shaun Walker
72a684a35a Merge pull request #6140 from sbwalker/dev
add comment related to ISettingsControl usage in Module Settings
2026-03-26 20:19:46 -07:00
sbwalker
f8ca688b2d add comment related to ISettingsControl usage in Module Settings 2026-03-26 20:19:30 -07:00
Shaun Walker
edb3f3750c Merge pull request #6139 from sbwalker/dev
improve help text
2026-03-26 19:42:14 -07:00
sbwalker
96291c4a0e improve help text 2026-03-26 19:38:28 -07:00
Shaun Walker
e9b756092e Merge pull request #6138 from sbwalker/dev
update to .NET SDK 10.0.5 (and latest dependencies)
2026-03-26 19:25:03 -07:00
sbwalker
8349f73e1e update to .NET SDK 10.0.5 (and latest dependencies) 2026-03-26 19:24:38 -07:00
Shaun Walker
7d810ead6c Merge pull request #6137 from sbwalker/dev
increment version to 10.1.2
2026-03-26 19:06:14 -07:00
sbwalker
d01622409a increment version to 10.1.2 2026-03-26 19:05:57 -07:00
Shaun Walker
c01fa810f2 Merge pull request #6136 from oqtane/revert-5968-dev
Revert "fix #5894 - allow user to select upgrade version"
2026-03-26 18:43:08 -07:00
Shaun Walker
64e51038a6 Revert "fix #5894 - allow user to select upgrade version" 2026-03-26 18:42:23 -07:00
Shaun Walker
4e07b41772 Merge pull request #6135 from sbwalker/dev
fix #5951 - add ISettingsControl declaration to Settings component
2026-03-26 18:39:19 -07:00
sbwalker
50b2b80778 fix #5951 - add ISettingsControl declaration to Settings component 2026-03-26 18:38:59 -07:00
Shaun Walker
f6d9300cd0 Merge pull request #6129 from sbwalker/dev
fix #6126 - add alias
2026-03-20 14:58:08 -04:00
sbwalker
ebcc3866ab fix #6126 - add alias 2026-03-20 14:57:43 -04:00
Shaun Walker
36ab6f40c7 Merge pull request #6117 from oqtane/master
10.1.1 Release
2026-03-06 15:49:56 -05:00
Shaun Walker
12809df732 Merge pull request #6116 from oqtane/dev
10.1.1 Release
2026-03-06 15:49:36 -05:00
Shaun Walker
82761530ac Update README.md 2026-03-06 15:48:45 -05:00
Shaun Walker
3eb1542b96 Merge pull request #6115 from sbwalker/dev
update azuredeploy.json
2026-03-06 15:45:11 -05:00
sbwalker
b27b38eccf update azuredeploy.json 2026-03-06 15:44:57 -05:00
Shaun Walker
e3b0bdd500 Merge pull request #6114 from leigh-pointer/Pkgs1011
Bump Radzen, Swashbuckle, MailKit, NodaTime
2026-03-06 15:10:07 -05:00
Leigh Pointer
957309b5f0 Bump Radzen, Swashbuckle, MailKit, NodaTime
Upgrade several dependencies across projects:
- Radzen.Blazor 9.0.0 -> 9.0.8 (Oqtane.Client.csproj and Oqtane.Client.nuspec)
- Swashbuckle.AspNetCore 10.1.2 -> 10.1.4 (Oqtane.Server.csproj and Oqtane.Server.nuspec)
- MailKit 4.14.1 -> 4.15.1 (Oqtane.Server.csproj and Oqtane.Server.nuspec)
- NodaTime 3.3.0 -> 3.3.1 (Oqtane.Shared.csproj and Oqtane.Shared.nuspec)
These are patch/minor dependency updates to incorporate fixes and improvements.
2026-03-06 20:31:17 +01:00
Shaun Walker
c7d32dc5f0 Merge pull request #6113 from sbwalker/dev
improve API
2026-03-06 11:44:45 -05:00
sbwalker
aaffb7b84d improve API 2026-03-06 11:44:29 -05:00
Shaun Walker
0648b23c75 Merge pull request #6112 from sbwalker/dev
fix #6111 - regression issue caused by #6106
2026-03-06 11:19:10 -05:00
sbwalker
f09b21295a fix #6111 - regression issue caused by #6106
Related Work Items: #6
2026-03-06 11:18:50 -05:00
Shaun Walker
f57d296fcb Merge pull request #6107 from sbwalker/dev
rename property to IPortableContext
2026-03-05 13:44:01 -05:00
sbwalker
d2e5ab61da rename property to IPortableContext 2026-03-05 13:43:45 -05:00
Shaun Walker
d7d3273018 Merge pull request #6106 from sbwalker/dev
add property to Module class to indicate how the IPortable interface is being invoked (Export Module, Import Module, Copy Page, Global Replace, Site Template)
2026-03-05 12:55:55 -05:00
sbwalker
c2bb6be2da add property to Module class to indicate how the IPortable interface is being invoked (Export Module, Import Module, Copy Page, Global Replace, Site Template) 2026-03-05 12:55:24 -05:00
Shaun Walker
976d8a3369 Merge pull request #6104 from sbwalker/dev
fix issue deleting Site Group Members
2026-03-04 13:22:07 -05:00
sbwalker
ae9beedca2 fix issue deleting Site Group Members 2026-03-04 13:21:50 -05:00
Shaun Walker
0d668f1469 Merge pull request #6103 from sbwalker/dev
Global Replace should include settings
2026-03-04 10:31:38 -05:00
sbwalker
8de67fdaec Global Replace should include settings 2026-03-04 10:31:21 -05:00
Shaun Walker
a4c94c0dda Merge pull request #6102 from sbwalker/dev
remove unnecessary using
2026-03-04 10:02:24 -05:00
sbwalker
91b15b5e2a remove unnecessary using 2026-03-04 10:02:06 -05:00
Shaun Walker
489629e642 Merge pull request #6101 from sbwalker/dev
prevent update of linked SiteGroup
2026-03-04 10:00:14 -05:00
sbwalker
ef92a88908 prevent update of linked SiteGroup 2026-03-04 09:59:52 -05:00
Shaun Walker
f249801541 Merge pull request #6100 from sbwalker/dev
resolve issue with Change Detection group
2026-03-04 09:09:24 -05:00
sbwalker
147ee8b1e7 resolve issue with Change Detection group 2026-03-04 09:09:07 -05:00
Shaun Walker
263091c27e Merge pull request #6099 from sbwalker/dev
copy page should copy page settings
2026-03-03 17:32:36 -05:00
sbwalker
aac1bb582b copy page should copy page settings 2026-03-03 17:32:19 -05:00
Shaun Walker
b097871d97 Merge pull request #6098 from sbwalker/dev
allow page attributes to support alias names
2026-03-03 15:48:44 -05:00
sbwalker
72542f0146 allow page attributes to support alias names 2026-03-03 15:48:25 -05:00
Shaun Walker
6a4f3bdb8e Merge pull request #6095 from sbwalker/dev
increment version to 10.1.1
2026-03-03 07:55:03 -05:00
sbwalker
e65f61bd65 increment version to 10.1.1 2026-03-03 07:54:45 -05:00
Shaun Walker
2c4dfe0ee0 Merge pull request #6093 from sbwalker/dev
fix #6091 - Add page route incorrect
2026-03-02 13:47:22 -05:00
sbwalker
6160941aee fix #6091 - Add page route incorrect 2026-03-02 13:47:01 -05:00
Shaun Walker
3db3d9561b Merge pull request #6090 from sbwalker/dev
do not reduce length of user agent
2026-02-27 14:36:58 -05:00
sbwalker
cf61a58b4c do not reduce length of user agent 2026-02-27 14:36:43 -05:00
Shaun Walker
e988a9f9c9 Merge pull request #6089 from sbwalker/dev
fix #6087 - module category filter
2026-02-27 14:24:42 -05:00
sbwalker
d1b40f0603 fix #6087 - module category filter 2026-02-27 14:24:26 -05:00
Shaun Walker
772f6ad490 Merge pull request #6088 from sbwalker/dev
use visitor tracking filter with url mapping
2026-02-27 14:17:38 -05:00
sbwalker
f1934028a1 use visitor tracking filter with url mapping 2026-02-27 14:15:03 -05:00
Shaun Walker
5cb0598025 Merge pull request #6085 from sbwalker/dev
add some defensive logic in file handler
2026-02-26 11:04:17 -05:00
sbwalker
18730c5e53 add some defensive logic in file handler 2026-02-26 11:03:58 -05:00
Shaun Walker
f79c3ae994 Merge pull request #6082 from oqtane/master
10.1.0 Release
2026-02-25 14:23:58 -05:00
Shaun Walker
779a3dd379 Merge pull request #6081 from oqtane/dev
10.1.0 Release
2026-02-25 14:23:35 -05:00
Shaun Walker
c3a0a96623 Update README.md 2026-02-25 14:21:58 -05:00
Shaun Walker
f9d6ad2c0f Merge pull request #6080 from sbwalker/dev
update azuredeploy to 10.1.0
2026-02-25 14:17:28 -05:00
sbwalker
1cb0a45715 update azuredeploy to 10.1.0 2026-02-25 14:17:14 -05:00
Shaun Walker
6464604f5c Merge pull request #6077 from sbwalker/dev
provide an indicator in Module Settings when a module is shared across multiple pages
2026-02-25 11:44:37 -05:00
sbwalker
573a914699 provide an indicator in Module Settings when a module is shared across multiple pages 2026-02-25 11:44:15 -05:00
Shaun Walker
ebbe618e98 Merge pull request #6076 from sbwalker/dev
set default module title in control panel when adding or copying existing module
2026-02-25 10:39:23 -05:00
sbwalker
0cc1b5a3e9 set default module title in control panel when adding or copying existing module 2026-02-25 10:38:59 -05:00
Shaun Walker
50ccd29872 Merge pull request #6075 from sbwalker/dev
do not display audit info when copying pages
2026-02-25 09:46:22 -05:00
sbwalker
00d14552b1 do not display audit info when copying pages 2026-02-25 09:46:05 -05:00
Shaun Walker
cad5694145 Merge pull request #6073 from sbwalker/dev
relocate folder hierarchy logic to folder repository (consistent with page approach)
2026-02-25 08:12:55 -05:00
sbwalker
06e555ddd1 relocate folder hierarchy logic to folder repository (consistent with page approach) 2026-02-25 08:12:31 -05:00
Shaun Walker
46aa5225c6 Merge pull request #6070 from sbwalker/dev
explicitly set module order in Default Site Template
2026-02-24 16:05:11 -05:00
sbwalker
09f6a1d531 explicitly set module order in Default Site Template 2026-02-24 16:04:55 -05:00
Shaun Walker
f8633fd390 Merge pull request #6069 from sbwalker/dev
add copy page functionality to control panel
2026-02-24 15:36:01 -05:00
sbwalker
9aad400038 add copy page functionality to control panel 2026-02-24 15:35:45 -05:00
Shaun Walker
91ce840592 Merge pull request #6068 from sbwalker/dev
improve change detection notification logic
2026-02-24 08:59:58 -05:00
sbwalker
458c8534c7 improve change detection notification logic 2026-02-24 08:59:40 -05:00
Shaun Walker
f7df3a4720 Merge pull request #6066 from sbwalker/dev
refactor synchronization job
2026-02-23 08:11:44 -05:00
sbwalker
36789495df refactor synchronization job 2026-02-23 08:11:29 -05:00
Shaun Walker
ed0b341f76 Merge pull request #6063 from sbwalker/dev
allow LanguageSwitcher to support culture and ui culture
2026-02-20 15:12:10 -05:00
sbwalker
0d4d51448e allow LanguageSwitcher to support culture and ui culture 2026-02-20 15:11:54 -05:00
Shaun Walker
3df2c04795 Merge pull request #6062 from sbwalker/dev
fix culture cookie so that it supports culture and ui culture
2026-02-20 14:53:57 -05:00
sbwalker
12f06a7662 fix culture cookie so that it supports culture and ui culture 2026-02-20 14:53:41 -05:00
Shaun Walker
f0cab1ca79 Merge pull request #6060 from sbwalker/dev
handle caching in Global Replace
2026-02-20 08:43:03 -05:00
sbwalker
ae0c4c1099 handle caching in Global Replace 2026-02-20 08:42:41 -05:00
Shaun Walker
dc4ded82b1 Merge pull request #6058 from sbwalker/dev
fix renaming issue
2026-02-19 14:26:01 -05:00
sbwalker
8752b24723 fix renaming issue 2026-02-19 14:25:42 -05:00
Shaun Walker
289252d39c Merge pull request #6056 from sbwalker/dev
support html encoded content
2026-02-19 13:42:47 -05:00
sbwalker
2736fa451c support html encoded content 2026-02-19 13:42:32 -05:00
Shaun Walker
31954a971c Merge pull request #6055 from sbwalker/dev
resolve DbContext issue
2026-02-19 13:27:34 -05:00
sbwalker
a6006ce1fe resolve DbContext issue 2026-02-19 13:27:19 -05:00
Shaun Walker
3db09a2fa6 Merge pull request #6054 from sbwalker/dev
remove unecessary using statements
2026-02-19 12:42:20 -05:00
sbwalker
5c2bd8093a remove unecessary using statements 2026-02-19 12:42:04 -05:00
Shaun Walker
8b8048724a Merge pull request #6053 from sbwalker/dev
change JobTask to SiteTask
2026-02-19 10:47:49 -05:00
sbwalker
060eaa7aff change JobTask to SiteTask 2026-02-19 10:47:30 -05:00
Shaun Walker
67dbc4b7ca Merge pull request #6052 from sbwalker/dev
add Job Tasks to enable the execution of adhoc asynchronous site-based workloads
2026-02-19 08:24:06 -05:00
sbwalker
0fd97d34d9 add Job Tasks to enable the execution of adhoc asynchronous site-based workloads 2026-02-19 08:23:11 -05:00
Shaun Walker
ba51342fd6 Merge pull request #6051 from sbwalker/dev
add new global replace service for bulk updating content
2026-02-18 13:59:41 -05:00
sbwalker
13a58ed099 add new global replace service for bulk updating content 2026-02-18 13:59:25 -05:00
Shaun Walker
48a70c8be3 Merge pull request #6050 from sbwalker/dev
remove unnecessary using
2026-02-17 16:35:30 -05:00
sbwalker
4db58c2866 remove unnecessary using 2026-02-17 16:35:15 -05:00
Shaun Walker
539bad1463 Merge pull request #6049 from sbwalker/dev
performance and scalability improvement - get the most recent HtmlText record directly from database and cache only a single object rather than the entire collection
2026-02-17 16:25:35 -05:00
sbwalker
df7f3f7bba performance and scalability improvement - get the most recent HtmlText record directly from database and cache only a single object rather than the entire collection 2026-02-17 16:25:10 -05:00
Shaun Walker
064448deaf Merge pull request #6047 from Raceeend/issue_6046_filerepository
#6046: FileRepository.GetFile: compare both filenames in the same cast.
2026-02-17 12:19:39 -05:00
Shaun Walker
b25279cdcf Delete Oqtane.Application/Client/Properties/launchSettings.json 2026-02-17 12:19:11 -05:00
Shaun Walker
26c8d00cca Merge pull request #6048 from sbwalker/dev
performance optimization to limit the number of HtmlText content versions
2026-02-17 12:17:20 -05:00
sbwalker
912ed66547 performance optimization to limit the number of HtmlText content versions 2026-02-17 12:16:58 -05:00
Kuyck, Pieter
6f6870b16d #6046: FileRepository.GetFile: compare both filenames in the same cast. 2026-02-17 15:59:20 +01:00
Shaun Walker
1ddf6fe74e Merge pull request #6045 from sbwalker/dev
changed terminology from Comparison to Change Detection for Site Group Type
2026-02-17 09:49:47 -05:00
sbwalker
3e0b5bfa09 changed terminology from Comparison to Change Detection for Site Group Type 2026-02-17 09:49:24 -05:00
Shaun Walker
f60df5c8dc Merge pull request #6044 from sbwalker/dev
improve cache busting for module/theme static assets (ie. do not require a restart)
2026-02-17 09:31:54 -05:00
sbwalker
3af03d308e improve cache busting for module/theme static assets (ie. do not require a restart) 2026-02-17 09:31:13 -05:00
Shaun Walker
7605fd7ec7 Merge pull request #6043 from sbwalker/dev
remove assemblies.log logic
2026-02-16 15:21:02 -05:00
sbwalker
e85b1001c6 remove assemblies.log logic 2026-02-16 15:20:46 -05:00
Shaun Walker
87620f2251 Merge pull request #6042 from sbwalker/dev
add Microsoft.AspNetCore.Authorization to _Imports
2026-02-16 10:30:22 -05:00
sbwalker
772da734dc add Microsoft.AspNetCore.Authorization to _Imports 2026-02-16 10:30:06 -05:00
Shaun Walker
646b6fea84 Merge pull request #6041 from sbwalker/dev
clean up unused resource keys
2026-02-16 08:08:37 -05:00
sbwalker
dd816f7c44 clean up unused resource keys 2026-02-16 08:08:20 -05:00
Shaun Walker
c601c0cdc4 Merge pull request #6040 from sbwalker/dev
add comment for clarification
2026-02-16 08:00:14 -05:00
sbwalker
a4f7d1f745 add comment for clarification 2026-02-16 07:59:55 -05:00
Shaun Walker
a4980eac33 Merge pull request #6039 from sbwalker/dev
resolve permission issue
2026-02-15 13:46:01 -05:00
sbwalker
d3c4e78baa resolve permission issue 2026-02-15 13:45:45 -05:00
Shaun Walker
5bf5d2d515 Merge pull request #6038 from sbwalker/dev
modifications for page attributes
2026-02-13 16:57:11 -05:00
sbwalker
657c6620d5 modifications for page attributes 2026-02-13 16:56:54 -05:00
Shaun Walker
de5725b92c Merge pull request #6036 from sbwalker/dev
provide support for @page route
2026-02-13 10:50:24 -05:00
sbwalker
d9db41ac99 provide support for @page route 2026-02-13 10:50:03 -05:00
Shaun Walker
dcd862900f Merge pull request #6031 from oqtane/revert-6027-revert-6012-IdentifyServices
Rename services to Client/Server and update refs
2026-02-11 14:20:44 -05:00
Shaun Walker
0f8d22e6f0 Merge pull request #6032 from oqtane/revert-6028-revert-6013-AppServcieInterface
Rename client and server module services
2026-02-11 14:16:52 -05:00
Shaun Walker
0d75e0f555 Revert "Revert "Rename client and server module services"" 2026-02-11 14:12:15 -05:00
Shaun Walker
84e8edb159 Revert "Revert "Rename services to Client/Server and update refs"" 2026-02-11 14:11:22 -05:00
Shaun Walker
edb6109db9 Merge pull request #6030 from sbwalker/dev
remove System.Net.Http.Json from default moduile template
2026-02-11 14:08:14 -05:00
sbwalker
4cc9348769 remove System.Net.Http.Json from default moduile template 2026-02-11 14:07:57 -05:00
Shaun Walker
1a6420ee45 Merge pull request #6029 from sbwalker/dev
update default module/theme template solution files to avoid building the linked Oqtane.Server project
2026-02-11 14:04:13 -05:00
sbwalker
486132f918 update default module/theme template solution files to avoid building the linked Oqtane.Server project 2026-02-11 14:03:46 -05:00
Shaun Walker
a18f317e85 Merge pull request #6028 from oqtane/revert-6013-AppServcieInterface
Revert "Rename client and server module services"
2026-02-11 13:54:25 -05:00
Shaun Walker
171735fac4 Revert "Rename client and server module services" 2026-02-11 13:54:10 -05:00
Shaun Walker
72d4361fb2 Merge pull request #6027 from oqtane/revert-6012-IdentifyServices
Revert "Rename services to Client/Server and update refs"
2026-02-11 13:52:51 -05:00
Shaun Walker
69fc1c9895 Revert "Rename services to Client/Server and update refs" 2026-02-11 13:52:35 -05:00
Shaun Walker
831bc31e06 Merge pull request #6013 from leigh-pointer/AppServcieInterface
Rename client and server module services
2026-02-11 13:46:01 -05:00
Shaun Walker
68445c40f7 Merge pull request #6012 from leigh-pointer/IdentifyServices
Rename services to Client/Server and update refs
2026-02-11 13:45:45 -05:00
Shaun Walker
2b2d960c3e Merge branch 'dev' into IdentifyServices 2026-02-11 13:45:36 -05:00
Shaun Walker
d687d81d0c Merge pull request #6026 from sbwalker/dev
improve content localization
2026-02-11 11:21:25 -05:00
sbwalker
2d19f21b2e improve content localization 2026-02-11 11:21:05 -05:00
Shaun Walker
e85a2a6cf5 Merge pull request #6025 from sbwalker/dev
improve user delete experience
2026-02-11 10:20:39 -05:00
sbwalker
6d35d40829 improve user delete experience 2026-02-11 10:20:19 -05:00
Shaun Walker
4b3377cc78 Merge pull request #6024 from sbwalker/dev
increment version to 10.1.0
2026-02-11 10:06:09 -05:00
sbwalker
971ba796ef increment version to 10.1.0 2026-02-11 10:05:46 -05:00
Shaun Walker
3195e4b46f Merge pull request #6023 from sbwalker/dev
update to .NET SDK 10.0.3 (and latest dependencies)
2026-02-11 09:23:48 -05:00
sbwalker
10c1779f84 update to .NET SDK 10.0.3 (and latest dependencies) 2026-02-11 09:23:23 -05:00
Shaun Walker
b5c9717f71 Merge pull request #6022 from sbwalker/dev
improvements to ISynchronizable
2026-02-11 08:59:27 -05:00
sbwalker
d13e6fcdad improvements to ISynchronizable 2026-02-11 08:59:10 -05:00
Shaun Walker
357337572e Merge pull request #6021 from sbwalker/dev
site group improvements
2026-02-10 13:16:27 -05:00
sbwalker
e95a6b774e site group improvements 2026-02-10 13:16:12 -05:00
Shaun Walker
59bdc4c04e Merge pull request #6019 from sbwalker/dev
improvements to site synchronization
2026-02-10 12:32:26 -05:00
sbwalker
ad300a58c1 improvements to site synchronization 2026-02-10 12:32:09 -05:00
Shaun Walker
8f25d9f06a Merge pull request #6018 from sbwalker/dev
improve log messages
2026-02-10 09:13:56 -05:00
sbwalker
3dcb391a14 improve log messages 2026-02-10 09:13:39 -05:00
Shaun Walker
9073b8a9ff Merge pull request #6017 from sbwalker/dev
improvements to site groups
2026-02-10 08:55:28 -05:00
sbwalker
6f2e676c00 improvements to site groups 2026-02-10 08:55:11 -05:00
Shaun Walker
dbd9388d4c Merge pull request #6016 from sbwalker/dev
refactoring of site groups
2026-02-09 14:43:52 -05:00
sbwalker
ddd6dfc475 refactoring of site groups 2026-02-09 13:58:38 -05:00
Shaun Walker
16e2efdd66 Merge pull request #6015 from sbwalker/dev
handle cache invalidation for site groups
2026-02-06 16:07:23 -05:00
sbwalker
c0e191537f handle cache invalidation for site groups 2026-02-06 16:07:07 -05:00
Shaun Walker
1652afc10f Merge pull request #6014 from sbwalker/dev
fix issue when loading languages for content localization
2026-02-06 15:39:48 -05:00
sbwalker
aab0dd96dd fix issue when loading languages for content localization 2026-02-06 15:37:24 -05:00
Leigh Pointer
783d01bf9f Rename client and server module services
Rename the client service class to Client[Module]Service and the server service file to Server[Module]Service, updating all references accordingly. Updated DI registration in ClientStartup to register Client[Module]Service, and updated Edit.razor and Index.razor to inject and call Client[Module]Service instead of [Module]Service. This clarifies client vs server implementations and avoids naming collisions.
2026-02-06 18:31:58 +01:00
Leigh Pointer
60b2e50511 Rename services to Client/Server and update refs
Rename the client service class/file from [Module]Service to Client[Module]Service and the server service to Server[Module]Service. Update Edit.razor and Index.razor to inject Client[Module]Service and adjust all calls accordingly. Update DI registration in ClientStartup to register Client[Module]Service. Also remove the System.Net.Http.Json package reference from the client .csproj. File renames and reference updates keep class names and registrations consistent between client and server templates.
2026-02-06 18:25:21 +01:00
Shaun Walker
6520f8ade4 Merge pull request #6011 from sbwalker/dev
improvements for site groups
2026-02-06 11:53:28 -05:00
sbwalker
57deeb6acf improvements for site groups 2026-02-06 11:53:10 -05:00
Shaun Walker
49102491a6 Merge pull request #6010 from sbwalker/dev
improve support for BodyContent
2026-02-05 15:33:24 -05:00
sbwalker
dff2261994 improve support for BodyContent 2026-02-05 15:33:08 -05:00
Shaun Walker
daca055f90 Merge pull request #6009 from sbwalker/dev
additional validation
2026-02-05 14:30:21 -05:00
sbwalker
b2ef3cc574 additional validation 2026-02-05 14:30:06 -05:00
Shaun Walker
8bd035b8f4 Merge pull request #6007 from sbwalker/dev
add new method for getting neutral cultures
2026-02-05 13:23:30 -05:00
sbwalker
8817af42bd add new method for getting neutral cultures 2026-02-05 13:23:14 -05:00
Shaun Walker
7b1a12f1e7 Merge pull request #6002 from leigh-pointer/patch-2
Enable CopyLocalLockFileAssemblies in project file
2026-02-05 07:58:21 -05:00
Leigh Pointer
d313c2f3a5 Enable CopyLocalLockFileAssemblies in project file 2026-02-03 19:14:51 +01:00
Shaun Walker
831f7c5738 Merge pull request #5999 from leigh-pointer/AppServcieInterface
Move I[Module]Service to Shared/Interfaces
2026-01-30 14:59:34 -05:00
Shaun Walker
7bb84fbe33 Merge pull request #5996 from leigh-pointer/ModTempInterface
Move I[Module]Service to Shared
2026-01-30 14:59:24 -05:00
Leigh Pointer
0dc9382215 Move I[Module]Service to Shared/Interfaces
Extract the I[Module]Service interface out of Client/Services/[Module]Service.cs and add it as a new shared interface file at Internal/Shared/Interfaces/I[Module]Service.cs. The inline interface definition was removed from [Module]Service.cs so the service class now implements the shared contract, centralizing the interface for reuse across components.
2026-01-30 17:59:29 +01:00
Leigh Pointer
563696c7a5 Removed the Reference to the External Client in the External Server project 2026-01-30 17:37:35 +01:00
Shaun Walker
66963a6508 Merge pull request #5998 from sbwalker/dev
change order of operations when saving in edit page
2026-01-30 09:17:28 -05:00
sbwalker
8ec4922cd3 change order of operations when saving in edit page 2026-01-30 09:17:13 -05:00
Shaun Walker
ed7d1eef14 Merge pull request #5997 from sbwalker/dev
fix #5988 - add inline script support to static rendering
2026-01-30 09:01:14 -05:00
sbwalker
7ab1184a04 fix #5988 - add inline script support to static rendering 2026-01-30 09:00:58 -05:00
Leigh Pointer
d638f9492d Move I[Module]Service to Shared
Relocate the I[Module]Service interface from the client Services file to a new Shared/Interfaces/I[Module]Service.cs so the interface becomes a shared contract. Remove the duplicate interface from the client-side [Module]Service implementation.
It was move in error, release 6.1.5 PR add a new Visual Studio Project Template #5493
2026-01-30 14:03:21 +01:00
Shaun Walker
6d4870f41a Merge pull request #5995 from sbwalker/dev
fix #5951 change order of operations when saving module settings
2026-01-29 16:39:06 -05:00
sbwalker
81f4f87493 fix #5951 change order of operations when saving module settings 2026-01-29 16:38:45 -05:00
Shaun Walker
4d1ba921dc Merge pull request #5994 from sbwalker/dev
add defensive logic
2026-01-29 11:13:02 -05:00
sbwalker
d6458eeaf6 add defensive logic 2026-01-29 11:12:45 -05:00
Shaun Walker
f4f935ab43 Merge pull request #5993 from sbwalker/dev
resolve issue replicating files
2026-01-29 10:59:47 -05:00
sbwalker
738ad9bbfa resolve issue replicating files 2026-01-29 10:59:31 -05:00
Shaun Walker
a470db594e Merge pull request #5992 from sbwalker/dev
refactor Site Groups
2026-01-28 19:06:33 -05:00
sbwalker
ce905499b0 refactor Site Groups 2026-01-28 19:06:18 -05:00
Shaun Walker
7e6b60405b Merge pull request #5990 from sbwalker/dev
add last synchronization date
2026-01-28 13:59:50 -05:00
sbwalker
8a4275c240 add last synchronization date 2026-01-28 13:59:35 -05:00
Shaun Walker
5735f65628 Merge pull request #5986 from ijaz-saeed/master
admin panel, new entry
2026-01-28 10:48:44 -05:00
Shaun Walker
8749ab7588 Merge pull request #5989 from sbwalker/dev
refactorimg Site Groups
2026-01-28 10:47:57 -05:00
sbwalker
912c01cdf8 refactorimg Site Groups 2026-01-28 10:47:39 -05:00
isaeed
d6bbc6b82f admin panel, new entry 2026-01-28 12:40:17 +05:00
Shaun Walker
1588d4626b Merge pull request #5985 from sbwalker/dev
only host users can synchronize sites
2026-01-27 17:05:47 -05:00
sbwalker
c95725d444 only host users can synchronize sites 2026-01-27 17:05:31 -05:00
Shaun Walker
d3e4c57ede Merge pull request #5981 from mdmontesinos/max-upload-size
Maximum upload file size parameter for FileManager
2026-01-27 16:53:00 -05:00
Shaun Walker
e34013ace5 Merge pull request #5984 from sbwalker/dev
introducing Site Groups
2026-01-27 16:51:48 -05:00
sbwalker
3be2b9c720 introducing Site Groups 2026-01-27 16:51:30 -05:00
David Montesinos
256af74e0c Maximum upload file size parameter for FileManager 2026-01-23 09:34:06 +01:00
Shaun Walker
0295b66c22 Update README.md 2026-01-20 15:07:42 -05:00
Shaun Walker
3393ece21d Merge pull request #5976 from oqtane/master
10.0.4 Release
2026-01-20 14:56:48 -05:00
Shaun Walker
1ac722bd06 Merge pull request #5975 from oqtane/dev
10.0.4 Release
2026-01-20 14:56:21 -05:00
Shaun Walker
d0fb8dc5fc Update README.md 2026-01-20 14:55:32 -05:00
Shaun Walker
6006e6f63c Merge pull request #5974 from sbwalker/dev
update azuredeploy.json
2026-01-20 14:51:45 -05:00
sbwalker
0a3f60c007 undo previous commit 2026-01-20 14:50:51 -05:00
sbwalker
472e8eadec update azuredeploy.json 2026-01-20 14:44:06 -05:00
Shaun Walker
d2c515d837 Merge pull request #5973 from sbwalker/dev
use primary database type if type not explicitly specified
2026-01-19 12:42:41 -05:00
sbwalker
0446bdb970 use primary database type if type not explicitly specified 2026-01-19 12:42:24 -05:00
Shaun Walker
adf2468373 Merge pull request #5972 from sbwalker/dev
include message to notify user of change in connection name format
2026-01-19 11:57:09 -05:00
sbwalker
54d3f1c659 include message to notify user of change in connection name format 2026-01-19 11:56:54 -05:00
Shaun Walker
e3b45a2329 Merge pull request #5971 from sbwalker/dev
allow SQL Management to support non-tenant databases
2026-01-19 11:36:47 -05:00
sbwalker
1a777b29e0 allow SQL Management to support non-tenant databases 2026-01-19 11:36:27 -05:00
Shaun Walker
0c3754ca86 Merge pull request #5970 from sbwalker/dev
remove SMTP Relay setting
2026-01-19 10:45:18 -05:00
sbwalker
b0211b2e6e remove SMTP Relay setting 2026-01-19 10:45:02 -05:00
Shaun Walker
20548710ce Merge pull request #5969 from sbwalker/dev
fix #5965 - validate user registration before adding user to a site
2026-01-19 10:13:17 -05:00
sbwalker
92f4a8b683 fix #5965 - validate user registration before adding user to a site 2026-01-19 10:13:01 -05:00
Shaun Walker
c1709db676 Merge pull request #5968 from sbwalker/dev
fix #5894 - allow user to select upgrade version
2026-01-19 08:25:14 -05:00
sbwalker
7d7ecf4757 fix #5894 - allow user to select upgrade version 2026-01-19 08:24:56 -05:00
Shaun Walker
f1c85d23d6 Merge pull request #5964 from sbwalker/dev
enable scheduled job automatically if SMTP is enabled in Site Settings
2026-01-16 11:58:21 -05:00
sbwalker
780e2a8484 enable scheduled job automatically if SMTP is enabled in Site Settings 2026-01-16 11:58:03 -05:00
Shaun Walker
0624d539bf Merge pull request #5963 from sbwalker/dev
utilize ReplyTo in NotificationJob
2026-01-16 11:35:49 -05:00
sbwalker
e98d84784a utilize ReplyTo in NotificationJob 2026-01-16 11:35:30 -05:00
Shaun Walker
1678817bf0 Merge pull request #5962 from zyhfish/task/fix-5961
Fix #5961: redirect to the external login page automatically.
2026-01-16 11:20:52 -05:00
Shaun Walker
bf0fb0cbae Merge pull request #5960 from zyhfish/task/fix-5959
Fix #5959: set OnRedirectToIdentityProvider events without overwrite previous settings.
2026-01-16 11:20:41 -05:00
Ben
9dc91aec58 Fix #5961: redirect to the external login page automatically. 2026-01-16 11:02:46 +08:00
Ben
8ba480f168 Fix #5959: set OnRedirectToIdentityProvider events without overwrite previous settings. 2026-01-16 10:28:59 +08:00
Ben
9d42dac7d9 Fix #5959: set OnRedirectToIdentityProvider events without overwrite previous settings. 2026-01-16 10:26:49 +08:00
Shaun Walker
236aa84f87 Merge pull request #5958 from sbwalker/dev
fix #5910 - eliminate double render of Login component
2026-01-14 15:54:57 -05:00
sbwalker
a6f7ec9bd5 fix #5910 - eliminate double render of Login component 2026-01-14 15:54:39 -05:00
Shaun Walker
2bf8c947b6 Merge pull request #5957 from sbwalker/dev
improve SMTP Relay description
2026-01-14 11:11:58 -05:00
sbwalker
c8872a5e99 improve SMTP Relay description 2026-01-14 11:11:42 -05:00
Shaun Walker
36f04708ba Merge pull request #5956 from sbwalker/dev
update license copyright date
2026-01-14 10:11:37 -05:00
sbwalker
3b990aa633 update license copyright date 2026-01-14 10:11:23 -05:00
Shaun Walker
3192ab8452 Merge pull request #5955 from sbwalker/dev
update copyright date
2026-01-14 10:08:59 -05:00
sbwalker
51638fdcb0 update copyright date 2026-01-14 10:08:44 -05:00
Shaun Walker
95f98b89fe Merge pull request #5954 from sbwalker/dev
update to .NET SDK 10.0.2 (and latest dependencies)
2026-01-14 09:51:55 -05:00
sbwalker
6c484ea9cd update to .NET SDK 10.0.2 (and latest dependencies) 2026-01-14 09:51:36 -05:00
Shaun Walker
ea0271ba79 Merge pull request #5949 from zyhfish/task/fix-5948
Fix #5948: re-render the module message when it's been changed.
2026-01-14 08:21:19 -05:00
Shaun Walker
526b797d03 Merge pull request #5953 from sbwalker/dev
improve Edit Job  UI
2026-01-14 08:20:48 -05:00
sbwalker
0d0efaf4ca improve Edit Job UI 2026-01-14 08:20:30 -05:00
f3175b6b06 Update .gitea/workflows/build-oqtane.yml
Some checks failed
build-oqtane / Build the oqtane (push) Has been cancelled
2026-01-13 20:42:35 +00:00
90e254fae6 Update .gitea/workflows/build-container.yml 2026-01-13 20:40:37 +00:00
4a7d088612 Merge tag 'v10.0.3' into dev 2026-01-13 11:08:55 +01:00
Ben
5c70f4f6a7 Fix #5948: re-render the module message when it's been changed. 2026-01-09 08:44:13 +08:00
Shaun Walker
b0d624034a Merge pull request #5947 from zyhfish/task/fix-5942
Fix #5942: set the next  execution time correctly.
2026-01-08 16:09:46 -05:00
Ben
43ee682d1f Fix #5942: set the next execution time correctly. 2026-01-08 11:39:32 +08:00
Shaun Walker
d4399955ac Merge pull request #5944 from sbwalker/dev
fix #5940 - add MySQL support to Oqtane 10
2026-01-05 15:50:01 -05:00
sbwalker
86f88c4f7c fix #5940 - add MySQL support to Oqtane 10 2026-01-05 15:49:42 -05:00
Shaun Walker
d6ea610764 Merge pull request #5939 from leigh-pointer/TabSecurity
Refine tab visibility authorization logic
2026-01-02 09:04:25 -05:00
Leigh Pointer
ff8417ed31 Refine tab visibility authorization logic
Updated the tab visibility logic to clarify and enforce the authorization hierarchy. Host-only tabs now strictly require the Host role, Admins bypass all checks except Host, and null SecurityAccessLevel still enforces RoleName and PermissionName if specified. Improved code comments for clarity and adjusted logic to ensure correct permission checks.
2025-12-31 11:10:10 +01:00
Shaun Walker
bf49e52cbb Merge pull request #5938 from sbwalker/dev
increment version to 10.0.4
2025-12-30 13:43:37 -05:00
sbwalker
2844b793fa increment version to 10.0.4 2025-12-30 13:43:11 -05:00
Shaun Walker
9218863dae Merge pull request #5937 from sbwalker/dev
fix #5930 - NotificationJob not sending emails (reverting #5848)
2025-12-30 08:33:51 -05:00
sbwalker
ef20d870ee fix #5930 - NotificationJob not sending emails (reverting #5848) 2025-12-30 08:33:28 -05:00
Shaun Walker
76c79206b6 Merge pull request #5935 from sbwalker/dev
clarify SMTP Relay site option to avoid confusion
2025-12-29 11:08:37 -05:00
sbwalker
b4c6b6b794 clarify SMTP Relay site option to avoid confusion 2025-12-29 11:08:10 -05:00
Shaun Walker
6db5c924c7 Merge pull request #5929 from oqtane/master
10.0.3 Release
2025-12-24 20:51:33 -05:00
Shaun Walker
51aecacee6 Merge pull request #5928 from oqtane/dev
10.0.3 Release
2025-12-24 20:51:14 -05:00
Shaun Walker
b3b8febd12 Update README.md 2025-12-24 20:49:25 -05:00
Shaun Walker
7c770d9a9d Merge pull request #5927 from sbwalker/dev
update azuredeploy.json
2025-12-24 20:47:58 -05:00
sbwalker
30b89fe56f update azuredeploy.json 2025-12-24 20:47:42 -05:00
Shaun Walker
06870f2577 Merge pull request #5926 from sbwalker/dev
fix migrations
2025-12-24 20:37:42 -05:00
sbwalker
e84170b8ea fix migrations 2025-12-24 20:37:25 -05:00
Shaun Walker
e8d26b2cb2 Merge pull request #5924 from sbwalker/dev
fix upgrade issue and increment version to 10.0.3
2025-12-24 19:34:43 -05:00
sbwalker
a8f87ea572 fix upgrade issue and increment version to 10.0.3 2025-12-24 19:34:24 -05:00
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
29ac9334ba revert 3f90653894
revert Wurde zu einem feedback website ummodeliert
2025-10-14 05:47:57 +00: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
3f90653894 Wurde zu einem feedback website ummodeliert 2025-09-26 11:20:56 +02:00
ea056165ca Merge tag 'v6.2.0' into dev 2025-09-25 13:38:07 +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
7a9941fe66 Add entrypoint, modify Dockerfile: Overwrite volume files 2025-05-30 17:40:18 +02:00
b3b39f583a Merge mirror with local history. 2025-05-30 11:51:43 +02:00
92c554e854 New: Register Component that Renders a Link (secondary) to the register URL including the redirect property. 2025-05-30 11:45:26 +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
391827222e Update README.md 2025-03-15 18:08:36 +00:00
c1721bd1a1 Update README.md 2025-03-15 18:08:00 +00:00
f6630ae241 Update README.md 2025-03-15 18:07:14 +00:00
424cab64a8 NEW: Docker builds 2025-03-15 18:59:18 +01:00
231 changed files with 7586 additions and 2445 deletions

View File

@@ -0,0 +1,23 @@
name: build-oqtane
on:
- push
jobs:
build:
name: Build the oqtane
runs-on: mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64
steps:
- name: "Git clone"
run: git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
- name: "Git checkout"
run: git checkout "${{ gitea.sha }}"
- name: "Oqtane Framework bauen"
run: dotnet build -c Release ./oqtane.framework/Oqtane.slnx
- name: "Oqtane Framework publish"
run: dotnet publish -c Release ./oqtane.framework/Oqtane.slnx -o ./out
- name: Upload Package
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
name: oqtane.framework-amd64
path: ./out

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>10.0.0</Version>
<Version>10.1.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.1.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
</PropertyGroup>

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# Build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source
COPY --link . .
RUN dotnet restore /source/Oqtane.sln
RUN dotnet build "/source/Oqtane.sln" -c Release -o /source/build/
# Publish
FROM build AS publish
RUN dotnet publish "Oqtane.Server/Oqtane.Server.csproj" -c Release -o /source/publish/
# Deploy
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS deploy
WORKDIR /codefiles
COPY --from=publish /source/publish/ /codefiles/
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2025 .NET Foundation
Copyright (c) 2018-2026 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

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.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.0" />
<PackageReference Include="Oqtane.Client" Version="10.1.2" />
</ItemGroup>
</Project>

View File

@@ -9,6 +9,7 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using Oqtane
@using Oqtane.Models

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.1.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.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
@@ -33,7 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.0" />
<PackageReference Include="Oqtane.Server" Version="10.1.2" />
</ItemGroup>
</Project>

View File

@@ -4,7 +4,7 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject I[Module]Service Client[Module]Service
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@@ -50,7 +50,7 @@
if (PageState.Action == "Edit")
{
_id = Int32.Parse(PageState.QueryString["id"]);
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
if ([Module] != null)
{
_name = [Module].Name;
@@ -81,14 +81,14 @@
[Module] [Module] = new [Module]();
[Module].ModuleId = ModuleState.ModuleId;
[Module].Name = _name;
[Module] = await [Module]Service.Add[Module]Async([Module]);
[Module] = await Client[Module]Service.Add[Module]Async([Module]);
await logger.LogInformation("[Module] Added {[Module]}", [Module]);
}
else
{
[Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId);
[Module].Name = _name;
await [Module]Service.Update[Module]Async([Module]);
await Client[Module]Service.Update[Module]Async([Module]);
await logger.LogInformation("[Module] Updated {[Module]}", [Module]);
}
NavigationManager.NavigateTo(NavigateUrl());

View File

@@ -3,7 +3,7 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@inject I[Module]Service [Module]Service
@inject I[Module]Service Client[Module]Service
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer
@@ -52,7 +52,7 @@ else
{
try
{
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
_[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
}
catch (Exception ex)
{
@@ -65,9 +65,9 @@ else
{
try
{
await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
await logger.LogInformation("[Module] Deleted {[Module]}", [Module]);
_[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
_[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
StateHasChanged();
}
catch (Exception ex)

View File

@@ -1,5 +1,6 @@
@namespace [Owner].Module.[Module]
@inherits ModuleBase
@implements Oqtane.Interfaces.ISettingsControl
@inject ISettingService SettingService
@inject IStringLocalizer<Settings> Localizer

View File

@@ -7,22 +7,10 @@ using Oqtane.Shared;
namespace [Owner].Module.[Module].Services
{
public interface I[Module]Service
public class Client[Module]Service : ServiceBase, I[Module]Service
{
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
public class [Module]Service : ServiceBase, I[Module]Service
{
public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("[Module]");

View File

@@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup
{
if (!services.Any(s => s.ServiceType == typeof(I[Module]Service)))
{
services.AddScoped<I[Module]Service, [Module]Service>();
services.AddScoped<I[Module]Service, Client[Module]Service>();
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Services
{
public interface I[Module]Service
{
Task<List<Models.[Module]>> Get[Module]sAsync(int ModuleId);
Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId);
Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]);
Task<Models.[Module]> Update[Module]Async(Models.[Module] [Module]);
Task Delete[Module]Async(int [Module]Id, int ModuleId);
}
}

View File

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

View File

@@ -57,6 +57,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IMigrationHistoryService, MigrationHistoryService>();
services.AddScoped<ISiteGroupService, SiteGroupService>();
services.AddScoped<ISiteGroupMemberService, SiteGroupMemberService>();
services.AddScoped<ISiteTaskService, SiteTaskService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers

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

@@ -0,0 +1,117 @@
@namespace Oqtane.Modules.Admin.GlobalReplace
@using System.Text.Json
@inherits ModuleBase
@inject ISiteTaskService SiteTaskService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="find" HelpText="Specify the content which needs to be replaced" ResourceKey="Find">Find What: </Label>
<div class="col-sm-9">
<input id="find" class="form-control" @bind="@_find" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="replace" HelpText="Specify the replacement content" ResourceKey="Replace">Replace With: </Label>
<div class="col-sm-9">
<input id="replace" class="form-control" @bind="@_replace" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="casesensitive" HelpText="Specify if the replacement operation should be case sensitive" ResourceKey="CaseSensitive">Match Case? </Label>
<div class="col-sm-9">
<select id="casesensitive" class="form-select" @bind="@_caseSensitive">
<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="site" HelpText="Specify if site information should be updated (ie. name, head content, body content, settings)" ResourceKey="Site">Site Info? </Label>
<div class="col-sm-9">
<select id="site" class="form-select" @bind="@_site">
<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="pages" HelpText="Specify if page information should be updated (ie. name, title, head content, body content, settings)" ResourceKey="Pages">Page Info? </Label>
<div class="col-sm-9">
<select id="pages" class="form-select" @bind="@_pages">
<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="modules" HelpText="Specify if module information should be updated (ie. title, header, footer, settings)" ResourceKey="Modules">Module Info? </Label>
<div class="col-sm-9">
<select id="modules" class="form-select" @bind="@_modules">
<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="content" HelpText="Specify if module content should be updated" ResourceKey="Content">Module Content? </Label>
<div class="col-sm-9">
<select id="content" class="form-select" @bind="@_content">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br /><br />
<ActionDialog Header="Global Replace" Message="This Operation is Permanent. Are You Sure You Wish To Proceed?" Action="Replace" Class="btn btn-primary" OnClick="@(async () => await Save())" ResourceKey="GlobalReplace" />
<br /><br />
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override string Title => "Global Replace";
private string _find;
private string _replace;
private string _caseSensitive = "True";
private string _site = "True";
private string _pages = "True";
private string _modules = "True";
private string _content = "True";
private async Task Save()
{
try
{
if (!string.IsNullOrEmpty(_find) && !string.IsNullOrEmpty(_replace))
{
var replace = new GlobalReplace
{
Find = _find,
Replace = _replace,
CaseSensitive = bool.Parse(_caseSensitive),
Site = bool.Parse(_site),
Pages = bool.Parse(_pages),
Modules = bool.Parse(_modules),
Content = bool.Parse(_content)
};
var siteTask = new SiteTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace));
await SiteTaskService.AddSiteTaskAsync(siteTask);
AddModuleMessage(Localizer["Success.Save"], MessageType.Success);
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Global Replace Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Save"], MessageType.Error);
}
}
}

View File

@@ -5,100 +5,109 @@
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required />
@if (_initialized)
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
@if (!_editable)
{
<ModuleMessage Message="@Localizer["JobNotEditable"]" Type="MessageType.Warning" />
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" required disabled />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" readonly />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required disabled="@(!_editable)" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<div class="col-sm-9">
<select id="enabled" class="form-select" @bind="@_isEnabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<div class="col-sm-9">
<select id="enabled" class="form-select" @bind="@_isEnabled" required disabled="@(!_editable)">
<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="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<div class="col-sm-9">
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required />
<select id="runs-every" class="form-select" @bind="@_frequency" required>
<option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<div class="col-sm-9">
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required disabled="@(!_editable)" />
<select id="runs-every" class="form-select" @bind="@_frequency" required disabled="@(!_editable)">
<option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" type="number" min="0" step="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required disabled="@(!_editable)" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div>
<div class="col">
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="starting" type="date" class="form-control" @bind="@_startDate" disabled="@(!_editable)" />
</div>
<div class="col">
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" disabled="@(!_editable)" />
</div>
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div>
<div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="ending" type="date" class="form-control" @bind="@_endDate" disabled="@(!_editable)" />
</div>
<div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" disabled="@(!_editable)" />
</div>
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="next" HelpText="The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job." ResourceKey="NextExecution">Next Execution: </Label>
<div class="col-sm-9">
<input id="next" class="form-control" @bind="@_nextDate" disabled />
</div>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
<br />
@if (_editable)
{
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
}
else
{
<button type="button" class="btn btn-danger" @onclick="DisableJob">@Localizer["Disable"]</button>
}
<NavLink class="btn btn-secondary ms-1" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
}
@code {
private ElementReference form;
private bool validated = false;
private bool _initialized = false;
private bool _editable = true;
private int _jobId;
private string _name = string.Empty;
private string _jobType = string.Empty;
@@ -113,26 +122,32 @@
private DateTime? _nextDate = null;
private DateTime? _nextTime = null;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
protected override async Task OnInitializedAsync()
{
await LoadJob();
}
protected async Task LoadJob()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_editable = !job.IsEnabled && !job.IsExecuting;
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate);
@@ -140,70 +155,107 @@
_nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn;
}
private async Task SaveJob()
{
if (!Utilities.ValidateEffectiveExpiryDates(_startDate, _endDate))
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
return;
_initialized = true;
}
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
private async Task SaveJob()
{
try
{
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.IsEnabled = bool.Parse(_isEnabled);
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
try
{
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory);
if (!job.IsEnabled || Utilities.ValidateEffectiveExpiryDates(job.StartDate, job.EndDate))
{
if (!job.IsEnabled || (job.StartDate >= DateTime.UtcNow || job.StartDate == null))
{
job.NextExecution = null;
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.StartDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
private async Task DisableJob()
{
try
{
var job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
if (job.IsExecuting)
{
AddModuleMessage(Localizer["Message.ExecutingError"], MessageType.Warning);
}
else
{
job.IsEnabled = false;
job.NextExecution = null;
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
}

View File

@@ -14,96 +14,136 @@
}
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;
public override List<Resource> Resources => new List<Resource>()
{
@@ -116,6 +156,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 +169,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 +219,15 @@ else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning);
}
else
{
if (_allowexternallogin && !_allowsitelogin)
{
// external login
ExternalLogin();
}
}
}
}
@@ -186,6 +238,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 +291,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 +305,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 +324,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 +355,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 +387,8 @@ else
}
else
{
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
StateHasChanged();
}
catch (Exception ex)
{
@@ -301,51 +397,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 +465,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 +483,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

@@ -46,7 +46,8 @@
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information">
<br />
<Section Name="Information" ResourceKey="Information">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduledefinitionname" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
@@ -97,7 +98,13 @@
}
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="fingerprint" HelpText="A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting)." ResourceKey="Fingerprint">Fingerprint: </Label>
<div class="col-sm-9">
<input id="fingerprint" class="form-control" @bind="@_fingerprint" disabled />
</div>
</div>
</div>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
@@ -231,6 +238,7 @@
private string _url = "";
private string _contact = "";
private string _license = "";
private string _fingerprint = "";
private List<Permission> _permissions = null;
private string _createdby;
private DateTime _createdon;
@@ -266,6 +274,7 @@
_url = moduleDefinition.Url;
_contact = moduleDefinition.Contact;
_license = moduleDefinition.License;
_fingerprint = moduleDefinition.Fingerprint;
_permissions = moduleDefinition.PermissionList;
_createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn;

View File

@@ -130,7 +130,7 @@ else
private async Task LoadModuleDefinitions()
{
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_packages = await PackageService.GetPackageUpdatesAsync("module");
}

View File

@@ -73,27 +73,29 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label>
<Label Class="col-sm-3" For="page" HelpText="The page that the module is located on. Please note that shared modules cannot be moved to other pages." ResourceKey="Page">Page: </Label>
<div class="col-sm-9">
<select id="page" class="form-select" @bind="@_pageId" required>
@if (PageState.Page.UserId != null)
{
@if (PageState.Page.UserId != null || _isShared)
{
<select id="page" class="form-select" @bind="@_pageId" required disabled>
<option value="@PageState.Page.PageId">@(PageState.Page.Name)</option>
}
else
</select>
}
else
{
<select id="page" class="form-select" @bind="@_pageId" required>
@if (_pages != null)
{
if (_pages != null)
foreach (Page p in _pages)
{
foreach (Page p in _pages)
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, p.PermissionList))
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
}
}
</select>
</select>
}
</div>
</div>
</div>
@@ -161,6 +163,7 @@
private string _pane;
private string _containerType;
private string _allPages = "false";
private bool _isShared = false;
private string _header = "";
private string _footer = "";
private string _permissionNames = "";
@@ -207,6 +210,7 @@
_expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate);
_allPages = pagemodule.Module.AllPages.ToString();
_isShared = pagemodule.Module.IsShared;
createdby = pagemodule.Module.CreatedBy;
createdon = pagemodule.Module.CreatedOn;
modifiedby = pagemodule.Module.ModifiedBy;
@@ -276,15 +280,40 @@
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (!string.IsNullOrEmpty(_title))
{
if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate))
{
AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning);
return;
}
}
// update module settings first
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy approach - module settings updated by convention (ie. by calling a public method named "UpdateSettings" in settings component)
// this method should be removed however the ISettingsControl declaration was not added to the default module template until version 10.1.2
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
// update container settings
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
// update page module
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
var pageId = pagemodule.PageId; // preserve
var pane = pagemodule.Pane; // preserve
pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title;
pagemodule.Pane = _pane;
@@ -302,33 +331,21 @@
pagemodule.Header = _header;
pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
// update page module order if page or pane changed
if (pageId != pagemodule.PageId || pane != pagemodule.Pane)
{
await PageModuleService.UpdatePageModuleOrderAsync(pageId, pane); // old page/pane
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); // new page/pane
}
// update module
var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList();
await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component )
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
NavigationManager.NavigateTo(PageState.ReturnUrl);
}
else

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))
@@ -46,7 +46,7 @@
<Label Class="col-sm-3" For="move" HelpText="Select the location where you would like the page to be moved in relation to other pages" ResourceKey="Move">Move: </Label>
<div class="col-sm-9">
<select id="move" class="form-select" @bind="@_insert" required>
@if (_parentid == _currentparentid)
@if (_parentid == _currentparentid && !_copy)
{
<option value="=">&lt;@Localizer["ThisLocation.Keep"]&gt;</option>
}
@@ -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">
@@ -221,7 +225,10 @@
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
@if (!_copy)
{
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions" Heading="Permissions">
<div class="container">
@@ -237,36 +244,40 @@
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div>
</TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules">
<Header>
@if (!_copy)
{
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td>
</Row>
</Pager>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td>
</Row>
</Pager>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
}
}
</TabStrip>
}
@@ -345,6 +356,7 @@
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private int _pageId;
private bool _copy = false;
private string _name;
private string _currentparentid;
private string _parentid = "-1";
@@ -390,6 +402,10 @@
{
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_pageId = Int32.Parse(PageState.QueryString["id"]);
if (PageState.QueryString.ContainsKey("copy"))
{
_copy = bool.Parse(PageState.QueryString["copy"]);
}
_page = await PageService.GetPageAsync(_pageId);
_icons = await SystemService.GetIconsAsync();
_iconresources = Utilities.GetFullTypeName(typeof(IconResources).AssemblyQualifiedName);
@@ -409,7 +425,7 @@
_children = new List<Page>();
foreach (Page p in _pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid, CultureInfo.InvariantCulture))))
{
if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
if ((p.PageId != _pageId || _copy) && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
@@ -436,6 +452,12 @@
_expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate);
_ispersonalizable = _page.IsPersonalizable.ToString();
if (_copy)
{
_insert = ">";
_childid = _page.PageId;
}
// appearance
_title = _page.Title;
_themetype = _page.ThemeType;
@@ -466,6 +488,19 @@
// permissions
_permissions = _page.PermissionList;
_updatemodulepermissions = "True";
if (_copy)
{
_permissions = _page.PermissionList.Select(item => new Permission
{
SiteId = item.SiteId,
EntityName = item.EntityName,
EntityId = -1,
PermissionName = item.PermissionName,
RoleName = item.RoleName,
UserId = item.UserId,
IsAuthorized = item.IsAuthorized,
}).ToList();
}
// page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@@ -480,6 +515,13 @@
_deletedon = _page.DeletedOn;
ThemeSettings();
if (_copy)
{
_name = "";
_path = "";
}
_initialized = true;
}
else
@@ -550,7 +592,7 @@
builder.OpenComponent(0, _themeSettingsType);
builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary);
builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
@@ -572,10 +614,18 @@
await ScrollToPageTop();
return;
}
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
{
string currentPath = _page.Path;
if (_copy)
{
_page = new Page();
_page.SiteId = PageState.Site.SiteId;
currentPath = "";
}
_page.Name = _name;
if (_parentid == "-1")
@@ -631,6 +681,13 @@
return;
}
// update theme settings
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
}
// default page properties
if (_insert != "=")
{
Page child;
@@ -684,7 +741,21 @@
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
}
_page = await PageService.UpdatePageAsync(_page);
if (_copy)
{
// create page
_page = await PageService.AddPageAsync(_page);
await PageService.CopyPageAsync(_pageId, _page.PageId, bool.Parse(_updatemodulepermissions));
await logger.LogInformation("Page Added {Page}", _page);
}
else
{
// update page
_page = await PageService.UpdatePageAsync(_page);
await logger.LogInformation("Page Saved {Page}", _page);
}
// update page order
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId);
if (_currentparentid == string.Empty)
{
@@ -695,12 +766,6 @@
await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid));
}
if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl)
{
await themeSettingsControl.UpdateSettings();
}
await logger.LogInformation("Page Saved {Page}", _page);
if (!string.IsNullOrEmpty(PageState.ReturnUrl))
{
NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload

View File

@@ -3,10 +3,11 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_initialized)
{
@@ -71,6 +72,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="Your preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
@@ -95,6 +108,8 @@
@code {
private bool _initialized = false;
private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Language> _languages;
private string _passwordrequirements;
private string _username = string.Empty;
private ElementReference form;
@@ -106,6 +121,7 @@
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private bool _userCreated = false;
private bool _allowsitelogin = true;
@@ -113,10 +129,13 @@
protected override async Task OnInitializedAsync()
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_culturecode = PageState.Site.CultureCode;
_initialized = true;
}
@@ -147,6 +166,7 @@
Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
CultureCode = _culturecode,
PhotoFileId = null
};
user = await UserService.AddUserAsync(user);

View File

@@ -11,11 +11,15 @@
@inject IThemeService ThemeService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject ILocalizationService LocalizationService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService
@inject IJobService JobService
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService
@inject ISiteGroupService SiteGroupService
@inject ISiteGroupMemberService SiteGroupMemberService
@if (_initialized)
{
@@ -28,22 +32,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="homepage" HelpText="Select the home page for the site (to be used if there is no page with a path of '/')" ResourceKey="HomePage">Home Page: </Label>
<div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (Page page in _pages)
{
if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<Label Class="col-sm-3" For="timezone" HelpText="The default time zone for the site" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@@ -54,6 +43,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The default language of the site's content" ResourceKey="Culture">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var culture in _cultures)
{
<option value="@culture.Name">@culture.DisplayName</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
@@ -207,13 +208,6 @@
</div>
@if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
@@ -264,15 +258,6 @@
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
@@ -348,57 +333,6 @@
</Section>
@if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@((context.IsDefault) ? SharedLocalizer["Yes"] : SharedLocalizer["No"])</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
</div>
</div>
</Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
<div class="container">
<div class="row mb-1 align-items-center">
@@ -411,8 +345,20 @@
</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>
<Label Class="col-sm-3" For="runtime" HelpText="The hosting model for UI components which require interactivity" ResourceKey="Runtime">Interactivity: </Label>
<div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option>
@@ -441,6 +387,180 @@
</div>
</div>
</Section>
<Section Name="Aliases" Heading="Urls" ResourceKey="Aliases">
<div class="container">
@if (!_addAlias)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The urls for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="aliases" class="form-select" value="@_aliasid" @onchange="(e => AliasChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var alias in _aliases)
{
<option value="@alias.AliasId">@alias.Name @((alias.IsDefault) ? "(" + Localizer["Default"] + ")" : "")</option>
}
</select>
@if (!_addAlias)
{
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
}
</div>
</div>
</div>
}
@if (_aliasid != -1 || _addAlias)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliasname" HelpText="A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="AliasName">Url: </Label>
<div class="col-sm-9">
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultalias" HelpText="The default alias for the site. Requests for non-default aliases will be redirected to the default alias." ResourceKey="DefaultAlias">Default? </Label>
<div class="col-sm-9">
<select id="defaultalias" class="form-select" @bind="@_defaultalias">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<div class="col-sm-3"></div>
<div class="col-sm-9">
@if (_aliasid != -1 || _addAlias)
{
<button type="button" class="btn btn-success me-2" @onclick="SaveAlias">@SharedLocalizer["Save"]</button>
}
@if (_aliasid != -1 && !_addAlias)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias())" ResourceKey="DeleteAlias" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", _aliasname])" />
}
@if (_addAlias)
{
<button type="button" class="btn btn-secondary" @onclick="CancelAlias">@SharedLocalizer["Cancel"]</button>
}
</div>
</div>
</div>
</Section>
<Section Name="SiteGroupMembers" Heading="Site Groups" ResourceKey="SiteGroupMembers">
<div class="container">
@if (!_addSiteGroup)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="group" HelpText="The site groups in this tenant (database)" ResourceKey="SiteGroupMembers">Group: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="group" class="form-select" value="@_siteGroupId" @onchange="(e => SiteGroupChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var siteGroup in _siteGroups)
{
<option value="@siteGroup.SiteGroupId">@siteGroup.Name</option>
}
</select>
@if (!_addSiteGroup)
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroup">@SharedLocalizer["Add"]</button>
}
</div>
</div>
</div>
}
@if (_siteGroupId != -1 || _addSiteGroup)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="groupname" HelpText="Name of the site group" ResourceKey="GroupName">Name: </Label>
<div class="col-sm-9">
<input id="groupname" class="form-control" @bind="@_groupName" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="grouptype" HelpText="Defines the specific behavior of the site group" ResourceKey="GroupType">Type: </Label>
<div class="col-sm-9">
<select id="grouptype" class="form-select" @bind="@_groupType">
<option value="@SiteGroupTypes.Synchronization">@Localizer[@SiteGroupTypes.Synchronization]</option>
<option value="@SiteGroupTypes.ChangeDetection">@Localizer[@SiteGroupTypes.ChangeDetection]</option>
<option value="@SiteGroupTypes.Localization">@Localizer[SiteGroupTypes.Localization]</option>
</select>
</div>
</div>
}
@if (_siteGroupId != -1)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="site" HelpText="The sites which are members of this site group" ResourceKey="Site">Members: </Label>
<div class="col-sm-9">
<div class="input-group">
<select id="site" class="form-select" value="@_siteId" @onchange="(e => SiteChanged(e))">
<option value="-1">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var site in _sites)
{
<option value="@site.SiteId">@site.Name</option>
}
</select>
@if (!_addSiteGroupMember)
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroupMember">@SharedLocalizer["Add"]</button>
}
else
{
<button type="button" class="btn btn-primary" @onclick="AddSiteGroupMember">@SharedLocalizer["Select"]</button>
}
</div>
</div>
</div>
}
@if (_siteGroupId != -1 && _siteId != -1)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="primary" HelpText="Indicates if the selected member is the primary site of the site group" ResourceKey="Primary">Primary? </Label>
<div class="col-sm-9">
<select id="primary" class="form-select" @bind="@_primary">
<option value="False">@SharedLocalizer["No"]</option>
<option value="True">@SharedLocalizer["Yes"]</option>
</select>
</div>
</div>
@if (_primary == "False" && !string.IsNullOrEmpty(_synchronized) && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronized" HelpText="The date/time when the site was last synchronized" ResourceKey="Synchronized">Synchronized: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="synchronized" class="form-control" @bind="@_synchronized" disabled />
@if (!string.IsNullOrEmpty(_synchronized))
{
<button type="button" class="btn btn-primary" @onclick="ResetSiteGroupMember">@SharedLocalizer["Reset"]</button>
}
</div>
</div>
</div>
}
}
<div class="row mb-1 align-items-center">
<div class="col-sm-3"></div>
<div class="col-sm-9">
@if ((_siteGroupId != -1 || _addSiteGroup))
{
<button type="button" class="btn btn-success me-2" @onclick="SaveSiteGroupMember">@SharedLocalizer["Save"]</button>
}
@if (_siteGroupId != -1 && !_addSiteGroup && _siteId != -1 && !_addSiteGroupMember)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteSiteGroupMember())" ResourceKey="DeleteSiteGroupMember" Class="btn btn-danger" Header="Delete Site Group" Message="@Localizer["Confirm.SiteGroupMember.Delete"]" />
}
@if (_addSiteGroup)
{
<button type="button" class="btn btn-secondary" @onclick="CancelSiteGroupMember">@SharedLocalizer["Cancel"]</button>
}
</div>
</div>
</div>
</Section>
<Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation">
<div class="container">
<div class="row mb-1 align-items-center">
@@ -481,10 +601,11 @@
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Culture> _cultures;
private string _name = string.Empty;
private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _isdeleted;
private string _sitemap = "";
private string _siteguid = "";
@@ -522,7 +643,6 @@
private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private int _retention = 30;
private string _pwaisenabled;
@@ -531,15 +651,28 @@
private int _pwasplashiconfileid = -1;
private FileManager _pwasplashiconfilemanager;
private string _rendermode = RenderModes.Interactive;
private string _enhancednavigation = "True";
private string _runtime = Runtimes.Server;
private string _prerender = "True";
private string _hybrid = "False";
private List<Alias> _aliases;
private int _aliasid = -1;
private string _aliasname;
private string _defaultalias;
private bool _addAlias = false;
private string _rendermode = RenderModes.Interactive;
private string _runtime = Runtimes.Server;
private string _prerender = "True";
private string _hybrid = "False";
private List<SiteGroup> _siteGroups = new List<SiteGroup>();
private List<Site> _sites = new List<Site>();
private int _siteGroupId = -1;
private int _siteId;
private string _groupName = string.Empty;
private string _groupType = SiteGroupTypes.Synchronization;
private string _primary = "True";
private string _synchronized = string.Empty;
private bool _addSiteGroup = false;
private bool _addSiteGroupMember = false;
private string _tenant = string.Empty;
private string _database = string.Empty;
@@ -567,16 +700,14 @@
if (site != null)
{
_timezones = TimeZoneService.GetTimeZones();
_cultures = await LocalizationService.GetNeutralCulturesAsync();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null)
{
_homepageid = site.HomePageId.Value.ToString();
}
_culturecode = site.CultureCode;
_isdeleted = site.IsDeleted.ToString();
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml";
_siteguid = site.SiteGuid;
@@ -640,7 +771,6 @@
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
}
@@ -656,10 +786,11 @@
}
// aliases
await GetAliases();
await LoadAliases();
// hosting model
_rendermode = site.RenderMode;
_enhancednavigation = site.EnhancedNavigation.ToString();
_runtime = site.Runtime;
_prerender = site.Prerender.ToString();
_hybrid = site.Hybrid.ToString();
@@ -669,7 +800,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;
@@ -679,6 +810,12 @@
}
}
// site groups
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await LoadSiteGroups();
}
// audit
_createdby = site.CreatedBy;
_createdon = site.CreatedOn;
@@ -746,7 +883,7 @@
{
site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.CultureCode = _culturecode;
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
// appearance
@@ -807,13 +944,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);
@@ -834,8 +969,18 @@
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
if (_smtpenabled == "True")
{
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
}
//cookie consent
@@ -874,17 +1019,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
@@ -945,7 +1090,10 @@
{
try
{
_smtpenabled = "True";
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
@@ -960,6 +1108,14 @@
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
await ScrollToPageTop();
@@ -976,95 +1132,100 @@
}
}
private async Task GetAliases()
private async Task LoadAliases()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList();
_aliasid = -1;
_addAlias = false;
}
private async void AliasChanged(ChangeEventArgs e)
{
_aliasid = int.Parse(e.Value.ToString());
if (_aliasid != -1)
{
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
var alias = _aliases.FirstOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
}
}
else
{
_aliasname = "";
_defaultalias = "False";
}
StateHasChanged();
}
private void AddAlias()
{
_aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false });
_aliasid = 0;
_aliasid = -1;
_aliasname = "";
_defaultalias = "False";
_addAlias = true;
StateHasChanged();
}
private void EditAlias(Alias alias)
private async Task DeleteAlias()
{
_aliasid = alias.AliasId;
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
await AliasService.DeleteAliasAsync(_aliasid);
await LoadAliases();
StateHasChanged();
}
private async Task DeleteAlias(Alias alias)
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
await GetAliases();
StateHasChanged();
}
}
private async Task SaveAlias()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
if (!string.IsNullOrEmpty(_aliasname))
{
if (!string.IsNullOrEmpty(_aliasname))
var aliases = await AliasService.GetAliasesAsync();
int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase);
if (protocolIndex != -1)
{
var aliases = await AliasService.GetAliasesAsync();
_aliasname = _aliasname.Substring(protocolIndex + 3);
}
int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase);
if (protocolIndex != -1)
var alias = aliases.FirstOrDefault(item => item.Name == _aliasname);
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
{
if (_aliasid == -1)
{
_aliasname = _aliasname.Substring(protocolIndex + 3);
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
var alias = aliases.FirstOrDefault(item => item.Name == _aliasname);
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
else
{
if (_aliasid == 0)
alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
else
{
alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid);
if (alias != null)
{
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
await GetAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
else // Duplicate alias
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
await ScrollToPageTop();
}
await LoadAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
else // Duplicate alias
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
await ScrollToPageTop();
}
}
}
private async Task CancelAlias()
{
await GetAliases();
await LoadAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
@@ -1074,4 +1235,205 @@
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
}
private async Task LoadSiteGroups()
{
_siteGroups = await SiteGroupService.GetSiteGroupsAsync();
_siteGroupId = -1;
_addSiteGroup = false;
StateHasChanged();
}
private async Task SiteGroupChanged(ChangeEventArgs e)
{
_siteGroupId = int.Parse(e.Value.ToString());
if (_siteGroupId != -1)
{
var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
if (group != null)
{
_groupName = group.Name;
_groupType = group.Type;
_siteId = -1;
_primary = "False";
_addSiteGroupMember = false;
await LoadSites();
}
}
StateHasChanged();
}
private async Task LoadSites()
{
var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId);
_sites = await SiteService.GetSitesAsync();
if (_addSiteGroupMember)
{
// include sites which are not members
_sites = _sites.ExceptBy(siteGroupMembers.Select(item => item.SiteId), item => item.SiteId).ToList();
}
else
{
// include sites which are members
_sites = _sites.Where(item => siteGroupMembers.Any(item2 => item2.SiteId == item.SiteId)).ToList();
var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
foreach (var site in _sites)
{
if (group.PrimarySiteId == site.SiteId)
{
site.Name += $" ({Localizer["Primary"]})";
}
else
{
site.Name += $" ({Localizer["Secondary"]})";
}
}
var siteGroupMember = siteGroupMembers.FirstOrDefault(item => item.SiteId == _siteId);
if (siteGroupMember != null)
{
_primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False";
_synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString();
}
}
}
private async Task SiteChanged(ChangeEventArgs e)
{
_siteId = int.Parse(e.Value.ToString());
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId);
if (siteGroupMember != null)
{
_primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False";
_synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString();
}
StateHasChanged();
}
private async Task AddSiteGroup()
{
_groupName = "";
_siteId = PageState.Site.SiteId;
_primary = "True";
_synchronized = "";
_addSiteGroup = true;
}
private async Task AddSiteGroupMember()
{
_addSiteGroupMember = !_addSiteGroupMember;
_siteId = -1;
await LoadSites();
}
private async Task SaveSiteGroupMember()
{
if (string.IsNullOrEmpty(_groupName))
{
AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning);
await ScrollToPageTop();
return;
}
SiteGroup siteGroup = null;
if (_siteGroupId == -1)
{
siteGroup = new SiteGroup
{
Name = _groupName,
Type = _groupType,
PrimarySiteId = _siteId,
Synchronize = false
};
siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup);
}
else
{
siteGroup = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId);
if (siteGroup != null)
{
siteGroup.Name = _groupName;
siteGroup.Type = _groupType;
siteGroup.PrimarySiteId = (_primary == "True") ? _siteId : siteGroup.PrimarySiteId;
siteGroup = await SiteGroupService.UpdateSiteGroupAsync(siteGroup);
}
else
{
siteGroup = null;
}
}
if (siteGroup != null)
{
if (_siteId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId);
if (siteGroupMember == null)
{
siteGroupMember = new SiteGroupMember
{
SiteGroupId = siteGroup.SiteGroupId,
SiteId = _siteId
};
await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember);
}
else
{
siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn;
await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember);
}
}
if (siteGroup.Type == SiteGroupTypes.Synchronization)
{
// enable synchronization job if it is not enabled already
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
await LoadSiteGroups();
}
}
private async Task CancelSiteGroupMember()
{
_groupName = "";
await LoadSiteGroups();
}
private async Task DeleteSiteGroupMember()
{
if (_siteGroupId != -1)
{
if (_siteId != -1)
{
var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId);
if (siteGroupMember != null)
{
await SiteGroupMemberService.DeleteSiteGroupMemberAsync(siteGroupMember.SiteGroupMemberId);
}
}
var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId);
if (!siteGroupMembers.Any())
{
await SiteGroupService.DeleteSiteGroupAsync(_siteGroupId);
}
await LoadSiteGroups();
}
}
private async Task ResetSiteGroupMember()
{
_synchronized = "";
}
}

View File

@@ -6,7 +6,6 @@
@inject ITenantService TenantService
@inject IAliasService AliasService
@inject ISiteService SiteService
@inject IThemeService ThemeService
@inject ISiteTemplateService SiteTemplateService
@inject IUserService UserService
@inject IInstallationService InstallationService
@@ -29,33 +28,9 @@ else
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder)." ResourceKey="Aliases">Urls: </Label>
<Label Class="col-sm-3" For="alias" HelpText="The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder)." ResourceKey="Aliases">Url: </Label>
<div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the default theme for the website" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
<select id="defaultTheme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
<option value="-">&lt;@Localizer["Theme.Select"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
<div class="col-sm-9">
<select id="defaultContainer" class="form-select" @bind="@_containertype" required>
<option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
<input id="alias" class="form-control" @bind="@_urls" required></input>
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -70,26 +45,6 @@ else
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="rendermode" HelpText="The default render mode for the site" ResourceKey="Rendermode">Render Mode: </Label>
<div class="col-sm-9">
<select id="rendermode" class="form-select" @bind="@_rendermode" required>
<option value="@RenderModes.Interactive">@(SharedLocalizer["RenderMode" + @RenderModes.Interactive])</option>
<option value="@RenderModes.Static">@(SharedLocalizer["RenderMode" + @RenderModes.Static])</option>
<option value="@RenderModes.Headless">@(SharedLocalizer["RenderMode" + @RenderModes.Headless])</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">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="@Runtimes.Server">@(SharedLocalizer["Runtime" + @Runtimes.Server])</option>
<option value="@Runtimes.WebAssembly">@(SharedLocalizer["Runtime" + @Runtimes.WebAssembly])</option>
<option value="@Runtimes.Auto">@(SharedLocalizer["Runtime" + @Runtimes.Auto])</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the database for the site" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9">
@@ -186,9 +141,6 @@ else
private bool _showConnectionString = false;
private string _connectionString = string.Empty;
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants;
private string _tenantid = "-";
@@ -200,11 +152,7 @@ else
private string _name = string.Empty;
private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _sitetemplatetype = "-";
private string _rendermode = RenderModes.Static;
private string _runtime = Runtimes.Server;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -215,19 +163,11 @@ else
{
_tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString();
}
_urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId);
_themes = ThemeService.GetThemeControls(_themeList);
if (_themes.Any(item => item.TypeName == Constants.DefaultTheme))
{
_themetype = Constants.DefaultTheme;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = _containers.First().TypeName;
}
_urls = PageState.Alias.Name + "/sitename";
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
if (_siteTemplates.Any(item => item.TypeName == Constants.DefaultSiteTemplate))
if (_siteTemplates.Any(item => item.TypeName == Constants.EmptySiteTemplate))
{
_sitetemplatetype = Constants.DefaultSiteTemplate;
_sitetemplatetype = Constants.EmptySiteTemplate;
}
_databases = await DatabaseService.GetDatabasesAsync();
@@ -281,37 +221,13 @@ else
StateHasChanged();
}
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = _containers.First().TypeName;
}
else
{
_containers = new List<ThemeControl>();
_containertype = "-";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error);
}
}
private async Task SaveSite()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-")
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _sitetemplatetype != "-")
{
_urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>();
@@ -399,12 +315,12 @@ else
{
config.SiteName = _name;
config.Aliases = _urls;
config.DefaultTheme = _themetype;
config.DefaultContainer = _containertype;
config.DefaultTheme = Constants.DefaultTheme;
config.DefaultContainer = Constants.DefaultContainer;
config.DefaultAdminContainer = "";
config.SiteTemplate = _sitetemplatetype;
config.RenderMode = _rendermode;
config.Runtime = _runtime;
config.RenderMode = RenderModes.Static;
config.Runtime = Runtimes.Server;
config.Register = false;
ShowProgressIndicator();

View File

@@ -83,14 +83,14 @@ else
{
@if (_connection != "-")
{
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
</div>
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9">
@@ -150,130 +150,149 @@ else
}
@code {
private string _connection = "-";
private Dictionary<string, object> _connections;
private List<Tenant> _tenants;
private List<Database> _databases;
private string _connection = "-";
private Dictionary<string, object> _connections;
private List<Tenant> _tenants;
private List<Database> _databases;
private string _name = string.Empty;
private string _databasetype = string.Empty;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _tenant = string.Empty;
private string _connectionstring = string.Empty;
private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty;
private List<Dictionary<string, string>> _results;
private string _name = string.Empty;
private string _databasetype = string.Empty;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _tenant = string.Empty;
private string _connectionstring = string.Empty;
private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty;
private List<Dictionary<string, string>> _results;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
protected override async Task OnInitializedAsync()
{
try
{
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private async void ConnectionChanged(ChangeEventArgs e)
{
try
{
_connection = (string)e.Value;
if (_connection != "-" && _connection != "+")
{
_connectionstring = _connections[_connection].ToString();
_tenant = "";
_databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null)
{
_tenant = tenant.Name;
private async void ConnectionChanged(ChangeEventArgs e)
{
try
{
_connection = (string)e.Value;
if (_connection != "-" && _connection != "+")
{
_connectionstring = _connections[_connection].ToString();
_tenant = "";
_databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null)
{
_tenant = tenant.Name;
// hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order
_databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name;
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
}
else
{
if (_connection.Contains(" ("))
{
_databasetype = _connection.Substring(_connection.LastIndexOf(" (") + 2).Replace(")", "");
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName;
}
}
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName;
}
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{
try
{
_databasetype = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{
try
{
_databasetype = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
if (database != null)
{
_databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder =>
{
builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent();
};
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
if (database != null)
{
_databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder =>
{
builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent();
};
}
}
private void ShowConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionstring = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
private void ShowConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionstring = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
private async Task Add()
{
var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionstring = databaseConfigControl.GetConnectionString();
}
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{
var settings = new Dictionary<string, object>();
private async Task Add()
{
var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionstring = databaseConfigControl.GetConnectionString();
}
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{
_name = _name + " (" + _databasetype +")";
var settings = new Dictionary<string, object>();
settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring);
await SystemService.UpdateSystemInfoAsync(settings);
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");

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

@@ -30,6 +30,7 @@
</div>
</div>
</form>
<br />
<Section Name="Information" ResourceKey="Information" Heading="Information">
<div class="container">
<div class="row mb-1 align-items-center">
@@ -81,6 +82,12 @@
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="fingerprint" HelpText="A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting)." ResourceKey="Fingerprint">Fingerprint: </Label>
<div class="col-sm-9">
<input id="fingerprint" class="form-control" @bind="@_fingerprint" disabled />
</div>
</div>
</div>
</Section>
<br />
@@ -117,6 +124,7 @@
private string _url = "";
private string _contact = "";
private string _license = "";
private string _fingerprint = "";
private List<Permission> _permissions = null;
private string _createdby;
private DateTime _createdon;
@@ -143,6 +151,7 @@
_url = theme.Url;
_contact = theme.Contact;
_license = theme.License;
_fingerprint = theme.Fingerprint;
_permissions = theme.PermissionList;
_createdby = theme.CreatedBy;
_createdon = theme.CreatedOn;

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

@@ -10,6 +10,7 @@
@inject IFileService FileService
@inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@@ -58,6 +59,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="Your preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
@@ -114,9 +127,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 +155,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 +178,7 @@
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
<div class="mt-2">@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />
@@ -448,6 +461,9 @@
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private List<Models.TimeZone> _timezones;
private IEnumerable<Language> _languages;
private bool _initialized = false;
private bool _allowtwofactor = false;
private bool _allowpasskeys = false;
@@ -458,8 +474,8 @@
private string _displayname = string.Empty;
private FileManager _filemanager;
private int _folderid = -1;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private int _photofileid = -1;
private File _photo = null;
private string _imagefiles = string.Empty;
@@ -493,12 +509,15 @@
if (PageState.User != null)
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
// identity section
_username = PageState.User.Username;
_email = PageState.User.Email;
_displayname = PageState.User.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
_culturecode = PageState.User.CultureCode;
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
{
@@ -572,6 +591,7 @@
user.Email = _email;
user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1)
{

View File

@@ -6,6 +6,7 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -55,6 +56,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The user's preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9">
@@ -129,12 +142,15 @@
@code {
private List<Models.TimeZone> _timezones;
private IEnumerable<Models.Language> _languages;
private bool _initialized = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _notify = "True";
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
@@ -147,6 +163,11 @@
try
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_timezoneid = PageState.Site.TimeZoneId;
_culturecode = PageState.Site.CultureCode;
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
foreach (var profile in _profiles)
{
@@ -157,8 +178,9 @@
profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}"));
}
}
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}
catch (Exception ex)
@@ -194,6 +216,7 @@
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);

View File

@@ -7,6 +7,7 @@
@inject ISettingService SettingService
@inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject ILanguageService LanguageService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -55,6 +56,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="culture" HelpText="The user's preferred language. Note that you will only be able to choose from languages supported on this site." ResourceKey="CultureCode">Language:</Label>
<div class="col-sm-9">
<select id="culture" class="form-select" @bind="@_culturecode">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var language in _languages)
{
<option value="@language.Code">@language.Name</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
@@ -106,8 +119,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 +135,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 +158,7 @@
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
<div class="mt-2">@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />
@@ -211,7 +224,7 @@
{
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True")
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _candelete)
{
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
@@ -224,17 +237,21 @@
private bool _allowpasskeys = false;
private bool _allowexternallogin = false;
private List<Models.TimeZone> _timezones;
private IEnumerable<Language> _languages;
private int _userid;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private string _culturecode = string.Empty;
private string _isdeleted;
private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private bool _candelete = false;
private string _passwordrequirements;
private string _password = string.Empty;
@@ -270,13 +287,17 @@
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null)
{
_timezones = TimeZoneService.GetTimeZones();
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
_username = user.Username;
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.User.TimeZoneId;
_culturecode = PageState.User.CultureCode;
_isdeleted = user.IsDeleted.ToString();
_candelete = user.IsDeleted;
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
@@ -344,6 +365,7 @@
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.CultureCode = _culturecode;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));

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

@@ -1,7 +1,7 @@
@namespace Oqtane.Modules.Admin.Users
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject ISiteTaskService SiteTaskService
@inject IStringLocalizer<Users> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -43,17 +43,9 @@
var fileid = _filemanager.GetFileId();
if (fileid != -1)
{
ShowProgressIndicator();
var results = await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid, bool.Parse(_notify));
if (bool.Parse(results["Success"]))
{
AddModuleMessage(string.Format(Localizer["Message.Import.Success"], results["Users"]), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error);
}
HideProgressIndicator();
var siteTask = new SiteTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}");
await SiteTaskService.AddSiteTaskAsync(siteTask);
AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success);
}
else
{

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

@@ -61,6 +61,12 @@
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" />
}
@if (MaxUploadFileSize > 0)
{
<div class="row my-1">
<small class="fw-light">@string.Format(Localizer["File.MaxSize"], MaxUploadFileSize)</small>
</div>
}
</div>
<div class="col-auto">
<button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@@ -163,6 +169,9 @@
[Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter]
public int MaxUploadFileSize { get; set; } = -1; // optional - maximum upload file size in MB
[Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
@@ -381,16 +390,39 @@
if (uploads.Length > 0)
{
string restricted = "";
string tooLarge = "";
foreach (var upload in uploads)
{
var filename = upload.Split(':')[0];
var fileparts = upload.Split(':');
var filename = fileparts[0];
if (MaxUploadFileSize > 0)
{
var filesizeBytes = long.Parse(fileparts[1]);
var filesizeMB = (double)filesizeBytes / (1024 * 1024);
if (filesizeMB > MaxUploadFileSize)
{
tooLarge += (tooLarge == "" ? "" : ",") + filename;
}
}
var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : "";
if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower()))
{
restricted += (restricted == "" ? "" : ",") + extension;
}
}
if (restricted == "")
if (restricted != "")
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
else if (tooLarge != "")
{
_message = string.Format(Localizer["Message.File.TooLarge"], tooLarge, MaxUploadFileSize);
_messagetype = MessageType.Warning;
}
else
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
@@ -490,11 +522,6 @@
tokenSource.Dispose();
}
}
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
}
else
{

View File

@@ -21,7 +21,7 @@
@if (_style == MessageStyle.Toast)
{
<div class="app-modulemessage-toast bottom-0 end-0" @key="DateTime.UtcNow">
<div class="app-modulemessage-toast bottom-0 end-0">
<div class="@_classname alert-dismissible fade show mb-3 rounded-end-0" role="alert">
@((MarkupString)Message)
@if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

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,14 +84,63 @@
}
}
/// <summary>
/// Determines if a tab should be visible based on user permissions.
/// Authorization follows this hierarchy:
/// 1. Host tabs (Security == Host): Only users with Host role can access (Admins excluded)
/// 2. Admin users: Bypass all other checks (except Host restrictions)
/// 3. SecurityAccessLevel check (null/Anonymous/View/Edit/Host):
/// - null: No security level restriction (proceeds to step 4)
/// - Anonymous: No authentication required
/// - View/Edit: Requires corresponding module permission
/// - Host: Only Host role can access
/// 4. Additional RoleName requirement (if specified)
/// 5. Additional PermissionName requirement (if specified)
///
/// Important: When Security is null, RoleName and PermissionName checks STILL apply
/// (Security = null doesn't mean unrestricted, it means "no security level required")
/// </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)
{
var authorized = false;
// Step 1: Check for Host-only restriction
if (tabPanel.Security == SecurityAccessLevel.Host)
{
return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
}
// Step 2: Admin bypass all restrictions except Host
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
// Step 3: If Security is null, check only RoleName and PermissionName
if (tabPanel.Security == null)
{
// Start with authorized = true for null security
bool isAuthorized = true;
// Only apply RoleName check if provided
if (!string.IsNullOrEmpty(tabPanel.RoleName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Only apply PermissionName check if provided
if (isAuthorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);
}
return isAuthorized;
}
// Handle other SecurityAccessLevel values
bool authorized = false; // Use different variable name or move declaration
switch (tabPanel.Security)
{
case null: // security not specified - assume SecurityAccessLevel.Anonymous
authorized = true;
break;
case SecurityAccessLevel.Anonymous:
authorized = true;
break;
@@ -101,13 +150,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: Additional RoleName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName))
{
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Step 5: Additional PermissionName 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

@@ -31,8 +31,8 @@
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["CreatedOn"]</th>
<th>@SharedLocalizer["CreatedBy"]</th>
<th>@Localizer["CreatedOn"]</th>
<th>@Localizer["CreatedBy"]</th>
</Header>
<Row>
<td><ActionLink Action="View" Security="SecurityAccessLevel.Edit" OnClick="@(async () => await View(context))" ResourceKey="View" /></td>
@@ -122,13 +122,9 @@
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
if (htmltext != null)
{
_view = htmltext.Content;
_view = Utilities.FormatContent(_view, PageState.Alias, "render");
StateHasChanged();
}
_view = htmltext.Content;
_view = Utilities.FormatContent(_view, PageState.Alias, "render");
StateHasChanged();
}
catch (Exception ex)
{
@@ -141,19 +137,15 @@
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId);
if (htmltext != null)
{
var content = htmltext.Content;
htmltext = new HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
await logger.LogInformation("Content Restored {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
var content = htmltext.Content;
htmltext = new HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
await logger.LogInformation("Content Restored {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
catch (Exception ex)
{
@@ -166,15 +158,11 @@
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId);
if (htmltext != null)
{
await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
await logger.LogInformation("Content Deleted {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
await logger.LogInformation("Content Deleted {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
catch (Exception ex)
{

View File

@@ -7,6 +7,18 @@ using Oqtane.Shared;
namespace Oqtane.Modules.HtmlText.Services
{
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public interface IHtmlTextService
{
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService
{
@@ -24,11 +36,6 @@ namespace Oqtane.Modules.HtmlText.Services
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
{
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmlText)
{
return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText);

View File

@@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
namespace Oqtane.Modules.HtmlText.Services
{
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public interface IHtmlTextService
{
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId);
Task<Models.HtmlText> AddHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
}

View File

@@ -16,6 +16,12 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="versions" ResourceKey="Versions" ResourceType="@resourceType" HelpText="The number of content versions to preserve (note that zero means unlimited)">Versions: </Label>
<div class="col-sm-9">
<input id="versions" type="number" min="0" max="9" step="1" class="form-control" @bind="@_versions" />
</div>
</div>
</div>
</form>
@@ -26,12 +32,14 @@
private bool validated = false;
private string _dynamictokens;
private string _versions = "5";
protected override void OnInitialized()
{
try
{
_dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false");
_versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "5");
}
catch (Exception ex)
{
@@ -45,6 +53,10 @@
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens);
if (int.TryParse(_versions, out int versions) && versions >= 0 && versions <= 9)
{
settings = SettingService.SetSetting(settings, "Versions", versions.ToString());
}
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)

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.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Radzen.Blazor" Version="10.0.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CaseSensitive.Text" xml:space="preserve">
<value>Match Case?</value>
</data>
<data name="CaseSensitive.HelpText" xml:space="preserve">
<value>Specify if the replacement operation should be case sensitive</value>
</data>
<data name="Content.Text" xml:space="preserve">
<value>Module Content?</value>
</data>
<data name="Content.HelpText" xml:space="preserve">
<value>Specify if module content should be updated</value>
</data>
<data name="Pages.Text" xml:space="preserve">
<value>Page Info?</value>
</data>
<data name="Pages.HelpText" xml:space="preserve">
<value>Specify if page information should be updated (ie. name, title, head content, body content settings)</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Site Info?</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>Specify if site information should be updated (ie. name, head content, body content, settings)</value>
</data>
<data name="Replace.Text" xml:space="preserve">
<value>Replace With:</value>
</data>
<data name="Replace.HelpText" xml:space="preserve">
<value>Specify the replacement content</value>
</data>
<data name="Modules.Text" xml:space="preserve">
<value>Module Info?</value>
</data>
<data name="Modules.HelpText" xml:space="preserve">
<value>Specify if module information should be updated (ie. title, header, footer settings)</value>
</data>
<data name="Success.Save" xml:space="preserve">
<value>Your Global Replace Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient.</value>
</data>
<data name="Error.Save" xml:space="preserve">
<value>Error Saving Global Replace</value>
</data>
<data name="Find.HelpText" xml:space="preserve">
<value>Specify the content which needs to be replaced</value>
</data>
<data name="Find.Text" xml:space="preserve">
<value>Find What:</value>
</data>
<data name="GlobalReplace.Header" xml:space="preserve">
<value>Global Replace</value>
</data>
<data name="GlobalReplace.Message" xml:space="preserve">
<value>This Operation is Permanent. Are You Sure You Wish To Proceed?</value>
</data>
</root>

View File

@@ -139,7 +139,7 @@
<value>Error Updating Job</value>
</data>
<data name="Message.Required.JobInfo" xml:space="preserve">
<value>You Must Provide The Job Name, Type, Frequency, and Retention</value>
<value>You Must Provide The Job Name, Frequency, and Retention</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the job name</value>
@@ -154,7 +154,7 @@
<value>Select how often you want the job to run</value>
</data>
<data name="Starting.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should start executing</value>
<value>Optionally enter the date and time when this job should start executing. If no date or time is specified, the job will execute immediately.</value>
</data>
<data name="Ending.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should stop executing</value>
@@ -163,7 +163,7 @@
<value>Number of log entries to retain for this job</value>
</data>
<data name="NextExecution.HelpText" xml:space="preserve">
<value>Optionally modify the date and time when this job should execute next</value>
<value>The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job.</value>
</data>
<data name="Type.Text" xml:space="preserve">
<value>Type: </value>
@@ -193,6 +193,15 @@
<value>Execute Once</value>
</data>
<data name="Message.StartEndDateError" xml:space="preserve">
<value>Start Date cannot be after End Date.</value>
<value>The Start Date Cannot Be Later Than The End Date</value>
</data>
<data name="Message.StartDateError" xml:space="preserve">
<value>The Start Date Cannot Be Prior To The Current Date</value>
</data>
<data name="Message.ExecutingError" xml:space="preserve">
<value>The Job Is Currently Executing. You Must Wait Until The Job Has Completed.</value>
</data>
<data name="JobNotEditable" xml:space="preserve">
<value>The Job Cannot Be Modified As It Is Currently Enabled. You Must Disable The Job To Change Its Settings.</value>
</data>
</root>

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

@@ -234,4 +234,10 @@
<data name="Pages.Heading" xml:space="preserve">
<value>Pages</value>
</data>
<data name="Fingerprint.Text" xml:space="preserve">
<value>Fingerprint:</value>
</data>
<data name="Fingerprint.HelpText" xml:space="preserve">
<value>A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting).</value>
</data>
</root>

View File

@@ -130,7 +130,7 @@
<value>Indicate if this module should be displayed on all pages</value>
</data>
<data name="Page.HelpText" xml:space="preserve">
<value>The page that the module is located on</value>
<value>The page that the module is located on. Please note that shared modules cannot be moved to other pages.</value>
</data>
<data name="Title.Text" xml:space="preserve">
<value>Title: </value>

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

@@ -186,4 +186,10 @@
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>Your preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root>

View File

@@ -132,9 +132,6 @@
<data name="DefaultAdminContainer" xml:space="preserve">
<value>Default Admin Container</value>
</data>
<data name="Smtp.Required.EnableNotificationJob" xml:space="preserve">
<value>** Please Note That SMTP Requires The Notification Job To Be Enabled In Scheduled Jobs</value>
</data>
<data name="Smtp.TestConfig" xml:space="preserve">
<value>Test SMTP Configuration</value>
</data>
@@ -322,16 +319,16 @@
<value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value>
</data>
<data name="DefaultAlias.Text" xml:space="preserve">
<value>Default Alias: </value>
<value>Default?</value>
</data>
<data name="Aliases.Heading" xml:space="preserve">
<value>Site Urls</value>
</data>
<data name="AliasName" xml:space="preserve">
<value>Url</value>
<data name="AliasName.Text" xml:space="preserve">
<value>Url:</value>
</data>
<data name="AliasDefault" xml:space="preserve">
<value>Default?</value>
<data name="Default" xml:space="preserve">
<value>Default</value>
</data>
<data name="Confirm.Alias.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
@@ -342,12 +339,6 @@
<data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value>
</data>
<data name="SmtpRelay.HelpText" xml:space="preserve">
<value>Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above.</value>
</data>
<data name="SmtpRelay.Text" xml:space="preserve">
<value>Relay Configured?</value>
</data>
<data name="SiteMap.HelpText" xml:space="preserve">
<value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value>
</data>
@@ -409,7 +400,7 @@
<value>Hybrid Enabled?</value>
</data>
<data name="Runtime.HelpText" xml:space="preserve">
<value>The render mode for UI components which require interactivity</value>
<value>The hosting model for UI components which require interactivity</value>
</data>
<data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value>
@@ -504,4 +495,76 @@
<data name="StartTlsWhenAvailable" xml:space="preserve">
<value>Use TLS When Available</value>
</data>
<data name="AliasName.HelpText" xml:space="preserve">
<value>A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder).</value>
</data>
<data name="SiteGroupMembers.Text" xml:space="preserve">
<value>Group:</value>
</data>
<data name="SiteGroupMembers.HelpText" xml:space="preserve">
<value>The site groups in this tenant (database)</value>
</data>
<data name="Primary.Text" xml:space="preserve">
<value>Primary?</value>
</data>
<data name="Primary.HelpText" xml:space="preserve">
<value>Indicates if the selected member is the primary site of the site group</value>
</data>
<data name="GroupName.Text" xml:space="preserve">
<value>Name:</value>
</data>
<data name="GroupName.HelpText" xml:space="preserve">
<value>Name of the site group</value>
</data>
<data name="Primary" xml:space="preserve">
<value>Primary</value>
</data>
<data name="Secondary" xml:space="preserve">
<value>Secondary</value>
</data>
<data name="Compare" xml:space="preserve">
<value>Compare</value>
</data>
<data name="Update" xml:space="preserve">
<value>Update</value>
</data>
<data name="DeleteSiteGroupMember.Header" xml:space="preserve">
<value>Delete Site Group Member</value>
</data>
<data name="Confirm.SiteGroupMember.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete This Member From The Site Group?</value>
</data>
<data name="Message.Required.GroupName" xml:space="preserve">
<value>Site Group Name Is Required</value>
</data>
<data name="Message.Site.Synchronize" xml:space="preserve">
<value>Site Submitted For Synchronization</value>
</data>
<data name="Site.Text" xml:space="preserve">
<value>Members:</value>
</data>
<data name="Site.HelpText" xml:space="preserve">
<value>The sites which are members of this site group</value>
</data>
<data name="Synchronized.Text" xml:space="preserve">
<value>Synchronized:</value>
</data>
<data name="GroupType.Text" xml:space="preserve">
<value>Type:</value>
</data>
<data name="GroupType.HelpText" xml:space="preserve">
<value>Defines the specific behavior of the site group</value>
</data>
<data name="Synchronized.HelpText" xml:space="preserve">
<value>The date/time when the site was last synchronized</value>
</data>
<data name="Synchronization" xml:space="preserve">
<value>Synchronization</value>
</data>
<data name="Localization" xml:space="preserve">
<value>Localization</value>
</data>
<data name="ChangeDetection" xml:space="preserve">
<value>Change Detection</value>
</data>
</root>

View File

@@ -123,29 +123,14 @@
<data name="SqlServer" xml:space="preserve">
<value>SQL Server</value>
</data>
<data name="Container.Select" xml:space="preserve">
<value>Select Container</value>
</data>
<data name="DefaultContainer.Text" xml:space="preserve">
<value>Default Container: </value>
</data>
<data name="Theme.Select" xml:space="preserve">
<value>Select Theme</value>
</data>
<data name="Aliases.HelpText" xml:space="preserve">
<value>The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value>
</data>
<data name="DefaultContainer.HelpText" xml:space="preserve">
<value>Select the default container for the site</value>
<value>The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder).</value>
</data>
<data name="Tenant.Text" xml:space="preserve">
<value>Database: </value>
</data>
<data name="Aliases.Text" xml:space="preserve">
<value>Urls: </value>
</data>
<data name="DefaultTheme.Text" xml:space="preserve">
<value>Default Theme: </value>
<value>Url: </value>
</data>
<data name="SiteTemplate.Select" xml:space="preserve">
<value>Select Site Template</value>
@@ -177,9 +162,6 @@
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the name of the site</value>
</data>
<data name="DefaultTheme.HelpText" xml:space="preserve">
<value>Select the default theme for the site</value>
</data>
<data name="SiteTemplate.HelpText" xml:space="preserve">
<value>Select the site template</value>
</data>
@@ -222,12 +204,6 @@
<data name="Error.Database.LoadConfig" xml:space="preserve">
<value>Error loading Database Configuration Control</value>
</data>
<data name="RenderMode.HelpText" xml:space="preserve">
<value>The default render mode for the site</value>
</data>
<data name="RenderMode.Text" xml:space="preserve">
<value>Render Mode: </value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value>
</data>
@@ -240,10 +216,4 @@
<data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value>
</data>
<data name="Runtime.HelpText" xml:space="preserve">
<value>The render mode for UI components which require interactivity</value>
</data>
<data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value>
</data>
</root>

View File

@@ -186,4 +186,10 @@
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="Fingerprint.Text" xml:space="preserve">
<value>Fingerprint:</value>
</data>
<data name="Fingerprint.HelpText" xml:space="preserve">
<value>A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting).</value>
</data>
</root>

View File

@@ -148,7 +148,7 @@
<value>You Cannot Perform A System Update In A Development Environment</value>
</data>
<data name="Disclaimer.Text" xml:space="preserve">
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process.</value>
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Enterprise Installations And Microsoft Azure Installations You Will Want To Use A Manual Upgrade Process.</value>
</data>
<data name="Backup.Text" xml:space="preserve">
<value>Backup Files?</value>

View File

@@ -288,4 +288,10 @@
<data name="Error.Passkey.Fail" xml:space="preserve">
<value>Passkey Could Not Be Created</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>Your preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root>

View File

@@ -168,4 +168,10 @@
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value />
</data>
</root>

View File

@@ -252,4 +252,10 @@
<data name="Message.Logins.None" xml:space="preserve">
<value>You Do Not Have Any External Logins For This Site</value>
</data>
<data name="CultureCode.Text" xml:space="preserve">
<value>Language:</value>
</data>
<data name="CultureCode.HelpText" xml:space="preserve">
<value>The user's preferred language. Note that you will only be able to choose from languages supported on this site.</value>
</data>
</root>

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

@@ -129,11 +129,8 @@
<data name="Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="Message.Import.Failure" xml:space="preserve">
<value>User Import Failed. Please Review Your Event Log For More Detailed Information.</value>
</data>
<data name="Message.Import.Success" xml:space="preserve">
<value>User Import Successful. {0} Users Imported.</value>
<value>Your User Import Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient.</value>
</data>
<data name="Message.Import.Validation" xml:space="preserve">
<value>You Must Specify A User File For Import</value>

View File

@@ -144,4 +144,10 @@
<data name="Message.File.Restricted" xml:space="preserve">
<value>Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information.</value>
</data>
<data name="File.MaxSize" xml:space="preserve">
<value>Maximum upload file size: {0} MB</value>
</data>
<data name="Message.File.TooLarge" xml:space="preserve">
<value>File(s) {0} exceed(s) the maximum upload size of {1} MB</value>
</data>
</root>

View File

@@ -123,4 +123,10 @@
<data name="DynamicTokens.Text" xml:space="preserve">
<value>Dynamic Tokens?</value>
</data>
<data name="Versions.HelpText" xml:space="preserve">
<value>The number of content versions to preserve (note that zero means unlimited)</value>
</data>
<data name="Versions.Text" xml:space="preserve">
<value>Versions:</value>
</data>
</root>

View File

@@ -204,6 +204,9 @@
<data name="System Update" xml:space="preserve">
<value>System Update</value>
</data>
<data name="Setting Management" xml:space="preserve">
<value>Setting Management</value>
</data>
<data name="Download" xml:space="preserve">
<value>Download</value>
</data>
@@ -480,4 +483,7 @@
<data name="Installed" xml:space="preserve">
<value>Installed</value>
</data>
<data name="Global Replace" xml:space="preserve">
<value>Global Replace</value>
</data>
</root>

View File

@@ -174,9 +174,6 @@
<data name="Title" xml:space="preserve">
<value>Title:</value>
</data>
<data name="System.Update" xml:space="preserve">
<value>Check For System Updates</value>
</data>
<data name="Visibility" xml:space="preserve">
<value>Visibility:</value>
</data>
@@ -200,5 +197,11 @@
</data>
<data name="Module.CopyExisting" xml:space="preserve">
<value>Copy Existing Module</value>
</data>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Synchronize Site</value>
</data>
<data name="Copy" xml:space="preserve">
<value>Copy Page</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

@@ -14,8 +14,9 @@ namespace Oqtane.Services
/// Set the localization cookie
/// </summary>
/// <param name="culture"></param>
/// <param name="uiCulture"></param>
/// <returns></returns>
Task SetLocalizationCookieAsync(string culture);
Task SetLocalizationCookieAsync(string culture, string uiCulture);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -23,7 +24,7 @@ namespace Oqtane.Services
{
public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public Task SetLocalizationCookieAsync(string culture)
public Task SetLocalizationCookieAsync(string culture, string uiCulture)
{
return Task.CompletedTask; // only used in server side rendering
}

View File

@@ -13,10 +13,16 @@ namespace Oqtane.Services
public interface ILocalizationService
{
/// <summary>
/// Returns a collection of supported cultures
/// Returns a collection of supported or installed cultures
/// </summary>
/// <returns></returns>
Task<IEnumerable<Culture>> GetCulturesAsync(bool installed);
/// <summary>
/// Returns a collection of neutral cultures
/// </summary>
/// <returns></returns>
Task<IEnumerable<Culture>> GetNeutralCulturesAsync();
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -26,6 +32,14 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("Localization");
public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed) => await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed)
{
return await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
}
public async Task<IEnumerable<Culture>> GetNeutralCulturesAsync()
{
return await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}/neutral");
}
}
}

View File

@@ -71,6 +71,15 @@ namespace Oqtane.Services
/// <param name="pageId"></param>
/// <returns></returns>
Task DeletePageAsync(int pageId);
/// <summary>
/// Copies the modules from one page to another
/// </summary>
/// <param name="fromPageId"></param>
/// <param name="toPageId"></param>
/// <param name="usePagePermissions"></param>
/// <returns></returns>
Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@@ -129,5 +138,10 @@ namespace Oqtane.Services
{
await DeleteAsync($"{Apiurl}/{pageId}");
}
public async Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions)
{
await PostAsync($"{Apiurl}/{fromPageId}/{toPageId}/{usePagePermissions}");
}
}
}

View File

@@ -0,0 +1,104 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Linq;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Role"/>s on a <see cref="Site"/>
/// </summary>
public interface ISiteGroupMemberService
{
/// <summary>
/// Get all <see cref="SiteGroupMember"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroupMember>> GetSiteGroupMembersAsync(int siteId, int siteGroupId);
/// <summary>
/// Get one specific <see cref="SiteGroupMember"/>
/// </summary>
/// <param name="siteGroupMemberId">ID-reference of a <see cref="SiteGroupMember"/></param>
/// <returns></returns>
Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteGroupMemberId);
/// <summary>
/// Get one specific <see cref="SiteGroupMember"/>
/// </summary>
/// <param name="siteId">ID-reference of a <see cref="Site"/></param>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteId, int siteGroupId);
/// <summary>
/// Add / save a new <see cref="SiteGroupMember"/> to the database.
/// </summary>
/// <param name="siteGroupMember"></param>
/// <returns></returns>
Task<SiteGroupMember> AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember);
/// <summary>
/// Update a <see cref="SiteGroupMember"/> in the database.
/// </summary>
/// <param name="siteGroupMember"></param>
/// <returns></returns>
Task<SiteGroupMember> UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember);
/// <summary>
/// Delete a <see cref="SiteGroupMember"/> in the database.
/// </summary>
/// <param name="siteGroupMemberId">ID-reference of a <see cref="SiteGroupMember"/></param>
/// <returns></returns>
Task DeleteSiteGroupMemberAsync(int siteGroupMemberId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteGroupMemberService : ServiceBase, ISiteGroupMemberService
{
public SiteGroupMemberService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteGroupMember");
public async Task<List<SiteGroupMember>> GetSiteGroupMembersAsync(int siteId, int siteGroupId)
{
return await GetJsonAsync<List<SiteGroupMember>>($"{Apiurl}?siteid={siteId}&groupid={siteGroupId}", Enumerable.Empty<SiteGroupMember>().ToList());
}
public async Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteGroupMemberId)
{
return await GetJsonAsync<SiteGroupMember>($"{Apiurl}/{siteGroupMemberId}");
}
public async Task<SiteGroupMember> GetSiteGroupMemberAsync(int siteId, int siteGroupId)
{
var siteGroupMembers = await GetSiteGroupMembersAsync(siteId, siteGroupId);
if (siteGroupMembers != null && siteGroupMembers.Count > 0)
{
return siteGroupMembers[0];
}
else
{
return null;
}
}
public async Task<SiteGroupMember> AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember)
{
return await PostJsonAsync<SiteGroupMember>(Apiurl, siteGroupMember);
}
public async Task<SiteGroupMember> UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember)
{
return await PutJsonAsync<SiteGroupMember>($"{Apiurl}/{siteGroupMember.SiteGroupId}", siteGroupMember);
}
public async Task DeleteSiteGroupMemberAsync(int siteGroupMemberId)
{
await DeleteAsync($"{Apiurl}/{siteGroupMemberId}");
}
}
}

View File

@@ -0,0 +1,94 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Linq;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Role"/>s on a <see cref="Site"/>
/// </summary>
public interface ISiteGroupService
{
/// <summary>
/// Get all <see cref="SiteGroup"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroup>> GetSiteGroupsAsync();
/// <summary>
/// Get all <see cref="SiteGroup"/>s
/// </summary>
/// <returns></returns>
Task<List<SiteGroup>> GetSiteGroupsAsync(int primarySiteId);
/// <summary>
/// Get one specific <see cref="SiteGroup"/>
/// </summary>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task<SiteGroup> GetSiteGroupAsync(int siteGroupId);
/// <summary>
/// Add / save a new <see cref="SiteGroup"/> to the database.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
Task<SiteGroup> AddSiteGroupAsync(SiteGroup siteGroup);
/// <summary>
/// Update a <see cref="SiteGroup"/> in the database.
/// </summary>
/// <param name="group"></param>
/// <returns></returns>
Task<SiteGroup> UpdateSiteGroupAsync(SiteGroup siteGroup);
/// <summary>
/// Delete a <see cref="SiteGroup"/> in the database.
/// </summary>
/// <param name="siteGroupId">ID-reference of a <see cref="SiteGroup"/></param>
/// <returns></returns>
Task DeleteSiteGroupAsync(int siteGroupId);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteGroupService : ServiceBase, ISiteGroupService
{
public SiteGroupService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteGroup");
public async Task<List<SiteGroup>> GetSiteGroupsAsync()
{
return await GetSiteGroupsAsync(-1);
}
public async Task<List<SiteGroup>> GetSiteGroupsAsync(int primarySiteId)
{
return await GetJsonAsync<List<SiteGroup>>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty<SiteGroup>().ToList());
}
public async Task<SiteGroup> GetSiteGroupAsync(int siteGroupId)
{
return await GetJsonAsync<SiteGroup>($"{Apiurl}/{siteGroupId}");
}
public async Task<SiteGroup> AddSiteGroupAsync(SiteGroup siteGroup)
{
return await PostJsonAsync<SiteGroup>(Apiurl, siteGroup);
}
public async Task<SiteGroup> UpdateSiteGroupAsync(SiteGroup siteGroup)
{
return await PutJsonAsync<SiteGroup>($"{Apiurl}/{siteGroup.SiteGroupId}", siteGroup);
}
public async Task DeleteSiteGroupAsync(int siteGroupId)
{
await DeleteAsync($"{Apiurl}/{siteGroupId}");
}
}
}

View File

@@ -0,0 +1,46 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage tasks (<see cref="SiteTask"/>)
/// </summary>
public interface ISiteTaskService
{
/// <summary>
/// Return a specific task
/// </summary>
/// <param name="siteTaskId"></param>
/// <returns></returns>
Task<SiteTask> GetSiteTaskAsync(int siteTaskId);
/// <summary>
/// Adds a new task
/// </summary>
/// <param name="siteTask"></param>
/// <returns></returns>
Task<SiteTask> AddSiteTaskAsync(SiteTask siteTask);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class SiteTaskService : ServiceBase, ISiteTaskService
{
public SiteTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("SiteTask");
public async Task<SiteTask> GetSiteTaskAsync(int siteTaskId)
{
return await GetJsonAsync<SiteTask>($"{Apiurl}/{siteTaskId}");
}
public async Task<SiteTask> AddSiteTaskAsync(SiteTask siteTask)
{
return await PostJsonAsync<SiteTask>(Apiurl, siteTask);
}
}
}

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"/>
@@ -154,15 +161,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<string> GetPasswordRequirementsAsync(int siteId);
/// <summary>
/// Bulk import of users
/// </summary>
/// <param name="siteId">ID of a <see cref="Site"/></param>
/// <param name="fileId">ID of a <see cref="File"/></param>
/// <param name="notify">Indicates if new users should be notified by email</param>
/// <returns></returns>
Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify);
/// <summary>
/// Get passkeys for a user
/// </summary>
@@ -211,6 +209,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 +280,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)
@@ -332,11 +342,6 @@ namespace Oqtane.Services
return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement);
}
public async Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify)
{
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
public async Task<List<UserPasskey>> GetPasskeysAsync(int userId)
{
return await GetJsonAsync<List<UserPasskey>>($"{Apiurl}/passkey?id={userId}");
@@ -366,5 +371,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

@@ -97,6 +97,7 @@
Alias = PageState.Alias,
Site = new Site
{
SiteId = PageState.Site.SiteId,
DefaultContainerType = PageState.Site.DefaultContainerType,
Settings = PageState.Site.Settings,
Themes = PageState.Site.Themes

View File

@@ -11,6 +11,7 @@
@inject ILogService logger
@inject ISettingService SettingService
@inject IJSRuntime jsRuntime
@inject ISiteGroupService SiteGroupService
@inject IServiceProvider ServiceProvider
@inject ILogService LoggingService
@inject IStringLocalizer<ControlPanelInteractive> Localizer
@@ -34,6 +35,11 @@
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button>
</div>
</div>
@if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId))
{
<hr class="app-rule" />
<button type="button" class="btn btn-secondary col-12 mt-1" @onclick="SynchronizeSite">@Localizer["Synchronize"]</button>
}
<hr class="app-rule" />
}
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -53,18 +59,29 @@
<button type="button" class="btn btn-danger col ms-1" @onclick="ConfirmDelete">@SharedLocalizer["Delete"]</button>
</div>
</div>
<div class="row d-flex">
<div class="col">
@if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button>
}
else
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("publish"))>@Localizer["Page.Publish"]</button>
}
@if (PageState.Page.UserId == null)
{
<div class="row d-flex mb-2">
<div class="col">
<button type="button" class="btn btn-secondary col-12" data-bs-dismiss="offcanvas" @onclick=@(async () => Navigate("Copy"))>@Localizer["Copy"]</button>
</div>
</div>
</div>
}
@if (!PageState.Page.Path.StartsWith("admin/"))
{
<div class="row d-flex">
<div class="col">
@if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button>
}
else
{
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("publish"))>@Localizer["Page.Publish"]</button>
}
</div>
</div>
}
<hr class="app-rule" />
@if (_deleteConfirmation)
@@ -149,7 +166,7 @@
<option value="@p.PageId">@p.Name</option>
}
</select>
<select class="form-select mt-1" @bind="@_moduleId">
<select class="form-select mt-1" value="@_moduleId" @onchange="(e => PageModuleChanged(e))">
<option value="-">&lt;@Localizer["Module.Select"]&gt;</option>
@foreach (Module module in _modules)
{
@@ -257,6 +274,7 @@
private List<Page> _pages = new List<Page>();
private List<Module> _modules = new List<Module>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteGroup> _siteGroups = new List<SiteGroup>();
private string _category = "Common";
private string _pane = "";
@@ -285,15 +303,16 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = PageState.Site.DefaultContainerType;
_allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId);
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList();
_siteGroups = await SiteGroupService.GetSiteGroupsAsync(PageState.Site.SiteId);
}
}
private void CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries).Contains(_category)).ToList();
_moduleDefinitionName = "-";
_message = "";
}
@@ -339,6 +358,20 @@
StateHasChanged();
}
private async Task PageModuleChanged(ChangeEventArgs e)
{
_moduleId = (string)e.Value;
if (_moduleId != "-")
{
_title = _modules.First(item => item.ModuleId == int.Parse(_moduleId)).Title;
}
else
{
_title = "";
}
StateHasChanged();
}
private async Task AddModule()
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
@@ -490,6 +523,11 @@
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break;
case "Copy":
// get page management moduleid
moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]);
NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}&copy=true&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}"));
break;
}
}
@@ -631,4 +669,14 @@
{
_message = "";
}
private async Task SynchronizeSite()
{
foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId))
{
group.Synchronize = true;
await SiteGroupService.UpdateSiteGroupAsync(group);
}
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true);
}
}

View File

@@ -83,7 +83,10 @@
get => "";
set
{
_showBanner = bool.Parse(value);
if (!bool.TryParse(value, out _showBanner))
{
_showBanner = false;
}
_togglePostback = true;
}
}

View File

@@ -6,22 +6,29 @@
@inject ILocalizationCookieService LocalizationCookieService
@inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1)
@if (PageState.Site.Languages.Count() > 1)
{
<div class="app-languages btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn @ButtonClass dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-globe"></span>
</button>
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@foreach (var culture in _supportedCultures)
@foreach (var language in PageState.Site.Languages)
{
@if (PageState.RenderMode == RenderModes.Interactive)
@if (_contentLocalization)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(culture.Name))" @onclick:preventDefault="true">@culture.DisplayName</a>
<a class="dropdown-item @(PageState.Site.CultureCode == language.Code ? "active" : String.Empty)" href="@(PageState.Alias.Protocol + language.AliasName)" data-enhance-nav="false">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)" data-enhance-nav="false">@culture.DisplayName</a>
@if (PageState.RenderMode == RenderModes.Interactive)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(language.Code))" @onclick:preventDefault="true">@language.Name</a>
}
else
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == language.Code ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + language.Code)" data-enhance-nav="false">@language.Name</a>
}
}
}
</div>
@@ -29,7 +36,7 @@
}
@code{
private IEnumerable<Culture> _supportedCultures;
private bool _contentLocalization;
private string MenuAlignment = string.Empty;
[Parameter]
@@ -41,14 +48,15 @@
{
MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty;
_supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name });
// determine if site is using content localization
_contentLocalization = PageState.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName));
if (PageState.QueryString.ContainsKey("culture"))
{
var culture = PageState.QueryString["culture"];
if (_supportedCultures.Any(item => item.Name == culture))
if (PageState.Site.Languages.Any(item => item.Code == culture))
{
await LocalizationCookieService.SetLocalizationCookieAsync(culture);
await LocalizationCookieService.SetLocalizationCookieAsync(PageState.Site.CultureCode, culture);
}
NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""));
}
@@ -58,7 +66,7 @@
{
if (culture != CultureInfo.CurrentUICulture.Name)
{
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(PageState.Site.CultureCode, culture));
var interop = new Interop(JSRuntime);
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax");
NavigationManager.NavigateTo(NavigationManager.Uri, true);

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

@@ -0,0 +1,44 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@inherits ThemeControlBase
@inject IStringLocalizer<Login> Localizer
@inject ISettingService SettingService
@inject IStringLocalizer<SharedResources> SharedLocalizer
<a href="@_registerurl" class="@CssClass">@SharedLocalizer["Register"]</a>
@code
{
private string _returnurl;
private string _registerurl;
[Parameter]
public string CssClass { get; set; } = "btn btn-secondary";
protected override void OnParametersSet()
{
if (!PageState.QueryString.ContainsKey("returnurl"))
{
// remember current url
_returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
}
else
{
// use existing value
_returnurl = PageState.QueryString["returnurl"];
}
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
{
_registerurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "");
_registerurl += (!_registerurl.Contains("?") ? "?" : "&") + "returnurl=" + (_registerurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
_registerurl = NavigateUrl("register", "returnurl=" + _returnurl);
}
Console.WriteLine($"Register URL: {_registerurl}");
}
}

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">
@@ -12,7 +16,7 @@
{
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top")
{
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
<ModuleMessage @key="_messageVersionTop" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
}
@DynamicComponent
@if (_progressIndicator)
@@ -21,7 +25,7 @@
}
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom")
{
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
<ModuleMessage @key="_messageVersionBottom" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
}
}
}
@@ -48,6 +52,8 @@
private MessageStyle _messageStyle;
private bool _progressIndicator = false;
private string _error;
private string _messageVersionTop = Guid.NewGuid().ToString();
private string _messageVersionBottom = Guid.NewGuid().ToString();
[Parameter]
public SiteState SiteState { get; set; }
@@ -67,37 +73,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();
};
}
}
@@ -126,6 +145,18 @@
_messageStyle = style;
_progressIndicator = false;
if (style == MessageStyle.Toast && !string.IsNullOrEmpty(_messageContent))
{
if (_messagePosition == "top")
{
_messageVersionTop = Guid.NewGuid().ToString();
}
else if (_messagePosition == "bottom")
{
_messageVersionBottom = Guid.NewGuid().ToString();
}
}
StateHasChanged();
}
}
@@ -165,4 +196,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;
@@ -227,15 +233,8 @@
if (page == null && route.PagePath == "") // naked path refers to site home page
{
if (site.HomePageId != null)
{
page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
}
if (page == null)
{
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
// fallback to use the first page in the collection
page = site.Pages.FirstOrDefault();
}
if (page == null)
{
@@ -626,11 +625,11 @@
{
if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site)
{
if (resource.Url.StartsWith("~"))
if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~"))
{
resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
}
if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
{
resource.Url = alias.BaseUrl + resource.Url;
}

View File

@@ -227,38 +227,4 @@
}
return stylesheets;
}
private string ManageScripts(List<Resource> resources, Alias alias)
{
var scripts = "";
if (resources != null)
{
foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Script && item.Location == ResourceLocation.Head))
{
var script = CreateScript(resource, alias);
if (!scripts.Contains(script, StringComparison.OrdinalIgnoreCase))
{
scripts += script + Environment.NewLine;
}
}
}
return scripts;
}
private string CreateScript(Resource resource, Alias alias)
{
if (!string.IsNullOrEmpty(resource.Url))
{
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
return "<script src=\"" + url + "\"" +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
"></script>";
}
else
{
// inline script
return "<script>" + resource.Content + "</script>";
}
}
}

View File

@@ -11,6 +11,7 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Oqtane.Client
@@ -27,3 +28,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.1.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.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.5" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.5" />
<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.1.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.1.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="Oqtane.Shared" version="10.1.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.5" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="10.0.6" 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.1.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.1.2/Oqtane.Framework.10.1.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane framework</tags>

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