Compare commits

...

319 Commits
v6.0.1 ... dev

Author SHA1 Message Date
391827222e Update README.md
All checks were successful
build-docker-imge / Build the docker container (push) Successful in 8m51s
2025-03-15 18:08:36 +00:00
c1721bd1a1 Update README.md
Some checks failed
build-docker-imge / Build the docker container (push) Failing after 5s
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
Shaun Walker
05d2096fb8
Update README.md 2025-03-12 14:34:31 -04:00
Shaun Walker
7683af81bc
Update README.md 2025-03-12 14:22:43 -04:00
Shaun Walker
bad10b3812
6.1.1 Release
6.1.1 Release
2025-03-12 14:21:41 -04:00
Shaun Walker
9f9522c2ed
6.1.1 Release
6.1.1 Release
2025-03-12 14:21:15 -04:00
Shaun Walker
62879c3e52
Merge pull request #5162 from sbwalker/dev
upgrade to .NET 9.0.3
2025-03-11 14:36:14 -04:00
sbwalker
45610f8dd7 upgrade to .NET 9.0.3 2025-03-11 14:35:59 -04:00
Shaun Walker
18102cbd78
Merge pull request #5160 from sbwalker/dev
sort endpoints by route
2025-03-11 13:11:34 -04:00
sbwalker
262d6a1529 sort endpoints by route 2025-03-11 13:11:19 -04:00
Shaun Walker
1124ddaf90
Merge pull request #5157 from zyhfish/task/fix-5156
Fix #5156: update the bind event to oninput.
2025-03-11 11:51:38 -04:00
Shaun Walker
981add3872
Merge pull request #5158 from sbwalker/dev
rename Cache service to OutputCache
2025-03-11 11:48:58 -04:00
sbwalker
8d4b30140e rename Cache service to OutputCache 2025-03-11 11:48:43 -04:00
Ben
b9c59137a8 Fix #5156: update the bind event to oninput. 2025-03-11 22:58:06 +08:00
Shaun Walker
0b1c7e06ca
Update README.md 2025-03-10 10:56:41 -04:00
Shaun Walker
fcaf80cba6
Update README.md 2025-03-10 10:56:11 -04:00
Shaun Walker
6358b9eabb
Merge pull request #5155 from sbwalker/dev
added Logout Everywhere option to User Settings
2025-03-10 10:53:25 -04:00
sbwalker
70a3fab1ff added Logout Everywhere option to User Settings 2025-03-10 10:53:05 -04:00
Shaun Walker
bdf86ace86
Merge pull request #5152 from sbwalker/dev
upgrade to ImageSharp 3.1.7 due to security vulnerability
2025-03-07 14:17:02 -05:00
sbwalker
d57132d1e4 upgrade to ImageSharp 3.1.7 due to security vulnerability 2025-03-07 14:16:47 -05:00
Shaun Walker
a6e87abf99
Merge pull request #5151 from sbwalker/dev
allow site settings to be overidden at host level
2025-03-07 14:15:31 -05:00
sbwalker
f1771610fe allow site settings to be overidden at host level 2025-03-07 14:15:16 -05:00
Shaun Walker
a88ea9780f
Update README.md 2025-03-06 15:45:30 -05:00
Shaun Walker
bca0866d72
Update README.md 2025-03-06 15:43:59 -05:00
Shaun Walker
cebed93abf
Update README.md 2025-03-06 15:42:48 -05:00
Shaun Walker
ee2b2e3569
Update README.md 2025-03-06 15:40:07 -05:00
Shaun Walker
cb8e9ee244
Update README.md 2025-03-06 15:38:18 -05:00
Shaun Walker
486184b16c
Update README.md 2025-03-06 15:36:28 -05:00
Shaun Walker
9f9bd1988f
Merge pull request #5149 from sbwalker/dev
update based on changes suggested by @adefwebserver
2025-03-06 15:25:37 -05:00
sbwalker
ba1bfd1bc0 update based on changes suggested by @adefwebserver 2025-03-06 15:25:25 -05:00
Shaun Walker
e12926e971
Merge pull request #5148 from sbwalker/dev
allow page and module settings to be included in site templates, improve terms and privacy default content, add Settings for HtmlText module
2025-03-06 14:46:38 -05:00
sbwalker
5b4db0de3b allow page and module settings to be included in site templates, improve terms and privacy default content, add Settings for HtmlText module 2025-03-06 14:46:17 -05:00
Shaun Walker
70ff55faa6
Merge pull request #5147 from sbwalker/dev
modify terminology
2025-03-06 09:43:55 -05:00
sbwalker
f2bd47d8bc modify terminology 2025-03-06 09:43:40 -05:00
Shaun Walker
e2b9c9e98e
Merge pull request #5146 from sbwalker/dev
prepare for 6.1.1
2025-03-05 15:02:39 -05:00
sbwalker
b0791a594f prepare for 6.1.1 2025-03-05 15:02:12 -05:00
Shaun Walker
ea5eaa6ed2
Merge pull request #5145 from sbwalker/dev
update to .NET 9.0.2
2025-03-05 14:55:00 -05:00
sbwalker
ec3fd1d585 update to .NET 9.0.2 2025-03-05 14:54:46 -05:00
Shaun Walker
d76de22977
Merge pull request #5144 from sbwalker/dev
modify localization text
2025-03-05 10:45:17 -05:00
sbwalker
c0e3483cc7 modify localization text 2025-03-05 10:44:34 -05:00
Shaun Walker
0994cdf3b6
Merge pull request #5141 from sbwalker/dev
sync interop.js changes with .NET MAUI
2025-03-04 16:25:24 -05:00
sbwalker
a76fd82262 sync interop.js changes with .NET MAUI 2025-03-04 16:25:10 -05:00
Shaun Walker
2f919c7d69
Merge pull request #5140 from sbwalker/dev
fix regression issue with Search component
2025-03-04 15:45:16 -05:00
sbwalker
f12592731b fix regression issue with Search component 2025-03-04 15:45:02 -05:00
Shaun Walker
4a20e1a25d
Merge pull request #5138 from zyhfish/task/improve-middle-screen-view
improve the styles in middle screen size.
2025-03-04 15:42:37 -05:00
Shaun Walker
cc7111c3ff
Merge pull request #5139 from sbwalker/dev
change module title for Terms
2025-03-04 15:40:58 -05:00
sbwalker
81972aed62 change module title for Terms 2025-03-04 15:40:44 -05:00
Ben
5e2092c6d4 improve the styles in middle screen size. 2025-03-04 23:31:21 +08:00
Shaun Walker
d136f8ac91
Merge pull request #5137 from sbwalker/dev
add nonce support
2025-03-03 16:49:41 -05:00
sbwalker
5b23917940 add nonce support 2025-03-03 16:49:28 -05:00
Shaun Walker
2cda0a3798
Merge pull request #5136 from sbwalker/dev
modify cookie consent text
2025-03-03 15:37:46 -05:00
sbwalker
f315ad1ce9 modify cookie consent text 2025-03-03 15:37:31 -05:00
Shaun Walker
49f1c273c2
Merge pull request #5134 from sbwalker/dev
add terms to upgrademanager
2025-03-03 13:36:47 -05:00
sbwalker
8518476c87 add terms to upgrademanager 2025-03-03 13:36:32 -05:00
Shaun Walker
a34ed756db
Merge pull request #5132 from mdmontesinos/sitemap-cache
resolves #4899: output cache for sitemap
2025-03-03 13:22:46 -05:00
Shaun Walker
a48232c4e3
Merge pull request #5133 from sbwalker/dev
add default privacy and terms
2025-03-03 13:22:33 -05:00
sbwalker
ac65e38390 add default privacy and terms 2025-03-03 13:22:17 -05:00
David Montesinos
eab3a753f5 resolves #4899: output cache for sitemap 2025-03-03 17:54:33 +01:00
Shaun Walker
9e5922e121
Merge pull request #5130 from zyhfish/task/fix-4936
Fix #4936: set the allow cookie value when refresh state.
2025-03-03 07:57:42 -05:00
Ben
bf57b23776 update the page state after policy changed. 2025-03-01 15:05:23 +08:00
Ben
8f4a20fd46 Fix #4936: set the allow cookie value when refresh state. 2025-03-01 14:16:42 +08:00
Shaun Walker
b3716da5ac
Merge pull request #5128 from zyhfish/task/fix-4936-new
move the styles to app.css.
2025-02-28 13:36:58 -05:00
Shaun Walker
2c129fd800
Merge pull request #5129 from sbwalker/dev
localization text change
2025-02-28 13:36:49 -05:00
sbwalker
6fb18e7a25 localization text change 2025-02-28 13:36:34 -05:00
Ben
38d28d6944 move the styles to app.css. 2025-02-28 23:53:53 +08:00
Shaun Walker
a187e1a7a2
Merge pull request #5127 from sbwalker/dev
fix RESX issue
2025-02-28 10:52:18 -05:00
sbwalker
7d7a19c7c2 fix RESX issue 2025-02-28 10:52:06 -05:00
Shaun Walker
6a2ae2153a
Merge pull request #5119 from zyhfish/task/fix-4936
update the cookie consent control.
2025-02-28 10:48:21 -05:00
Shaun Walker
f50ba1a91e
Merge branch 'dev' into task/fix-4936 2025-02-28 10:47:24 -05:00
Shaun Walker
b09575dbd6
Merge pull request #5126 from sbwalker/dev
provide option to assign a theme to a site
2025-02-28 10:45:40 -05:00
sbwalker
c52ee3d91d provide option to assign a theme to a site 2025-02-28 10:45:25 -05:00
Ben
1ced5c0425 update the cookie consent control. 2025-02-28 23:32:17 +08:00
Shaun Walker
e399a5c9b1
Merge pull request #5124 from sbwalker/dev
add missing maxlength attributes
2025-02-27 10:51:37 -05:00
sbwalker
08dff5fb67 add missing maxlength attributes 2025-02-27 10:51:22 -05:00
Shaun Walker
912760f2a7
Update SECURITY.md 2025-02-26 15:29:49 -05:00
Shaun Walker
4b62fdbf93
Update SECURITY.md 2025-02-26 15:27:33 -05:00
Shaun Walker
df593d43a7
Update SECURITY.md 2025-02-26 15:25:25 -05:00
Shaun Walker
89b1fba771
Merge pull request #5123 from sbwalker/dev
remove package validation logic
2025-02-26 12:54:06 -05:00
sbwalker
5505c91ae0 remove package validation logic 2025-02-26 12:53:53 -05:00
Shaun Walker
cc720ff399
Merge pull request #5122 from sbwalker/dev
remove unnecessary log message
2025-02-26 12:12:14 -05:00
sbwalker
29f07f6c56 remove unnecessary log message 2025-02-26 12:11:56 -05:00
Shaun Walker
a69e197a1f
Merge pull request #5121 from zyhfish/task/fix-5116
Fix #5116: parse the value as UTC time.
2025-02-26 07:45:55 -05:00
Ben
6dddd8eff8 only allow essential cookies when cookie consent not granted. 2025-02-26 19:41:58 +08:00
Ben
51aada8922 Fix #5116: parse the value as UTC time. 2025-02-26 18:25:24 +08:00
Ben
b47bf40e8f update the cookie consent control. 2025-02-25 11:36:47 +08:00
Shaun Walker
48151bf365
Merge pull request #5118 from sbwalker/dev
remove IJSRuntime reference as it was causing a compilation warning
2025-02-24 16:15:51 -05:00
sbwalker
659950996d remove IJSRuntime reference as it was causing a compilation warning 2025-02-24 16:15:35 -05:00
Shaun Walker
6e656a4d0a
Merge pull request #5114 from zyhfish/task/fix-4936
Fix #4936: add the cookie consent theme control.
2025-02-24 16:08:22 -05:00
Ben
bf308dd13d enable child component of cookie consent control. 2025-02-24 22:32:19 +08:00
Ben
982f3b1943 Fix #4936: add the cookie consent theme control. 2025-02-22 09:49:33 +08:00
Shaun Walker
7a4ea8cf1b
Merge pull request #5094 from zyhfish/task/set-fa-ir-culture
Fix #5054: resolve the issue in fa-IR language.
2025-02-19 07:36:16 -05:00
Shaun Walker
7c0482a87c
Merge pull request #5111 from sbwalker/dev
remove warning message related to no jobs being registered
2025-02-18 11:50:57 -05:00
sbwalker
101ededd89 remove warning message related to no jobs being registered 2025-02-18 11:50:36 -05:00
Shaun Walker
a8cbc0040e
Merge pull request #5108 from zyhfish/task/fix-5103
Fix #5103: return a copy of the assembly list.
2025-02-18 11:40:30 -05:00
Shaun Walker
ed91bb445b
Merge pull request #5110 from sbwalker/dev
improve HostedServiceBase so that scheduled jobs can be registered during installation
2025-02-18 11:36:00 -05:00
sbwalker
f158a222f4 improve HostedServiceBase so that scheduled jobs can be registered during installation 2025-02-18 11:35:38 -05:00
Shaun Walker
46bcad1fca
Merge pull request #5109 from sbwalker/dev
clean up scheduled jobs which have been uninstalled
2025-02-18 09:12:48 -05:00
sbwalker
5e147afb9f clean up scheduled jobs which have been uninstalled 2025-02-18 09:12:26 -05:00
Ben
b061d4593f Fix #5103: return a copy of the assembly list. 2025-02-18 22:02:25 +08:00
Shaun Walker
3fa520b4ef
Merge pull request #5107 from sbwalker/dev
make purge job output more readable
2025-02-18 08:53:38 -05:00
sbwalker
2df05b4afd make purge job output more readable 2025-02-18 08:53:23 -05:00
Shaun Walker
e0569a6748
Merge pull request #5104 from sbwalker/dev
fix visitor purge logic
2025-02-17 13:22:11 -05:00
sbwalker
2e6ab398d9 fix visitor purge logic 2025-02-17 13:21:58 -05:00
Ben
94b03d2a6b update settings for all RTL languages. 2025-02-16 10:25:43 +08:00
Shaun Walker
f84fe30bb6
Merge pull request #5097 from sbwalker/dev
synchronize BlazorScriptReload changes
2025-02-14 09:17:58 -05:00
sbwalker
049ddef531 synchronize BlazorScriptReload changes 2025-02-14 09:17:44 -05:00
Shaun Walker
a1a214c742
Merge pull request #5096 from sbwalker/dev
add another constructor for Script class
2025-02-14 09:09:14 -05:00
sbwalker
c40a483ffa add another constructor for Script class 2025-02-14 09:09:00 -05:00
Ben
aff99acfae Fix #5054: resolve the issue in fa-IR language. 2025-02-12 20:18:10 +08:00
Shaun Walker
628129c08d
Update README.md 2025-02-11 12:00:41 -05:00
Shaun Walker
679d34dfdf
Merge pull request #5093 from oqtane/master
6.1.0 Release
2025-02-11 11:49:29 -05:00
Shaun Walker
b2f65903ae
Merge pull request #5092 from oqtane/dev
6.1.0 Release
2025-02-11 11:49:06 -05:00
Shaun Walker
2daefe0382
Merge pull request #5091 from tvatavuk/patch-1
Minor fix in ThemeController.cs
2025-02-11 11:41:52 -05:00
Tonći Vatavuk
1214a11704
Minor fix in ThemeController.cs
update `SharedReference` to use "Oqtane.Shared" for PackageReference code
2025-02-11 14:35:13 +01:00
Shaun Walker
e55e0118c2
Merge pull request #5090 from sbwalker/dev
fix #5089 - remove upgrade cleanup logic as .NET 9.0.1 moves assemblies back to /bin folder
2025-02-10 16:53:42 -05:00
sbwalker
a1ac81e907 fix #5089 - remove upgrade cleanup logic as .NET 9.0.1 moves assemblies back to /bin folder 2025-02-10 16:53:22 -05:00
Shaun Walker
14ad68bf69
Merge pull request #5088 from sbwalker/dev
modify RemoveAssemblies method so that it only runs once - not for every tenant
2025-02-10 16:27:24 -05:00
sbwalker
e3118c6e99 modify RemoveAssemblies method so that it only runs once - not for every tenant 2025-02-10 16:27:05 -05:00
Shaun Walker
b41aeab8f8
Merge pull request #5087 from sbwalker/dev
update Provider property to Pomelo
2025-02-10 10:47:07 -05:00
sbwalker
1a738b358e update Provider property to Pomelo 2025-02-10 10:46:44 -05:00
Shaun Walker
f4b1e8035b
Merge pull request #5086 from sbwalker/dev
improve notification add and update methods
2025-02-10 09:50:35 -05:00
sbwalker
324e985247 improve notification add and update methods 2025-02-10 09:50:21 -05:00
Shaun Walker
60faacd7d0
Merge pull request #5085 from sbwalker/dev
fix comment
2025-02-10 08:21:13 -05:00
sbwalker
d4a4d7c346 fix comment 2025-02-10 08:20:58 -05:00
Shaun Walker
189f8f1d27
Merge pull request #5082 from sbwalker/dev
enhance purge job to trim broken urls based on retention policy
2025-02-09 13:02:21 -05:00
sbwalker
ed353461da enhance purge job to trim broken urls based on retention policy 2025-02-09 13:02:07 -05:00
Shaun Walker
e9330d6c62
Merge pull request #5080 from zyhfish/task/fix-5079
Fix #5079: Retrieve all Url Mapping records
2025-02-09 12:35:28 -05:00
Shaun Walker
f53e7cc3f6
Merge pull request #5081 from sbwalker/dev
fix #5072 - administrators should be allowed to send system notifications
2025-02-09 12:34:38 -05:00
sbwalker
4f4258d532 fix #5072 - administrators should be allowed to send system notifications 2025-02-09 12:34:17 -05:00
Ben
c80910f355 Fix #5079: remove the records limit. 2025-02-08 11:51:38 +08:00
Shaun Walker
12470ab178
Merge pull request #5075 from mdmontesinos/dev
fix #5074: generate cancellation token for file upload
2025-02-07 07:53:16 -05:00
Shaun Walker
704e091e9b
Merge pull request #5076 from sbwalker/dev
synchronize interop script changes with .NET MAUI
2025-02-07 07:52:58 -05:00
sbwalker
f30f1e5c1f synchronize interop script changes with .NET MAUI 2025-02-07 07:52:43 -05:00
David Montesinos
0741ce2197 fix #5074: generate cancellation token for file upload 2025-02-07 09:11:52 +01:00
Shaun Walker
fc81bae9b7
Update README.md 2025-02-06 15:16:50 -05:00
Shaun Walker
3fa68e4f96
Merge pull request #5071 from sbwalker/dev
moved file setting to File Management and added Max Chunk Size
2025-02-06 15:10:29 -05:00
sbwalker
05a767c7be moved file setting to File Management and added Max Chunk Size 2025-02-06 15:10:14 -05:00
Shaun Walker
8c1e8f6377
Merge pull request #5070 from sbwalker/dev
fix #5067 - add support for Guid data types
2025-02-06 14:17:06 -05:00
sbwalker
0fbae8d7da fix #5067 - add support for Guid data types 2025-02-06 14:16:53 -05:00
Shaun Walker
cec4b339f5
Merge pull request #5069 from mdmontesinos/dev-test
fix #5058: ensure sequential file and chunk uploads to avoid overload
2025-02-06 13:56:35 -05:00
David Montesinos
1a7656d8ee fix #5058: ensure sequential file and chunk uploads to avoid overload 2025-02-06 19:21:51 +01:00
Shaun Walker
e173815810
Merge pull request #5068 from sbwalker/dev
fix LogLevel for file upload error
2025-02-06 12:21:51 -05:00
sbwalker
620c768e05 fix LogLevel for file upload error 2025-02-06 12:21:34 -05:00
Shaun Walker
7740679077
Merge pull request #5066 from sbwalker/dev
fix #5061 - configure Page Management for personalizable pages
2025-02-06 11:16:46 -05:00
sbwalker
1ebc8ebff3 fix #5061 - configure Page Management for personalizable pages 2025-02-06 11:16:30 -05:00
Shaun Walker
fa4fac70d5
Merge pull request #5065 from sbwalker/dev
improve file upload validation and error handling on server
2025-02-06 08:20:13 -05:00
sbwalker
8c83a18f93 improve file upload validation and error handling on server 2025-02-06 08:19:57 -05:00
Shaun Walker
a151ecfda0
Merge pull request #5064 from sbwalker/dev
modified file upload error message to reflect new behavior
2025-02-05 19:10:10 -05:00
sbwalker
dec0c0649c modified file upload error message to reflect new behavior 2025-02-05 19:09:55 -05:00
Shaun Walker
a356f893ac
Merge pull request #5063 from sbwalker/dev
remove uploadFile() method as it is not used
2025-02-05 17:08:56 -05:00
sbwalker
e2af4f74c3 remove uploadFile() method as it is not used 2025-02-05 17:08:32 -05:00
Shaun Walker
99022b76e5
Merge pull request #5062 from sbwalker/dev
file upload improvements
2025-02-05 16:48:59 -05:00
sbwalker
9dd6dc7523 file upload improvements 2025-02-05 16:48:34 -05:00
Shaun Walker
6f588200d7
Merge pull request #5057 from sbwalker/dev
fix #5044 - improve file part removal logic
2025-02-03 11:00:37 -05:00
sbwalker
f3dbeae28e fix #5044 - improve file part removal logic 2025-02-03 11:00:22 -05:00
Shaun Walker
9f70361298
Merge pull request #5056 from sbwalker/dev
add additional Script class constructors
2025-02-03 10:35:32 -05:00
sbwalker
534353ce13 add additional Script class constructors 2025-02-03 10:35:17 -05:00
Shaun Walker
3f391a7354
Merge pull request #5052 from sbwalker/dev
modify button spacing
2025-01-31 14:51:06 -05:00
sbwalker
0dd0752710 modify button spacing 2025-01-31 14:50:54 -05:00
Shaun Walker
710fae4b0e
Merge pull request #5051 from sbwalker/dev
add user impersonation
2025-01-31 11:14:28 -05:00
sbwalker
de6c57a7ee add user impersonation 2025-01-31 11:14:13 -05:00
Shaun Walker
7eee1fcd6a
Merge pull request #5050 from sbwalker/dev
make Kestrel the default profile in launchjSettings.json
2025-01-31 09:18:59 -05:00
sbwalker
1fd2aedf96 make Kestrel the default profile in launchjSettings.json 2025-01-31 09:18:44 -05:00
Shaun Walker
ffdd7c063b
Merge pull request #5049 from sbwalker/dev
added a ScriptsLoaded property in ModuleBase and ThemeBase for flow control in Interactive rendering scenarios
2025-01-31 08:42:56 -05:00
sbwalker
a87af264eb added a ScriptsLoaded property in ModuleBase and ThemeBase for flow control in Interactive rendering scenarios 2025-01-31 08:42:36 -05:00
Shaun Walker
3640cd2fdd
Merge pull request #5046 from sbwalker/dev
fix upgrade issue which can occur in development environments
2025-01-30 08:40:08 -05:00
sbwalker
f7cf25c4bb fix upgrade issue which can occur in development environments 2025-01-30 08:39:49 -05:00
Shaun Walker
77dbd0d4c7
Merge pull request #5045 from sbwalker/dev
update version to 6.1.0
2025-01-30 08:08:34 -05:00
sbwalker
5a77c83e68 update version to 6.1.0 2025-01-30 08:08:16 -05:00
Shaun Walker
0a6763e08c
Merge pull request #5043 from sbwalker/dev
change ResourceLoadBehavior Never to None
2025-01-29 19:05:20 -05:00
sbwalker
b5aa206670 change ResourceLoadBehavior Never to None 2025-01-29 19:05:05 -05:00
Shaun Walker
dbb4d9b64b
Merge pull request #5042 from sbwalker/dev
fix logic to retrieve access token
2025-01-29 16:03:11 -05:00
sbwalker
6775edfd66 fix logic to retrieve access token 2025-01-29 16:02:55 -05:00
Shaun Walker
b06750ed65
Merge pull request #5040 from sbwalker/dev
remove method which was relocated to PageRepository
2025-01-29 13:12:59 -05:00
sbwalker
57879c1891 remove method which was relocated to PageRepository 2025-01-29 13:12:46 -05:00
Shaun Walker
65b55a76f2
Merge pull request #5039 from sbwalker/dev
improve asset caching help text
2025-01-29 12:40:02 -05:00
sbwalker
8562a68306 improve asset caching help text 2025-01-29 12:39:47 -05:00
Shaun Walker
160da46b5a
Merge pull request #5038 from sbwalker/dev
remove Environment.IsDevelopment logic
2025-01-29 12:27:57 -05:00
sbwalker
ae5f70a739 remove Environment.IsDevelopment logic 2025-01-29 12:27:42 -05:00
Shaun Walker
4456e57466
Merge pull request #5036 from sbwalker/dev
use fingerprint term consistently
2025-01-29 10:42:34 -05:00
sbwalker
24cd090c61 use fingerprint term consistently 2025-01-29 10:42:19 -05:00
Shaun Walker
4f849f5d5f
Merge pull request #5035 from sbwalker/dev
performance improvement when loading files within a folder
2025-01-29 10:23:11 -05:00
sbwalker
db2e86e84c performance improvement when loading files within a folder 2025-01-29 10:22:56 -05:00
Shaun Walker
14748ce2b3
Merge pull request #5034 from sbwalker/dev
use Configuration service as it already exists
2025-01-29 08:28:08 -05:00
sbwalker
527509732c use Configuration service as it already exists 2025-01-29 08:22:21 -05:00
Shaun Walker
aa9214477c
Merge pull request #5033 from sbwalker/dev
fix #5018 - redirect file download to login page
2025-01-28 16:31:03 -05:00
sbwalker
db24ed8b55 fix #5018 - redirect file download to login page 2025-01-28 16:30:49 -05:00
Shaun Walker
349d1849d9
Merge pull request #5032 from sbwalker/dev
use deterministic hash in file server image generation
2025-01-28 15:55:56 -05:00
sbwalker
188be2fa8c use deterministic hash in file server image generation 2025-01-28 15:55:40 -05:00
Shaun Walker
5c4d7df734
Merge pull request #5031 from sbwalker/dev
remove Oqtane.Server.staticwebassets.endpoints.json from release packages
2025-01-28 14:39:48 -05:00
sbwalker
37de18c670 remove Oqtane.Server.staticwebassets.endpoints.json from release packages 2025-01-28 14:39:30 -05:00
Shaun Walker
0001e3844b
Merge pull request #5030 from sbwalker/dev
provides options to control caching for static assets
2025-01-28 14:30:13 -05:00
sbwalker
65f171f701 provides options to control caching for static assets 2025-01-28 14:29:58 -05:00
Shaun Walker
c4308c239c
Merge pull request #5007 from RahulKaushik007/fix-static-file-caching
Fixes #5005: Add Browser Caching for Static Assets
2025-01-28 13:21:29 -05:00
Shaun Walker
2b6af3cb37
Merge pull request #5029 from sbwalker/dev
fix localization text
2025-01-28 13:18:32 -05:00
sbwalker
c5a16fbbc1 fix localization text 2025-01-28 13:18:18 -05:00
Shaun Walker
1db83f509b
Merge pull request #5028 from sbwalker/dev
add caching support for folders
2025-01-28 12:47:37 -05:00
sbwalker
2a06304a2c add caching support for folders 2025-01-28 12:47:23 -05:00
Shaun Walker
7bbe684135
Merge pull request #5027 from sbwalker/dev
improve terminology consistency
2025-01-28 08:56:19 -05:00
sbwalker
a996a88fc4 improve terminology consistency 2025-01-28 08:56:05 -05:00
Shaun Walker
8cf9e7db51
Merge pull request #5026 from sbwalker/dev
add Fingerprint property to ModuleBase and ThemeBase
2025-01-28 08:44:01 -05:00
sbwalker
ed981c67b7 add Fingerprint property to ModuleBase and ThemeBase 2025-01-28 08:43:48 -05:00
Shaun Walker
6a77a0a5b9
Merge pull request #5025 from sbwalker/dev
improve terminology
2025-01-28 08:34:31 -05:00
sbwalker
bfb4b4431b improve terminology 2025-01-28 08:34:15 -05:00
Shaun Walker
3de44c0335
Merge pull request #5024 from sbwalker/dev
add ThemeState property to ThemeBase
2025-01-28 08:27:25 -05:00
sbwalker
37afd1aec9 add ThemeState property to ThemeBase 2025-01-28 08:27:10 -05:00
Shaun Walker
b2ac561673
Merge pull request #5021 from thabaum/update-package-dependencies-9.0.1
Fixes #5020: Updates package dependencies 9.0.1 (Latest)
2025-01-27 16:44:33 -05:00
Shaun Walker
c9bf7d9138
Merge pull request #5022 from sbwalker/dev
fix #5005 - adds versioning (ie. fingerprinting) for static assets - core, modules, and themes.
2025-01-27 16:35:07 -05:00
sbwalker
153a689bdb fix #5005 - adds versioning (ie. fingerprinting) for static assets - core, modules, and themes. 2025-01-27 16:34:47 -05:00
Cody
26b88f1a22
Update Package Dependencies to 9.0.1 2025-01-27 05:13:31 -08:00
Cody
c66a5d028f
Update Package Dependencies to 9.0.1 2025-01-27 05:13:01 -08:00
Cody
8441c95a5c
Update Package Dependencies to 9.0.1 2025-01-27 05:11:49 -08:00
Cody
e0e32b0199
Update Package Dependencies to 9.0.1 and 9.0.30 2025-01-27 05:09:19 -08:00
Cody
2bb76564e9
Update Package Dependencies to 9.0.1 2025-01-27 05:06:46 -08:00
Cody
86ec25d4de
Update Package Dependencies to 9.0.1 2025-01-27 05:05:42 -08:00
Cody
ed9929963c
Update Package Dependencies to 9.0.1 2025-01-27 05:04:34 -08:00
Cody
36f50118ac
Update Package Dependencies to 9.0.1 2025-01-27 05:03:46 -08:00
Cody
72ddf27504
Update Package Dependency to 9.2.0 2025-01-27 05:03:03 -08:00
Cody
9bd36931ff
Update Package Dependencies to 9.0.1 2025-01-27 05:02:10 -08:00
Cody
056ef7a3d5
Update Package Dependencies to 9.0.1 2025-01-27 05:00:08 -08:00
Shaun Walker
e483945d05
Merge pull request #5017 from sbwalker/dev
fix #5014 - page content scripts not loading on initial page request in Interactive rendering
2025-01-24 14:29:42 -05:00
sbwalker
7a9c637e03 fix #5014 - page content scripts not loading on initial page request in Interactive rendering 2025-01-24 14:29:23 -05:00
Shaun Walker
09ce543ea6
Merge pull request #5013 from sbwalker/dev
allow packages to be managed across installations
2025-01-23 09:08:16 -05:00
sbwalker
0ef24ebc3f allow packages to be managed across installations 2025-01-23 09:08:02 -05:00
Shaun Walker
a4419d3af6
Merge pull request #5011 from sbwalker/dev
remove GetButtonSize method
2025-01-22 07:43:38 -05:00
sbwalker
935983c02a remove GetButtonSize method 2025-01-22 07:43:23 -05:00
Shaun Walker
bd54ce5017
Merge pull request #4998 from leigh-pointer/ActionDialogSize
Update ActionDialog Add method to ensure consistent button sizing
2025-01-22 07:42:04 -05:00
Leigh Pointer
af6ed78b8e Update ActionDialog.razor 2025-01-22 07:57:52 +01:00
Leigh Pointer
f72438996d Merge remote-tracking branch 'upstream/dev' into ActionDialogSize 2025-01-22 07:36:01 +01:00
Shaun Walker
9db2a55a5a
Merge pull request #5010 from sbwalker/dev
fix #4964 - use bearer token if it already exists
2025-01-21 16:55:19 -05:00
sbwalker
950d90badb fix #4964 - use bearer token if it already exists 2025-01-21 16:55:02 -05:00
Shaun Walker
1864d180af
Merge pull request #5003 from sdi2121/patch-1
Update Oqtane.Server.csproj - MySQL deploy to Azure error
2025-01-21 15:58:44 -05:00
Shaun Walker
0e82e98382
Merge pull request #5006 from mdmontesinos/patch-1
FIX: File server MimeType not updated after image conversion
2025-01-21 15:58:33 -05:00
Shaun Walker
46023d35dc
Merge pull request #5009 from sbwalker/dev
fix #4976 - manage hierarchical path updates and page deletion
2025-01-21 15:58:08 -05:00
sbwalker
90d2e0a40b fix #4976 - manage hierarchical path updates and page deletion 2025-01-21 15:57:48 -05:00
Shaun Walker
5f884e0796
Merge pull request #5008 from sbwalker/dev
fix #4965 - improve user/site management
2025-01-21 12:21:50 -05:00
sbwalker
16477052e2 fix #4965 - improve user/site management 2025-01-21 12:21:27 -05:00
RahulKaushik007
66a05603f7 Fix static file caching headers 2025-01-21 17:42:08 +05:30
RahulKaushik007
fe2a883386 Fix static file caching headers 2025-01-21 17:26:47 +05:30
David Montesinos
ca7fdaa125
FIX: File server MimeType not updated after image conversion 2025-01-20 12:17:22 +01:00
sdi2121
1283ec2008
Update Oqtane.Server.csproj
Fixes Azure manual deployment build from local code build. May need an additional fix in MySQL library (Line 24 - MySQLDatabase.cs)
2025-01-18 13:53:04 -05:00
Shaun Walker
d1f78f9048
Update LICENSE 2025-01-17 12:57:32 -05:00
Shaun Walker
45f43bfade
Merge pull request #5002 from sbwalker/dev
update copyright year
2025-01-17 12:57:14 -05:00
sbwalker
4793ab4bc9 update copyright year 2025-01-17 12:56:59 -05:00
Shaun Walker
88b174dea8
Merge pull request #5001 from sbwalker/dev
remove unused method
2025-01-17 12:50:42 -05:00
sbwalker
06ca382bd7 remove unused method 2025-01-17 12:50:26 -05:00
Shaun Walker
b09175a8db
Merge pull request #5000 from sbwalker/dev
allow entry of name during installation
2025-01-17 11:14:49 -05:00
sbwalker
677f68b08d allow entry of name during installation 2025-01-17 11:14:35 -05:00
Shaun Walker
8058b8dba4
Merge pull request #4999 from sbwalker/dev
improve error messages
2025-01-17 07:54:49 -05:00
sbwalker
4bc26f13c1 improve error messages 2025-01-17 07:54:34 -05:00
Leigh Pointer
e6cf77e724 Update ActionDialog Add method to ensure consistent button sizing
This PR introduces a new private method GetButtonSize() to enhance the consistency of button sizing within our UI. The method specifically checks for the presence of the "btn-sm" class in the Action Button's class list and applies the same sizing to the Cancel Button if found.
2025-01-17 13:34:16 +01:00
Shaun Walker
f8737c112e
Merge pull request #4997 from sbwalker/dev
reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true"
2025-01-16 15:06:36 -05:00
sbwalker
74b72ed9d4 reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true" 2025-01-16 15:06:15 -05:00
Shaun Walker
4950391201
Merge pull request #4996 from sbwalker/dev
allow data-reload to support true or always
2025-01-16 14:22:06 -05:00
sbwalker
64a38d6e45 allow data-reload to support true or always 2025-01-16 14:21:52 -05:00
Shaun Walker
e2d8ee53f8
Merge pull request #4995 from sbwalker/dev
script reload improvements
2025-01-16 14:06:28 -05:00
sbwalker
0204ff8dd5 script reload improvements 2025-01-16 14:06:13 -05:00
Shaun Walker
4630ee6e93
Merge pull request #4992 from beolafsen/dev
Trim ModuleOwner and ModuleName before create
2025-01-16 12:29:00 -05:00
Shaun Walker
c4f2abf143
Merge pull request #4994 from sbwalker/dev
fix #4986 - allow Resources which have Reload specified to be used in Interactive rendering
2025-01-16 12:26:30 -05:00
sbwalker
e7444a0194 fix #4986 - allow Resources which have Reload specified to be used in Interactive rendering 2025-01-16 12:26:10 -05:00
Shaun Walker
ffed7305ed
Merge pull request #4993 from sbwalker/dev
fix #4984 - path mapping for personalized pages
2025-01-16 09:25:43 -05:00
sbwalker
334054bcd4 fix #4984 - path mapping for personalized pages 2025-01-16 09:25:27 -05:00
beolafsen
5bb98eb5b2 Trim ModuleOwner and ModuleName before create 2025-01-16 09:32:18 +01:00
Shaun Walker
e842bd882a
Merge pull request #4991 from sbwalker/dev
fix #4984 - redirect not working for personalized pages
2025-01-15 11:57:00 -05:00
sbwalker
74bfb46f73 fix #4984 - redirect not working for personalized pages 2025-01-15 11:56:44 -05:00
Shaun Walker
cd45bf4b70
Merge pull request #4989 from sbwalker/dev
fix #4984 - ensure personalized page path does not contain illegal characters
2025-01-14 15:43:00 -05:00
sbwalker
51600bbcb0 fix #4984 - ensure personalized page path does not contain illegal characters 2025-01-14 15:42:40 -05:00
Shaun Walker
8811a9bcaa
Merge pull request #4988 from sbwalker/dev
introduce RemoveAssemblies() method in UpgradeManager
2025-01-14 08:43:38 -05:00
sbwalker
4521f8a774 introduce RemoveAssemblies() method in UpgradeManager 2025-01-14 08:43:23 -05:00
Shaun Walker
5b427783f8
Merge pull request #4987 from zyhfish/task/fix-4954
Fix #4954: use Pomelo.EntityFrameworkCore.MySql package.
2025-01-14 08:34:34 -05:00
Ben
9508983b15 Fix #4954: use Pomelo.EntityFrameworkCore.MySql package. 2025-01-14 19:58:56 +08:00
Shaun Walker
fd09912cd7
Merge pull request #4978 from beolafsen/dev
Issue #4977
2025-01-13 16:00:06 -05:00
Shaun Walker
01cc8584b6
Merge pull request #4968 from leigh-pointer/unused
Removed unused Using statements from the SiteTemplates
2025-01-13 15:56:39 -05:00
Shaun Walker
c0b104e7c8
Merge pull request #4962 from thabaum/prepare-6.0.2-update-dependencies
Fixes #4961 - Updates Oqtane version to 6.0.2 and update related project dependencies to latest.
2025-01-13 15:55:13 -05:00
Shaun Walker
9a82021a82
Merge pull request #4983 from sbwalker/dev
include option for external login to save tokens
2025-01-13 15:14:27 -05:00
sbwalker
1fb54a0b0f include option for external login to save tokens 2025-01-13 15:14:13 -05:00
Shaun Walker
aa5ea61638
Merge pull request #4982 from sbwalker/dev
improve filtering logic in UserRole API
2025-01-13 14:42:36 -05:00
sbwalker
a59ec0258b improve filtering logic in UserRole API 2025-01-13 14:42:19 -05:00
Shaun Walker
b403f5cf71
Merge pull request #4980 from sbwalker/dev
fix comment spelling
2025-01-13 07:50:21 -05:00
sbwalker
0ac6a62b86 fix comment spelling 2025-01-13 07:50:05 -05:00
Shaun Walker
ed3743d3b6
Merge pull request #4979 from sbwalker/dev
fix #4969 - improve feedback and flow when connection string points to an invalid database
2025-01-13 07:48:50 -05:00
sbwalker
3468cba000 fix #4969 - improve feedback and flow when connection string points to an invalid database 2025-01-13 07:48:30 -05:00
beolafsen
5a4cdc5354 Issue #4977 2025-01-13 07:31:35 +01:00
Leigh Pointer
af4e19a57e Removed unused Using statements from the SiteTemplates 2025-01-07 04:14:31 +01:00
Cody
26bb743679
Prepare 6.0.2 2024-12-31 10:37:27 -08:00
Cody
96cc726e22
Prepare 6.0.2 2024-12-31 10:35:00 -08:00
Cody
f4b00b01d0
Prepare 6.0.2 2024-12-31 10:34:21 -08:00
Cody
127b2ca86d
Prepare 6.0.2 2024-12-31 10:32:09 -08:00
Cody
4b8b93e1b8
Prepare 6.0.2 2024-12-31 10:31:47 -08:00
Cody
3aea412fe9
Prepare 6.0.2 2024-12-31 10:31:23 -08:00
Cody
2aef96ad4f
Prepare 6.0.2 2024-12-31 10:29:52 -08:00
Cody
ec0a77230c
Prepare 6.0.2 2024-12-31 10:29:32 -08:00
Cody
b35e4bddd0
Prepare 6.0.2 2024-12-31 10:28:48 -08:00
Cody
aa32beb341
Update Oqtane.Shared.csproj 2024-12-31 10:28:32 -08:00
Cody
efafe89b42
Prepare 6.0.2 2024-12-31 10:26:45 -08:00
Cody
5ef2e49d9c
Prepare 6.0.2 2024-12-31 10:25:34 -08:00
Cody
1cfbf61a30
Prepare 6.0.2 2024-12-31 10:23:15 -08:00
Cody
2bb5494b84
Prepare 6.0.2 2024-12-31 10:22:10 -08:00
Cody
e8a41ccb47
Prepare 6.0.2 2024-12-31 10:21:10 -08:00
Cody
7184f7f635
Prepare 6.0.2 and update package dependencies 2024-12-31 10:20:13 -08:00
Cody
cc5727b7fa
Prepare 6.0.2 and update package dependencies 2024-12-31 10:16:35 -08:00
Shaun Walker
7f3d6ef6a5
Merge pull request #4959 from sbwalker/dev
fix #4957 - unable to login after password reset
2024-12-31 08:18:14 -05:00
sbwalker
44ce68097b fix #4957 - unable to login after password reset 2024-12-31 08:17:58 -05:00
Shaun Walker
d976cc6c19
Update SECURITY.md 2024-12-25 10:04:25 -05:00
Shaun Walker
d19d7d2a43
Merge pull request #4948 from zyhfish/task/fix-4947
Fix #4947: check the 2FA settings.
2024-12-24 08:46:43 -05:00
Shaun Walker
9bfaa02f97
Merge pull request #4953 from sbwalker/dev
add back System.Text.Json to Shared project (#4929)
2024-12-23 14:48:10 -05:00
sbwalker
2d9396b245 add back System.Text.Json to Shared project (#4929) 2024-12-23 14:47:17 -05:00
Shaun Walker
56e0da64ee
Merge pull request #4952 from sbwalker/dev
updated default module template to use Service consistently
2024-12-23 14:10:07 -05:00
sbwalker
997e9213f2 updated default module template to use Service consistently 2024-12-23 14:09:54 -05:00
Shaun Walker
366569a23b
Merge pull request #4951 from sbwalker/dev
update package references
2024-12-23 11:24:38 -05:00
sbwalker
36d5747b4f update package references 2024-12-23 11:24:24 -05:00
Shaun Walker
ea026c726c
Merge pull request #4950 from sbwalker/dev
remove unnecessary reference to System.Text.Json in Shared project
2024-12-23 11:16:49 -05:00
sbwalker
1e71e32c74 remove unnecessary reference to System.Text.Json in Shared project 2024-12-23 11:16:34 -05:00
Shaun Walker
ed729bbd4f
Merge pull request #4949 from sbwalker/dev
fix #4946 - allow administrators to access user roles via API
2024-12-23 09:27:04 -05:00
sbwalker
1a925221b7 fix #4946 - allow administrators to access user roles via API 2024-12-23 09:26:50 -05:00
Ben
af7b4db062 Fix #4947: check the 2FA settings. 2024-12-23 22:10:51 +08:00
Shaun Walker
cfefe35e3f
Update README.md 2024-12-20 16:56:05 -05:00
168 changed files with 4378 additions and 1822 deletions

View File

@ -0,0 +1,25 @@
name: build-docker-imge
on:
- push
jobs:
build:
name: Build the docker container
runs-on: ubuntu-latest
steps:
- name: "Git clone"
run: git clone ${{ gitea.server_url }}/${{ gitea.repository }}.git .
- name: "Git checkout"
run: git checkout "${{ gitea.sha }}"
- uses: aevea/action-kaniko@master
name: Run Kaniko to build our api docker container.
with:
image: kocoded/oqtane.framework
tag: ${{ git.workflow_sha }}
tag_with_latest: github.ref == 'refs/heads/master'
registry: git.kocoder.xyz
username: ${{ secrets.CI_RUNNER_USER }}
password: ${{ secrets.CI_RUNNER_TOKEN }}
build_file: Dockerfile
target: deploy

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# 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 /app
COPY --from=publish /source/publish/ /app/
ENTRYPOINT ["dotnet", "Oqtane.Server.dll"]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2024 .NET Foundation
Copyright (c) 2018-2025 .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

@ -52,6 +52,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -71,14 +71,14 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Provide a username for the primary user account" ResourceKey="Username">Username:</Label>
<div class="col-sm-9">
<input id="username" type="text" class="form-control" @bind="@_hostUsername" />
<input id="username" type="text" class="form-control" maxlength="256" @bind="@_hostUsername" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordType" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<input id="password" type="@_passwordType" class="form-control" maxlength="256" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button>
</div>
</div>
@ -87,7 +87,7 @@
<Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<input id="confirm" type="@_confirmPasswordType" class="form-control" maxlength="256" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword" tabindex="-1">@_toggleConfirmPassword</button>
</div>
</div>
@ -95,7 +95,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Provide the email address for the host user account" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
<input type="text" class="form-control" @bind="@_hostEmail" />
<input type="text" class="form-control" maxlength="256" @bind="@_hostEmail" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Provide the full name of the host user" ResourceKey="Name">Full Name:</Label>
<div class="col-sm-9">
<input type="text" class="form-control" maxlength="50" @bind="@_hostName" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -153,6 +159,7 @@
private string _toggleConfirmPassword = string.Empty;
private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty;
private string _hostName = string.Empty;
private List<SiteTemplate> _templates;
private string _template = Constants.DefaultSiteTemplate;
private bool _register = true;
@ -236,7 +243,7 @@
}
}
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@") && !string.IsNullOrEmpty(_hostName))
{
var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword);
if (result.Succeeded)
@ -256,7 +263,7 @@
HostUsername = _hostUsername,
HostPassword = _hostPassword,
HostEmail = _hostEmail,
HostName = _hostUsername,
HostName = _hostName,
TenantName = TenantNames.Master,
IsNewTenant = true,
SiteName = Constants.DefaultSite,

View File

@ -49,18 +49,24 @@
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label>
<div class="col-sm-9">
<input id="capacity" class="form-control" @bind="@_capacity" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cachecontrol" HelpText="Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI." ResourceKey="CacheControl">Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" maxlength="50" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended)." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
</div>
@if (PageState.QueryString.ContainsKey("id"))
{
@ -100,8 +106,9 @@
private int _parentId = -1;
private string _name;
private string _type = FolderTypes.Private;
private string _imagesizes = string.Empty;
private string _capacity = "0";
private string _cachecontrol = string.Empty;
private string _imagesizes = string.Empty;
private bool _isSystem;
private List<Permission> _permissions = null;
private string _createdBy;
@ -132,8 +139,9 @@
_parentId = folder.ParentId ?? -1;
_name = folder.Name;
_type = folder.Type;
_imagesizes = folder.ImageSizes;
_capacity = folder.Capacity.ToString();
_cachecontrol = folder.CacheControl;
_imagesizes = folder.ImageSizes;
_isSystem = folder.IsSystem;
_permissions = folder.PermissionList;
_createdBy = folder.CreatedBy;
@ -204,8 +212,9 @@
folder.SiteId = PageState.Site.SiteId;
folder.Name = _name;
folder.Type = _type;
folder.ImageSizes = _imagesizes;
folder.Capacity = int.Parse(_capacity);
folder.CacheControl = _cachecontrol;
folder.ImageSizes = _imagesizes;
folder.IsSystem = _isSystem;
folder.PermissionList = _permissionGrid.GetPermissionList();

View File

@ -3,54 +3,92 @@
@inject NavigationManager NavigationManager
@inject IFolderService FolderService
@inject IFileService FileService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_files != null)
@if (_files == null)
{
<div class="row">
<div class="col-md mb-1">
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />
</div>
<div class="col-md-8 mb-1">
<div class="input-group">
<span class="input-group-text">@Localizer["Folder"]:</span>
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
<p>
<em>@SharedLocalizer["Loading"]</em>
</p>
}
else
{
<TabStrip>
<TabPanel Name="Files" Heading="Files" ResourceKey="Files">
<div class="row">
<div class="col-md mb-1">
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />
</div>
<div class="col-md-8 mb-1">
<div class="input-group">
<span class="input-group-text">@Localizer["Folder"]:</span>
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
</div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div>
<Pager Items="@_files" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Modified"]</th>
<th>@Localizer["Type"]</th>
<th>@Localizer["Size"]</th>
</Header>
<Row>
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@context.ModifiedOn</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row>
</Pager>
@if (_files.Count == 0)
{
<div class="text-center">@Localizer["NoFiles"]</div>
}
@if (_files.Count != 0)
{
<Pager Items="@_files" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Modified"]</th>
<th>@Localizer["Type"]</th>
<th>@Localizer["Size"]</th>
</Header>
<Row>
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@context.ModifiedOn</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row>
</Pager>
}
else
{
<div class="text-center">@Localizer["NoFiles"]</div>
}
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maxChunkSize" HelpText="Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks)." ResourceKey="MaxChunkSize">Max Upload Chunk Size (MB): </Label>
<div class="col-sm-9">
<input id="maxChunkSize" type="number" min="1" max="10" step="1" class="form-control" @bind="@_maxChunkSize" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
}
@code {
@ -58,6 +96,10 @@
private int _folderId = -1;
private List<File> _files;
private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private int _maxChunkSize = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
@ -71,6 +113,13 @@
_folderId = _folders[0].FolderId;
await GetFiles();
}
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
_maxChunkSize = int.Parse(SettingService.GetSetting(settings, "MaxChunkSize", "1"));
}
catch (Exception ex)
{
@ -115,4 +164,23 @@
AddModuleMessage(string.Format(Localizer["Error.File.Delete"], file.Name), MessageType.Error);
}
}
private async Task SaveSiteSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "MaxChunkSize", _maxChunkSize.ToString(), false);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
}

View File

@ -55,10 +55,6 @@ else
protected override async Task OnInitializedAsync()
{
await GetJobs();
if (_jobs.Count == 0)
{
AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning);
}
}
private async Task GetJobs()

View File

@ -29,12 +29,12 @@ else
{
<div class="form-group">
<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" required />
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" @bind:event="oninput" required />
</div>
<div class="form-group mt-2">
<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" required />
<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>

View File

@ -111,6 +111,8 @@
private async Task CreateModule()
{
validated = true;
_owner = _owner.Trim();
_module = _module.Trim();
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{

View File

@ -63,24 +63,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed. This value must be specified within the module's IModule interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -244,7 +227,6 @@
private string _moduledefinitionname = "";
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -445,27 +427,5 @@
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
private string Browse(Page page) => string.IsNullOrEmpty(page.Url) ? NavigateUrl(page.Path) : page.Url;
}

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" required />
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@ -101,13 +101,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" />
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="Optionally enter a url which this page should redirect to when a user navigates to it" ResourceKey="Redirect">Redirect: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" />
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -147,7 +147,7 @@
<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" />
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -186,13 +186,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
<div class="col-sm-9">
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3"></textarea>
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodycontent" HelpText="Optionally enter content to be included in the page body (ie. script tags)" ResourceKey="BodyContent">Body Content: </Label>
<div class="col-sm-9">
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3"></textarea>
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>

View File

@ -116,7 +116,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
@ -205,13 +205,13 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="headcontent" HelpText="Optionally enter content to be included in the page head (ie. meta, link, or script tags)" ResourceKey="HeadContent">Head Content: </Label>
<div class="col-sm-9">
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3"></textarea>
<textarea id="headcontent" class="form-control" @bind="@_headcontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="bodycontent" HelpText="Optionally enter content to be included in the page body (ie. script tags)" ResourceKey="BodyContent">Body Content: </Label>
<div class="col-sm-9">
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3"></textarea>
<textarea id="bodycontent" class="form-control" @bind="@_bodycontent" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>
@ -263,6 +263,12 @@
<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="path" HelpText="Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash." ResourceKey="PersonalizedUrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</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">

View File

@ -13,71 +13,71 @@
}
else
{
<TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
@if (!_pages.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoPage.Deleted"]</p>
}
else
{
<Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
@if (!_pages.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoPage.Deleted"]</p>
}
else
{
<Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Path"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
}
</TabPanel>
<TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules">
@if (!_modules.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoModule.Deleted"]</p>
}
else
{
<Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Page"]</th>
<th>@Localizer["Module"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
}
</TabPanel>
</TabStrip>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Path</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
}
</TabPanel>
<TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules">
@if (!_modules.Where(item => item.IsDeleted).Any())
{
<br />
<p>@Localizer["NoModule.Deleted"]</p>
}
else
{
<Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Page"]</th>
<th>@Localizer["Module"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
</Row>
</Pager>
<br />
<ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
}
</TabPanel>
</TabStrip>
}
@code {
private List<Page> _pages;
private List<Module> _modules;
private List<Page> _pages;
private List<Module> _modules;
private int _pagePage = 1;
private int _pageModule = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -105,12 +105,25 @@ else
{
try
{
page.IsDeleted = false;
await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Restored {Page}", page);
await Load();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
var validated = true;
if (page.ParentId != null)
{
var parent = _pages.Find(item => item.PageId == page.ParentId);
validated = !parent.IsDeleted;
}
if (validated)
{
page.IsDeleted = false;
await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Restored {Page}", page);
AddModuleMessage(Localizer["Success.Page.Restore"], MessageType.Success);
await Load();
StateHasChanged();
}
else
{
AddModuleMessage(Localizer["Message.Page.Restore"], MessageType.Warning);
}
}
catch (Exception ex)
{
@ -125,9 +138,9 @@ else
{
await PageService.DeletePageAsync(page.PageId);
await logger.LogInformation("Page Permanently Deleted {Page}", page);
AddModuleMessage(Localizer["Success.Page.Delete"], MessageType.Success);
await Load();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
@ -148,10 +161,10 @@ else
}
await logger.LogInformation("Pages Permanently Deleted");
AddModuleMessage(Localizer["Success.Pages.Delete"], MessageType.Success);
await Load();
HideProgressIndicator();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
@ -169,6 +182,7 @@ else
pagemodule.IsDeleted = false;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await logger.LogInformation("Module Restored {Module}", module);
AddModuleMessage(Localizer["Success.Module.Restore"], MessageType.Success);
await Load();
StateHasChanged();
}
@ -185,6 +199,7 @@ else
{
await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
await logger.LogInformation("Module Permanently Deleted {Module}", module);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
await Load();
StateHasChanged();
}
@ -205,6 +220,7 @@ else
await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
}
await logger.LogInformation("Modules Permanently Deleted");
AddModuleMessage(Localizer["Success.Modules.Delete"], MessageType.Success);
await Load();
HideProgressIndicator();
StateHasChanged();

View File

@ -58,7 +58,7 @@ else
<td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td>
<td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -180,27 +180,28 @@ else
private async Task DeleteUserRole(int UserRoleId)
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
try
{
try
var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId);
if (userrole.Role.Name == RoleNames.Registered)
{
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
else
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
else
catch (Exception ex)
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
}
}
}

View File

@ -14,6 +14,7 @@
@inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService
@if (_initialized)
{
@ -50,11 +51,12 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing" ResourceKey="SiteMap">Site Map: </Label>
<Label Class="col-sm-3" For="sitemap" HelpText="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." ResourceKey="SiteMap">Site Map: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="sitemap" class="form-control" @bind="@_sitemap" disabled />
<a href="@_sitemap" class="btn btn-secondary" target="_new">@Localizer["Browse"]</a>
<button type="button" class="btn btn-danger" @onclick="EvictSitemapOutputCache">@Localizer["SiteMap.EvictCache"]</button>
</div>
</div>
</div>
@ -72,20 +74,8 @@
</div>
</div>
<br />
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<Section Name="Theme" Heading="Theme" ResourceKey="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico,png,gif" @ref="_faviconfilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the sites default theme" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
@ -126,6 +116,32 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookieconsent" HelpText="Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent." ResourceKey="CookieConsent">Cookie Consent: </Label>
<div class="col-sm-9">
<select id="cookieconsent" class="form-select" @bind="@_cookieconsent">
<option value="">@SharedLocalizer["Disabled"]</option>
<option value="optin">@Localizer["OptIn"]</option>
<option value="optout">@Localizer["OptOut"]</option>
</select>
</div>
</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="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico,png,gif" @ref="_faviconfilemanager" />
</div>
</div>
</div>
</Section>
<Section Name="Functionality" Heading="Functionality" ResourceKey="Functionality">
@ -144,18 +160,6 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
@ -427,11 +431,11 @@
private string _themetype = "";
private string _containertype = "";
private string _admincontainertype = "";
private string _cookieconsent = "";
private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = "";
private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private string _headcontent = string.Empty;
private string _bodycontent = string.Empty;
@ -518,6 +522,7 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_cookieconsent = SettingService.GetSetting(settings, "CookieConsent", string.Empty);
// functionality
var textEditors = ServiceProvider.GetServices<ITextEditor>();
@ -528,8 +533,6 @@
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
// page content
_headcontent = site.HeadContent;
@ -732,10 +735,11 @@
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
//cookie consent
settings = SettingService.SetSetting(settings, "CookieConsent", _cookieconsent);
// functionality
settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
@ -930,4 +934,9 @@
_aliasname = "";
StateHasChanged();
}
private async Task EvictSitemapOutputCache() {
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
}
}

View File

@ -133,16 +133,30 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager: </Label>
<Label Class="col-sm-3" For="cachecontrol" HelpText="Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled." ResourceKey="CacheControl">Static Asset Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager Url: </Label>
<div class="col-sm-9">
<input id="packageregistryurl" class="form-control" @bind="@_packageregistryurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryemail" HelpText="Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations." ResourceKey="PackageManagerEmail">Package Manager Email: </Label>
<div class="col-sm-9">
<input id="packageregistryemail" class="form-control" @bind="@_packageregistryemail" />
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
<br /><br />
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<a class="btn btn-secondary" href="api/endpoint" target="_new">@Localizer["Endpoints"]</a>
</TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log">
<div class="container">
@ -179,9 +193,11 @@
private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty;
private string _cachecontrol = string.Empty;
private string _packageregistryurl = string.Empty;
private string _packageregistryemail = string.Empty;
private string _log = string.Empty;
private string _log = string.Empty;
protected override async Task OnInitializedAsync()
{
@ -209,9 +225,11 @@
_detailederrors = systeminfo["DetailedErrors"].ToString();
_logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_cachecontrol = systeminfo["CacheControl"].ToString();
_packageregistryurl = systeminfo["PackageRegistryUrl"].ToString();
}
_packageregistryemail = systeminfo["PackageRegistryEmail"].ToString();
}
systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null)
@ -229,8 +247,10 @@
settings.Add("Logging:LogLevel:Default", _logginglevel);
settings.Add("Logging:LogLevel:Notify", _notificationlevel);
settings.Add("UseSwagger", _swagger);
settings.Add("PackageRegistryUrl", _packageregistryurl);
await SystemService.UpdateSystemInfoAsync(settings);
settings.Add("CacheControl", _cachecontrol);
settings.Add("PackageRegistryUrl", _packageregistryurl);
settings.Add("PackageRegistryEmail", _packageregistryemail);
await SystemService.UpdateSystemInfoAsync(settings);
AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
}
catch (Exception ex)

View File

@ -45,24 +45,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -116,7 +99,6 @@
private string _name;
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -185,27 +167,4 @@
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
}

View File

@ -4,6 +4,7 @@
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IPackageService PackageService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -19,6 +20,7 @@ else
<Pager Items="@_themes">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
@ -32,10 +34,11 @@ else
<td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.ThemeId.ToString())" ResourceKey="EditTheme" /></td>
<td>
@if (context.AssemblyName != Constants.ClientId)
{
{
<ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
}
}
</td>
<td><NavLink class="btn btn-secondary" href="@NavigateUrl("admin/site")">@Localizer["Assign"]</NavLink></td>
<td>@context.Name</td>
<td>@context.Version</td>
<td>

View File

@ -3,6 +3,7 @@
@inject NavigationManager NavigationManager
@inject IUrlMappingService UrlMappingService
@inject ISiteService SiteService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -62,7 +63,13 @@ else
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of broken urls to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
@ -73,6 +80,7 @@ else
private bool _mapped = true;
private List<UrlMapping> _urlMappings;
private string _capturebrokenurls;
private int _retention = 30;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -80,7 +88,10 @@ else
{
await GetUrlMappings();
_capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString();
}
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = int.Parse(SettingService.GetSetting(settings, "UrlMappingRetention", "30"));
}
private async void MappedChanged(ChangeEventArgs e)
{
@ -124,7 +135,12 @@ else
var site = PageState.Site;
site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls);
await SiteService.UpdateSiteAsync(site);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "UrlMappingRetention", _retention.ToString(), true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{

View File

@ -6,6 +6,7 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IFileService FileService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -51,15 +52,18 @@
<input id="displayname" class="form-control" @bind="@displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted">
<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="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<div class="col-sm-9">
@ -127,8 +131,15 @@
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost)
{
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True")
{
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
<br /><br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
}
@ -146,6 +157,7 @@
private string isdeleted;
private string lastlogin;
private string lastipaddress;
private bool ishost = false;
private List<Profile> profiles;
private Dictionary<string, string> userSettings;
@ -180,6 +192,7 @@
isdeleted = user.IsDeleted.ToString();
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
lastipaddress = user.LastIPAddress;
ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
userSettings = user.Settings;
createdby = user.CreatedBy;
@ -226,8 +239,10 @@
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
}
user = await UserService.UpdateUserAsync(user);
if (user != null)
@ -259,6 +274,44 @@
}
}
private async Task ImpersonateUser()
{
try
{
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username);
// post back to the server so that the cookies are set correctly
var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = username, returnurl = PageState.Alias.Path };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
await interop.SubmitForm(url, fields);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", username, ex.Message);
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
}
}
private async Task DeleteUser()
{
try
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId)
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Permanently Deleted {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
}
}
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)

View File

@ -17,171 +17,180 @@ else
{
<TabStrip>
<TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Text="Import Users" Class="btn btn-secondary ms-2" Action="Users" Security="SecurityAccessLevel.Admin" ResourceKey="ImportUsers"/>
<Pager Items="@users" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("Username"))">@Localizer["Username"]<i class="@(SetSortIcon("Username"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("DisplayName"))">@Localizer["Name"]<i class="@(SetSortIcon("DisplayName"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("Email"))">@Localizer["Email"]<i class="@(SetSortIcon("Email"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("LastLoginOn"))">@Localizer["LastLoginOn"]<i class="@(SetSortIcon("LastLoginOn"))"></i></th>
</Header>
<Row>
<td>
</Header>
<Row>
<td>
<ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td>
<td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td>
<td>
</td>
<td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId || context.User.IsDeleted)" ResourceKey="DeleteUser" />
</td>
<td>
<ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td>
<td>@context.User.Username</td>
<td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row>
</Pager>
</td>
<td>@context.User.Username</td>
<td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<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="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">
<select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<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 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>
}
else
{
<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 Login?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</div>
</div>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<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?</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">
<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="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">
<select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@if (_providertype != "")
{
<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 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>
}
else
{
<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 Login?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</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?</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="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">
<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">
<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>
}
</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>
</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">
<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>
</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">
@ -201,77 +210,77 @@ else
</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))">
<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>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</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>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</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>
<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>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</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>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</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">
@ -291,32 +300,32 @@ else
</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>
<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="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>
<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">
@ -334,10 +343,10 @@ else
</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="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">
@ -346,16 +355,16 @@ else
</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="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">
@ -374,26 +383,35 @@ else
</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="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>
<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">
@ -404,51 +422,51 @@ else
</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>
</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>
</TabPanel>
</TabStrip>
</TabStrip>
}
@code {
@ -460,6 +478,7 @@ else
private string _cookiename;
private string _cookieexpiration;
private string _alwaysremember;
private string _logouteverywhere;
private string _minimumlength;
private string _uniquecharacters;
@ -497,6 +516,7 @@ else
private string _roleclaimmappings;
private string _synchronizeroles;
private string _profileclaimtypes;
private string _savetokens;
private string _domainfilter;
private string _createusers;
private string _verifyusers;
@ -519,7 +539,7 @@ else
await LoadUsersAsync(true);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -528,6 +548,7 @@ else
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", "");
_alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false");
_logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false");
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
@ -577,6 +598,7 @@ else
_roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", "");
_synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
@ -600,19 +622,31 @@ else
{
try
{
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
await LoadUsersAsync(true);
StateHasChanged();
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
{
user.IsDeleted = true;
await UserService.UpdateUserAsync(user);
await logger.LogInformation("User Soft Deleted {User}", user);
}
}
else
{
var userrole = await UserRoleService.GetUserRoleAsync(UserRole.UserRoleId);
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success);
await LoadUsersAsync(true);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
}
}
@ -633,6 +667,7 @@ else
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false);
settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
@ -666,6 +701,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:SaveTokens", _savetokens, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);

View File

@ -53,17 +53,17 @@ else
<p align="center">
<Pager Items="@userroles">
<Header>
<th>@Localizer["Roles"]</th>
<th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th>
<th>&nbsp;</th>
<th>@Localizer["Roles"]</th>
<th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th>
<th>&nbsp;</th>
</Header>
<Row>
<td>@context.Role.Name</td>
<td>@Utilities.UtcAsLocalDate(context.EffectiveDate)</td>
<td>@Utilities.UtcAsLocalDate(context.ExpiryDate)</td>
<td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.Name == RoleNames.Host && userid == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -171,8 +171,18 @@ else
{
try
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId);
if (userrole.Role.Name == RoleNames.Registered)
{
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
else
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success);
await GetUserRoles();
StateHasChanged();

View File

@ -22,9 +22,9 @@
<div class="modal-footer">
@if (!string.IsNullOrEmpty(Action))
{
<button type="button" class="@Class" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button>
<button type="button" class="@ConfirmClass" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button>
}
<button type="button" class="btn btn-secondary" @onclick="DisplayModal">@SharedLocalizer["Cancel"]</button>
<button type="button" class="@CancelClass" @onclick="DisplayModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
@ -66,12 +66,12 @@ else
{
<form method="post" @formname="@($"ActionDialogConfirmForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="Confirm" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button>
<button type="submit" class="@ConfirmClass">@((MarkupString)_iconSpan) @Text</button>
</form>
}
<form method="post" @formname="@($"ActionDialogCancelForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn btn-secondary">@SharedLocalizer["Cancel"]</button>
<button type="submit" class="@CancelClass">@SharedLocalizer["Cancel"]</button>
</form>
</div>
</div>
@ -128,6 +128,12 @@ else
[Parameter]
public string Class { get; set; } // optional
[Parameter]
public string ConfirmClass { get; set; } // optional - for Confirm modal button
[Parameter]
public string CancelClass { get; set; } // optional - for Cancel modal button
[Parameter]
public bool Disabled { get; set; } // optional
@ -168,6 +174,16 @@ else
Class = "btn btn-success";
}
if (string.IsNullOrEmpty(ConfirmClass))
{
ConfirmClass = Class;
}
if (string.IsNullOrEmpty(CancelClass))
{
CancelClass = "btn btn-secondary";
}
if (!string.IsNullOrEmpty(EditMode))
{
_editmode = bool.Parse(EditMode);

View File

@ -3,8 +3,8 @@
@inherits ModuleControlBase
@inject IFolderService FolderService
@inject IFileService FileService
@inject ISettingService SettingService
@inject IUserService UserService
@inject ISettingService SettingService
@inject IStringLocalizer<FileManager> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -157,6 +157,9 @@
[Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
@ -359,6 +362,8 @@
}
if (restricted == "")
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
try
{
// upload the files
@ -377,57 +382,21 @@
}
}
var chunksize = ChunkSize;
if (chunksize == 1)
{
// if ChunkSize parameter is not overridden use the site setting
chunksize = int.Parse(SettingService.GetSetting(PageState.Site.Settings, "MaxChunkSize", "1"));
}
if (!ShowProgress)
{
_uploading = true;
StateHasChanged();
}
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt);
// uploading is asynchronous so we need to poll to determine if uploads are completed
var success = true;
int upload = 0;
while (upload < uploads.Length && success)
{
success = false;
var filename = uploads[upload].Split(':')[0];
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
var megabits = (size / 1048576.0) * 8; // binary conversion
var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload
var uploadtime = (megabits / uploadspeed); // seconds
var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds)
var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds
int attempts = 0;
while (attempts < maxattempts && !success)
{
attempts += 1;
Thread.Sleep(sleep);
if (Folder == Constants.PackagesFolder)
{
var files = await FileService.GetFilesAsync(folder);
if (files != null && files.Any(item => item.Name == filename))
{
success = true;
}
}
else
{
var file = await FileService.GetFileAsync(int.Parse(folder), filename);
if (file != null)
{
success = true;
}
}
}
if (success)
{
upload++;
}
}
// upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token);
// reset progress indicators
if (ShowProgress)
@ -452,7 +421,7 @@
}
else
{
await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads);
await logger.LogError("File Upload Failed {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
@ -482,6 +451,10 @@
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
_uploading = false;
await tokenSource.CancelAsync();
}
finally {
tokenSource.Dispose();
}
}

View File

@ -2,6 +2,7 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject IHtmlTextService HtmlTextService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@if (PageState.EditMode)
@ -36,6 +37,10 @@
{
content = htmltext.Content;
content = Utilities.FormatContent(content, PageState.Alias, "render");
if (bool.Parse(SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false")))
{
content = ReplaceTokens(content);
}
}
else
{

View File

@ -15,7 +15,7 @@ namespace Oqtane.Modules.HtmlText
Version = "1.0.1",
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
ReleaseVersions = "1.0.0,1.0.1",
SettingsType = string.Empty,
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client",
Resources = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }

View File

@ -0,0 +1,55 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> 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="dynamictokens" ResourceKey="DynamicTokens" ResourceType="@resourceType" HelpText="Do you wish to allow tokens to be dynamically replaced? Please note that this will affect the performance of your site.">Dynamic Tokens? </Label>
<div class="col-sm-9">
<select id="dynamictokens" class="form-select" @bind="@_dynamictokens">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</form>
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private ElementReference form;
private bool validated = false;
private string _dynamictokens;
protected override void OnInitialized()
{
try
{
_dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using System.Dynamic;
using System.Reflection;
namespace Oqtane.Modules
{
@ -18,6 +19,7 @@ namespace Oqtane.Modules
private Logger _logger;
private string _urlparametersstate;
private Dictionary<string, string> _urlparameters;
private bool _scriptsloaded = false;
protected Logger logger => _logger ?? (_logger = new Logger(this));
@ -34,7 +36,7 @@ namespace Oqtane.Modules
protected PageState PageState { get; set; }
[CascadingParameter]
protected Module ModuleState { get; set; }
protected Models.Module ModuleState { get; set; }
[Parameter]
public RenderModeBoundary RenderModeBoundary { get; set; }
@ -98,7 +100,7 @@ namespace Oqtane.Modules
var inline = 0;
foreach (Resource resource in resources)
{
if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload)
if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive)
{
if (!string.IsNullOrEmpty(resource.Url))
{
@ -117,6 +119,7 @@ namespace Oqtane.Modules
await interop.IncludeScripts(scripts.ToArray());
}
}
_scriptsloaded = true;
}
}
@ -125,6 +128,14 @@ namespace Oqtane.Modules
return PageState?.RenderId == ModuleState?.RenderId;
}
public bool ScriptsLoaded
{
get
{
return _scriptsloaded;
}
}
// path method
public string ModulePath()
@ -132,6 +143,15 @@ namespace Oqtane.Modules
return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/";
}
// fingerprint hash code for static assets
public string Fingerprint
{
get
{
return ModuleState.ModuleDefinition.Fingerprint;
}
}
// url methods
// navigate url
@ -394,6 +414,79 @@ namespace Oqtane.Modules
await interop.ScrollTo(0, 0, "smooth");
}
public string ReplaceTokens(string content)
{
return ReplaceTokens(content, null);
}
public string ReplaceTokens(string content, object obj)
{
var tokens = new List<string>();
var pos = content.IndexOf("[");
if (pos != -1)
{
if (content.IndexOf("]", pos) != -1)
{
var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1);
if (token.Contains(":"))
{
tokens.Add(token.Substring(1, token.Length - 2));
}
}
pos = content.IndexOf("[", pos + 1);
}
if (tokens.Count != 0)
{
foreach (string token in tokens)
{
var segments = token.Split(":");
if (segments.Length >= 2 && segments.Length <= 3)
{
var objectName = string.Join(":", segments, 0, segments.Length - 1);
var propertyName = segments[segments.Length - 1];
var propertyValue = "";
switch (objectName)
{
case "ModuleState":
propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString();
break;
case "PageState":
propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString();
break;
case "PageState:Alias":
propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString();
break;
case "PageState:Site":
propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString();
break;
case "PageState:Page":
propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString();
break;
case "PageState:User":
propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString();
break;
case "PageState:Route":
propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString();
break;
default:
if (obj != null && obj.GetType().Name == objectName)
{
propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString();
}
break;
}
if (propertyValue != null)
{
content = content.Replace("[" + token + "]", propertyValue);
}
}
}
}
return content;
}
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{

View File

@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>6.0.1</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -22,10 +22,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -8,20 +8,20 @@
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Oqtane.Client": {
"Oqtane": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:44358/"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -186,4 +186,10 @@
<data name="Message.Username.Invalid" xml:space="preserve">
<value>The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits.</value>
</data>
<data name="Name.Text" xml:space="preserve">
<value>Full Name:</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Provide the full name of the host user</value>
</data>
</root>

View File

@ -175,7 +175,7 @@
<value>Capacity:</value>
</data>
<data name="ImageSizes.HelpText" xml:space="preserve">
<value>Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes.</value>
<value>Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended).</value>
</data>
<data name="ImageSizes.Text" xml:space="preserve">
<value>Image Sizes:</value>
@ -198,4 +198,10 @@
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
<data name="CacheControl.Text" xml:space="preserve">
<value>Caching:</value>
</data>
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI.</value>
</data>
</root>

View File

@ -165,4 +165,31 @@
<data name="UploadFiles.Text" xml:space="preserve">
<value>Upload Files</value>
</data>
<data name="Files.Heading" xml:space="preserve">
<value>Files</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="MaxChunkSize.Text" xml:space="preserve">
<value>Max Upload Chunk Size (MB):</value>
</data>
<data name="MaxChunkSize.HelpText" xml:space="preserve">
<value>Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks).</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
</root>

View File

@ -180,9 +180,6 @@
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
<data name="Message.NoJobs" xml:space="preserve">
<value>Please Note That After An Initial Installation You Must &lt;a href={0}&gt;Restart&lt;/a&gt; The Application In Order To Activate The Default Scheduled Jobs.</value>
</data>
<data name="Refresh.Text" xml:space="preserve">
<value>Refresh</value>
</data>

View File

@ -228,18 +228,6 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
<data name="Browse" xml:space="preserve">
<value>Browse</value>
</data>

View File

@ -169,7 +169,7 @@
<value>Select whether the page is part of the site navigation or hidden</value>
</data>
<data name="UrlPath.HelpText" xml:space="preserve">
<value>Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'.</value>
<value>Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'.</value>
</data>
<data name="Redirect.HelpText" xml:space="preserve">
<value>Optionally enter a url which this page should redirect to when a user navigates to it</value>
@ -297,4 +297,10 @@
<data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value>
</data>
<data name="PersonalizedUrlPath.Text" xml:space="preserve">
<value>Url Path:</value>
</data>
<data name="PersonalizedUrlPath.HelpText" xml:space="preserve">
<value>Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash.</value>
</data>
</root>

View File

@ -195,4 +195,25 @@
<data name="Modules.Heading" xml:space="preserve">
<value>Modules</value>
</data>
<data name="Message.Page.Restore" xml:space="preserve">
<value>You Cannot Restore A Page If Its Parent Is Deleted</value>
</data>
<data name="Success.Page.Restore" xml:space="preserve">
<value>Page Restored Successfully</value>
</data>
<data name="Success.Page.Delete" xml:space="preserve">
<value>Page Deleted Successfully</value>
</data>
<data name="Success.Pages.Deleted" xml:space="preserve">
<value>All Pages Deleted Successfully</value>
</data>
<data name="Success.Module.Restore" xml:space="preserve">
<value>Module Restored Successfully</value>
</data>
<data name="Success.Module.Delete" xml:space="preserve">
<value>Module Deleted Successfully</value>
</data>
<data name="Success.Modules.Delete" xml:space="preserve">
<value>All Modules Deleted Successfully</value>
</data>
</root>

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NoCriteria" xml:space="preserve">
<value>You Must Provide Some Search Criteria</value>
<value>Please Enter Some Search Criteria</value>
</data>
<data name="NoResult" xml:space="preserve">
<value>No Content Matches The Criteria Provided</value>

View File

@ -349,7 +349,7 @@
<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</value>
<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>
<data name="SiteMap.Text" xml:space="preserve">
<value>Site Map:</value>
@ -402,18 +402,6 @@
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="HybridEnabled.HelpText" xml:space="preserve">
<value>Specifies if the site can be integrated with an external .NET MAUI hybrid application</value>
</data>
@ -438,4 +426,25 @@
<data name="System" xml:space="preserve">
<value>System</value>
</data>
<data name="CookieConsent.HelpText" xml:space="preserve">
<value>Specify if cookie consent is enabled on this site. Please note this option must be used in conjunction with a Theme which supports cookie consent.</value>
</data>
<data name="CookieConsent.Text" xml:space="preserve">
<value>Cookie Consent:</value>
</data>
<data name="OptIn" xml:space="preserve">
<value>Opt-In (GDPR)</value>
</data>
<data name="OptOut" xml:space="preserve">
<value>Opt-Out (CCPA)</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="SiteMap.EvictCache" xml:space="preserve">
<value>Clear Cache</value>
</data>
<data name="Success.SiteMap.CacheEvicted" xml:space="preserve">
<value>Site Map Cache Cleared</value>
</data>
</root>

View File

@ -117,8 +117,8 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Access.ApiFramework" xml:space="preserve">
<value>Access Swagger API</value>
<data name="Swagger" xml:space="preserve">
<value>Swagger UI</value>
</data>
<data name="FrameworkVersion.HelpText" xml:space="preserve">
<value>Framework Version</value>
@ -220,10 +220,10 @@
<value>You Have Been Successfully Registered For Updates</value>
</data>
<data name="PackageManager.HelpText" xml:space="preserve">
<value>Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation.</value>
<value>Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation.</value>
</data>
<data name="PackageManager.Text" xml:space="preserve">
<value>Package Manager:</value>
<value>Package Manager Url:</value>
</data>
<data name="Swagger.HelpText" xml:space="preserve">
<value>Specify If Swagger Is Enabled For Your Server API</value>
@ -294,4 +294,19 @@
<data name="Process.Text" xml:space="preserve">
<value>Process: </value>
</data>
<data name="PackageManagerEmail.Text" xml:space="preserve">
<value>Package Manager Email:</value>
</data>
<data name="PackageManagerEmail.HelpText" xml:space="preserve">
<value>Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations.</value>
</data>
<data name="CacheControl.Text" xml:space="preserve">
<value>Static Asset Caching:</value>
</data>
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled.</value>
</data>
<data name="Endpoints" xml:space="preserve">
<value>API Endpoints</value>
</data>
</root>

View File

@ -180,16 +180,4 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
</root>

View File

@ -156,4 +156,7 @@
<data name="Enabled" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Assign" xml:space="preserve">
<value>Assign</value>
</data>
</root>

View File

@ -162,4 +162,10 @@
<data name="Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="Retention.HelpText" xml:space="preserve">
<value>Number of days of broken urls to retain</value>
</data>
</root>

View File

@ -195,4 +195,19 @@
<data name="LastLogin.Text" xml:space="preserve">
<value>Last Login:</value>
</data>
<data name="DeleteUser.Header" xml:space="preserve">
<value>Delete User</value>
</data>
<data name="DeleteUser.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="DeleteUser.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete This User?</value>
</data>
<data name="Impersonate" xml:space="preserve">
<value>Impersonate</value>
</data>
<data name="Error.User.Impersonate" xml:space="preserve">
<value>Unable To Impersonate User</value>
</data>
</root>

View File

@ -495,4 +495,22 @@
<data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value>
</data>
<data name="SaveTokens.Text" xml:space="preserve">
<value>Save Tokens?</value>
</data>
<data name="SaveTokens.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="Success.DeleteUser" xml:space="preserve">
<value>User Deleted Successfully</value>
</data>
<data name="Error.DeleteUser" xml:space="preserve">
<value>Error Deleting User</value>
</data>
<data name="LogoutEverywhere.Text" xml:space="preserve">
<value>Logout Everywhere?</value>
</data>
<data name="LogoutEverywhere.HelpText" xml:space="preserve">
<value>Do you want users to be logged out of every active session on any device, or only their current session?</value>
</data>
</root>

View File

@ -127,7 +127,7 @@
<value>Error Loading Files</value>
</data>
<data name="Error.File.Upload" xml:space="preserve">
<value>File Upload Failed Or Is Still In Progress</value>
<value>File Upload Failed</value>
</data>
<data name="Message.File.NotSelected" xml:space="preserve">
<value>You Have Not Selected A File To Upload</value>

View File

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

View File

@ -427,7 +427,7 @@
<value>At Least One Uppercase Letter</value>
</data>
<data name="Password.ValidationCriteria" xml:space="preserve">
<value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site.</value>
<value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Complexity Requirements For This Site.</value>
</data>
<data name="ProfileInvalid" xml:space="preserve">
<value>{0} Is Not Valid</value>
@ -474,4 +474,7 @@
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="Path" xml:space="preserve">
<value>Path</value>
</data>
</root>

View File

@ -0,0 +1,129 @@
<?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="Confirm" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="ConsentNotice" xml:space="preserve">
<value>I agree to use cookies to provide the best possible user experience for this site. I understand that I can change these preferences at any time.</value>
</data>
<data name="Privacy" xml:space="preserve">
<value>Privacy</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema

View File

@ -0,0 +1,47 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Net.Http;
using System;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Globalization;
namespace Oqtane.Services
{
/// <inheritdoc cref="ICookieConsentService" />
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class CookieConsentService : ServiceBase, ICookieConsentService
{
public CookieConsentService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("CookieConsent");
public async Task<bool> IsActionedAsync()
{
return await GetJsonAsync<bool>($"{ApiUrl}/IsActioned");
}
public async Task<bool> CanTrackAsync(bool optOut)
{
return await GetJsonAsync<bool>($"{ApiUrl}/CanTrack?optout=" + optOut);
}
public async Task<string> CreateActionedCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/CreateActionedCookie");
return cookie ?? string.Empty;
}
public async Task<string> CreateConsentCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/CreateConsentCookie");
return cookie ?? string.Empty;
}
public async Task<string> WithdrawConsentCookieAsync()
{
var cookie = await GetStringAsync($"{ApiUrl}/WithdrawConsentCookie");
return cookie ?? string.Empty;
}
}
}

View File

@ -56,10 +56,5 @@ namespace Oqtane.Services
{
await PostAsync($"{ApiUrl}/restart");
}
public async Task RegisterAsync(string email)
{
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
}
}
}

View File

@ -0,0 +1,42 @@
using Oqtane.Models;
using System;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to retrieve cookie consent information.
/// </summary>
public interface ICookieConsentService
{
/// <summary>
/// Get cookie consent bar actioned status
/// </summary>
/// <returns></returns>
Task<bool> IsActionedAsync();
/// <summary>
/// Get cookie consent status
/// </summary>
/// <returns></returns>
Task<bool> CanTrackAsync(bool optOut);
/// <summary>
/// create actioned cookie
/// </summary>
/// <returns></returns>
Task<string> CreateActionedCookieAsync();
/// <summary>
/// create consent cookie
/// </summary>
/// <returns></returns>
Task<string> CreateConsentCookieAsync();
/// <summary>
/// widhdraw consent cookie
/// </summary>
/// <returns></returns>
Task<string> WithdrawConsentCookieAsync();
}
}

View File

@ -34,13 +34,5 @@ namespace Oqtane.Services
/// </summary>
/// <returns>internal status/message object</returns>
Task RestartAsync();
/// <summary>
/// Registers a new <see cref="User"/>
/// </summary>
/// <param name="email">Email of the user to be registered</param>
/// <returns></returns>
Task RegisterAsync(string email);
}
}

View File

@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage cache
/// </summary>
public interface IOutputCacheService
{
/// <summary>
/// Evicts the output cache for a specific tag
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
Task EvictByTag(string tag);
}
}

View File

@ -0,0 +1,23 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
/// <inheritdoc cref="IOutputCacheService" />
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class OutputCacheService : ServiceBase, IOutputCacheService
{
public OutputCacheService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("OutputCache");
public async Task EvictByTag(string tag)
{
await DeleteAsync($"{ApiUrl}/{tag}");
}
}
}

View File

@ -1,11 +1,6 @@
@namespace Oqtane.Themes.BlazorTheme
@inherits ThemeBase
<div class="breadcrumbs">
<Breadcrumbs />
</div>
<div class="row flex-xl-nowrap gx-0">
<div class="sidebar">
<nav class="navbar">
@ -22,13 +17,18 @@
<Login />
<ControlPanel LanguageDropdownAlignment="right" />
</div>
<div class="breadcrumbs">
<Breadcrumbs />
</div>
</div>
<div class="container">
<div class="row px-4">
<Pane Name="@PaneNames.Admin" />
<CookieConsent />
</div>
</div>
</div>
</div>
@code {

View File

@ -131,6 +131,7 @@
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
PageState.EditMode = true;
}
if (_showEditMode)
@ -153,7 +154,7 @@
{
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString()));
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString().ToLower()));
}
}
}

View File

@ -573,7 +573,7 @@
else
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url, everywhere = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false")) };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}

View File

@ -0,0 +1,168 @@
@namespace Oqtane.Themes.Controls
@inherits ThemeControlBase
@inject ISettingService SettingService
@inject ICookieConsentService CookieConsentService
@inject IStringLocalizer<CookieConsent> Localizer
@if (_enabled && !Hidden)
{
<div class="gdpr-consent-bar bg-light text-dark @(_showBanner ? "p-3" : "p-0") pe-5 fixed-bottom">
<form method="post" @formname="CookieConsentForm" @onsubmit="async () => await AcceptPolicy()" data-enhance>
@if (_showBanner)
{
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<div class="container-fluid">
<div class="row">
<div class="col-9 col-xl-10">
@if (PageState.RenderMode == RenderModes.Static)
{
<input type="checkbox" name="cantrack" checked="@_canTrack" value="1" class="form-check-input me-2" />
}
else
{
<input type="checkbox" name="cantrack" @bind="@_canTrack" value="1" class="form-check-input me-2" />
}
@((MarkupString)Convert.ToString(Localizer["ConsentNotice"]))
</div>
<div class="col-3 col-xl-2">
<div class="row">
<div class="col-md-6 col-xs-6 text-center">
<button class="btn btn-primary mb-1 px-0 w-100" type="submit">@((MarkupString)Convert.ToString(Localizer["Confirm"]))</button>
</div>
@if (ShowPrivacyLink)
{
<div class="col-md-6 col-xs-6 text-center">
<a class="btn btn-secondary mb-1 px-0 w-100" href="/privacy" target="_blank">@((MarkupString)Convert.ToString(Localizer["Privacy"]))</a>
</div>
}
</div>
</div>
</div>
</div>
}
</form>
<form method="post" @formname="CookieConsentToggleForm" @onsubmit="async () => await ToggleBanner()" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
@if (_showBanner)
{
<input type="hidden" name="showbanner" value="false" />
<button type="submit" class="btn btn-light text-dark btn-sm position-absolute btn-hide">
<i class="oi oi-chevron-bottom"></i>
</button>
}
else
{
<input type="hidden" name="showbanner" value="true" />
<button type="submit" class="btn btn-light text-dark btn-sm position-absolute btn-show">
<i class="oi oi-chevron-top"></i>
</button>
}
</form>
</div>
}
@code {
private bool _showBanner;
private bool _enabled;
private bool _optout;
private bool _actioned;
private bool _canTrack;
private bool _consentPostback;
private bool _togglePostback;
[Parameter]
public bool Hidden { get; set; }
[Parameter]
public bool ShowPrivacyLink { get; set; } = true;
[SupplyParameterFromForm(FormName = "CookieConsentToggleForm")]
public string ShowBanner
{
get => "";
set
{
_showBanner = bool.Parse(value);
_togglePostback = true;
}
}
[SupplyParameterFromForm(FormName = "CookieConsentForm")]
public string CanTrack
{
get => "";
set
{
_canTrack = !string.IsNullOrEmpty(value);
_consentPostback = true;
}
}
protected override async Task OnInitializedAsync()
{
var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty);
_enabled = !string.IsNullOrEmpty(cookieConsentSetting);
_optout = cookieConsentSetting == "optout";
_actioned = await CookieConsentService.IsActionedAsync();
if (!_consentPostback)
{
_canTrack = await CookieConsentService.CanTrackAsync(_optout);
}
if (!_togglePostback)
{
_showBanner = !_actioned;
}
}
private async Task AcceptPolicy()
{
var cookieString = string.Empty;
if (_optout)
{
cookieString = _canTrack ? await CookieConsentService.WithdrawConsentCookieAsync() : await CookieConsentService.CreateConsentCookieAsync();
}
else
{
cookieString = _canTrack ? await CookieConsentService.CreateConsentCookieAsync() : await CookieConsentService.WithdrawConsentCookieAsync();
}
//update the page state
PageState.AllowCookies = _canTrack;
if (!string.IsNullOrEmpty(cookieString))
{
var interop = new Interop(JSRuntime);
await interop.SetCookieString(cookieString);
_actioned = true;
_showBanner = false;
StateHasChanged();
}
}
private async Task ToggleBanner()
{
if (!_actioned)
{
var cookieString = await CookieConsentService.CreateActionedCookieAsync();
if (!string.IsNullOrEmpty(cookieString))
{
var interop = new Interop(JSRuntime);
await interop.SetCookieString(cookieString);
_actioned = true;
}
}
if (PageState.RenderMode == RenderModes.Interactive)
{
_showBanner = !_showBanner;
StateHasChanged();
}
}
}

View File

@ -8,14 +8,15 @@
{
@if (PageState.Runtime == Runtime.Hybrid)
{
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
<button type="button" class="@CssClass" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
<input type="hidden" name="everywhere" value="@everywhere" />
<button type="submit" class="@CssClass">@Localizer["Logout"]</button>
</form>
}
}
@ -23,7 +24,7 @@
{
@if (ShowLogin)
{
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a>
<a href="@loginurl" class="@CssClass">@SharedLocalizer["Login"]</a>
}
}
</span>
@ -32,4 +33,6 @@
{
[Parameter]
public bool ShowLogin { get; set; } = true;
[Parameter]
public string CssClass { get; set; } = "btn btn-primary";
}

View File

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Providers;
using Oqtane.Security;
using Oqtane.Services;
@ -26,6 +25,7 @@ namespace Oqtane.Themes.Controls
protected string loginurl;
protected string logouturl;
protected string returnurl;
protected string everywhere;
protected override void OnParametersSet()
{
@ -57,6 +57,7 @@ namespace Oqtane.Themes.Controls
// set logout url
logouturl = Utilities.TenantUrl(PageState.Alias, "/pages/logout/");
everywhere = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false");
// verify anonymous users can access current page
if (UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList) && Utilities.IsEffectiveAndNotExpired(PageState.Page.EffectiveDate, PageState.Page.ExpiryDate))
@ -98,7 +99,7 @@ namespace Oqtane.Themes.Controls
else // this condition is only valid for legacy Login button inheriting from LoginBase
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = returnurl };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = returnurl, everywhere = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:LogoutEverywhere", "false")) };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(logouturl, fields);
}

View File

@ -8,13 +8,13 @@
<span class="app-profile">
@if (PageState.User != null)
{
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@PageState.User.Username</a>
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="@CssClass">@PageState.User.Username</a>
}
else
{
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a>
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="@CssClass">@Localizer["Register"]</a>
}
}
</span>
@ -23,6 +23,8 @@
[Parameter]
public bool ShowRegister { get; set; }
[Parameter]
public string CssClass { get; set; } = "btn btn-primary";
private string _returnurl = "";

View File

@ -22,7 +22,7 @@
</div>
</div>
</div>
<Pane Name="Top Full Width" />
<Pane Name="Top Full Width" />
<div class="container">
<div class="row">
<div class="col-md-12">
@ -107,13 +107,14 @@
{
<Pane Name="Footer" />
}
</div>
<CookieConsent />
</div>
</main>
@code {
public override string Name => "Default Theme";
public override string Panes => PaneNames.Default + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width,Footer";
public override string Panes => PaneNames.Default + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width,Footer";
private bool _login = true;
private bool _register = true;

View File

@ -13,9 +13,9 @@
<select id="scope" class="form-select" value="@_scope" @onchange="(e => ScopeChanged(e))">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<option value="site">@Localizer["Site"]</option>
<option value="site">@Localizer["Site"]</option>
}
<option value="page">@Localizer["Page"]</option>
<option value="page">@Localizer["Page"]</option>
</select>
</div>
</div>

View File

@ -15,6 +15,8 @@ namespace Oqtane.Themes
{
public abstract class ThemeBase : ComponentBase, IThemeControl
{
private bool _scriptsloaded = false;
[Inject]
protected ILogService LoggingService { get; set; }
@ -62,7 +64,7 @@ namespace Oqtane.Themes
var inline = 0;
foreach (Resource resource in resources)
{
if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload)
if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive)
{
if (!string.IsNullOrEmpty(resource.Url))
{
@ -82,6 +84,25 @@ namespace Oqtane.Themes
}
}
}
_scriptsloaded = true;
}
public bool ScriptsLoaded
{
get
{
return _scriptsloaded;
}
}
// property for obtaining theme information about this theme component
public Theme ThemeState
{
get
{
var type = GetType().Namespace + ", " + GetType().Assembly.GetName().Name;
return PageState?.Site.Themes.FirstOrDefault(item => item.ThemeName == type);
}
}
// path method
@ -91,6 +112,15 @@ namespace Oqtane.Themes
return PageState?.Alias.BaseUrl + "/Themes/" + GetType().Namespace + "/";
}
// fingerprint hash code for static assets
public string Fingerprint
{
get
{
return ThemeState.Fingerprint;
}
}
// url methods
// navigate url

View File

@ -70,7 +70,7 @@
if (!script.Contains("><") && !script.Contains("data-reload"))
{
// add data-reload attribute to inline script
headcontent = headcontent.Replace(script, script.Replace("<script", "<script data-reload=\"true\""));
headcontent = headcontent.Replace(script, script.Replace("<script", "<script data-reload=\"always\""));
}
index = headcontent.IndexOf("<script", index + 1);
}

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Oqtane.UI
{
@ -36,6 +37,19 @@ namespace Oqtane.UI
}
}
public Task SetCookieString(string cookieString)
{
try
{
_jsRuntime.InvokeVoidAsync("Oqtane.Interop.setCookieString", cookieString);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
public ValueTask<string> GetCookie(string name)
{
try
@ -209,17 +223,22 @@ namespace Oqtane.UI
}
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
{
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1);
return Task.CompletedTask;
}
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default)
{
try
{
_jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.uploadFiles",
posturl, folder, id, antiforgerytoken, jwt);
return Task.CompletedTask;
return _jsRuntime.InvokeAsync<bool>(
"Oqtane.Interop.uploadFiles", cancellationToken,
posturl, folder, id, antiforgerytoken, jwt, chunksize);
}
catch
{
return Task.CompletedTask;
return new ValueTask<bool>(Task.FromResult(false));
}
}

View File

@ -27,6 +27,7 @@ namespace Oqtane.UI
public bool IsInternalNavigation { get; set; }
public Guid RenderId { get; set; }
public bool Refresh { get; set; }
public bool AllowCookies { get; set; }
public List<Page> Pages
{

View File

@ -14,13 +14,14 @@
@inject IUrlMappingService UrlMappingService
@inject ILogService LogService
@inject ISettingService SettingService
@inject ICookieConsentService CookieConsentService
@inject IJSRuntime JSRuntime
@implements IHandleAfterRender
@implements IDisposable
@if (!string.IsNullOrEmpty(_error))
{
<ModuleMessage Message="@_error" Type="@MessageType.Warning" />
<ModuleMessage Message="@_error" Type="@MessageType.Warning" />
}
@DynamicComponent
@ -244,7 +245,9 @@
// look for personalized page
if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))
{
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId);
var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.PageId}";
var path = (user.Settings.ContainsKey(settingName)) ? user.Settings[settingName] : Utilities.GetFriendlyUrl(user.Username);
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + path, site.SiteId);
if (personalized != null)
{
// redirect to the personalized page
@ -291,6 +294,14 @@
// load additional metadata for modules
(page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
//cookie consent
var _allowCookies = PageState?.AllowCookies;
if(!_allowCookies.HasValue)
{
var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
_allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout");
}
// populate page state (which acts as a client-side cache for subsequent requests)
_pagestate = new PageState
{
@ -314,7 +325,8 @@
ReturnUrl = returnurl,
IsInternalNavigation = _isInternalNavigation,
RenderId = Guid.NewGuid(),
Refresh = false
Refresh = false,
AllowCookies = _allowCookies.GetValueOrDefault(true)
};
OnStateChange?.Invoke(_pagestate);
@ -389,7 +401,7 @@
if (themetype != null)
{
// get resources for theme (ITheme)
page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName));
page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint);
var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
if (themeobject != null)
@ -399,7 +411,7 @@
panes = themeobject.Panes;
}
// get resources for theme control
page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace);
page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace, theme.Fingerprint);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
@ -409,7 +421,7 @@
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace);
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint);
}
}
@ -453,7 +465,7 @@
if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime)))
{
page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName));
page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint);
// handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -502,7 +514,7 @@
module.RenderMode = moduleobject.RenderMode;
module.Prerender = moduleobject.Prerender;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -523,7 +535,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint);
}
// container settings component
@ -534,7 +546,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint);
}
}
}
@ -593,7 +605,7 @@
return (page, modules);
}
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name)
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint)
{
if (resources != null)
{
@ -613,7 +625,7 @@
// ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{
pageresources.Add(resource.Clone(level, name));
pageresources.Add(resource.Clone(level, name, fingerprint));
}
}
}

View File

@ -11,8 +11,6 @@
RenderFragment DynamicComponent { get; set; }
private string lastPagePath = "";
protected override void OnParametersSet()
{
// handle page redirection
@ -92,8 +90,9 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender && PageState.Page.Path != lastPagePath)
if (!firstRender)
{
// site content
if (!string.IsNullOrEmpty(PageState.Site.HeadContent) && PageState.Site.HeadContent.Contains("<script"))
{
await InjectScripts(PageState.Site.HeadContent, ResourceLocation.Head);
@ -102,6 +101,7 @@
{
await InjectScripts(PageState.Site.BodyContent, ResourceLocation.Body);
}
// page content
if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script"))
{
await InjectScripts(PageState.Page.HeadContent, ResourceLocation.Head);
@ -110,7 +110,6 @@
{
await InjectScripts(PageState.Page.BodyContent, ResourceLocation.Body);
}
lastPagePath = PageState.Page.Path;
}
// style sheets
@ -191,16 +190,13 @@
}
else
{
if (dataAttributes == null || !dataAttributes.ContainsKey("data-reload") || dataAttributes["data-reload"] != "false")
if (id == "")
{
if (id == "")
{
count += 1;
id = $"page{PageState.Page.PageId}-script{count}";
}
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
count += 1;
id = $"page{PageState.Page.PageId}-script{count}";
}
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
}
index = content.IndexOf("<script", index + 1);
}

View File

@ -1,9 +1,9 @@
using System.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using MySql.Data.MySqlClient;
using MySql.EntityFrameworkCore.Metadata;
using Oqtane.Databases;
namespace Oqtane.Database.MySQL
@ -21,11 +21,11 @@ namespace Oqtane.Database.MySQL
public MySQLDatabase() :base(_name, _friendlyName) { }
public override string Provider => "MySql.EntityFrameworkCore";
public override string Provider => "Pomelo.EntityFrameworkCore.MySql";
public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name)
{
return table.Column<int>(name: name, nullable: false).Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn);
return table.Column<int>(name: name, nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
}
public override string ConcatenateSql(params string[] values)
@ -86,7 +86,7 @@ namespace Oqtane.Database.MySQL
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{
return optionsBuilder.UseMySQL(connectionString);
return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
}
private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query)

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,8 +33,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.0-preview" />
<PackageReference Include="MySql.Data" Version="9.1.0" />
<PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup>
<ItemGroup>
@ -42,7 +42,7 @@
</ItemGroup>
<ItemGroup>
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)Pomelo.EntityFrameworkCore.MySql.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
</ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(MySQLFiles)" Outputs="@(MySQLFiles->'%(DestinationPath)')">

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,8 +34,8 @@
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>6.0.1</Version>
<Version>6.1.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -30,7 +30,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>6.0.1</ApplicationDisplayVersion>
<ApplicationDisplayVersion>6.1.1</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@ -67,14 +67,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.0" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.3" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.40" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.40" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.40" />
</ItemGroup>
<ItemGroup>

View File

@ -14,6 +14,9 @@ Oqtane.Interop = {
}
document.cookie = cookieString;
},
setCookieString: function (cookieString) {
document.cookie = cookieString;
},
getCookie: function (name) {
name = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);
@ -120,13 +123,22 @@ Oqtane.Interop = {
this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore);
}
},
includeScript: function (id, src, integrity, crossorigin, type, content, location) {
includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) {
var script;
if (src !== "") {
script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]");
}
else {
script = document.getElementById(id);
if (id !== "") {
script = document.getElementById(id);
} else {
const scripts = document.querySelectorAll("script:not([src])");
for (let i = 0; i < scripts.length; i++) {
if (scripts[i].textContent.includes(content)) {
script = scripts[i];
}
}
}
}
if (script !== null) {
script.remove();
@ -152,37 +164,36 @@ Oqtane.Interop = {
else {
script.innerHTML = content;
}
script.async = false;
this.addScript(script, location)
.then(() => {
if (src !== "") {
console.log(src + ' loaded');
}
else {
console.log(id + ' loaded');
}
})
.catch(() => {
if (src !== "") {
console.error(src + ' failed');
}
else {
console.error(id + ' failed');
}
});
if (dataAttributes !== null) {
for (var key in dataAttributes) {
script.setAttribute(key, dataAttributes[key]);
}
}
try {
this.addScript(script, location);
} catch (error) {
if (src !== "") {
console.error("Failed to load external script: ${src}", error);
} else {
console.error("Failed to load inline script: ${content}", error);
}
}
}
},
addScript: function (script, location) {
if (location === 'head') {
document.head.appendChild(script);
}
if (location === 'body') {
document.body.appendChild(script);
}
return new Promise((resolve, reject) => {
script.async = false;
script.defer = false;
return new Promise((res, rej) => {
script.onload = res();
script.onerror = rej();
script.onload = () => resolve();
script.onerror = (error) => reject(error);
if (location === 'head') {
document.head.appendChild(script);
} else {
document.body.appendChild(script);
}
});
},
includeScripts: async function (scripts) {
@ -222,10 +233,10 @@ Oqtane.Interop = {
if (scripts[s].crossorigin !== '') {
element.crossOrigin = scripts[s].crossorigin;
}
if (scripts[s].es6module === true) {
element.type = "module";
if (scripts[s].type !== '') {
element.type = scripts[s].type;
}
if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) {
if (scripts[s].dataAttributes !== null) {
for (var key in scripts[s].dataAttributes) {
element.setAttribute(key, scripts[s].dataAttributes[key]);
}
@ -300,97 +311,107 @@ Oqtane.Interop = {
}
return files;
},
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) {
var success = true;
var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id);
var totalSize = 0;
for (var i = 0; i < fileinput.files.length; i++) {
totalSize += fileinput.files[i].size;
}
let uploadSize = 0;
if (!chunksize || chunksize < 1) {
chunksize = 1; // 1 MB default
}
if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;");
progressinfo.innerHTML = '';
progressbar.setAttribute("style", "width: 100%; display: inline;");
progressinfo.setAttribute('style', 'display: inline;');
if (fileinput.files.length > 1) {
progressinfo.innerHTML = fileinput.files[0].name + ', ...';
}
else {
progressinfo.innerHTML = fileinput.files[0].name;
}
progressbar.setAttribute('style', 'width: 100%; display: inline;');
progressbar.value = 0;
}
var files = fileinput.files;
var totalSize = 0;
for (var i = 0; i < files.length; i++) {
totalSize = totalSize + files[i].size;
const uploadFile = (file) => {
const chunkSize = chunksize * (1024 * 1024);
const totalParts = Math.ceil(file.size / chunkSize);
let partCount = 0;
const uploadPart = () => {
const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
return new Promise((resolve, reject) => {
let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name);
var credentials = 'same-origin';
var headers = new Headers();
headers.append('PartCount', partCount + 1);
headers.append('TotalParts', totalParts);
if (jwt !== "") {
headers.append('Authorization', 'Bearer ' + jwt);
credentials = 'include';
}
return fetch(posturl, {
method: 'POST',
headers: headers,
credentials: credentials,
body: formdata
})
.then(response => {
if (!response.ok) {
if (progressinfo !== null) {
progressinfo.innerHTML = 'Error: ' + response.statusText;
}
throw new Error('Failed');
}
return;
})
.then(data => {
partCount++;
if (progressbar !== null) {
uploadSize += chunk.size;
var percent = Math.ceil((uploadSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
if (partCount < totalParts) {
uploadPart().then(resolve).catch(reject);
}
else {
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
};
return uploadPart();
};
try {
for (const file of fileinput.files) {
await uploadFile(file);
}
} catch (error) {
success = false;
}
var maxChunkSizeMB = 1;
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
var uploadedSize = 0;
for (var i = 0; i < files.length; i++) {
var fileChunk = [];
var file = files[i];
var fileStreamPos = 0;
var endPos = bufferChunkSize;
while (fileStreamPos < file.size) {
fileChunk.push(file.slice(fileStreamPos, endPos));
fileStreamPos = endPos;
endPos = fileStreamPos + bufferChunkSize;
}
var totalParts = fileChunk.length;
var partCount = 0;
while (chunk = fileChunk.shift()) {
partCount++;
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder);
data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
if (jwt !== "") {
request.setRequestHeader('Authorization', 'Bearer ' + jwt);
request.withCredentials = true;
}
request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
if (files.length === 1) {
progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
}
};
request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) {
uploadedSize = uploadedSize + e.total;
var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) {
if (files.length === 1) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
}
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
}
};
request.send(data);
}
if (i === files.length - 1) {
fileinput.value = '';
}
}
fileinput.value = '';
return success;
},
refreshBrowser: function (verify, wait) {
async function attemptReload (verify) {

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>6.0.1</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

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

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Server</id>
<version>6.0.1</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>6.0.1</version>
<version>6.1.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

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

View File

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

View File

@ -36,6 +36,7 @@ if "%%~nxi" == "%%j" set /A found=1
)
if not !found! == 1 rmdir /Q/S "%%i"
)
del "..\Oqtane.Server\bin\Release\net9.0\publish\Oqtane.Server.staticwebassets.endpoints.json"
del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json"
ren "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.release.json" "appsettings.json"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1"

View File

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

View File

@ -7,6 +7,7 @@
@using Microsoft.AspNetCore.Localization
@using Microsoft.Net.Http.Headers
@using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Authentication
@using Oqtane.Client
@using Oqtane.UI
@using Oqtane.Repository
@ -30,6 +31,8 @@
@inject IUrlMappingRepository UrlMappingRepository
@inject IVisitorRepository VisitorRepository
@inject IJwtManager JwtManager
@inject ICookieConsentService CookieConsentService
@inject ISettingService SettingService
@if (_initialized)
{
@ -39,7 +42,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/app.css?v=@_fingerprint" />
@if (_scripts.Contains("PWA Manifest"))
{
<link id="app-manifest" rel="manifest" />
@ -70,15 +73,15 @@
}
<script src="_framework/blazor.web.js"></script>
<script src="js/app.js"></script>
<script src="js/loadjs.min.js"></script>
<script src="js/interop.js"></script>
<script src="js/app.js?v=@_fingerprint"></script>
<script src="js/loadjs.min.js?v=@_fingerprint"></script>
<script src="js/interop.js?v=@_fingerprint"></script>
@((MarkupString)_scripts)
@((MarkupString)_bodyResources)
@if (_renderMode == RenderModes.Static)
{
<page-script src="./js/reload.js"></page-script>
<page-script src="./js/reload.js?v=@_fingerprint"></page-script>
}
}
else
@ -94,6 +97,7 @@
private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server;
private bool _prerender = true;
private string _fingerprint = "";
private int _visitorId = -1;
private string _antiForgeryToken = "";
private string _remoteIPAddress = "";
@ -105,6 +109,7 @@
private string _styleSheets = "";
private string _scripts = "";
private string _message = "";
private bool _allowCookies;
private PageState _pageState;
// CascadingParameter is required to access HttpContext
@ -136,6 +141,11 @@
_renderMode = site.RenderMode;
_runtime = site.Runtime;
_prerender = site.Prerender;
_fingerprint = site.Fingerprint;
var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
_allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout");
var modules = new List<Module>();
Route route = new Route(url, alias.Path);
@ -166,7 +176,7 @@
modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
}
if (site.VisitorTracking)
if (site.VisitorTracking && _allowCookies)
{
TrackVisitor(site.SiteId);
}
@ -174,7 +184,7 @@
// get jwt token for downstream APIs
if (Context.User.Identity.IsAuthenticated)
{
CreateJwtToken(alias);
await GetJwtToken(alias);
}
// includes resources
@ -241,7 +251,8 @@
ReturnUrl = "",
IsInternalNavigation = false,
RenderId = Guid.NewGuid(),
Refresh = true
Refresh = true,
AllowCookies = _allowCookies
};
}
else
@ -441,13 +452,19 @@
}
}
private void CreateJwtToken(Alias alias)
private async Task GetJwtToken(Alias alias)
{
var sitesettings = Context.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
// bearer token may have been provided by remote Identity Provider and persisted using SaveTokens = true
_authorizationToken = await Context.GetTokenAsync("access_token");
if (string.IsNullOrEmpty(_authorizationToken))
{
_authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
// generate bearer token if a secret has been configured in User Settings
var sitesettings = Context.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
_authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
}
}
}
@ -514,7 +531,7 @@
private void AddScript(Resource resource, Alias alias)
{
var script = CreateScript(resource, alias);
if (resource.Location == Shared.ResourceLocation.Head && !resource.Reload)
if (resource.Location == Shared.ResourceLocation.Head && resource.LoadBehavior != ResourceLoadBehavior.BlazorPageScript)
{
if (!_headResources.Contains(script))
{
@ -532,11 +549,27 @@
private string CreateScript(Resource resource, Alias alias)
{
if (!resource.Reload)
if (resource.LoadBehavior == ResourceLoadBehavior.BlazorPageScript)
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
else
{
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
var dataAttributes = "";
if (!resource.DataAttributes.ContainsKey("data-reload"))
{
switch (resource.LoadBehavior)
{
case ResourceLoadBehavior.Once:
dataAttributes += " data-reload=\"once\"";
break;
case ResourceLoadBehavior.Always:
dataAttributes += " data-reload=\"always\"";
break;
}
}
if (resource.DataAttributes != null && resource.DataAttributes.Count > 0)
{
foreach (var attribute in resource.DataAttributes)
@ -552,10 +585,6 @@
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>";
}
else
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
}
private void SetLocalizationCookie(string cookieValue)
@ -583,13 +612,13 @@
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
if (theme != null)
{
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode);
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode);
}
else
{
// fallback to default Oqtane theme
theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme));
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode);
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode);
}
var type = Type.GetType(themeType);
if (type != null)
@ -597,7 +626,7 @@
var obj = Activator.CreateInstance(type) as IThemeControl;
if (obj != null)
{
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode);
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Fingerprint, site.RenderMode);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
@ -607,7 +636,7 @@
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode);
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint, site.RenderMode);
}
}
@ -616,7 +645,7 @@
var typename = "";
if (module.ModuleDefinition != null)
{
resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode);
// handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -662,7 +691,7 @@
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
if (moduleobject != null)
{
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -683,7 +712,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode);
}
// container settings component
@ -693,7 +722,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint, site.RenderMode);
}
}
}
@ -709,7 +738,7 @@
{
if (module.ModuleDefinition?.Resources != null)
{
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode);
}
}
}
@ -717,7 +746,7 @@
return resources;
}
private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string rendermode)
private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint, string rendermode)
{
if (resources != null)
{
@ -737,7 +766,7 @@
// ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{
pageresources.Add(resource.Clone(level, name));
pageresources.Add(resource.Clone(level, name, fingerprint));
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Shared;
using System;
using System.Globalization;
using Oqtane.Infrastructure;
using Oqtane.Services;
using System.Threading.Tasks;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class CookieConsentController : Controller
{
private readonly ICookieConsentService _cookieConsentService;
public CookieConsentController(ICookieConsentService cookieConsentService)
{
_cookieConsentService = cookieConsentService;
}
[HttpGet("IsActioned")]
public async Task<bool> IsActioned()
{
return await _cookieConsentService.IsActionedAsync();
}
[HttpGet("CanTrack")]
public async Task<bool> CanTrack(string optout)
{
return await _cookieConsentService.CanTrackAsync(bool.Parse(optout));
}
[HttpGet("CreateActionedCookie")]
public async Task<string> CreateActionedCookie()
{
return await _cookieConsentService.CreateActionedCookieAsync();
}
[HttpGet("CreateConsentCookie")]
public async Task<string> CreateConsentCookie()
{
return await _cookieConsentService.CreateConsentCookieAsync();
}
[HttpGet("WithdrawConsentCookie")]
public async Task<string> WithdrawConsentCookie()
{
return await _cookieConsentService.WithdrawConsentCookieAsync();
}
}
}

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Oqtane.Models;
using Oqtane.Shared;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class EndpointController : Controller
{
private readonly IEnumerable<EndpointDataSource> _endpointSources;
public EndpointController(IEnumerable<EndpointDataSource> endpointSources)
{
_endpointSources = endpointSources;
}
// GET api/<controller>
[HttpGet]
[Authorize(Roles = RoleNames.Host)]
public ActionResult Get()
{
var endpoints = _endpointSources
.SelectMany(item => item.Endpoints)
.OfType<RouteEndpoint>();
var output = endpoints.Select(
item =>
{
var controller = item.Metadata
.OfType<ControllerActionDescriptor>()
.FirstOrDefault();
var action = controller != null
? $"{controller.ControllerName}.{controller.ActionName}"
: null;
var controllerMethod = controller != null
? $"{controller.ControllerTypeInfo.FullName}:{controller.MethodInfo.Name}"
: null;
return new
{
Method = item.Metadata.OfType<HttpMethodMetadata>().FirstOrDefault()?.HttpMethods?[0],
Route = $"/{item.RoutePattern.RawText.TrimStart('/')}",
Action = action,
ControllerMethod = controllerMethod
};
}
).OrderBy(item => item.Route);
return Json(output);
}
}
}

View File

@ -21,6 +21,8 @@ using System.Net.Http;
using Microsoft.AspNetCore.Cors;
using System.IO.Compression;
using Oqtane.Services;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http.HttpResults;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@ -427,75 +429,98 @@ namespace Oqtane.Controllers
// POST api/<controller>/upload
[EnableCors(Constants.MauiCorsPolicy)]
[HttpPost("upload")]
public async Task<IActionResult> UploadFile(string folder, IFormFile formfile)
public async Task<IActionResult> UploadFile([FromForm] string folder, IFormFile formfile)
{
if (string.IsNullOrEmpty(folder))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Does Not Contain A Folder");
return StatusCode((int)HttpStatusCode.Forbidden);
}
if (formfile == null || formfile.Length <= 0)
{
return NoContent();
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Does Not Contain A File");
return StatusCode((int)HttpStatusCode.Forbidden);
}
// ensure filename is valid
string token = ".part_";
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))))
if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return NoContent();
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return StatusCode((int)HttpStatusCode.Forbidden);
}
// ensure headers exist
if (!Request.Headers.TryGetValue("PartCount", out StringValues partcount) || !int.TryParse(partcount, out int partCount) || partCount <= 0 ||
!Request.Headers.TryGetValue("TotalParts", out StringValues totalparts) || !int.TryParse(totalparts, out int totalParts) || totalParts <= 0)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Is Missing Required Headers");
return StatusCode((int)HttpStatusCode.Forbidden);
}
// create file name using header values
string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000");
string folderPath = "";
int FolderId;
if (int.TryParse(folder, out FolderId))
try
{
Folder Folder = _folders.GetFolder(FolderId);
if (Folder != null && Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, Folder.PermissionList))
int FolderId;
if (int.TryParse(folder, out FolderId))
{
folderPath = _folders.GetFolderPath(Folder);
}
}
else
{
FolderId = -1;
if (User.IsInRole(RoleNames.Host))
{
folderPath = GetFolderPath(folder);
}
}
if (!string.IsNullOrEmpty(folderPath))
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, formfile.FileName), FileMode.Create))
{
await formfile.CopyToAsync(stream);
}
string upload = await MergeFile(folderPath, formfile.FileName);
if (upload != "" && FolderId != -1)
{
var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload));
if (file != null)
Folder Folder = _folders.GetFolder(FolderId);
if (Folder != null && Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, Folder.PermissionList))
{
if (file.FileId == 0)
{
file = _files.AddFile(file);
}
else
{
file = _files.UpdateFile(file);
}
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload));
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
folderPath = _folders.GetFolderPath(Folder);
}
}
else
{
FolderId = -1;
if (User.IsInRole(RoleNames.Host))
{
folderPath = GetFolderPath(folder);
}
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return NoContent();
if (!string.IsNullOrEmpty(folderPath))
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, fileName), FileMode.Create))
{
await formfile.CopyToAsync(stream);
}
string upload = await MergeFile(folderPath, fileName);
if (upload != "" && FolderId != -1)
{
var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload));
if (file != null)
{
if (file.FileId == 0)
{
file = _files.AddFile(file);
}
else
{
file = _files.UpdateFile(file);
}
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload));
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
}
}
return NoContent();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
return StatusCode((int)HttpStatusCode.Forbidden);
}
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "File Upload Attempt Failed {Folder} {File}", folder, formfile.FileName);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
private async Task<string> MergeFile(string folder, string filename)
@ -510,10 +535,10 @@ namespace Oqtane.Controllers
filename = Path.GetFileNameWithoutExtension(filename); // base filename
string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist ( note that file parts can arrive out of order )
// if all of the file parts exist (note that file parts can arrive out of order)
if (fileparts.Length == totalparts && CanAccessFiles(fileparts))
{
// merge file parts into temp file ( in case another user is trying to get the file )
// merge file parts into temp file (in case another user is trying to get the file)
bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{
@ -536,17 +561,23 @@ namespace Oqtane.Controllers
// clean up file parts
foreach (var file in Directory.GetFiles(folder, "*" + token + "*"))
{
// file name matches part or is more than 2 hours old (ie. a prior file upload failed)
if (fileparts.Contains(file) || System.IO.File.GetCreationTime(file).ToUniversalTime() < DateTime.UtcNow.AddHours(-2))
if (fileparts.Contains(file))
{
System.IO.File.Delete(file);
try
{
System.IO.File.Delete(file);
}
catch
{
// unable to delete part - ignore
}
}
}
// rename temp file
if (success)
{
// remove file if it already exists (as well as any thumbnails)
// remove file if it already exists (as well as any thumbnails which may exist)
foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*"))
{
if (Path.GetExtension(file) != ".tmp")

View File

@ -60,9 +60,9 @@ namespace Oqtane.Controllers
{
installation = _databaseManager.Install(config);
if (installation.Success && config.Register)
if (installation.Success)
{
await RegisterContact(config.HostEmail);
await RegisterContact(config.HostEmail, config.HostName, config.Register);
}
}
else
@ -171,7 +171,8 @@ namespace Oqtane.Controllers
}
}
return assemblyList;
});
}).ToList();
}
// GET api/<controller>/load?list=x,y
@ -257,7 +258,7 @@ namespace Oqtane.Controllers
}
}
private async Task RegisterContact(string email)
private async Task RegisterContact(string email, string name, bool register)
{
try
{
@ -268,7 +269,7 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}")).ConfigureAwait(false);
var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}&register={register.ToString().ToLower()}")).ConfigureAwait(false);
}
}
}
@ -278,14 +279,6 @@ namespace Oqtane.Controllers
}
}
// GET api/<controller>/register?email=x
[HttpPost("register")]
[Authorize(Roles = RoleNames.Host)]
public async Task Register(string email)
{
await RegisterContact(email);
}
public struct ClientAssembly
{
public ClientAssembly(string filepath, bool hashfilename)
@ -294,7 +287,7 @@ namespace Oqtane.Controllers
DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath);
if (hashfilename)
{
HashedName = GetDeterministicHashCode(filepath).ToString("X8") + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath);
HashedName = Utilities.GenerateSimpleHash(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath);
}
else
{
@ -305,25 +298,5 @@ namespace Oqtane.Controllers
public string FilePath { get; private set; }
public string HashedName { get; private set; }
}
private static int GetDeterministicHashCode(string value)
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < value.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ value[i];
if (i == value.Length - 1)
break;
hash2 = ((hash2 << 5) + hash2) ^ value[i + 1];
}
return hash1 + (hash2 * 1566083941);
}
}
}
}

View File

@ -155,7 +155,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)]
public Notification Post([FromBody] Notification notification)
{
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId))
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && (IsAuthorized(notification.FromUserId) || (notification.FromUserId == null && User.IsInRole(RoleNames.Admin))))
{
if (!User.IsInRole(RoleNames.Admin))
{
@ -181,17 +181,45 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)]
public Notification Put(int id, [FromBody] Notification notification)
{
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId)))
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null)
{
if (!User.IsInRole(RoleNames.Admin) && notification.FromUserId != null)
bool update = false;
if (IsAuthorized(notification.FromUserId))
{
// content must be HTML encoded for non-admins to prevent HTML injection
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
notification.Body = WebUtility.HtmlEncode(notification.Body);
// notification belongs to current authenticated user - update is allowed
if (!User.IsInRole(RoleNames.Admin))
{
// content must be HTML encoded for non-admins to prevent HTML injection
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
notification.Body = WebUtility.HtmlEncode(notification.Body);
}
update = true;
}
else
{
if (IsAuthorized(notification.ToUserId))
{
// notification was sent to current authenticated user - only isread and isdeleted properties can be updated
var isread = notification.IsRead;
var isdeleted = notification.IsDeleted;
notification = _notifications.GetNotification(notification.NotificationId);
notification.IsRead = isread;
notification.IsDeleted = isdeleted;
update = true;
}
}
if (update)
{
notification = _notifications.UpdateNotification(notification);
_syncManager.AddSyncEvent(_alias, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Put Attempt {Notification}", notification);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
notification = null;
}
notification = _notifications.UpdateNotification(notification);
_syncManager.AddSyncEvent(_alias, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);
}
else
{

View File

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class OutputCacheController : Controller
{
private readonly IOutputCacheService _cacheService;
public OutputCacheController(IOutputCacheService cacheService)
{
_cacheService = cacheService;
}
// DELETE api/<controller>/{tag}
[HttpDelete("{tag}")]
[Authorize(Roles = RoleNames.Admin)]
public async Task EvictByTag(string tag)
{
await _cacheService.EvictByTag(tag);
}
}
}

View File

@ -12,6 +12,8 @@ using Oqtane.Infrastructure;
using Oqtane.Enums;
using System.Net.Http.Headers;
using System.Text.Json;
using Oqtane.Managers;
using System.Net;
// ReSharper disable PartialTypeWithSinglePart
namespace Oqtane.Controllers
@ -20,13 +22,15 @@ namespace Oqtane.Controllers
public class PackageController : Controller
{
private readonly IInstallationManager _installationManager;
private readonly IUserManager _userManager;
private readonly IWebHostEnvironment _environment;
private readonly IConfigManager _configManager;
private readonly ILogManager _logger;
public PackageController(IInstallationManager installationManager, IWebHostEnvironment environment, IConfigManager configManager, ILogManager logger)
public PackageController(IInstallationManager installationManager, IUserManager userManager, IWebHostEnvironment environment, IConfigManager configManager, ILogManager logger)
{
_installationManager = installationManager;
_userManager = userManager;
_environment = environment;
_configManager = configManager;
_logger = logger;
@ -45,7 +49,7 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, url + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}&sort={sort}");
packages = await GetJson<List<Package>>(client, url + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}");
}
}
return packages;
@ -64,7 +68,7 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}");
packages = await GetJson<List<Package>>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}");
}
}
return packages;
@ -83,7 +87,7 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
package = await GetJson<Package>(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}");
package = await GetJson<Package>(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}");
}
if (package != null)
@ -117,6 +121,24 @@ namespace Oqtane.Controllers
return package;
}
private string GetPackageRegistryEmail()
{
var email = _configManager.GetSetting("PackageRegistryEmail", "");
if (string.IsNullOrEmpty(email))
{
if (User.Identity.IsAuthenticated)
{
var user = _userManager.GetUser(User.Identity.Name, -1);
if (user != null)
{
email = user.Email;
_configManager.AddOrUpdateSetting("PackageRegistryEmail", email, true);
}
}
}
return email;
}
private async Task<T> GetJson<T>(HttpClient httpClient, string url)
{
try

View File

@ -9,7 +9,8 @@ using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using System;
using System.Xml.Linq;
using Microsoft.AspNetCore.Diagnostics;
namespace Oqtane.Controllers
{
@ -189,15 +190,16 @@ namespace Oqtane.Controllers
User user = _userPermissions.GetUser(User);
if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid))
{
page = _pages.GetPage(parent.Path + "/" + user.Username, parent.SiteId);
var path = Utilities.GetFriendlyUrl(user.Username);
page = _pages.GetPage(parent.Path + "/" + path, parent.SiteId);
if (page == null)
{
page = new Page();
page.SiteId = parent.SiteId;
page.ParentId = parent.PageId;
page.Name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username;
page.Path = parent.Path + "/" + user.Username;
page.Title = page.Name + " - " + parent.Name;
page.Name = user.Username;
page.Path = parent.Path + "/" + path;
page.Title = ((!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username) + " - " + parent.Name;
page.Order = 0;
page.IsNavigation = false;
page.Url = "";
@ -250,6 +252,11 @@ namespace Oqtane.Controllers
_syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// set user personalized page path
var setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false };
_settings.AddSetting(setting);
_syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update);
}
}
else
@ -274,18 +281,14 @@ namespace Oqtane.Controllers
// get current page permissions
var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList();
page = _pages.UpdatePage(page);
// preserve new path and deleted status
var newPath = page.Path;
var deleted = page.IsDeleted;
page.Path = currentPage.Path;
page.IsDeleted = currentPage.IsDeleted;
// save url mapping if page path changed
if (currentPage.Path != page.Path)
{
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path);
if (urlMapping != null)
{
urlMapping.MappedUrl = page.Path;
_urlMappings.UpdateUrlMapping(urlMapping);
}
}
// update page
UpdatePage(page, page.PageId, page.Path, newPath, deleted);
// get differences between current and new page permissions
var added = GetPermissionsDifferences(page.PermissionList, currentPermissions);
@ -315,6 +318,7 @@ namespace Oqtane.Controllers
});
}
}
// permissions removed
foreach (Permission permission in removed)
{
@ -338,8 +342,29 @@ namespace Oqtane.Controllers
}
}
_syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// personalized page
if (page.UserId != null && currentPage.Path != page.Path)
{
// set user personalized page path
var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.ParentId}";
var path = page.Path.Substring(page.Path.LastIndexOf("/") + 1);
var settings = _settings.GetSettings(EntityNames.User, page.UserId.Value).ToList();
var setting = settings.FirstOrDefault(item => item.SettingName == settingName);
if (setting == null)
{
setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = settingName, SettingValue = path, IsPrivate = false };
_settings.AddSetting(setting);
}
else
{
setting.SettingValue = path;
_settings.UpdateSetting(setting);
}
_syncManager.AddSyncEvent(_alias, EntityNames.User, page.UserId.Value, SyncEventActions.Update);
}
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page);
}
else
@ -351,6 +376,39 @@ namespace Oqtane.Controllers
return page;
}
private void UpdatePage(Page page, int pageId, string oldPath, string newPath, bool deleted)
{
var update = (page.PageId == pageId);
if (oldPath != newPath)
{
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, page.Path);
if (urlMapping != null)
{
urlMapping.MappedUrl = newPath + page.Path.Substring(oldPath.Length);
_urlMappings.UpdateUrlMapping(urlMapping);
}
page.Path = newPath + page.Path.Substring(oldPath.Length);
update = true;
}
if (deleted != page.IsDeleted)
{
page.IsDeleted = deleted;
update = true;
}
if (update)
{
_pages.UpdatePage(page);
_syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update);
}
// update any children
foreach (var _page in _pages.GetPages(page.SiteId).Where(item => item.ParentId == page.PageId))
{
UpdatePage(_page, pageId, oldPath, newPath, deleted);
}
}
private List<Permission> GetPermissionsDifferences(List<Permission> permissions1, List<Permission> permissions2)
{
var differences = new List<Permission>();

View File

@ -64,7 +64,7 @@ namespace Oqtane.Controllers
}
else
{
// suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies
// suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies or private browsing sessions
if (entityName != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Settings {EntityName} {EntityId}", entityName, entityId);

View File

@ -53,7 +53,9 @@ namespace Oqtane.Controllers
systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information"));
systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error"));
systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true"));
systeminfo.Add("CacheControl", _configManager.GetSetting("CacheControl", ""));
systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl));
systeminfo.Add("PackageRegistryEmail", _configManager.GetSetting("PackageRegistryEmail", ""));
break;
case "log":
string log = "";

View File

@ -280,7 +280,7 @@ namespace Oqtane.Controllers
{
{ "FrameworkVersion", theme.Version },
{ "ClientReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{theme.Version}\" />" },
{ "SharedReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{theme.Version}\" />" },
{ "SharedReference", $"<PackageReference Include=\"Oqtane.Shared\" Version=\"{theme.Version}\" />" },
};
});
}

View File

@ -217,7 +217,7 @@ namespace Oqtane.Controllers
// DELETE api/<controller>/5?siteid=x
[HttpDelete("{id}")]
[Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")]
[Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Host}")]
public async Task Delete(int id, string siteid)
{
User user = _users.GetUser(id, false);

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