Compare commits

...

437 Commits

Author SHA1 Message Date
Shaun Walker
6c58ab4554 Merge pull request #5439 from oqtane/master
6.1.4 Release
2025-07-30 15:11:01 -04:00
Shaun Walker
085187cfac 6.1.4 Release
6.1.4 Release
2025-07-30 15:10:42 -04:00
Shaun Walker
3d0f0a5adc Merge pull request #5437 from sbwalker/dev
synchronize app.css with .NET MAUI
2025-07-30 13:40:41 -04:00
sbwalker
eae8b431ee synchronize app.css with .NET MAUI 2025-07-30 13:40:25 -04:00
Shaun Walker
e3a34446c0 Merge pull request #5436 from sbwalker/dev
synchronize interop,js with .NET MAUI
2025-07-30 13:35:56 -04:00
sbwalker
bfe57c3ac7 synchronize interop,js with .NET MAUI 2025-07-30 13:35:39 -04:00
Shaun Walker
d4001be716 Merge pull request #5435 from sbwalker/dev
fix #5364 - add ability to specify preferred Container per Pane
2025-07-30 10:43:51 -04:00
sbwalker
662a1817f2 fix #5364 - add ability to specify preferred Container per Pane 2025-07-30 10:43:36 -04:00
Shaun Walker
2c99ef412d Merge pull request #5434 from sbwalker/dev
use consistent terminology
2025-07-30 10:01:11 -04:00
sbwalker
f53ed5b13b use consistent terminology 2025-07-30 10:00:57 -04:00
Shaun Walker
b5d51838c6 Merge pull request #5433 from sbwalker/dev
allow specific time zones to be excluded
2025-07-30 09:29:56 -04:00
sbwalker
92fd70198a allow specific time zones to be excluded 2025-07-30 09:29:43 -04:00
Shaun Walker
7f1990f851 Merge pull request #5432 from sbwalker/dev
fix incorrect resource reference
2025-07-30 08:48:04 -04:00
sbwalker
797d7afc3e fix incorrect resource reference 2025-07-30 08:47:50 -04:00
Shaun Walker
c5a23cdfa0 Merge pull request #5431 from sbwalker/dev
update Oqtane theme to Bootstrap 5.3.7
2025-07-30 08:30:54 -04:00
sbwalker
906358f1f8 update Oqtane theme to Bootstrap 5.3.7 2025-07-30 08:30:40 -04:00
Shaun Walker
638f2a59c5 Merge pull request #5430 from sbwalker/dev
use margin rather than padding
2025-07-30 08:16:20 -04:00
sbwalker
cf9b4b869c use margin rather than padding 2025-07-30 08:16:07 -04:00
Shaun Walker
671c52fbbb Merge pull request #5429 from leigh-pointer/CDN-Bootstrap
Discussion #5426 updated and returned to https://cdnjs.com/
2025-07-30 08:07:52 -04:00
Leigh Pointer
6c0e2a62e7 Discussion #5426 updated and returned to https://cdnjs.com/
Updated and styles tested - reload.js needs still testing?
2025-07-30 12:53:59 +02:00
Shaun Walker
1b78c9ad81 Merge pull request #5428 from sbwalker/dev
use consistent naming
2025-07-29 16:36:43 -04:00
sbwalker
7a4b98aec9 use consistent naming 2025-07-29 16:36:28 -04:00
Shaun Walker
9ef6c15014 Merge pull request #5427 from sbwalker/dev
fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site
2025-07-29 16:20:37 -04:00
sbwalker
f4cea3fe03 fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site 2025-07-29 16:20:07 -04:00
Shaun Walker
5dd9b1ec91 Merge pull request #5425 from sbwalker/dev
fix #5346 - deleting role should remove associated useroles
2025-07-29 09:05:54 -04:00
sbwalker
658059806b fix #5346 - deleting role should remove associated useroles 2025-07-29 09:05:37 -04:00
Shaun Walker
4f8a18451c Merge pull request #5424 from sbwalker/dev
fix #5346 - deleting role should remove associated permissions
2025-07-29 08:40:54 -04:00
sbwalker
b1770ebb76 fix #5346 - deleting role should remove associated permissions 2025-07-29 08:40:38 -04:00
Shaun Walker
6923065d86 Merge pull request #5423 from sbwalker/dev
fix #5348 - ensure time zones work consistently on all platforms
2025-07-29 08:11:56 -04:00
sbwalker
9f097521f6 fix #5348 - ensure time zones work consistently on all platforms 2025-07-29 08:11:42 -04:00
Shaun Walker
235e5c1d3a Merge pull request #5421 from sbwalker/dev
improve TimeZoneService
2025-07-28 17:00:47 -04:00
sbwalker
e179976fe8 improve TimeZoneService 2025-07-28 17:00:27 -04:00
Shaun Walker
082726b405 Merge pull request #5420 from sbwalker/dev
fix #5372 - add support for sending SMTP emails using OAuth
2025-07-28 10:26:34 -04:00
sbwalker
91c5309855 fix #5372 - add support for sending SMTP emails using OAuth 2025-07-28 10:26:18 -04:00
Shaun Walker
92be1e7a5c Merge pull request #5419 from sbwalker/dev
add OAuth support to Notification Job (#5372)
2025-07-28 09:06:55 -04:00
sbwalker
cceda1db1e add OAuth support to Notification Job (#5372) 2025-07-28 09:06:36 -04:00
Shaun Walker
a59191cea7 Merge pull request #5416 from sbwalker/dev
fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations
2025-07-25 15:22:54 -04:00
sbwalker
b0dee4a60c fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations 2025-07-25 15:22:26 -04:00
Shaun Walker
3f33f2b9df Merge pull request #5412 from sbwalker/dev
fix #5410 - allow duplicate email addresses
2025-07-23 16:40:27 -04:00
sbwalker
97116b4e0c fix #5410 - allow duplicate email addresses 2025-07-23 16:40:12 -04:00
Shaun Walker
a5f51ff9a1 Merge pull request #5411 from sbwalker/dev
localize time zone names
2025-07-23 14:52:34 -04:00
sbwalker
962488fd34 localize time zone names 2025-07-23 14:52:18 -04:00
Shaun Walker
190d973b77 Merge pull request #5406 from leigh-pointer/Refs
Solutions References update
2025-07-22 16:14:13 -04:00
Shaun Walker
397e0b3f71 Merge pull request #5408 from sbwalker/dev
improve user experience of permissions grid
2025-07-22 16:12:48 -04:00
sbwalker
83ba9ca73e improve user experience of permissions grid 2025-07-22 16:07:52 -04:00
Shaun Walker
3d08138686 Merge pull request #5407 from sbwalker/dev
improve documentation
2025-07-22 09:23:42 -04:00
sbwalker
262fa6b99b improve documentation 2025-07-22 09:23:26 -04:00
Leigh Pointer
372db9dcfa Solutions References update
MySql.Data 9.4.0
HtmlAgilityPack 1.12.2
2025-07-22 07:45:03 +02:00
Shaun Walker
e9dc52919c Merge pull request #5405 from sbwalker/dev
fix Control Panel to initialize extended module permissions when module is added or copied
2025-07-21 16:34:56 -04:00
sbwalker
a981dd0e97 fix Control Panel to initialize extended module permissions when module is added or copied 2025-07-21 16:34:34 -04:00
Shaun Walker
7c2775119b Merge pull request #5404 from sbwalker/dev
add new option to FileManager component to anonymize filenames during upload
2025-07-21 09:14:30 -04:00
sbwalker
0be7f1bdb5 add new option to FileManager component to anonymize filenames during upload 2025-07-21 09:14:07 -04:00
Shaun Walker
8446b9e8d5 Merge pull request #5392 from thabaum/patch-15 2025-07-15 14:28:50 -04:00
Shaun Walker
ce404668d3 Merge pull request #5391 from thabaum/6.1.4-dependencies 2025-07-15 14:28:28 -04:00
Cody
948fab50ee [FIX] #5164 – Raise z‑index for .app‑moduleactions .dropdown‑menu to 9999 2025-07-14 17:10:07 -07:00
Cody
9690f1df48 [FIX] oqtane#5164 – Raise z‑index for .app‑moduleactions .dropdown‑menu to 9999 2025-07-14 17:09:24 -07:00
Cody
5a24f87293 [FIX] #5164 ‑ Set z‑index for .dropdown‑menu in .app‑moduleactions 2025-07-14 17:07:39 -07:00
Cody
d2ff49fe73 [FIX] #5164 - Set z-index for .dropdown-menu in .app-moduleactions 2025-07-14 16:10:50 -07:00
Cody
e9035df9d2 Update Package Dependencies 2025-07-14 13:40:52 -07:00
Shaun Walker
1ddf21f4fc Merge pull request #5387 from mdmontesinos/feat-nodatime 2025-07-11 07:18:10 -04:00
David Montesinos
63d2ded038 Merge branch 'dev' into feat-nodatime 2025-07-11 09:07:12 +02:00
Shaun Walker
7b8e0e48c0 Merge pull request #5385 from leigh-pointer/907 2025-07-11 02:28:38 -04:00
David Montesinos
bb52402a17 feat: handle timezones and conversions with NodaTime 2025-07-09 12:09:00 +02:00
Leigh Pointer
13d9cb461b Update Oqtane Maui project to 9.0.7 2025-07-09 03:42:26 +02:00
Leigh Pointer
0a994afd67 Update References .NetCore 9.0.7 2025-07-09 02:52:30 +02:00
Shaun Walker
57a1257750 Merge pull request #5384 from sbwalker/dev
update External Login default values for Facebook OAuth2
2025-07-08 16:27:58 -04:00
sbwalker
b0c1d36bab update External Login default values for Facebook OAuth2 2025-07-08 16:27:35 -04:00
Shaun Walker
0621751968 Merge pull request #5383 from sbwalker/dev
resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user
2025-07-08 16:04:37 -04:00
sbwalker
461330773a resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user 2025-07-08 16:04:19 -04:00
Shaun Walker
818a97cc2c Merge pull request #5382 from sbwalker/dev
bump version to 6.1.4
2025-07-08 13:20:43 -04:00
sbwalker
17045073c8 bump version to 6.1.4 2025-07-08 13:20:28 -04:00
Shaun Walker
7a818ee698 Merge pull request #5381 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:15:10 -04:00
sbwalker
668e0cb4eb update to .NET SDK 9.0.6 2025-07-08 13:14:53 -04:00
Shaun Walker
741b16ca4e Merge pull request #5380 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:12:06 -04:00
sbwalker
85a376b17d update to .NET SDK 9.0.6 2025-07-08 13:11:52 -04:00
Shaun Walker
df86cd909c Merge pull request #5379 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:09:24 -04:00
sbwalker
ac236607f5 update to .NET SDK 9.0.6 2025-07-08 13:09:10 -04:00
Shaun Walker
19813b7eb6 Merge pull request #5378 from sbwalker/dev
remove unused variable
2025-07-07 12:42:51 -04:00
sbwalker
cb5e4e076f remove unused variable 2025-07-07 12:42:35 -04:00
Shaun Walker
48fca77f59 Merge pull request #5276 from leigh-pointer/Bootstrap
Updated to Bootstrap 5.3.5
2025-07-07 12:40:39 -04:00
Shaun Walker
76372451aa Merge pull request #5370 from zyhfish/task/fix-5363
Fix #5363: update SettingService.MergeSettings.
2025-07-07 12:40:14 -04:00
Shaun Walker
34cd197122 Merge pull request #5376 from mdmontesinos/feat-mailkit
feat: replace System.Net.Mail with MailKit (#5372)
2025-07-07 12:40:05 -04:00
David Montesinos
6b567364f9 feat: use appropriate UseSSL equivalent in MailKit 2025-07-04 14:55:02 +02:00
David Montesinos
711de49571 feat: replace System.Net.Mail with MailKit (#5372) 2025-07-04 12:55:40 +02:00
Shaun Walker
9a0f7ad83f Merge pull request #5375 from sbwalker/dev
fix #5374 Visitor Settings not returned due to change in Visitor cookie format
2025-07-03 16:45:19 -04:00
sbwalker
0d3d693799 fix #5374 Visitor Settings not returned due to change in Visitor cookie format 2025-07-03 16:44:59 -04:00
Ben
b63590d6c7 Fix #5363: update SettingService.MergeSettings. 2025-07-03 15:42:11 +08:00
Leigh Pointer
5f3a3d4d54 Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-06-13 19:58:18 +02:00
Shaun Walker
b1a8c28283 Merge pull request #5356 from leigh-pointer/Schedular
Fix for Scheduled Jobs UI #5354
2025-06-13 08:36:47 -04:00
Leigh Pointer
1412737036 Date / Time validations
This PR ensures time fields are required when dates are set, using Oqtane validation and dynamically toggles the required attribute on time inputs when their corresponding date fields have values. Benefits:
- Uses Oqtane's validation for a polished UX.
- Reduces custom validation code.
- Aligns with our internal form logic.

- Tested across all date/time scenarios—works flawlessly!
**Testing Confirmed:**
- Date + Time Provided → Saves successfully.
- No Date + No Time → Optional (no validation).
- Date + No Time → Browser blocks submission with icon error.
2025-06-10 12:27:55 +02:00
Shaun Walker
ffb3f4fa50 Merge pull request #5353 from mdmontesinos/fix-cookieconsent
fix #5352: remove requests to cookie consent service when not enabled
2025-06-09 15:57:59 -04:00
Leigh Pointer
ff450ca43a Fix for Scheduled Jobs UI #5354
This PR addresses an issue where null date/time values could cause exceptions when processing job scheduling.
Changes Made:
- Added proper null checks for _startDate, _startTime, _endDate, _endTime, _nextDate, and _nextTime
- Improved parsing safety for _retentionHistory using int.TryParse()
- Added validation to fail early with meaningful error messages

Impact:

Prevents NullReferenceException and InvalidOperationException when date/time fields are missing
2025-06-09 10:29:43 +02:00
David Montesinos
d4f0805108 fix #5352: remove requests to cookie consent service when not enabled 2025-06-06 10:05:40 +02:00
Shaun Walker
64ce69d1c7 Merge pull request #5351 from sbwalker/dev
stop gap fix to mitigate date conversion exceptions on WebAssembly
2025-06-05 10:38:51 -04:00
sbwalker
ca3cb48091 Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2025-06-05 10:37:31 -04:00
sbwalker
85085bf4c7 stop gap fix to mitigate date conversion exceptions on WebAssembly 2025-06-05 10:37:25 -04:00
Shaun Walker
873af6b598 Merge pull request #5349 from leigh-pointer/References
Server References Updated
2025-06-05 09:32:34 -04:00
Shaun Walker
c423895f31 Merge pull request #5350 from sbwalker/dev
rendering optimizations
2025-06-05 09:32:12 -04:00
sbwalker
4418e27c29 rendering optimizations 2025-06-05 09:31:54 -04:00
Leigh Pointer
f776977af8 Server References Updated
update SixLabors.ImageSharp
update Swashbuckle.AspNetCore
2025-06-04 13:28:49 +02:00
Leigh Pointer
c13ce3d0f1 Update Index.razor
Deprecated .text-muted will be replaced by .text-body-secondary in v6.
2025-06-03 15:24:43 +02:00
Leigh Pointer
2c4c669ea2 Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-30 16:06:19 +02:00
Shaun Walker
29fe3dfd0b Merge pull request #5344 from sbwalker/dev
update Azure ARM template to 6.1.3
2025-05-29 17:05:04 -04:00
sbwalker
985e50d415 update Azure ARM template to 6.1.3 2025-05-29 17:04:45 -04:00
Shaun Walker
11150b6a10 Update README.md 2025-05-29 17:03:20 -04:00
Shaun Walker
c499acdc4a Merge pull request #5342 from oqtane/master
Merge pull request #5341 from oqtane/dev
2025-05-29 15:59:32 -04:00
Shaun Walker
b24e3252d9 Merge pull request #5341 from oqtane/dev
6.1.3 Release
2025-05-29 15:58:55 -04:00
Shaun Walker
e15787b1e4 Merge pull request #5340 from sbwalker/dev
change id for header/footer
2025-05-29 15:04:28 -04:00
sbwalker
d5f19d97e2 change id for header/footer 2025-05-29 15:04:12 -04:00
Shaun Walker
5543a4aeed Merge pull request #5339 from sbwalker/dev
fix #5329 - clear Options after updating User Settings
2025-05-29 11:53:29 -04:00
sbwalker
9c333232e2 fix #5329 - clear Options after updating User Settings 2025-05-29 11:53:14 -04:00
Shaun Walker
d52b95ea23 Merge pull request #5333 from leigh-pointer/TokenReplace
Fix for ModuleBase ReplaceTokens #5332
2025-05-28 13:11:28 -04:00
Leigh Pointer
ef4fbcbb8a Update ModuleBase.cs
This method replaces all tokens in the format [Object:Property] or [Object:SubObject:Property] within a string.
Efficient string parsing and reflection ensure flexibility with performance.
It supports deeply nested properties, optional default fallback values (e.g. [PageState:User:Email|default@email.com]), and uses caching to optimize repeated token resolution without regex.
2025-05-28 17:30:19 +02:00
Shaun Walker
aa454b411f Merge pull request #5335 from thabaum/Update-Swashbuckle.AspNetCore-Package-Dependency-to-8.1.2
Fixes #5331: Updates Swashbuckle.AspNetCore Package Dependency to 8.1.2
2025-05-28 08:23:39 -04:00
Cody
543e9339c7 Update Swashbuckle.AspNetCore Package Dependency to 8.1.2 2025-05-25 09:34:09 -07:00
Leigh Pointer
7fff5c0d18 Fix for ModuleBase ReplaceTokens #5332
Replaced the ReplaceTokens logic to replace all tokens in the string
2025-05-25 10:55:49 +02:00
Shaun Walker
fa79f3f6fa Merge pull request #5326 from sbwalker/dev
fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase
2025-05-19 21:01:02 -07:00
sbwalker
c098839881 fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase 2025-05-19 21:00:35 -07:00
Shaun Walker
8d0d88c1b9 Merge pull request #5325 from sbwalker/dev
ensure Content folder is empty when packaging
2025-05-19 18:32:42 -04:00
sbwalker
2b6ba0f410 ensure Content folder is empty when packaging 2025-05-19 15:32:27 -07:00
Shaun Walker
11235009c0 Merge pull request #5324 from sbwalker/dev
imprvoe help text
2025-05-19 18:15:03 -04:00
sbwalker
4b05f7fdad imprvoe help text 2025-05-19 15:14:49 -07:00
Shaun Walker
338b0ae509 Merge pull request #5320 from sbwalker/dev
use consistent authorization method
2025-05-16 12:11:16 -04:00
sbwalker
a437082952 use consistent authorization method 2025-05-16 12:11:03 -04:00
Shaun Walker
ca9aba7b3b Merge pull request #5319 from sbwalker/dev
improve comment
2025-05-16 11:53:17 -04:00
sbwalker
fe9f189734 improve comment 2025-05-16 11:53:04 -04:00
Shaun Walker
018ac612f4 Merge pull request #5318 from sbwalker/dev
improve messaging
2025-05-16 11:47:06 -04:00
sbwalker
5bde40ec2b improve messaging 2025-05-16 11:46:53 -04:00
Shaun Walker
4d5780c192 Merge pull request #5317 from sbwalker/dev
Fix #4789 - allow user email verification to be managed by administrator
2025-05-16 11:13:20 -04:00
sbwalker
ff6a810ad5 Fix #4789 - allow user email verification to be managed by administrator 2025-05-16 11:13:03 -04:00
Shaun Walker
feec01ba00 Merge pull request #5316 from sbwalker/dev
fix spelling mistake
2025-05-16 09:40:10 -04:00
sbwalker
1f05d12ef5 fix spelling mistake 2025-05-16 09:39:57 -04:00
Shaun Walker
31aba14507 Merge pull request #5315 from sbwalker/dev
fix initialization issue related to time zones
2025-05-16 09:09:21 -04:00
sbwalker
bbd6f13f36 fix initialization issue related to time zones 2025-05-16 09:09:07 -04:00
Shaun Walker
68edbbbdb9 Merge pull request #5314 from sbwalker/dev
improve filename validation in module content export
2025-05-16 08:26:04 -04:00
sbwalker
eb5a0dc1c9 improve filename validation in module content export 2025-05-16 08:25:50 -04:00
Shaun Walker
7cea4f1792 Merge pull request #5312 from sbwalker/dev
update module export resource info
2025-05-15 11:06:20 -04:00
sbwalker
c57c6abb1b update module export resource info 2025-05-15 11:06:04 -04:00
Shaun Walker
a25b706c7b Merge pull request #5311 from sbwalker/dev
allow filename to be provided during module export
2025-05-15 10:59:10 -04:00
sbwalker
5d077e843d allow filename to be provided during module export 2025-05-15 10:58:55 -04:00
Shaun Walker
65bf3e9899 Merge pull request #5310 from sbwalker/dev
allow module import from a file
2025-05-15 09:34:37 -04:00
sbwalker
51ba3a01f5 allow module import from a file 2025-05-15 09:34:19 -04:00
Shaun Walker
f9ca611b8b Merge pull request #5309 from sbwalker/dev
improve module export so that content can be saved to a file
2025-05-15 08:56:37 -04:00
sbwalker
a49b8728fd improve module export so that content can be saved to a file 2025-05-15 08:56:21 -04:00
Leigh Pointer
018737c42a Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-15 11:52:51 +02:00
Shaun Walker
9234b91089 Merge pull request #5306 from sbwalker/dev
fix #5200 - sort folders alphabetically, display folders hierarchically
2025-05-14 15:52:04 -04:00
sbwalker
f3fcef52dd fix #5200 - sort folders alphabetically, display folders hierarchically 2025-05-14 15:51:51 -04:00
Shaun Walker
7f8b741981 Merge pull request #5305 from sbwalker/dev
fix issue with module header/footer
2025-05-14 14:20:54 -04:00
sbwalker
f1791a709c fix issue with module header/footer 2025-05-14 14:20:44 -04:00
Shaun Walker
30307fb05e Merge pull request #5304 from sbwalker/dev
support for module header and footer content
2025-05-14 12:18:51 -04:00
sbwalker
57d443be8d support for module header and footer content 2025-05-14 12:18:37 -04:00
Shaun Walker
84844c5043 Merge pull request #5303 from sbwalker/dev
move ConfigureOqtaneAssemblies to occur before UseEndpoints
2025-05-14 11:13:12 -04:00
sbwalker
9000f05961 move ConfigureOqtaneAssemblies to occur before UseEndpoints 2025-05-14 11:12:58 -04:00
Shaun Walker
6bef9b5c6e Merge pull request #5302 from sbwalker/dev
update default templates to .NET SDK 9.0.5
2025-05-13 16:52:51 -04:00
sbwalker
ffef1f4820 update default templates to .NET SDK 9.0.5 2025-05-13 16:52:39 -04:00
Shaun Walker
10c7bdbcaa Merge pull request #5301 from sbwalker/dev
upgrade to .NET SDK 9.0.5
2025-05-13 16:49:59 -04:00
sbwalker
e8f9888a41 upgrade to .NET SDK 9.0.5 2025-05-13 16:49:46 -04:00
Shaun Walker
8bac702be6 Merge pull request #5300 from sbwalker/dev
rollback change which moved ConfigureOqtaneAssemblies
2025-05-13 16:39:08 -04:00
sbwalker
128bcecfe3 rollback change which moved ConfigureOqtaneAssemblies(env); 2025-05-13 16:38:48 -04:00
Shaun Walker
1390b3c489 Merge pull request #5299 from sbwalker/dev
fix #5398 - editing page permissions
2025-05-13 15:49:30 -04:00
sbwalker
a0f41341ac fix #5398 - editing page permissions 2025-05-13 15:49:16 -04:00
Shaun Walker
8ffa7ef7ff Merge pull request #5297 from sbwalker/dev
adding time zone support to admin modules
2025-05-13 13:55:15 -04:00
sbwalker
deb4607081 adding time zone support to admin modules 2025-05-13 13:55:01 -04:00
Shaun Walker
49c62f80e8 Merge pull request #5296 from sbwalker/dev
display local datetimes in the Job Scheduler (using time zones)
2025-05-13 11:29:40 -04:00
sbwalker
139793f3c0 display local datetimes in the Job Scheduler (using time zones) 2025-05-13 11:29:26 -04:00
Shaun Walker
f237cb9655 Merge pull request #5295 from sbwalker/dev
add time zone support for sites and users
2025-05-13 09:24:33 -04:00
sbwalker
9f18c460d8 add time zone support for sites and users 2025-05-13 09:24:17 -04:00
Shaun Walker
306a41b442 Merge pull request #5293 from sbwalker/dev
fix #5292 - fix External Login Provider Info link
2025-05-12 11:32:42 -04:00
sbwalker
b53f54295d fix #5292 - fix External Login Provider Info link 2025-05-12 11:32:27 -04:00
Shaun Walker
7e4f066694 Merge pull request #5291 from sbwalker/dev
fix #5287 - allow deletion of folder which contains files
2025-05-12 08:42:12 -04:00
sbwalker
90d72489d9 fix #5287 - allow deletion of folder which contains files 2025-05-12 08:41:57 -04:00
Shaun Walker
045c455324 Merge pull request #5286 from sbwalker/dev
update version to 6.1.3
2025-05-08 16:09:12 -04:00
sbwalker
60da903360 update version to 6.1.3 2025-05-08 16:09:00 -04:00
Shaun Walker
6d2a71f37e Merge pull request #5283 from thabaum/update-6.1.3
Fixes #5267: Updates Project Dependencies and Version to 6.1.3
2025-05-08 16:07:48 -04:00
Shaun Walker
c8f60a12a4 Merge pull request #5277 from ohba-ikuo/dev
Modify the module's server-side resource path.
2025-05-07 17:27:15 -04:00
Shaun Walker
11b2d3aa43 Merge pull request #5284 from sbwalker/dev
change Synchronize button to Check For Updates to improve clarity
2025-05-07 17:12:26 -04:00
sbwalker
3d9c81d850 change Synchronize button to Check For Updates to improve clarity 2025-05-07 17:12:14 -04:00
Cody
8ccb1a24f8 Update Version to 6.1.3 2025-05-07 10:25:27 -07:00
Cody
48c6796128 Update Version to 6.1.3 2025-05-07 10:24:36 -07:00
Cody
fc403f920b Update Version to 6.1.3 2025-05-07 10:24:13 -07:00
Cody
ca19496b5c Update Version to 6.1.3 2025-05-07 10:23:32 -07:00
Cody
0ae38f8a40 Update Version to 6.1.3 2025-05-07 10:23:11 -07:00
Cody
ad868ba841 Update Version to 6.1.3 2025-05-07 10:22:47 -07:00
Cody
f2aa39aa85 Update Version to 6.1.3 2025-05-07 10:22:04 -07:00
Cody
e76e0fc351 Update Version to 6.1.3 2025-05-07 10:20:52 -07:00
Cody
a348913888 Update Version to 6.1.3 2025-05-07 10:20:15 -07:00
Cody
5507006c53 Update Version to 6.1.3 2025-05-07 10:19:05 -07:00
Cody
994429f098 Update Version to 6.1.3 2025-05-07 10:18:40 -07:00
Cody
8efdcb9c49 Update Version to 6.1.3 2025-05-07 10:17:54 -07:00
Cody
db9a40db2b Update Version to 6.1.3 2025-05-07 10:17:17 -07:00
Cody
25667499e6 Update Package Dependencies & Version to 6.1.3 2025-05-07 10:16:38 -07:00
Cody
a728cd2d91 Update Package Dependencies & Version to 6.1.3 2025-05-07 10:14:12 -07:00
Cody
3f5f3ef10b Update Version to 6.1.3 2025-05-07 10:10:58 -07:00
Leigh Pointer
3811b8f0c0 Theme Template updated 2025-05-07 11:46:07 +02:00
Ikuo Ohba
0d708124c2 Undo Oqtane.Server.csproj 2025-05-07 08:31:34 +09:00
Ikuo Ohba
d31f73df14 Merge branch 'oqtane:dev' into dev 2025-05-07 06:20:48 +09:00
Shaun Walker
3fed45438b Merge pull request #5281 from thabaum/HtmlText.cs-Unused-Namespaces
Fixes #5280: Remove Unused Namespaces from HtmlText.cs in HtmlText Module
2025-05-06 16:50:10 -04:00
Cody
8aa967fa1b Remove Unnecessary Namespaces
Removes Unnecessary Namespaces 'System' and 'System.ComponentModel.DataAnnotations.Schema;'.
2025-05-06 13:29:26 -07:00
Ikuo Ohba
6eaa3e342c Merge branch 'dev' of https://github.com/ohba-ikuo/oqtane.framework into dev 2025-05-02 23:45:18 +09:00
Ikuo Ohba
6fc9e60f62 fixed serverside resource path 2025-05-02 23:45:13 +09:00
Leigh Pointer
d81514e9be Update for Blazor Theme 2025-05-02 12:19:58 +02:00
Leigh Pointer
14b0d7abf0 Updated to Bootstrap 5
Updated to Bootstrap 5.3.5
Update bootswatch Cyborg to 5.3.5 using https://cdn.jsdelivr.net because it is not available at https://cdnjs.com/libraries
2025-05-02 12:16:55 +02:00
Shaun Walker
c39ffcf51c Merge pull request #5275 from sbwalker/dev
add new Register Url and Profile Url options to User Management / Settings
2025-05-01 23:33:10 -04:00
sbwalker
6f60a91f4c add new Register Url and Profile Url options to User Management / Settings 2025-05-01 23:32:37 -04:00
Shaun Walker
8031df6f28 Merge pull request #5274 from sbwalker/dev
use new GetSettingValue() method
2025-04-30 14:35:11 -04:00
sbwalker
da1e859fda use new GetSettingValue() method 2025-04-30 14:34:54 -04:00
Shaun Walker
8d4d25f1d1 Merge pull request #5273 from sbwalker/dev
use new GetSettingValue() method
2025-04-30 14:18:44 -04:00
sbwalker
6aff27778d use new GetSettingValue() method 2025-04-30 14:18:29 -04:00
Shaun Walker
ca2dcbfec0 Merge pull request #5272 from sbwalker/dev
remove unecessary using statment
2025-04-30 13:55:24 -04:00
sbwalker
24b666a382 remove unecessary using statment 2025-04-30 13:55:11 -04:00
Shaun Walker
9e34295529 Merge pull request #5270 from leigh-pointer/ModBase
GetUrlParameters crash
2025-04-30 13:47:21 -04:00
Shaun Walker
10c55d056b Merge pull request #5271 from sbwalker/dev
resolve issue with host setting overrides
2025-04-30 13:47:06 -04:00
sbwalker
753ab3bdd7 resolve issue with host setting overrides 2025-04-30 13:46:52 -04:00
Leigh Pointer
feee8def6f GetUrlParameters crash
The _urlparametersstate variable is not initialized so in GetUrlParameters it causes a crash
2025-04-29 09:50:15 +02:00
Shaun Walker
c208f12f8c Merge pull request #5266 from sbwalker/dev
add a convenience method to get a setting value server-side
2025-04-28 12:43:04 -04:00
sbwalker
dc926bf838 add a convenience method to get a setting value server-side 2025-04-28 12:42:50 -04:00
Shaun Walker
55a76b5204 Merge pull request #5265 from sbwalker/dev
fix #5229 - move IServerStartup.Configure execution until later so that it is possible to register custom endpoints
2025-04-28 12:35:49 -04:00
sbwalker
53b837e763 fix #5229 - move IServerStartup.Configure execution until later so that it is possible to register custom endpoints 2025-04-28 12:34:48 -04:00
Shaun Walker
3a551cdf25 Merge pull request #5257 from leigh-pointer/AddMDfiles
Updated UploadableFiles constant
2025-04-22 17:00:03 -04:00
Shaun Walker
c0c7f87dc9 Merge pull request #5256 from leigh-pointer/Packages
Packages updated
2025-04-22 16:59:49 -04:00
Leigh Pointer
ac77fd138b Updated UploadableFiles constant
Updated UploadableFiles constant to allow for Markdown files to be uploaded.
2025-04-22 10:08:20 +02:00
Leigh Pointer
30d6e9d67c Merge branch 'dev' into Packages 2025-04-22 09:13:15 +02:00
Leigh Pointer
47db4334a1 Packages updated
MySql.Data to 9.3.0
HtmlAgilityPack to 1.12.1
washbuckle.AspNetCore to 8.1.1
2025-04-22 09:11:53 +02:00
Shaun Walker
fce97179e1 Merge pull request #5255 from sbwalker/dev
url mapping improvements
2025-04-21 15:14:52 -04:00
sbwalker
4f16cd2d01 url mapping improvements 2025-04-21 15:14:39 -04:00
Shaun Walker
fa587691b1 Merge pull request #5254 from sbwalker/dev
improve validation in Url Mapping
2025-04-21 14:14:03 -04:00
sbwalker
e0044658f9 improve validation in Url Mapping 2025-04-21 14:13:49 -04:00
Shaun Walker
53de1ddb36 Merge pull request #5253 from mdmontesinos/files-optimization
Files server optimization
2025-04-21 13:57:41 -04:00
David Montesinos
da7b046092 Remove extra using 2025-04-21 18:47:34 +02:00
David Montesinos
d888d83a98 Simplify files etag calculation 2025-04-21 18:44:16 +02:00
David Montesinos
430f83e8e9 Only compute hash when file has query string 2025-04-21 16:46:55 +02:00
David Montesinos
e7acd14faa Replace MD5 hash with a longer simple hash 2025-04-21 15:51:25 +02:00
David Montesinos
1b00fa74bc Compute file server etag with MD5 and always include ModifiedOn 2025-04-21 11:14:24 +02:00
David Montesinos
4d572d8173 Allow earlier return in files server 2025-04-21 10:48:48 +02:00
Shaun Walker
dbda85d8d9 Merge pull request #5248 from sbwalker/dev
UX improvements for System Update
2025-04-15 09:20:35 -04:00
sbwalker
95cb5dd66c UX improvements for System Update 2025-04-15 09:20:18 -04:00
Shaun Walker
f64e1c3a6a Merge pull request #5245 from sbwalker/dev
.NET MAUI client was changed from 0.0.0.0 to 0.0.0.1 in .NET 9
2025-04-11 15:54:17 -04:00
sbwalker
26a686c412 .NET MAUI client was changed from 0.0.0.0 to 0.0.0.1 in .NET 9 2025-04-11 15:54:02 -04:00
Shaun Walker
2505383f53 Merge pull request #5244 from leigh-pointer/Swash
Swagger Updated to latest
2025-04-11 09:03:29 -04:00
Leigh Pointer
1a1e9ac6be Swagger Updated to latest
Swashbuckle.AspNetCore update from 8.1.0 > 8.1.1
2025-04-11 09:24:18 +02:00
Shaun Walker
7f20a3179e Merge pull request #5241 from sbwalker/dev
fix issue with new UserProfile parameters
2025-04-10 14:36:57 -04:00
sbwalker
46431f0187 fix issue with new UserProfile parameters 2025-04-10 14:36:42 -04:00
Shaun Walker
1ff8ec78c9 Merge pull request #5240 from sbwalker/dev
backup parameter needs to be backward compatible
2025-04-10 12:27:39 -04:00
sbwalker
7840230c62 backup parameter needs to be backward compatible 2025-04-10 12:27:21 -04:00
Shaun Walker
523db0a005 Merge pull request #5239 from sbwalker/dev
update Deploy To Azure to the 6.1.2 release
2025-04-10 12:04:40 -04:00
sbwalker
cc906d49ba update Deploy To Azure to the 6.1.2 release 2025-04-10 12:04:26 -04:00
Shaun Walker
fc23af89d3 Update README.md 2025-04-10 11:54:58 -04:00
Shaun Walker
5d8829ba63 Merge pull request #5238 from oqtane/master
6.1.2 Release
2025-04-10 11:48:58 -04:00
Shaun Walker
31ccd80894 Merge pull request #5237 from oqtane/dev
6.1.2 Release
2025-04-10 11:48:36 -04:00
Shaun Walker
bac2234616 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg 2025-04-10 07:49:01 -04:00
Shaun Walker
bd61db76a3 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg 2025-04-10 07:48:52 -04:00
Shaun Walker
bc99e3b992 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg 2025-04-10 07:48:42 -04:00
Shaun Walker
b7314b0813 Delete Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg 2025-04-10 07:48:31 -04:00
Shaun Walker
4759bd569f Merge pull request #5235 from sbwalker/dev
fix incorrect path in theme template
2025-04-09 17:21:36 -04:00
sbwalker
b88c28f864 fix incorrect path in theme template 2025-04-09 17:21:23 -04:00
Shaun Walker
774ccb05f8 Merge pull request #5234 from sbwalker/dev
removing ShutdownTimeout specification as it was changed in .NET 7 to 30 seconds (https://github.com/dotnet/runtime/pull/63712)
2025-04-09 14:58:56 -04:00
sbwalker
0ac48cba34 removing ShutdownTimeout specification as it was changed in .NET 7 to 30 seconds (https://github.com/dotnet/runtime/pull/63712) 2025-04-09 14:58:30 -04:00
Shaun Walker
e36880fe3a Merge pull request #5233 from sbwalker/dev
prepare for 6.1.2 release
2025-04-09 11:46:30 -04:00
sbwalker
713cf5de2c prepare for 6.1.2 release 2025-04-09 11:46:16 -04:00
Shaun Walker
0fa336411f Merge pull request #5232 from sbwalker/dev
update to .NET 9.0.4
2025-04-09 11:41:08 -04:00
sbwalker
8ebdb09d68 update to .NET 9.0.4 2025-04-09 11:40:54 -04:00
Shaun Walker
40bc53001e Merge pull request #5231 from sbwalker/dev
improve sitemap detection in robots.txt
2025-04-09 11:26:53 -04:00
sbwalker
b1656d1eea improve sitemap detection in robots.txt 2025-04-09 11:26:33 -04:00
Shaun Walker
7aa54bf979 Merge pull request #5230 from sbwalker/dev
resolve issue with host role support  in external login
2025-04-09 10:55:32 -04:00
sbwalker
231f9bca84 resolve issue with host role support in external login 2025-04-09 10:55:16 -04:00
Shaun Walker
e4f8596c19 Merge pull request #5227 from sbwalker/dev
fix #5223 - allow robots.txt to be customized for each site
2025-04-08 09:23:35 -04:00
sbwalker
020b7233d0 fix #5223 - allow robots.txt to be customized for each site 2025-04-08 09:23:22 -04:00
Shaun Walker
85fc0b3e2f Merge pull request #5226 from sbwalker/dev
optimize the System Update process
2025-04-07 13:19:07 -04:00
sbwalker
5dcc7c14f3 optimize the System Update process 2025-04-07 13:18:52 -04:00
Shaun Walker
7993d27b11 Merge pull request #5224 from sbwalker/dev
added new Azure SQL database provider
2025-04-07 10:48:20 -04:00
sbwalker
1f8c54ce74 added new Azure SQL database provider 2025-04-07 10:48:02 -04:00
Shaun Walker
73a414a34b Merge pull request #5221 from sbwalker/dev
improve help text
2025-04-02 09:47:31 -04:00
sbwalker
8fa19c4a51 improve help text 2025-04-02 09:47:16 -04:00
Shaun Walker
0667ae3e15 Merge pull request #5220 from sbwalker/dev
update help text
2025-04-02 09:36:49 -04:00
sbwalker
db1d00cd07 update help text 2025-04-02 09:36:32 -04:00
Shaun Walker
b27f092bef Merge pull request #5219 from sbwalker/dev
use dynamic .NET major version value
2025-04-01 15:29:26 -04:00
sbwalker
4eaea8e586 use dynamic .NET major version value 2025-04-01 15:29:10 -04:00
Shaun Walker
89cd7d3bbb Update README.md 2025-04-01 14:30:49 -04:00
Shaun Walker
2fff1d8d21 Merge pull request #5218 from sbwalker/dev
removing connection string section
2025-04-01 13:55:09 -04:00
sbwalker
850631f00e removing connection string section 2025-04-01 13:54:39 -04:00
Shaun Walker
1cea8846cf Merge pull request #5217 from sbwalker/dev
fix azuredeploy
2025-04-01 11:04:02 -04:00
sbwalker
af48a48559 fix azuredeploy 2025-04-01 11:03:46 -04:00
Shaun Walker
655c1762aa Merge pull request #5216 from sbwalker/dev
azuredeploy changes to use ZIP Deploy
2025-04-01 10:56:47 -04:00
sbwalker
f706ccfd87 new version using ZIP Deploy 2025-04-01 10:56:04 -04:00
Shaun Walker
71e4c7f117 Merge pull request #5214 from sbwalker/dev
remove .deployment file as we are deploying from a package rather than from source code
2025-03-31 16:22:08 -04:00
sbwalker
ad6182f4bd remove .deployment file as we are deploying from a package rather than from source code 2025-03-31 16:21:45 -04:00
Shaun Walker
86bf0f65b0 Merge pull request #5213 from sbwalker/dev
modifications to use WEBSITE_RUN_FROM_PACKAGE
2025-03-31 16:06:24 -04:00
sbwalker
7742f7747d modifications to use WEBSITE_RUN_FROM_PACKAGE 2025-03-31 16:06:08 -04:00
Shaun Walker
eb998c41f2 Merge pull request #5212 from sbwalker/dev
fix #4929 deploy to azure
2025-03-31 13:59:12 -04:00
sbwalker
657bd7c97c fix #4929 deploy to azure 2025-03-31 13:58:03 -04:00
Shaun Walker
c8286148c1 Merge pull request #5211 from sbwalker/dev
fix #5194 - improve performance of retrieving scheduled job logs
2025-03-31 13:16:52 -04:00
sbwalker
e6ba2cce62 fix #5194 - improve performance of retrieving scheduled job logs 2025-03-31 13:16:35 -04:00
Shaun Walker
6105ff44b4 Merge pull request #5210 from sbwalker/dev
fix #5207 add support for username and displayname in permissions grid
2025-03-31 10:04:43 -04:00
sbwalker
72da77be01 fix #5207 add support for username and displayname in permissions grid 2025-03-31 10:04:26 -04:00
Shaun Walker
4c29b31f1b Merge pull request #5209 from sbwalker/dev
delete files before deleting folder
2025-03-31 08:58:39 -04:00
sbwalker
6e640108ed delete files before deleting folder 2025-03-31 08:58:23 -04:00
Shaun Walker
157322441d Merge pull request #5206 from zyhfish/task/fix-5205
Fix #5205: specific the date time as UTC kind.
2025-03-31 08:40:28 -04:00
Shaun Walker
61d967e6af Merge pull request #5208 from sbwalker/dev
allow custom urls in UserProfile component
2025-03-31 08:38:46 -04:00
sbwalker
99f2158e55 allow custom urls in UserProfile component 2025-03-31 08:38:30 -04:00
Ben
1cba78cc4e Fix #5205: specific the date time as UTC kind. 2025-03-29 09:29:49 +08:00
Shaun Walker
1770c1ee11 Merge pull request #5201 from sbwalker/dev
include external login support for host role
2025-03-26 17:11:52 -04:00
sbwalker
a57fbea0cc include external login support for host role 2025-03-26 17:11:29 -04:00
Shaun Walker
f0c27c83f1 Merge pull request #5197 from zyhfish/task/clean-build
suppress build warnings.
2025-03-26 11:16:47 -04:00
Ben
7873ca564c supress build warnings. 2025-03-26 08:12:10 +08:00
Shaun Walker
5ebc1fec24 Merge pull request #5195 from zyhfish/task/fix-5191
Fix #5191: trigger event when folder changed.
2025-03-25 19:46:24 -04:00
Shaun Walker
f2559b7d4d Merge pull request #5196 from sbwalker/dev
fix #5193 - prevent scheduled jobs from blocking startup
2025-03-25 19:07:24 -04:00
sbwalker
1ee92a248e fix #5193 - prevent scheduled jobs from blocking startup 2025-03-25 19:07:01 -04:00
Ben
8376f98f21 Fix #5191: trigger event when folder changed. 2025-03-25 22:23:51 +08:00
Shaun Walker
810a3e0171 Merge pull request #5188 from sbwalker/dev
prevent stylesheet resources from being duplicated
2025-03-21 17:34:21 -04:00
sbwalker
2eac9c3795 prevent stylesheet resources from being duplicated 2025-03-21 17:34:07 -04:00
Shaun Walker
75f2425668 Merge pull request #5187 from sbwalker/dev
remove unnecessary using statements
2025-03-21 10:10:32 -04:00
sbwalker
2dd1d7e926 remove unnecessary using statements 2025-03-21 10:10:19 -04:00
Shaun Walker
5bb05a0a51 Merge pull request #5185 from sbwalker/dev
fix page order for new Privacy and Terms pages
2025-03-21 08:19:26 -04:00
sbwalker
bc2c5b00c6 fix page order for new Privacy and Terms pages 2025-03-21 08:19:09 -04:00
Shaun Walker
09f5e158dd Merge pull request #5181 from sbwalker/dev
add ability to Synchronize local modules and themes with Marketplace
2025-03-19 14:37:50 -04:00
sbwalker
4656471a0a add ability to Synchronize local modules and themes with Marketplace 2025-03-19 14:37:36 -04:00
Shaun Walker
69d58a4273 Merge pull request #5179 from leigh-pointer/SwashbuckleUpdate8
Swashbuckle Update
2025-03-19 08:42:40 -04:00
Leigh Pointer
53a27677d4 Swashbuckle Update
Update Swashbuckle  to version 8.0
2025-03-19 12:07:15 +01:00
Shaun Walker
f243ad0348 Merge pull request #5178 from sbwalker/dev
add caching support for ImageUrl
2025-03-18 14:29:23 -04:00
sbwalker
b4ce6bbb42 add caching support for ImageUrl 2025-03-18 14:29:09 -04:00
Shaun Walker
fa32937045 Merge pull request #5177 from sbwalker/dev
MySql.Data is still required for raw query operations
2025-03-18 09:24:53 -04:00
sbwalker
812e5f3c8e MySql.Data is still required for raw query operations 2025-03-18 09:24:39 -04:00
Shaun Walker
e2981e802c Merge pull request #5176 from sbwalker/dev
fix #5173 - MySQL Database Provider not incuding MySqlConnector dependency
2025-03-18 09:08:07 -04:00
sbwalker
4ae4705c73 fix #5173 - MySQL Database Provider not incuding MySqlConnector dependency 2025-03-18 09:07:23 -04:00
Shaun Walker
fbf4b12713 Merge pull request #5168 from zyhfish/task/fix-cookie-consent-layout
adjust the cookie consent layout in small screen.
2025-03-17 13:45:14 -04:00
Shaun Walker
e4ece3e0dc Merge pull request #5174 from sbwalker/dev
notifications should only convert line breaks to HTML for plain text messages
2025-03-17 13:45:02 -04:00
sbwalker
9f231421be notifications should only convert line breaks to HTML for plain text messages 2025-03-17 13:44:40 -04:00
Ben
b4fdbb5e48 adjust the layout in small screen. 2025-03-13 21:54:43 +08:00
Ben
fbf62ca30d adjust the cookie consent layout in small screen. 2025-03-13 20:54:50 +08: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
232 changed files with 5843 additions and 2412 deletions

View File

@@ -1,2 +0,0 @@
[config]
project = Oqtane.Server/Oqtane.Server.csproj

View File

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

View File

@@ -0,0 +1,119 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
@inject IStringLocalizer<SqlServerConfig> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service." ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="security" HelpText="Select your security method" ResourceKey="Security">Security:</Label>
<div class="col-sm-9">
<select id="security" class="form-select custom-select" @bind="@_security">
<option value="integrated" selected>@Localizer["Integrated"]</option>
<option value="custom">@Localizer["Custom"]</option>
</select>
</div>
</div>
@if (_security == "custom")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uid" HelpText="Enter the username to use for the database" ResourceKey="Uid">User Id:</Label>
<div class="col-sm-9">
<input id="uid" type="text" class="form-control" @bind="@_uid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="pwd" type="@_passwordType" class="form-control" @bind="@_pwd" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button>
</div>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="encryption" HelpText="Specify if you are using an encrypted database connection. It is highly recommended to use encryption in a production environment." ResourceKey="Encryption">Encryption:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_encryption">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
@if (_encryption == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True." ResourceKey="TrustServerCertificate">Certificate:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate">
<option value="true">@Localizer["Self Signed"]</option>
<option value="false">@Localizer["Verifiable"]</option>
</select>
</div>
</div>
}
@code {
private string _server = "tcp:{SQL Server Name}.database.windows.net,1433";
private string _database = "{SQL Database Name}";
private string _security = "custom";
private string _uid = "{SQL Administrator Login}";
private string _pwd = String.Empty;
private string _passwordType = "password";
private string _togglePassword = string.Empty;
private string _encryption = "true";
private string _trustservercertificate = "false";
protected override void OnInitialized()
{
_togglePassword = SharedLocalizer["ShowPassword"];
}
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database))
{
connectionString = $"Data Source={_server};Initial Catalog={_database};";
}
if (_security == "integrated")
{
connectionString += "Integrated Security=SSPI;";
}
else
{
connectionString += $"User ID={_uid};Password={_pwd};";
}
connectionString += $"Encrypt={_encryption};";
connectionString += $"TrustServerCertificate={_trustservercertificate};";
return connectionString;
}
private void TogglePassword()
{
if (_passwordType == "password")
{
_passwordType = "text";
_togglePassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordType = "password";
_togglePassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@@ -4,7 +4,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) " ResourceKey="Server">Server:</Label>
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service." ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>

View File

@@ -15,7 +15,7 @@
<div class="row">
<div class="mx-auto text-center">
<img src="oqtane-black.png" />
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET 9)</div>
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET @Environment.Version.Major)</div>
</div>
</div>
<hr class="app-rule" />

View File

@@ -15,22 +15,34 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_parentId" required>
@if (PageState.QueryString.ContainsKey("id"))
{
@if (_parentId == -1)
{
<select id="parent" class="form-select" @bind="@_parentId" required>
<option value="-1">&lt;@Localizer["NoParent"]&gt;</option>
}
</select>
}
else
{
<select id="parent" class="form-select" @bind="@_parentId" required>
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</select>
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
@if (_isSystem)
{
<input id="name" class="form-control" @bind="@_name" readonly />
}
else
{
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
}
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -229,7 +241,6 @@
if (folder != null)
{
await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId);
await logger.LogInformation("Folder Saved {Folder}", folder);
NavigationManager.NavigateTo(NavigateUrl());
}
@@ -265,17 +276,9 @@
}
if (!isparent)
{
var files = await FileService.GetFilesAsync(_folderId);
if (files.Count == 0)
{
await FolderService.DeleteFolderAsync(_folderId);
await logger.LogInformation("Folder Deleted {Folder}", _folderId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.Folder.Files.InvalidDelete"], MessageType.Warning);
}
await FolderService.DeleteFolderAsync(_folderId);
await logger.LogInformation("Folder Deleted {Folder}", _folderId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{

View File

@@ -53,7 +53,7 @@ else
<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>@UtcToLocal(context.ModifiedOn)</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row>

View File

@@ -45,7 +45,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="4" required />
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required />
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -56,7 +56,7 @@
<input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div>
<div class="col">
<input id="starting" type="time" class="form-control" placeholder="hh:mm" @bind="@_startTime" />
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" />
</div>
</div>
</div>
@@ -69,7 +69,7 @@
<input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div>
<div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" />
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" />
</div>
</div>
</div>
@@ -82,7 +82,7 @@
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" />
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div>
</div>
</div>
@@ -132,13 +132,13 @@
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = Utilities.UtcAsLocalDate(job.StartDate);
_startTime = Utilities.UtcAsLocalDateTime(job.StartDate);
_endDate = Utilities.UtcAsLocalDate(job.EndDate);
_endTime = Utilities.UtcAsLocalDateTime(job.EndDate);
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate);
_retentionHistory = job.RetentionHistory.ToString();
_nextDate = Utilities.UtcAsLocalDate(job.NextExecution);
_nextTime = Utilities.UtcAsLocalDateTime(job.NextExecution);
_nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
@@ -176,10 +176,18 @@
{
job.Interval = int.Parse(_interval);
}
job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime);
job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime);
job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime);
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory);
try
{
@@ -198,5 +206,4 @@
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
}

View File

@@ -29,7 +29,7 @@ else
<td>@context.Name</td>
<td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td>
<td>@DisplayFrequency(context.Interval, context.Frequency)</td>
<td>@context.NextExecution?.ToLocalTime()</td>
<td>@UtcToLocal(context.NextExecution)</td>
<td>
@if (context.IsStarted)
{
@@ -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

@@ -23,8 +23,8 @@ else
<Row>
<td>@context.Job.Name</td>
<td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td>
<td>@context.StartDate</td>
<td>@context.FinishDate</td>
<td>@UtcToLocal(context.StartDate)</td>
<td>@UtcToLocal(context.FinishDate)</td>
</Row>
<Detail>
<td colspan="4">@((MarkupString)context.Notes)</td>
@@ -44,14 +44,12 @@ else
private async Task GetJobLogs()
{
_jobLogs = await JobLogService.GetJobLogsAsync();
var jobId = -1;
if (PageState.QueryString.ContainsKey("id"))
{
_jobLogs = _jobLogs.Where(item => item.JobId == Int32.Parse(PageState.QueryString["id"])).ToList();
jobId = int.Parse(PageState.QueryString["id"]);
}
_jobLogs = _jobLogs.OrderByDescending(item => item.JobLogId).ToList();
_jobLogs = await JobLogService.GetJobLogsAsync(jobId);
}
private string DisplayStatus(bool isExecuting, bool? succeeded)

View File

@@ -21,20 +21,18 @@ else
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br />
<br />
<br /><br />
}
@if (_allowsitelogin)
{
<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>
@@ -49,15 +47,11 @@ else
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
{
<br />
<br />
<br /><br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
}
}
@@ -144,7 +138,7 @@ else
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else

View File

@@ -141,7 +141,7 @@
var log = await LogService.GetLogAsync(_logId);
if (log != null)
{
_logDate = log.LogDate.ToString(CultureInfo.CurrentCulture);
_logDate = UtcToLocal(log.LogDate).Value.ToString(CultureInfo.CurrentCulture);
_level = log.Level;
_feature = log.Feature;
_function = log.Function;

View File

@@ -64,7 +64,7 @@ else
</Header>
<Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Text="Details" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td>
<td class="@GetClass(context.Function)">@UtcToLocal(context.LogDate)</td>
<td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td>
<td class="@GetClass(context.Function)">@context.Function</td>

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">
@@ -90,7 +73,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the module" ResourceKey="ReferenceUrl">Reference Url: </Label>
<Label Class="col-sm-3" For="url" HelpText="The url of the module" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
@@ -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

@@ -13,32 +13,32 @@
}
else
{
<div class="container">
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" />
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == _category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
</div>
</div>
</div>
<Pager Items="@_moduleDefinitions">
<div class="container">
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == _category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
</div>
</div>
</div>
<Pager Items="@_moduleDefinitions">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
@@ -61,17 +61,17 @@ else
<td>@context.Name</td>
<td>@context.Version</td>
<td>
@if (context.IsEnabled)
{
<span>@SharedLocalizer["Yes"]</span>
}
else
{
<span>@SharedLocalizer["No"]</span>
}
@if (context.IsEnabled)
{
<span>@SharedLocalizer["Yes"]</span>
}
else
{
<span>@SharedLocalizer["No"]</span>
}
</td>
<td>
@if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
@if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{
<span>@SharedLocalizer["Yes"]</span>
}
@@ -87,9 +87,9 @@ else
@((MarkupString)PurchaseLink(context.PackageName))
</td>
<td>
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
@@ -153,10 +153,10 @@ else
link = "<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</a>";
}
}
}
}
return link;
}
}
}
return link;
}
private string SupportLink(string packagename, string version)
{
@@ -172,52 +172,75 @@ else
return link;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
}
return version;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
}
return version;
}
private async Task DownloadModule(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(packagename, version);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DownloadModule(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(packagename, version);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DeleteModule(ModuleDefinition moduleDefinition)
{
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private async Task DeleteModule(ModuleDefinition moduleDefinition)
{
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private async Task CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
private async Task CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
await LoadModuleDefinitions();
}
}
private async Task Synchronize()
{
try
{
ShowProgressIndicator();
foreach (var moduleDefinition in _moduleDefinitions)
{
if (!string.IsNullOrEmpty(moduleDefinition.PackageName) && !_packages.Any(item => item.PackageId == moduleDefinition.PackageName))
{
var package = await PackageService.GetPackageAsync(moduleDefinition.PackageName, moduleDefinition.Version, false);
}
}
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Module.Synchronize"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Synchronizing Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Module.Synchronize"], MessageType.Error);
}
}
}

View File

@@ -5,24 +5,57 @@
@inject IStringLocalizer<Export> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="The Exported Module Content" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea>
<TabStrip>
<TabPanel Name="Content" Heading="Content" ResourceKey="Content">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Select the Export option and you will be able to view the module content" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea>
</div>
</div>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ExportText">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="File" Heading="File" ResourceKey="File">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="folder" HelpText="Select a folder where you wish to save the exported content" ResourceKey="Folder">Folder: </Label>
<div class="col-sm-9">
<FileManager ShowFiles="false" ShowUpload="false" @ref="_filemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="filename" HelpText="Specify a name for the file (without an extension)" ResourceKey="Filename">Filename: </Label>
<div class="col-sm-9">
<input id="content" type="text" class="form-control" @bind="@_filename" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ExportFile">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _content = string.Empty;
private FileManager _filemanager;
private string _filename = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content";
private async Task ExportModule()
protected override void OnInitialized()
{
_filename = Utilities.GetFriendlyUrl(ModuleState.Title);
}
private async Task ExportText()
{
try
{
@@ -35,4 +68,34 @@
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
private async Task ExportFile()
{
try
{
var folderid = _filemanager.GetFolderId();
if (folderid != -1 && !string.IsNullOrEmpty(_filename))
{
var fileid = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename);
if (fileid != -1)
{
AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Content.Export"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
}

View File

@@ -2,20 +2,27 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IFileService FileService
@inject IStringLocalizer<Import> 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="content" HelpText="Enter The Module Content To Import" ResourceKey="Content">Content: </Label>
<Label Class="col-sm-3" For="file" HelpText="Optionally upload or select a file to import for this module" ResourceKey="File">File: </Label>
<div class="col-sm-9">
<FileManager Filter="json" OnSelectFile="OnSelectFile" />
</div>
</div>
<hr />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Provide the module content to import" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</form>
@@ -28,6 +35,12 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content";
private async Task OnSelectFile(int fileId)
{
var bytes = await FileService.DownloadFileAsync(fileId);
_content = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
private async Task ImportModule()
{
validated = true;

View File

@@ -97,6 +97,23 @@
</div>
</div>
</div>
<br />
<Section Name="ModuleContent" Heading="Content" ResourceKey="ModuleContent">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduleheader" HelpText="Optionally provide content to be injected above the module instance" ResourceKey="Header">Header: </Label>
<div class="col-sm-9">
<textarea id="moduleheader" class="form-control" @bind="@_header" rows="3" maxlength="4000"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="modulefooter" HelpText="Optionally provide content to be injected below the module instance" ResourceKey="Footer">Footer: </Label>
<div class="col-sm-9">
<textarea id="modulefooter" class="form-control" @bind="@_footer" rows="3" maxlength="4000"></textarea>
</div>
</div>
</div>
</Section>
}
</TabPanel>
<TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@@ -144,6 +161,8 @@
private string _pane;
private string _containerType;
private string _allPages = "false";
private string _header = "";
private string _footer = "";
private string _permissionNames = "";
private List<Permission> _permissions = null;
private string _pageId;
@@ -167,37 +186,47 @@
protected override async Task OnInitializedAsync()
{
SetModuleTitle(Localizer["ModuleSettings.Title"]);
_title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.PermissionList;
_pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn;
_effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (ModuleState.ModuleDefinition != null)
{
_module = ModuleState.ModuleDefinition.Name;
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
_pageId = pagemodule.PageId.ToString();
_title = pagemodule.Title;
_pane = pagemodule.Pane;
_containerType = pagemodule.ContainerType;
if (string.IsNullOrEmpty(_containerType))
{
_containerType = (!string.IsNullOrEmpty(PageState.Page.DefaultContainerType)) ? PageState.Page.DefaultContainerType : PageState.Site.DefaultContainerType;
}
_header = pagemodule.Header;
_footer = pagemodule.Footer;
_effectivedate = Utilities.UtcAsLocalDate(pagemodule.EffectiveDate);
_expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate);
_allPages = pagemodule.Module.AllPages.ToString();
createdby = pagemodule.Module.CreatedBy;
createdon = pagemodule.Module.CreatedOn;
modifiedby = pagemodule.Module.ModifiedBy;
modifiedon = pagemodule.Module.ModifiedOn;
_permissions = pagemodule.Module.PermissionList;
if (pagemodule.Module.ModuleDefinition != null)
{
_module = pagemodule.Module.ModuleDefinition.Name;
_permissionNames = pagemodule.Module.ModuleDefinition?.PermissionNames;
if (!string.IsNullOrEmpty(pagemodule.Module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
_moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.SettingsType);
}
else
{
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
_moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
}
if (_moduleSettingsType != null)
{
@@ -218,7 +247,7 @@
}
else
{
AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error);
AddModuleMessage(string.Format(Localizer["Error.Module.Load"], pagemodule.Module.ModuleDefinitionName), MessageType.Error);
}
var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
@@ -270,10 +299,12 @@
{
pagemodule.ContainerType = string.Empty;
}
pagemodule.Header = _header;
pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState;
var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList();

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

@@ -30,16 +30,16 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parent" HelpText="Select the parent for the page in the site hierarchy" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pages)
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pages)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList) && page.PageId != _pageId)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList) && page.PageId != _pageId)
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
</select>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -205,18 +205,21 @@
<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>
</Section>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
</TabPanel>
@@ -225,15 +228,28 @@
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" />
</div>
<br /><br />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="updatemodulepermissions" HelpText="Specify if changes made to page permissions should be propagated to the modules on this page" ResourceKey="UpdateModulePermissions">Update Module Permissions? </Label>
<div class="col-sm-9">
<select id="updatemodulepermissions" class="form-select" @bind="@_updatemodulepermissions" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div>
</TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
@@ -247,8 +263,10 @@
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
<br />
}
</TabStrip>
}
@@ -299,19 +317,21 @@
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</TabPanel>
<br />
}
</TabStrip>
}
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>
}
@@ -348,6 +368,7 @@
private string _bodycontent;
private List<Permission> _permissions = null;
private PermissionGrid _permissionGrid;
private string _updatemodulepermissions;
private List<Module> _pageModules;
private string _createdby;
private DateTime _createdon;
@@ -436,6 +457,7 @@
// permissions
_permissions = _page.PermissionList;
_updatemodulepermissions = "True";
// page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@@ -651,6 +673,7 @@
if (_page.UserId == null)
{
_page.PermissionList = _permissionGrid.GetPermissionList();
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
}
_page = await PageService.UpdatePageAsync(_page);

View File

@@ -6,77 +6,95 @@
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@if (PageState.Site.AllowRegistration)
@if (_initialized)
{
if (!_userCreated)
@if (PageState.Site.AllowRegistration)
{
if (PageState.User != null)
if (!_userCreated)
{
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
}
else
{
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<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="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
if (PageState.User != null)
{
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
}
else
{
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<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="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br />
<br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
<br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
}
}
}
}
else
{
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
}
else
{
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
}
}
@code {
private bool _initialized = false;
private List<Models.TimeZone> _timezones;
private string _passwordrequirements;
private string _username = string.Empty;
private ElementReference form;
@@ -87,6 +105,7 @@ else
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private bool _userCreated = false;
private bool _allowsitelogin = true;
@@ -96,6 +115,9 @@ else
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}
protected override void OnParametersSet()
@@ -124,6 +146,7 @@ else
Password = _password,
Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
PhotoFileId = null
};
user = await UserService.AddUserAsync(user);

View File

@@ -46,7 +46,7 @@
<Row>
<div class="search-item mb-2">
<h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4>
<p class="mb-0 text-muted">@((MarkupString)context.Snippet)</p>
<p class="mb-0 text-body-secondary">@((MarkupString)context.Snippet)</p>
</div>
</Row>
</Pager>

View File

@@ -10,10 +10,12 @@
@inject IAliasService AliasService
@inject IThemeService ThemeService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService
@if (_initialized)
{
@@ -40,6 +42,18 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9">
@@ -50,11 +64,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 +87,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 +129,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">
@@ -165,80 +194,125 @@
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SmtpEnabled">Enabled? </Label>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
<input id="host" class="form-control" @bind="@_smtphost" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port number for the SMTP server. Please note this field is required if you provide a host name." ResourceKey="Port">Port: </Label>
<div class="col-sm-9">
<input id="port" class="form-control" @bind="@_smtpport" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="UseSsl">SSL Enabled: </Label>
<div class="col-sm-9">
<select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<select id="smtpenabled" class="form-select" value="@_smtpenabled" @onchange="(e => SMTPEnabledChanged(e))">
<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="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off"/>
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
@if (_smtpenabled == "True")
{
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmtpSender">Email Sender: </Label>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
<input id="host" class="form-control" @bind="@_smtphost" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port number for the SMTP server. Please note this field is required if you provide a host name." ResourceKey="Port">Port: </Label>
<div class="col-sm-9">
<input id="port" class="form-control" @bind="@_smtpport" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SMTPEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="smtpenabled" class="form-select" @bind="@_smtpenabled">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="SmtpSSL">SSL Required: </Label>
<div class="col-sm-9">
<select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<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="retention" HelpText="Number of days of notifications 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 class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpauthentication" HelpText="Specify the SMTP authentication type" ResourceKey="SMTPAuthentication">Authentication: </Label>
<div class="col-sm-9">
<select id="smtpauthentication" class="form-select" value="@_smtpauthentication" @onchange="(e => SMTPAuthenticationChanged(e))">
<option value="Basic">@Localizer["Basic"]</option>
<option value="OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
@if (_smtpauthentication == "Basic")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpauthority" HelpText="The Authority Url for the SMTP provider" ResourceKey="SmtpAuthority">Authority Url:</Label>
<div class="col-sm-9">
<input id="smtpauthority" class="form-control" @bind="@_smtpauthority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpclientid" HelpText="The Client ID for the SMTP provider" ResourceKey="SmtpClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="smtpclientid" class="form-control" @bind="@_smtpclientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpclientsecret" HelpText="The Client Secret for the SMTP provider" ResourceKey="SmtpClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_smtpclientsecrettype" id="smtpclientsecret" class="form-control" @bind="@_smtpclientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSmtpClientSecret">@_togglesmtpclientsecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpscopes" HelpText="A list of Scopes for the SMTP provider (separated by commas)" ResourceKey="SmtpScopes">Scopes:</Label>
<div class="col-sm-9">
<input id="smtpscopes" class="form-control" @bind="@_smtpscopes" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email address which emails will be sent from. Please note that this email address usually needs to be authorized with the SMTP provider." ResourceKey="SmtpSender">Email Sender: </Label>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications 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>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
}
</div>
</Section>
<Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings">
@@ -400,9 +474,11 @@
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private string _name = string.Empty;
private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _isdeleted;
private string _sitemap = "";
private string _siteguid = "";
@@ -415,24 +491,32 @@
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 _imagefiles = string.Empty;
private string _headcontent = string.Empty;
private string _bodycontent = string.Empty;
private string _smtpenabled = "False";
private string _smtpauthentication = "Basic";
private string _smtphost = string.Empty;
private string _smtpport = string.Empty;
private string _smtpssl = "False";
private string _smtpssl = "True";
private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty;
private string _smtpauthority = string.Empty;
private string _smtpclientid = string.Empty;
private string _smtpclientsecret = string.Empty;
private string _smtpclientsecrettype = "password";
private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private string _smtpenabled = "True";
private int _retention = 30;
private string _pwaisenabled;
@@ -476,11 +560,13 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
{
_timezones = TimeZoneService.GetTimeZones();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null)
{
_homepageid = site.HomePageId.Value.ToString();
@@ -505,6 +591,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>();
@@ -513,23 +600,29 @@
_textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName));
}
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_imagefiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
// page content
_headcontent = site.HeadContent;
_bodycontent = site.BodyContent;
// SMTP
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False");
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpauthentication = SettingService.GetSetting(settings, "SMTPAuthentication", "Basic");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpauthority = SettingService.GetSetting(settings, "SMTPAuthority", string.Empty);
_smtpclientid = SettingService.GetSetting(settings, "SMTPClientId", string.Empty);
_smtpclientsecret = SettingService.GetSetting(settings, "SMTPClientSecret", string.Empty);
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
// PWA
@@ -632,6 +725,7 @@
if (site != null)
{
site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
@@ -709,14 +803,22 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true);
settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true);
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
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);
@@ -774,6 +876,46 @@
}
}
private void SMTPAuthenticationChanged(ChangeEventArgs e)
{
_smtpauthentication = (string)e.Value;
StateHasChanged();
}
private void SMTPEnabledChanged(ChangeEventArgs e)
{
_smtpenabled = (string)e.Value;
StateHasChanged();
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private void ToggleSmtpClientSecret()
{
if (_smtpclientsecrettype == "password")
{
_smtpclientsecrettype = "text";
_togglesmtpclientsecret = SharedLocalizer["HidePassword"];
}
else
{
_smtpclientsecrettype = "password";
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
}
}
private async Task SendEmail()
{
if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
@@ -784,8 +926,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true);
settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true);
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
@@ -806,20 +953,6 @@
}
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@@ -913,4 +1046,9 @@
_aliasname = "";
StateHasChanged();
}
private async Task EvictSitemapOutputCache() {
await CacheService.EvictByTag(Constants.SitemapOutputCacheTag);
AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success);
}
}

View File

@@ -153,8 +153,10 @@
</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["Swagger"]</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">

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">
@@ -72,7 +55,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the theme" ResourceKey="ReferenceUrl">Reference Url: </Label>
<Label Class="col-sm-3" For="url" HelpText="The url of the theme" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</div>
@@ -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
@@ -14,11 +15,12 @@
else
{
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<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>
@@ -170,4 +173,27 @@ else
AddModuleMessage(Localizer["Error.Theme.Delete"], MessageType.Error);
}
}
private async Task Synchronize()
{
try
{
ShowProgressIndicator();
foreach (var theme in _themes)
{
if (!string.IsNullOrEmpty(theme.PackageName) && !_packages.Any(item => item.PackageId == theme.PackageName))
{
await PackageService.GetPackageAsync(theme.PackageName, theme.Version, false);
}
}
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Theme.Synchronize"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Synchronizing Themes {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Theme.Synchronize"], MessageType.Error);
}
}
}

View File

@@ -13,9 +13,26 @@
<TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable)
{
<ModuleMessage Type="MessageType.Info" Message="Select The Download Button To Download The Framework Upgrade Package And Then Select Upgrade"></ModuleMessage>
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
@if (!_downloaded)
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
}
else
{
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
}
}
else
{
@@ -23,7 +40,6 @@
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>
@@ -31,7 +47,17 @@
<FileManager Folder="@Constants.PackagesFolder" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
</TabPanel>
</TabStrip>
@@ -39,8 +65,10 @@
@code {
private bool _initialized = false;
private bool _downloaded = false;
private Package _package;
private bool _upgradeavailable = false;
private string _backup = "True";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -86,7 +114,7 @@
ShowProgressIndicator();
var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(), 10);
await InstallationService.Upgrade();
await InstallationService.Upgrade(bool.Parse(_backup));
}
catch (Exception ex)
{
@@ -102,6 +130,7 @@
ShowProgressIndicator();
await PackageService.DownloadPackageAsync(packageid, version);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version);
_downloaded = true;
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
}

View File

@@ -8,13 +8,16 @@
<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="url" HelpText="The fully qualified Url for this site" ResourceKey="Url">Url:</Label>
<Label Class="col-sm-3" For="url" HelpText="A Url identifying a path to a specific page in the site (absolute or relative)" ResourceKey="Url">Url:</Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" required />
<div class="input-group">
<input id="url" class="form-control" @bind="@_url" maxlength="500" required />
<button type="button" class="btn btn-primary" @onclick="GenerateUrl">@Localizer["Generate"]</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label>
<Label Class="col-sm-3" For="mappedurl" HelpText="A Url where the user will be redirected (absolute or relative). Use '/' for site root path." ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div>
@@ -26,64 +29,80 @@
</form>
@code {
private ElementReference form;
private bool validated = false;
private ElementReference form;
private bool validated = false;
private string _url = string.Empty;
private string _mappedurl = string.Empty;
private string _url = string.Empty;
private string _mappedurl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private async Task SaveUrlMapping()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url != _mappedurl)
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
private async Task SaveUrlMapping()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url != _mappedurl)
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url;
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url;
if (_url.StartsWith(url))
{
var urlmapping = new UrlMapping();
urlmapping.SiteId = PageState.Site.SiteId;
var route = new Route(_url, PageState.Alias.Path);
urlmapping.Url = route.PagePath;
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping.Requests = 0;
urlmapping.CreatedOn = DateTime.UtcNow;
urlmapping.RequestedOn = DateTime.UtcNow;
_mappedurl = _mappedurl.Replace(url, "");
_mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
try
{
urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message);
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning);
}
}
else
{
if (_url.StartsWith(url))
{
var urlmapping = new UrlMapping();
urlmapping.SiteId = PageState.Site.SiteId;
urlmapping.Url = new Route(_url, PageState.Alias.Path).PagePath;
urlmapping.MappedUrl = _mappedurl;
urlmapping.Requests = 0;
urlmapping.CreatedOn = DateTime.UtcNow;
urlmapping.RequestedOn = DateTime.UtcNow;
try
{
urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message);
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.DuplicateUrlMapping"], MessageType.Warning);
}
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void GenerateUrl()
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
var chars = "abcdefghijklmnopqrstuvwxyz";
Random rnd = new Random();
for (int i = 0; i < 5; i++)
{
url += chars.Substring(rnd.Next(0, chars.Length - 1), 1);
}
_url = url;
}
}

View File

@@ -8,13 +8,13 @@
<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="url" HelpText="A fully qualified Url for this site" ResourceKey="Url">Url:</Label>
<Label Class="col-sm-3" For="url" HelpText="A Url identifying a path to a specific page in the site (absolute or relative)" ResourceKey="Url">Url:</Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label>
<Label Class="col-sm-3" For="mappedurl" HelpText="A Url where the user will be redirected (absolute or relative). Use '/' for site root path." ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div>
@@ -67,8 +67,11 @@
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_mappedurl = _mappedurl.Replace(url, "");
_mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid);
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping.MappedUrl = _mappedurl;
urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());

View File

@@ -48,7 +48,7 @@ else
}
</td>
<td>@context.Requests</td>
<td>@context.RequestedOn</td>
<td>@UtcToLocal(context.RequestedOn)</td>
</Row>
</Pager>
</TabPanel>

View File

@@ -9,6 +9,7 @@
@inject INotificationService NotificationService
@inject IFileService FileService
@inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@@ -16,9 +17,9 @@
@if (_initialized)
{
@if (PageState.User != null && photo != null)
@if (PageState.User != null && _photo != null)
{
<img src="@ImageUrl(photofileid, 400, 400)" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
<img src="@ImageUrl(_photofileid, 400, 400)" alt="@_displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
}
else
{
@@ -31,7 +32,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly />
<input id="username" class="form-control" @bind="@_username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
@@ -47,17 +48,17 @@
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
@if (allowtwofactor)
@if (_allowtwofactor)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required>
<select id="twofactor" class="form-select" @bind="@_twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
@@ -67,19 +68,31 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<FileManager FileId="@photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" />
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@_photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@_photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@_folderid" @ref="_filemanager" />
</div>
</div>
</div>
@@ -91,17 +104,17 @@
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
@foreach (Profile profile in _profiles)
{
var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (p.Category != category)
if (p.Category != _category)
{
<div class="col text-center pb-2">
@p.Category
</div>
category = p.Category;
_category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@@ -150,12 +163,12 @@
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
else
{
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
}
else
@@ -163,12 +176,12 @@
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
else
{
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)" />
}
}
}
@@ -179,12 +192,12 @@
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
}
else
@@ -192,12 +205,12 @@
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
@attributes="@(p.MaxLength > 0 ? new Dictionary<string, object> {{"maxlength", p.MaxLength }} : null)"></textarea>
}
}
}
@@ -220,11 +233,11 @@
<option value="from">@Localizer["Items.Sent"]</option>
</select>
<br />
@if (filter == "to")
@if (_filter == "to")
{
@if (notifications.Any())
@if (_notifications.Any())
{
<Pager Items="@notifications">
<Pager Items="@_notifications">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
@@ -260,15 +273,15 @@
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
_notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
@_notificationSummary
}
else
{
<b>@notificationSummary</b>
<b>@_notificationSummary</b>
}
</td>
</Detail>
@@ -285,9 +298,9 @@
}
else
{
@if (notifications.Any())
@if (_notifications.Any())
{
<Pager Items="@notifications">
<Pager Items="@_notifications">
<Header>
<th style="width: 1px;"></th>
<th style="width: 1px;"></th>
@@ -324,15 +337,15 @@
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
}
notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
_notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body;
}
@if (context.IsRead)
{
@notificationSummary
@_notificationSummary
}
else
{
<b>@notificationSummary</b>
<b>@_notificationSummary</b>
}
</td>
</Detail>
@@ -356,27 +369,30 @@
@code {
private bool _initialized = false;
private string _passwordrequirements;
private string username = string.Empty;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty;
private bool allowtwofactor = false;
private string twofactor = "False";
private string email = string.Empty;
private string displayname = string.Empty;
private FileManager filemanager;
private int folderid = -1;
private int photofileid = -1;
private File photo = null;
private string _ImageFiles = string.Empty;
private List<Profile> profiles;
private Dictionary<string, string> userSettings;
private string category = string.Empty;
private string _confirm = string.Empty;
private bool _allowtwofactor = false;
private string _twofactor = "False";
private string _email = string.Empty;
private string _displayname = string.Empty;
private FileManager _filemanager;
private int _folderid = -1;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private int _photofileid = -1;
private File _photo = null;
private string _imagefiles = string.Empty;
private string filter = "to";
private List<Notification> notifications;
private string notificationSummary = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _userSettings;
private string _category = string.Empty;
private string _filter = "to";
private List<Notification> _notifications;
private string _notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
@@ -386,43 +402,40 @@
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_timezones = TimeZoneService.GetTimeZones();
if (PageState.User != null)
{
username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email;
displayname = PageState.User.DisplayName;
if (string.IsNullOrEmpty(email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
_username = PageState.User.Username;
_twofactor = PageState.User.TwoFactorRequired.ToString();
_email = PageState.User.Email;
_displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
// get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
{
folderid = folder.FolderId;
_folderid = folder.FolderId;
}
if (PageState.User.PhotoFileId != null)
{
photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
_photofileid = PageState.User.PhotoFileId.Value;
_photo = await FileService.GetFileAsync(_photofileid);
}
else
{
photofileid = -1;
photo = null;
_photofileid = -1;
_photo = null;
}
userSettings = PageState.User.Settings;
_userSettings = PageState.User.Settings;
var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_ImageFiles = SettingService.GetSetting(userSettings, "ImageFiles", Constants.ImageFiles);
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles;
_imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles);
_imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
await LoadNotificationsAsync();
@@ -442,13 +455,13 @@
private async Task LoadNotificationsAsync()
{
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
_notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
_notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
}
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue);
string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
@@ -460,38 +473,39 @@
{
try
{
if (username != string.Empty && email != string.Empty)
if (_username != string.Empty && _email != string.Empty)
{
if (_password == confirm)
if (_password == _confirm)
{
if (ValidateProfiles())
{
var user = PageState.User;
user.Username = username;
user.Username = _username;
user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor);
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId();
user.TwoFactorRequired = bool.Parse(_twofactor);
user.Email = _email;
user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.TimeZoneId = _timezoneid;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
_photofileid = user.PhotoFileId.Value;
_photo = await FileService.GetFileAsync(_photofileid);
}
else
{
photofileid = -1;
photo = null;
_photofileid = -1;
_photo = null;
}
user = await UserService.UpdateUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(userSettings, PageState.User.UserId);
await SettingService.UpdateUserSettingsAsync(_userSettings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
if (!string.IsNullOrEmpty(PageState.ReturnUrl))
@@ -557,12 +571,12 @@
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)
foreach (Profile profile in _profiles)
{
var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{
userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue);
_userSettings = SettingService.SetSetting(_userSettings, profile.Name, profile.DefaultValue);
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
@@ -594,7 +608,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value);
_userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
}
private async Task Delete(Notification Notification)
@@ -624,7 +638,7 @@
private async void FilterChanged(ChangeEventArgs e)
{
filter = (string)e.Value;
_filter = (string)e.Value;
await LoadNotificationsAsync();
StateHasChanged();
}
@@ -634,7 +648,7 @@
try
{
ShowProgressIndicator();
foreach(var Notification in notifications)
foreach(var Notification in _notifications)
{
if (!Notification.IsDeleted)
{

View File

@@ -5,6 +5,7 @@
@inject IUserService UserService
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -12,7 +13,7 @@
{
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null)
@if (_profiles != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
@@ -27,12 +28,33 @@
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Verified?</Label>
<div class="col-sm-9">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<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="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9">
@@ -48,20 +70,20 @@
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
@foreach (Profile profile in _profiles)
{
var p = profile;
if (p.Category != category)
if (p.Category != _category)
{
<div class="col text-center pb-2">
<strong>@p.Category</strong>
</div>
category = p.Category;
_category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
@@ -103,14 +125,17 @@
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True";
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
private string _category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@@ -118,8 +143,10 @@
{
try
{
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>();
_timezones = TimeZoneService.GetTimeZones();
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}
catch (Exception ex)
@@ -131,7 +158,7 @@
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
@@ -152,7 +179,9 @@
user.Username = _username;
user.Password = ""; // will be auto generated
user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);
@@ -160,7 +189,7 @@
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
@@ -185,12 +214,12 @@
private bool ValidateProfiles()
{
foreach (Profile profile in profiles)
foreach (Profile profile in _profiles)
{
var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{
settings = SettingService.SetSetting(settings, profile.Name, profile.DefaultValue);
_settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
@@ -217,6 +246,6 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
var value = (string)e.Value;
settings = SettingService.SetSetting(settings, SettingName, value);
_settings = SettingService.SetSetting(_settings, SettingName, value);
}
}

View File

@@ -6,6 +6,7 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -17,13 +18,13 @@
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username"></Label>
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username">Username:</Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly />
<input id="username" class="form-control" @bind="@_username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
@@ -32,32 +33,53 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Verified?</Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
<select id="confirmed" class="form-select" @bind="@_confirmed">
<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="displayname" HelpText="The full name of the user" ResourceKey="DisplayName">Full Name:</Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="timezone" HelpText="Your time zone" ResourceKey="TimeZone">Time Zone:</Label>
<div class="col-sm-9">
<select id="timezone" class="form-select" @bind="@_timezoneid">
<option value="">&lt;@SharedLocalizer["Not Specified"]&gt;</option>
@foreach (var timezone in _timezones)
{
<option value="@timezone.Id">@timezone.DisplayName</option>
}
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted">Deleted?</Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted">
<select id="isdeleted" class="form-select" @bind="@_isdeleted">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
@@ -65,15 +87,15 @@
</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>
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin">Last Login:</Label>
<div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@lastlogin" readonly />
<input id="lastlogin" class="form-control" @bind="@_lastlogin" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label>
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress">Last IP Address:</Label>
<div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@lastipaddress" readonly />
<input id="lastipaddress" class="form-control" @bind="@_lastipaddress" readonly />
</div>
</div>
</div>
@@ -81,15 +103,15 @@
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
@foreach (Profile profile in _profiles)
{
var p = profile;
if (p.Category != category)
if (p.Category != _category)
{
<div class="col text-center pb-2">
<strong>@p.Category</strong>
</div>
category = p.Category;
_category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@@ -128,47 +150,50 @@
</div>
</TabPanel>
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost)
@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")
@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" />
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
<br /><br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private int userid;
private string username = string.Empty;
private int _userid;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty;
private string email = string.Empty;
private string displayname = string.Empty;
private string isdeleted;
private string lastlogin;
private string lastipaddress;
private bool ishost = false;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _isdeleted;
private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private List<Profile> profiles;
private Dictionary<string, string> userSettings;
private string category = string.Empty;
private List<Profile> _profiles;
private Dictionary<string, string> _settings;
private string _category = string.Empty;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private string deletedby;
private DateTime? deletedon;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private string _deletedby;
private DateTime? _deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@@ -178,29 +203,32 @@
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_timezones = TimeZoneService.GetTimeZones();
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{
userid = UserId;
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
_userid = UserId;
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null)
{
username = user.Username;
email = user.Email;
displayname = user.DisplayName;
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);
_username = user.Username;
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
_isdeleted = user.IsDeleted.ToString();
_lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn));
_lastipaddress = user.LastIPAddress;
_ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
userSettings = user.Settings;
createdby = user.CreatedBy;
createdon = user.CreatedOn;
modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy;
deletedon = user.DeletedOn;
_settings = user.Settings;
_createdby = user.CreatedBy;
_createdon = user.CreatedOn;
_modifiedby = user.ModifiedBy;
_modifiedon = user.ModifiedOn;
_deletedby = user.DeletedBy;
_deletedon = user.DeletedOn;
}
}
@@ -208,14 +236,14 @@
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message);
await logger.LogError(ex, "Error Loading User {UserId} {Error}", _userid, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
private string GetProfileValue(string SettingName, string DefaultValue)
{
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue);
string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
@@ -227,27 +255,29 @@
{
try
{
if (username != string.Empty && email != string.Empty)
if (_username != string.Empty && _email != string.Empty)
{
if (_password == confirm)
if (_password == _confirm)
{
if (ValidateProfiles())
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Username = _username;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
}
user = await UserService.UpdateUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId);
await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
@@ -269,7 +299,7 @@
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", username, email, ex.Message);
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
}
}
@@ -278,17 +308,17 @@
{
try
{
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username);
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 };
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);
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", _username, ex.Message);
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
}
}
@@ -297,9 +327,9 @@
{
try
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId)
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _userid != PageState.User.UserId)
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
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());
@@ -307,19 +337,19 @@
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message);
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)
foreach (Profile profile in _profiles)
{
var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue))
{
userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue);
_settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue);
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
@@ -346,7 +376,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName)
{
var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value);
_settings = SettingService.SetSetting(_settings, SettingName, value);
}
private void TogglePassword()

View File

@@ -17,171 +17,183 @@ 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>
</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>
</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}", UtcToLocal(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 (_allowregistration == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="registerurl" HelpText="Optionally provide a custom registration url" ResourceKey="RegisterUrl">Register Url:</Label>
<div class="col-sm-9">
<input id="registerurl" class="form-control" @bind="@_registerurl" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileurl" HelpText="Optionally provide a custom profile url" ResourceKey="ProfileUrl">Profile Url:</Label>
<div class="col-sm-9">
<input id="profileurl" class="form-control" @bind="@_profileurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireconfirmedemail" HelpText="Do you want to require registered users to verify their email address before they are allowed to log in?" ResourceKey="RequireConfirmedEmail">Require Verified Email?</Label>
<div class="col-sm-9">
<select id="requireconfirmedemail" class="form-select" @bind="@_requireconfirmedemail">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor Authentication?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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 +213,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 +303,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 +346,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 +358,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,11 +386,11 @@ 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>
<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">
@@ -389,20 +401,20 @@ else
</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="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">
@@ -412,63 +424,87 @@ else
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowhostrole" HelpText="Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation." ResourceKey="AllowHostRole">Allow Host Role?</Label>
<div class="col-sm-9">
<select id="allowhostrole" class="form-select" @bind="@_allowhostrole" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Local Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="jwtsecret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<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 {
private List<UserRole> users;
private string _allowregistration;
private string _allowsitelogin;
private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _twofactor;
private string _cookiename;
private string _cookieexpiration;
private string _alwaysremember;
private string _logouteverywhere;
private string _minimumlength;
private string _uniquecharacters;
@@ -510,6 +546,8 @@ else
private string _domainfilter;
private string _createusers;
private string _verifyusers;
private string _allowhostrole;
private string _allowsitelogin;
private string _secret;
private string _secrettype = "password";
@@ -529,8 +567,10 @@ else
await LoadUsersAsync(true);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@@ -538,6 +578,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");
@@ -591,6 +632,8 @@ else
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
_allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false");
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
}
private async Task LoadUsersAsync(bool load)
@@ -648,14 +691,17 @@ else
await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
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);
@@ -693,6 +739,8 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);

View File

@@ -101,8 +101,8 @@
_url = visitor.Url;
_referrer = visitor.Referrer;
_visits = visitor.Visits.ToString();
_visited = visitor.VisitedOn.ToString(CultureInfo.CurrentCulture);
_created = visitor.CreatedOn.ToString(CultureInfo.CurrentCulture);
_visited = UtcToLocal(visitor.VisitedOn).Value.ToString(CultureInfo.CurrentCulture);
_created = UtcToLocal(visitor.CreatedOn).Value.ToString(CultureInfo.CurrentCulture);
if (visitor.UserId != null)
{

View File

@@ -53,8 +53,8 @@ else
</td>
<td>@context.Language</td>
<td>@context.Visits</td>
<td>@context.VisitedOn</td>
<td>@context.CreatedOn</td>
<td>@UtcToLocal(context.VisitedOn)</td>
<td>@UtcToLocal(context.CreatedOn)</td>
</Row>
</Pager>
</TabPanel>
@@ -100,6 +100,18 @@ else
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
<TabPanel Name="Robots" Heading="Robots.txt" ResourceKey="Robots">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="robots" HelpText="Specify your robots.txt instructions to provide bots with guidance on which parts of your site should be indexed" ResourceKey="Robots">Instructions: </Label>
<div class="col-sm-9">
<textarea id="robots" class="form-control" @bind="@_robots" rows="3"></textarea>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
}
@@ -113,6 +125,7 @@ else
private string _filter = "";
private int _retention = 30;
private string _correlation = "true";
private string _robots = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@@ -139,7 +152,8 @@ else
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter);
_retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30"));
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
}
_robots = SettingService.GetSetting(settings, "Robots", "");
}
private async void TypeChanged(ChangeEventArgs e)
{
@@ -191,6 +205,7 @@ else
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true);
settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true);
settings = SettingService.SetSetting(settings, "Robots", _robots, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);

View File

@@ -52,7 +52,7 @@
if (CreatedOn != null)
{
_text += $" {Localizer["On"]} <b>{CreatedOn.Value.ToString(DateTimeFormat)}</b>";
_text += $" {Localizer["On"]} <b>{UtcToLocal(CreatedOn).Value.ToString(DateTimeFormat)}</ b >";
}
_text += "</p>";
@@ -69,7 +69,7 @@
if (ModifiedOn != null)
{
_text += $" {Localizer["On"]} <b>{ModifiedOn.Value.ToString(DateTimeFormat)}</b>";
_text += $" {Localizer["On"]} <b>{UtcToLocal(ModifiedOn).Value.ToString(DateTimeFormat)}</ b >";
}
_text += "</p>";
@@ -86,7 +86,7 @@
if (DeletedOn != null)
{
_text += $" {Localizer["On"]} <b>{DeletedOn.Value.ToString(DateTimeFormat)}</b>";
_text += $" {Localizer["On"]} <b>{UtcToLocal(DeletedOn).Value.ToString(DateTimeFormat)}</ b >";
}
_text += "</p>";

View File

@@ -157,12 +157,22 @@
[Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public bool AnonymizeUploadFilenames { get; set; } = false; // optional - indicate if file names should be anonymized on upload - 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
[Parameter]
public EventCallback<int> OnSelectFolder { get; set; } // optional - executes a method in the calling component when a folder is selected
[Parameter]
public EventCallback<int> OnSelectFile { get; set; } // optional - executes a method in the calling component when a file is selected
[Obsolete("Use OnSelectFile instead.")]
[Parameter]
public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected
@@ -300,6 +310,8 @@
FileId = -1;
_file = null;
_image = string.Empty;
await OnSelectFolder.InvokeAsync(FolderId);
StateHasChanged();
}
catch (Exception ex)
@@ -315,7 +327,10 @@
_message = string.Empty;
FileId = int.Parse((string)e.Value);
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
StateHasChanged();
}
@@ -396,7 +411,7 @@
}
// upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token);
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, AnonymizeUploadFilenames, tokenSource.Token);
// reset progress indicators
if (ShowProgress)
@@ -438,7 +453,10 @@
{
FileId = file.FileId;
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
await OnUpload.InvokeAsync(FileId);
}
await GetFiles();
@@ -489,7 +507,10 @@
await GetFiles();
FileId = -1;
await SetImage();
#pragma warning disable CS0618
await OnSelect.InvokeAsync(FileId);
#pragma warning restore CS0618
await OnSelectFile.InvokeAsync(FileId);
StateHasChanged();
}
catch (Exception ex)

View File

@@ -9,62 +9,26 @@
@if (_permissions != null)
{
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
@foreach (Role role in _roles)
{
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<td>@role.Name</td>
<th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, role.Name, -1) Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td>
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
}
</tbody>
</table>
<br />
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
@foreach (Role role in _roles)
{
<tr>
<td>@user.DisplayName</td>
<td>@role.Name</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, "", user.UserId) Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
<td style="text-align: center;">
<TriStateCheckBox Value="@GetPermissionValue(permissionname, role.Name, -1)" Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td>
}
</tr>
@@ -72,206 +36,248 @@
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
{
<tr>
<td>@user.DisplayName (@user.Username)</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value="@GetPermissionValue(permissionname, "", user.UserId)" Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
</td>
}
</tr>
}
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col-11">
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div>
</div>
</div>
<div class="row">
<div class="col-11">
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div>
</div>
</div>
}
@code {
private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles;
private List<User> _users = new List<User>();
private AutoComplete _user;
private string _message = string.Empty;
private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles;
private List<User> _users = new List<User>();
private AutoComplete _user;
private string _message = string.Empty;
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public List<Permission> PermissionList { get; set; }
[Parameter]
public List<Permission> PermissionList { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles.RemoveAll(item => item.Name == RoleNames.Host);
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
_roles.RemoveAll(item => item.Name == RoleNames.Host); // remove host role
// get permission names
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit);
}
else
{
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// get permission names
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit);
}
else
{
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{
foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
}
else
{
foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
}
}
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{
foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
}
else
{
foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
}
}
}
private string GetPermissionName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetPermissionName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
return true;
}
else
{
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
var disabled = false;
private void PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
}
}
}
// administrator role permissions can only be changed by a host
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
disabled = true;
}
// API permissions can only be changed by an administrator
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
disabled = true;
}
return disabled;
}
private bool? PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_permissions.Remove(permission);
}
// system roles cannot be denied - only custom roles can be denied
var role = _roles.FirstOrDefault(item => item.Name == roleName);
if (value != null && !value.Value && role.IsSystem)
{
value = null;
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
}
}
return value;
}
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase) || item.User.Username.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName + " (" + item.User.Username + ")");
}
private async Task AddUser()
@@ -305,29 +311,20 @@
private void ValidatePermissions()
{
// remove deny all users, unauthenticated, and registered users
var permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Everyone || item.RoleName == RoleNames.Unauthenticated || item.RoleName == RoleNames.Registered)).ToList();
foreach (var permission in permissions)
{
_permissions.Remove(permission);
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
// remove deny administrators and host users
permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList();
foreach (var permission in permissions)
// remove host role permissions
var permissions = _permissions.Where(item => item.RoleName == RoleNames.Host).ToList();
foreach (var permission in permissions)
{
_permissions.Remove(permission);
}
// add host role permissions if administrator role is not assigned (to prevent lockout)
foreach (var permissionname in _permissionnames)
{
_permissions.Remove(permission);
}
foreach (var permissionname in _permissionnames)
{
// add administrators role if neither host or administrator is assigned
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)))
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Admin, null, true));
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Host, null, true));
}
}
}

View File

@@ -16,7 +16,7 @@
public bool Disabled { get; set; }
[Parameter]
public Action<bool?> OnChange { get; set; }
public Func<bool?, bool?> OnChange { get; set; }
protected override void OnInitialized()
{
@@ -41,27 +41,35 @@
break;
}
_value = OnChange(_value);
SetImage();
OnChange(_value);
}
}
private void SetImage()
{
switch (_value)
if (!Disabled)
{
case true:
_src = "images/checked.png";
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";
_title = string.Empty;
break;
switch (_value)
{
case true:
_src = "images/checked.png";
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";
_title = string.Empty;
break;
}
}
else
{
_src = "images/disabled.png";
_title = Localizer["PermissionDisabled"];
}
StateHasChanged();

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

@@ -1,22 +1,23 @@
using Microsoft.AspNetCore.Components;
using Oqtane.Shared;
using Oqtane.Models;
using System.Threading.Tasks;
using Oqtane.Services;
using System;
using Oqtane.Enums;
using Oqtane.UI;
using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Modules
{
public abstract class ModuleBase : ComponentBase, IModuleControl
{
private Logger _logger;
private string _urlparametersstate;
private string _urlparametersstate = string.Empty;
private Dictionary<string, string> _urlparameters;
private bool _scriptsloaded = false;
@@ -35,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; }
@@ -61,7 +62,7 @@ namespace Oqtane.Modules
public Dictionary<string, string> UrlParameters {
get
{
if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters)
if (string.IsNullOrEmpty(_urlparametersstate) || _urlparametersstate != PageState.UrlParameters)
{
_urlparametersstate = PageState.UrlParameters;
_urlparameters = GetUrlParameters(UrlParametersTemplate);
@@ -78,18 +79,21 @@ namespace Oqtane.Modules
{
List<Resource> resources = null;
var type = GetType();
if (type.BaseType == typeof(ModuleBase))
if (type.IsSubclassOf(typeof(ModuleBase)))
{
if (PageState.Page.Resources != null)
if (type.IsSubclassOf(typeof(ModuleControlBase)))
{
resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList();
if (Resources != null)
{
resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList();
}
}
}
else // modulecontrolbase
{
if (Resources != null)
else // ModuleBase
{
resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList();
if (PageState.Page.Resources != null)
{
resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList();
}
}
}
if (resources != null && resources.Any())
@@ -413,6 +417,130 @@ 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)
{
// Using StringBuilder avoids the performance penalty of repeated string allocations
// that occur with string.Replace or string concatenation inside loops.
var sb = new StringBuilder();
var cache = new Dictionary<string, string>(); // Cache to store resolved tokens
int index = 0;
// Loop through content to find and replace all tokens
while (index < content.Length)
{
int start = content.IndexOf('[', index); // Find start of token
if (start == -1)
{
sb.Append(content, index, content.Length - index); // Append remaining content
break;
}
int end = content.IndexOf(']', start); // Find end of token
if (end == -1)
{
sb.Append(content, index, content.Length - index); // Append unmatched content
break;
}
sb.Append(content, index, start - index); // Append content before token
string token = content.Substring(start + 1, end - start - 1); // Extract token without brackets
string[] parts = token.Split('|', 2); // Separate default fallback if present
string key = parts[0];
string fallback = parts.Length == 2 ? parts[1] : null;
if (!cache.TryGetValue(token, out string replacement)) // Check cache first
{
replacement = "[" + token + "]"; // Default replacement is original token
string[] segments = key.Split(':');
if (segments.Length >= 2)
{
object current = GetTarget(segments[0], obj); // Start from root object
for (int i = 1; i < segments.Length && current != null; i++)
{
var type = current.GetType();
var prop = type.GetProperty(segments[i]);
current = prop?.GetValue(current);
}
if (current != null)
{
replacement = current.ToString();
}
else if (fallback != null)
{
replacement = fallback; // Use fallback if available
}
}
cache[token] = replacement; // Store in cache
}
sb.Append(replacement); // Append replacement value
index = end + 1; // Move index past token
}
return sb.ToString();
}
// Resolve the object instance for a given object name
// Easy to extend with additional object types
private object GetTarget(string name, object obj)
{
return name switch
{
"ModuleState" => ModuleState,
"PageState" => PageState,
_ => (obj != null && obj.GetType().Name == name) ? obj : null // Fallback to obj
};
}
// date conversion methods
public DateTime? UtcToLocal(DateTime? datetime)
{
// Early return if input is null
if (datetime == null)
return null;
string timezoneId = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezoneId = PageState.User.TimeZoneId;
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezoneId = PageState.Site.TimeZoneId;
}
return Utilities.UtcAsLocalDateTime(datetime, timezoneId);
}
public DateTime? LocalToUtc(DateTime? datetime)
{
// Early return if input is null
if (datetime == null)
return null;
string timezoneId = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezoneId = PageState.User.TimeZoneId;
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezoneId = PageState.Site.TimeZoneId;
}
return Utilities.LocalDateAndTimeAsUtc(datetime, timezoneId);
}
// 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.1.0</Version>
<Version>6.1.4</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.1.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@@ -22,16 +22,22 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Installer\Controls\AzureSqlConfig.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>

View File

@@ -121,7 +121,7 @@
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) </value>
<value>Enter the database server name. This might include a port number as well if you are using a cloud service.</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>

View File

@@ -135,9 +135,6 @@
<data name="Error.Folder.Save" xml:space="preserve">
<value>Error Saving Folder</value>
</data>
<data name="Message.Folder.Files.InvalidDelete" xml:space="preserve">
<value>Folder Has Files And Cannot Be Deleted</value>
</data>
<data name="Message.Folder.Subfolders.InvalidDelete" xml:space="preserve">
<value>Folder Has Subfolders And Cannot Be Deleted</value>
</data>

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

@@ -121,10 +121,10 @@
<value>Forgot Password</value>
</data>
<data name="Success.Account.Verified" xml:space="preserve">
<value>User Account Verified Successfully. You Can Now Login With Your Username And Password Below.</value>
<value>User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.</value>
</data>
<data name="Message.Account.NotVerified" xml:space="preserve">
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
<value>User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value>
@@ -133,7 +133,7 @@
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value>
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That New User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification Containing Further Instructions.</value>
</data>
<data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value>

View File

@@ -147,8 +147,8 @@
<data name="Owner.HelpText" xml:space="preserve">
<value>The owner or creator of the module</value>
</data>
<data name="ReferenceUrl.HelpText" xml:space="preserve">
<value>The reference url of the module</value>
<data name="Url.HelpText" xml:space="preserve">
<value>The url of the module</value>
</data>
<data name="Contact.HelpText" xml:space="preserve">
<value>The contact for the module</value>
@@ -171,8 +171,8 @@
<data name="Owner.Text" xml:space="preserve">
<value>Owner: </value>
</data>
<data name="ReferenceUrl.Text" xml:space="preserve">
<value>Reference Url: </value>
<data name="Url.Text" xml:space="preserve">
<value>Url: </value>
</data>
<data name="Contact.Text" xml:space="preserve">
<value>Contact: </value>
@@ -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

@@ -159,4 +159,13 @@
<data name="Enabled" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Check For Updates</value>
</data>
<data name="Success.Module.Synchronize" xml:space="preserve">
<value>Module Information Has Been Retrieved From The Marketplace</value>
</data>
<data name="Error.Module.Synchronize" xml:space="preserve">
<value>Error Retrieving Module Information From The Marketplace</value>
</data>
</root>

View File

@@ -121,7 +121,7 @@
<value>Export</value>
</data>
<data name="Content.HelpText" xml:space="preserve">
<value>The Exported Module Content</value>
<value>Select the Export option and you will be able to view the module content</value>
</data>
<data name="Content.Text" xml:space="preserve">
<value>Content: </value>
@@ -135,4 +135,25 @@
<data name="Export Content" xml:space="preserve">
<value>Export Content</value>
</data>
<data name="Content.Heading" xml:space="preserve">
<value>Content</value>
</data>
<data name="File.Heading" xml:space="preserve">
<value>File</value>
</data>
<data name="Folder.Text" xml:space="preserve">
<value>Folder:</value>
</data>
<data name="Folder.HelpText" xml:space="preserve">
<value>Select a folder where you wish to save the exported content</value>
</data>
<data name="Message.Content.Export" xml:space="preserve">
<value>Please Select A Folder And Provide A Filename Before Choosing Export</value>
</data>
<data name="Filename.Text" xml:space="preserve">
<value>Filename:</value>
</data>
<data name="Filename.HelpText" xml:space="preserve">
<value>Specify a name for the file (without an extension)</value>
</data>
</root>

View File

@@ -189,4 +189,19 @@
<data name="ModuleSettings.Title" xml:space="preserve">
<value>Module Settings</value>
</data>
<data name="Header.Text" xml:space="preserve">
<value>Header:</value>
</data>
<data name="Header.HelpText" xml:space="preserve">
<value>Optionally provide content to be injected above the module instance</value>
</data>
<data name="Footer.Text" xml:space="preserve">
<value>Footer:</value>
</data>
<data name="Footer.HelpText" xml:space="preserve">
<value>Optionally provide content to be injected below the module instance</value>
</data>
<data name="ModuleContent.Heading" xml:space="preserve">
<value>Content</value>
</data>
</root>

View File

@@ -303,4 +303,10 @@
<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>
<data name="UpdateModulePermissions.Text" xml:space="preserve">
<value>Update Module Permissions?</value>
</data>
<data name="UpdateModulePermissions.HelpText" xml:space="preserve">
<value>Specify if changes made to page permissions should be propagated to the modules on this page</value>
</data>
</root>

View File

@@ -180,4 +180,10 @@
<data name="Login" xml:space="preserve">
<value>Already have account? Login now.</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</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

@@ -192,7 +192,7 @@
<data name="Port.HelpText" xml:space="preserve">
<value>Enter the port number for the SMTP server. Please note this field is required if you provide a host name.</value>
</data>
<data name="UseSsl.HelpText" xml:space="preserve">
<data name="SmtpSSL.HelpText" xml:space="preserve">
<value>Specify if SSL is required for your SMTP server</value>
</data>
<data name="SmtpUsername.HelpText" xml:space="preserve">
@@ -202,7 +202,7 @@
<value>Enter the password for your SMTP account</value>
</data>
<data name="SmtpSender.HelpText" xml:space="preserve">
<value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value>
<value>Enter the email address which emails will be sent from. Please note that this email address usually needs to be authorized with the SMTP server.</value>
</data>
<data name="EnablePWA.HelpText" xml:space="preserve">
<value>Select whether you would like this site to be available as a Progressive Web Application (PWA)</value>
@@ -240,8 +240,8 @@
<data name="Port.Text" xml:space="preserve">
<value>Port: </value>
</data>
<data name="UseSsl.Text" xml:space="preserve">
<value>SSL Enabled: </value>
<data name="SmtpSSL.Text" xml:space="preserve">
<value>SSL Required: </value>
</data>
<data name="SmtpUsername.Text" xml:space="preserve">
<value>Username: </value>
@@ -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>
@@ -372,10 +372,10 @@
<data name="PageContent.Heading" xml:space="preserve">
<value>Page Content</value>
</data>
<data name="SMTPEnabled.HelpText" xml:space="preserve">
<data name="SmtpEnabled.HelpText" xml:space="preserve">
<value>Specify if SMTP is enabled for this site</value>
</data>
<data name="SMTPEnabled.Text" xml:space="preserve">
<data name="SmtpEnabled.Text" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Version.HelpText" xml:space="preserve">
@@ -426,4 +426,67 @@
<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>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The default time zone for the site</value>
</data>
<data name="Basic" xml:space="preserve">
<value>Basic</value>
</data>
<data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0 (OAuth2)</value>
</data>
<data name="SmtpAuthentication.Text" xml:space="preserve">
<value>Authentication:</value>
</data>
<data name="SmtpAuthentication.HelpText" xml:space="preserve">
<value>Specify the SMTP authentication type</value>
</data>
<data name="SmtpClientID.Text" xml:space="preserve">
<value>Client ID:</value>
</data>
<data name="SmtpClientID.HelpText" xml:space="preserve">
<value>The Client ID for the SMTP provider</value>
</data>
<data name="SmtpClientSecret.Text" xml:space="preserve">
<value>Client Secret:</value>
</data>
<data name="SmtpClientSecret.HelpText" xml:space="preserve">
<value>The Client Secret for the SMTP provider</value>
</data>
<data name="SmtpScopes.Text" xml:space="preserve">
<value>Scopes:</value>
</data>
<data name="SmtpScopes.HelpText" xml:space="preserve">
<value>A list of Scopes for the SMTP provider (separated by commas)</value>
</data>
<data name="SmtpAuthority.Text" xml:space="preserve">
<value>Authority Url:</value>
</data>
<data name="SmtpAuthority.HelpText" xml:space="preserve">
<value>The Authority Url for the SMTP provider</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="Swagger" xml:space="preserve">
<value>Access Swagger UI</value>
<value>Swagger UI</value>
</data>
<data name="FrameworkVersion.HelpText" xml:space="preserve">
<value>Framework Version</value>
@@ -306,4 +306,7 @@
<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

@@ -132,8 +132,8 @@
<data name="Owner.Text" xml:space="preserve">
<value>Owner: </value>
</data>
<data name="ReferenceUrl.Text" xml:space="preserve">
<value>Reference Url: </value>
<data name="Url.Text" xml:space="preserve">
<value>Url: </value>
</data>
<data name="Contact.Text" xml:space="preserve">
<value>Contact: </value>
@@ -153,8 +153,8 @@
<data name="Owner.HelpText" xml:space="preserve">
<value>The owner or creator of the theme</value>
</data>
<data name="ReferenceUrl.HelpText" xml:space="preserve">
<value>The reference url of the theme</value>
<data name="Url.HelpText" xml:space="preserve">
<value>The url of the theme</value>
</data>
<data name="Contact.HelpText" xml:space="preserve">
<value>The contact for the theme</value>
@@ -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

@@ -146,7 +146,7 @@
</data>
<data name="InstallTheme.Text" xml:space="preserve">
<value>Install Theme</value>
</data>
</data>
<data name="ViewTheme.Text" xml:space="preserve">
<value>View</value>
</data>
@@ -156,4 +156,16 @@
<data name="Enabled" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Assign" xml:space="preserve">
<value>Assign</value>
</data>
<data name="Synchronize" xml:space="preserve">
<value>Check For Updates</value>
</data>
<data name="Success.Theme.Synchronize" xml:space="preserve">
<value>Theme Information Has Been Retrieved From The Marketplace</value>
</data>
<data name="Error.Theme.Synchronize" xml:space="preserve">
<value>Error Retrieving Theme Information From The Marketplace</value>
</data>
</root>

View File

@@ -124,7 +124,7 @@
<value>Error Downloading Framework Package</value>
</data>
<data name="Framework.HelpText" xml:space="preserve">
<value>Upload a framework package and select Install to complete the installation</value>
<value>Upload A Framework Package (Oqtane.Framework.#.#.#.nupkg) And Then Select Upgrade</value>
</data>
<data name="Framework.Text" xml:space="preserve">
<value>Framework: </value>
@@ -144,13 +144,16 @@
<data name="Message.Text" xml:space="preserve">
<value>Framework Is Already Up To Date</value>
</data>
<data name="MessageUpgrade.Text" xml:space="preserve">
<value>Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade</value>
</data>
<data name="Localhost.Text" xml:space="preserve">
<value>You Cannot Perform A System Update In A Development Environment</value>
</data>
<data name="Disclaimer.Text" xml:space="preserve">
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. Also Note That The System Update Capability Is Not Recommended When Using Microsoft Azure Due To Environmental Limitations.</value>
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process.</value>
</data>
<data name="Backup.Text" xml:space="preserve">
<value>Backup Files?</value>
</data>
<data name="Backup.HelpText" xml:space="preserve">
<value>Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade.</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
@@ -121,10 +121,10 @@
<value>Redirect To:</value>
</data>
<data name="MappedUrl.HelpText" xml:space="preserve">
<value>A relative or absolute Url where the user will be redirected</value>
<value>A Url where the user will be redirected (absolute or relative). Use '/' for site root path.</value>
</data>
<data name="Url.HelpText" xml:space="preserve">
<value>An absolute Url identifying a path to a specific page in the site</value>
<value>A Url identifying a path to a specific page in the site (absolute or relative)</value>
</data>
<data name="Url.Text" xml:space="preserve">
<value>Url:</value>
@@ -141,4 +141,7 @@
<data name="Message.SaveUrlMapping" xml:space="preserve">
<value>The Url must belong to the current site</value>
</data>
<data name="Generate" xml:space="preserve">
<value>Generate</value>
</data>
</root>

View File

@@ -121,10 +121,10 @@
<value>Redirect To:</value>
</data>
<data name="MappedUrl.HelpText" xml:space="preserve">
<value>A relative or absolute Url where the user will be redirected</value>
<value>A Url where the user will be redirected (absolute or relative). Use '/' for site root path.</value>
</data>
<data name="Url.HelpText" xml:space="preserve">
<value>A relative Url identifying a path to a specific page in the site</value>
<value>A Url identifying a path to a specific page in the site (absolute or relative)</value>
</data>
<data name="Url.Text" xml:space="preserve">
<value>Url:</value>

View File

@@ -147,9 +147,6 @@
<data name="Message.User.NoLogIn" xml:space="preserve">
<value>Current User Is Not Logged In</value>
</data>
<data name="Message.User.NoEmail" xml:space="preserve">
<value>You Must Provide An Email Address For Your User Account</value>
</data>
<data name="Error.Profile.Load" xml:space="preserve">
<value>Error Loading User Profile</value>
</data>
@@ -246,4 +243,10 @@
<data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>Your time zone</value>
</data>
</root>

View File

@@ -156,4 +156,16 @@
<data name="Notify.Text" xml:space="preserve">
<value>Notify?</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
<data name="Confirmed.Text" xml:space="preserve">
<value>Verified?</value>
</data>
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
</root>

View File

@@ -210,4 +210,16 @@
<data name="Error.User.Impersonate" xml:space="preserve">
<value>Unable To Impersonate User</value>
</data>
<data name="TimeZone.Text" xml:space="preserve">
<value>Time Zone:</value>
</data>
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
<data name="Confirmed.Text" xml:space="preserve">
<value>Verified?</value>
</data>
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
</root>

View File

@@ -132,6 +132,18 @@
<data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow Registration? </value>
</data>
<data name="RegisterUrl.HelpText" xml:space="preserve">
<value>Optionally provide a custom registration url</value>
</data>
<data name="RegisterUrl.Text" xml:space="preserve">
<value>Register Url: </value>
</data>
<data name="ProfileUrl.HelpText" xml:space="preserve">
<value>Optionally provide a custom user profile url</value>
</data>
<data name="ProfileUrl.Text" xml:space="preserve">
<value>Profile Url: </value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
@@ -208,7 +220,7 @@
<value>Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site.</value>
</data>
<data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Login?</value>
<value>Allow Local Login?</value>
</data>
<data name="Authority.HelpText" xml:space="preserve">
<value>The authority url or issuer url associated with the identity provider</value>
@@ -358,7 +370,13 @@
<value>Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value>
<value>Two Factor Authentication?</value>
</data>
<data name="RequireConfirmedEmail.HelpText" xml:space="preserve">
<value>Do you want to require registered users to verify their email address before they are allowed to log in?</value>
</data>
<data name="RequireConfirmedEmail.Text" xml:space="preserve">
<value>Require Verified Email?</value>
</data>
<data name="Disabled" xml:space="preserve">
<value>Disabled</value>
@@ -490,7 +508,7 @@
<value>Info</value>
</data>
<data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0</value>
<value>OAuth 2.0 (OAuth2)</value>
</data>
<data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value>
@@ -507,4 +525,16 @@
<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>
<data name="AllowHostRole.Text" xml:space="preserve">
<value>Allow Host Role?</value>
</data>
<data name="AllowHostRole.HelpText" xml:space="preserve">
<value>Indicate if host roles are supported from the identity provider. Please use caution with this option as it allows the host user to administrate every site within your installation.</value>
</data>
</root>

View File

@@ -198,4 +198,13 @@
<data name="Duration.Text" xml:space="preserve">
<value>Session Duration:</value>
</data>
<data name="Robots.Heading" xml:space="preserve">
<value>Robots.txt</value>
</data>
<data name="Robots.Text" xml:space="preserve">
<value>Instructions:</value>
</data>
<data name="Robots.HelpText" xml:space="preserve">
<value>Specify your robots.txt instructions to provide bots with guidance on which parts of your site should be indexed</value>
</data>
</root>

View File

@@ -123,4 +123,7 @@
<data name="PermissionDenied" xml:space="preserve">
<value>Permission Denied</value>
</data>
<data name="PermissionDisabled" xml:space="preserve">
<value>Permission Disabled</value>
</data>
</root>

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

@@ -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,120 @@
<?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>
</root>

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

@@ -42,14 +42,6 @@ namespace Oqtane.Services
return await PutJsonAsync<Folder>($"{ApiUrl}/{folder.FolderId}", folder);
}
public async Task UpdateFolderOrderAsync(int siteId, int folderId, int? parentId)
{
var parent = parentId == null
? string.Empty
: parentId.ToString();
await PutAsync($"{ApiUrl}/?siteid={siteId}&folderid={folderId}&parentid={parent}");
}
public async Task DeleteFolderAsync(int folderId)
{
await DeleteAsync($"{ApiUrl}/{folderId}");

View File

@@ -47,9 +47,9 @@ namespace Oqtane.Services
return await PostJsonAsync<InstallConfig,Installation>(ApiUrl, config);
}
public async Task<Installation> Upgrade()
public async Task<Installation> Upgrade(bool backup)
{
return await GetJsonAsync<Installation>($"{ApiUrl}/upgrade");
return await GetJsonAsync<Installation>($"{ApiUrl}/upgrade/?backup={backup}");
}
public async Task RestartAsync()

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

@@ -39,15 +39,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<Folder> UpdateFolderAsync(Folder folder);
/// <summary>
/// Update the internal Folder-Order within the list of Folders.
/// </summary>
/// <param name="siteId">Reference to the <see cref="Site"/></param>
/// <param name="folderId">Reference to a <see cref="Folder"/> for the security check</param>
/// <param name="parentId">Reference to the Parent <see cref="Folder"/> or null - this Folders children will be re-sorted.</param>
/// <returns></returns>
Task UpdateFolderOrderAsync(int siteId, int folderId, int? parentId);
/// <summary>
/// Delete a <see cref="Folder"/>
/// </summary>

View File

@@ -26,8 +26,9 @@ namespace Oqtane.Services
/// <summary>
/// Starts the upgrade process
/// </summary>
/// <param name="backup">indicates if files should be backed up during upgrade</param>
/// <returns>internal status/message object</returns>
Task<Installation> Upgrade();
Task<Installation> Upgrade(bool backup);
/// <summary>
/// Restarts the installation

View File

@@ -10,10 +10,11 @@ namespace Oqtane.Services
public interface IJobLogService
{
/// <summary>
/// Return a list of all <see cref="JobLog"/> entries
/// Return a list of <see cref="JobLog"/> entries
/// </summary>
/// <param name="jobId"></param>
/// <returns></returns>
Task<List<JobLog>> GetJobLogsAsync();
Task<List<JobLog>> GetJobLogsAsync(int jobId);
/// <summary>
/// Return a <see cref="JobLog"/> entry for the given Id

View File

@@ -56,7 +56,18 @@ namespace Oqtane.Services
/// Exports a given module
/// </summary>
/// <param name="moduleId"></param>
/// <returns>module in JSON</returns>
/// <param name="pageId"></param>
/// <returns>module content in JSON format</returns>
Task<string> ExportModuleAsync(int moduleId, int pageId);
/// <summary>
/// Exports a given module
/// </summary>
/// <param name="moduleId"></param>
/// <param name="pageId"></param>
/// <param name="folderId"></param>
/// <param name="filename"></param>
/// <returns>file id</returns>
Task<int> ExportModuleAsync(int moduleId, int pageId, int folderId, string filename);
}
}

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

@@ -256,7 +256,7 @@ namespace Oqtane.Services
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPrivate);
Dictionary<string, string> MergeSettings(Dictionary<string, string> settings1, Dictionary<string, string> settings2);
Dictionary<string, string> MergeSettings(Dictionary<string, string> baseSettings, Dictionary<string, string> overwriteSettings);
[Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)]

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Services
{
/// <summary>
/// Service to retrieve <see cref="TimeZone"/> entries
/// </summary>
public interface ITimeZoneService
{
/// <summary>
/// Get the list of time zones
/// </summary>
/// <returns></returns>
List<TimeZone> GetTimeZones();
}
}

View File

@@ -15,10 +15,9 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("JobLog");
public async Task<List<JobLog>> GetJobLogsAsync()
public async Task<List<JobLog>> GetJobLogsAsync(int jobId)
{
List<JobLog> joblogs = await GetJsonAsync<List<JobLog>>(Apiurl);
return joblogs.OrderBy(item => item.StartDate).ToList();
return await GetJsonAsync<List<JobLog>>($"{Apiurl}?jobid={jobId}");
}
public async Task<JobLog> GetJobLogAsync(int jobLogId)

View File

@@ -47,8 +47,13 @@ namespace Oqtane.Services
}
public async Task<string> ExportModuleAsync(int moduleId, int pageId)
{
{
return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}");
}
public async Task<int> ExportModuleAsync(int moduleId, int pageId, int folderId, string filename)
{
return await PostJsonAsync<string,int>($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null);
}
}
}

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

@@ -266,27 +266,25 @@ namespace Oqtane.Services
return settings;
}
public Dictionary<string, string> MergeSettings(Dictionary<string, string> settings1, Dictionary<string, string> settings2)
public Dictionary<string, string> MergeSettings(Dictionary<string, string> baseSettings, Dictionary<string, string> overwriteSettings)
{
if (settings1 == null)
var settings = baseSettings != null ? new Dictionary<string, string>(baseSettings) : new Dictionary<string, string>();
if (overwriteSettings != null)
{
settings1 = new Dictionary<string, string>();
}
if (settings2 != null)
{
foreach (var setting in settings2)
foreach (var setting in overwriteSettings)
{
if (settings1.ContainsKey(setting.Key))
if (settings.ContainsKey(setting.Key))
{
settings1[setting.Key] = setting.Value;
settings[setting.Key] = setting.Value;
}
else
{
settings1.Add(setting.Key, setting.Value);
settings.Add(setting.Key, setting.Value);
}
}
}
return settings1;
return settings;
}
[Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)]

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Localization;
using NodaTime.TimeZones;
using NodaTime;
using Oqtane.Documentation;
using NodaTime.Extensions;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class TimeZoneService : ITimeZoneService
{
private readonly IStringLocalizer<TimeZoneResources> _TimeZoneLocalizer;
public TimeZoneService(IStringLocalizer<TimeZoneResources> TimeZoneLocalizer)
{
_TimeZoneLocalizer = TimeZoneLocalizer;
}
public List<Models.TimeZone> GetTimeZones()
{
var timezones = new List<Models.TimeZone>();
foreach (var tz in DateTimeZoneProviders.Tzdb.GetAllZones()
// only include timezones which have a country code defined or are US timezones
.Where(item => !string.IsNullOrEmpty(TzdbDateTimeZoneSource.Default.ZoneLocations.FirstOrDefault(l => l.ZoneId == item.Id)?.CountryCode) || item.Id.ToLower().Contains("us/"))
// order by UTC offset (ie. -11:00 to +14:00)
.OrderBy(item => item.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks))
{
// get localized display name
var displayname = _TimeZoneLocalizer[tz.Id].Value;
if (displayname == tz.Id)
{
// use default "friendly" display format
displayname = displayname.Replace("_", " ").Replace("/", " / ");
}
// time zones can be excluded from the list by providing an empty translation in the localization file
if (!string.IsNullOrEmpty(displayname))
{
// include offset prefix
var offset = tz.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks;
displayname = "(UTC" + (offset >= 0 ? "+" : "-") + new DateTime(Math.Abs(offset)).ToString("HH:mm") + ") " + displayname;
timezones.Add(new Models.TimeZone()
{
Id = tz.Id,
DisplayName = displayname
});
}
}
return timezones;
}
}
}

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 {
@@ -37,7 +37,7 @@
public override List<Resource> Resources => new List<Resource>()
{
// obtained from https://cdnjs.com/libraries
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", "anonymous"),
new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"),
new Stylesheet(ThemePath() + "Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
};

View File

@@ -20,6 +20,7 @@
protected override void OnParametersSet()
{
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// only include properties required by the ModuleActionsInteractive component
_pageState = new PageState
{
Alias = PageState.Alias,

View File

@@ -91,6 +91,7 @@
}
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// only include properties required by the ControlPanelInteractive component
_pageState = new PageState
{
Alias = PageState.Alias,

View File

@@ -353,7 +353,7 @@
module.PageId = PageState.Page.PageId;
module.ModuleDefinitionName = _moduleDefinitionName;
module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId);
module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName);
module = await ModuleService.AddModuleAsync(module);
newModuleId = module.ModuleId;
@@ -365,7 +365,7 @@
module.SiteId = PageState.Page.SiteId;
module.PageId = PageState.Page.PageId;
module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId);
module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName);
module = await ModuleService.AddModuleAsync(module);
var moduleContent = await ModuleService.ExportModuleAsync(int.Parse(_moduleId), PageState.Page.PageId);
@@ -430,9 +430,11 @@
}
}
private List<Permission> GenerateDefaultPermissions(int siteId)
private List<Permission> GenerateDefaultPermissions(int siteId, string moduleDefinitionName)
{
var permissions = new List<Permission>();
// set module view permissions
if (_visibility == "view")
{
// set module view permissions to page view permissions
@@ -443,8 +445,22 @@
// set module view permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit);
}
// set module edit permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.Edit, PermissionNames.Edit);
// set remaining module permissions
var permissionNames = PermissionNames.Edit;
var moduleDefinition = _allModuleDefinitions.FirstOrDefault(item => item.ModuleDefinitionName == moduleDefinitionName);
if (moduleDefinition != null && !string.IsNullOrEmpty(moduleDefinition.PermissionNames))
{
permissionNames = moduleDefinition.PermissionNames; // custom module permissions
}
foreach (var permission in permissionNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (permission != PermissionNames.View)
{
// set module permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, permission, PermissionNames.Edit);
}
}
return permissions;
}
@@ -573,7 +589,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,170 @@
@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 ? "px-0 py-3 pt-5 pt-sm-3 pe-sm-5 ps-sm-3" : "p-0") 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);
if (!_enabled) return;
_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

@@ -15,6 +15,7 @@
<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" />
<input type="hidden" name="everywhere" value="@everywhere" />
<button type="submit" class="@CssClass">@Localizer["Logout"]</button>
</form>
}

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

@@ -1,20 +1,20 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@inherits ThemeControlBase
@inject ISettingService SettingService
@inject IStringLocalizer<UserProfile> Localizer
@inject NavigationManager NavigationManager
<span class="app-profile">
@if (PageState.User != null)
{
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="@CssClass">@PageState.User.Username</a>
<a href="@_profileurl" class="@CssClass">@PageState.User.Username</a>
}
else
{
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="@CssClass">@Localizer["Register"]</a>
<a href="@_registerurl" class="@CssClass">@Localizer["Register"]</a>
}
}
</span>
@@ -23,23 +23,68 @@
[Parameter]
public bool ShowRegister { get; set; }
[Parameter]
public string CssClass { get; set; } = "btn btn-primary";
[Parameter]
public string RegisterUrl { get; set; } // optional parameter to specify a custom registration url
[Parameter]
public string ProfileUrl { get; set; } // optional parameter to specify a custom user profile url
private string _registerurl = "";
private string _profileurl = "";
private string _returnurl = "";
protected override void OnParametersSet()
{
if (!PageState.QueryString.ContainsKey("returnurl"))
{
// remember current url
_returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
}
else
{
// use existing value
_returnurl = PageState.QueryString["returnurl"];
}
{
if (!PageState.QueryString.ContainsKey("returnurl"))
{
// remember current url
_returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
}
else
{
// use existing value
_returnurl = PageState.QueryString["returnurl"];
}
if (!string.IsNullOrEmpty(RegisterUrl))
{
_registerurl = RegisterUrl;
_registerurl += (!_registerurl.Contains("?") ? "?" : "&") + "returnurl=" + (_registerurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
{
_registerurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "");
_registerurl += (!_registerurl.Contains("?") ? "?" : "&") + "returnurl=" + (_registerurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
_registerurl = NavigateUrl("register", "returnurl=" + _returnurl);
}
}
if (!string.IsNullOrEmpty(ProfileUrl))
{
_profileurl = ProfileUrl;
_profileurl += (!_profileurl.Contains("?") ? "?" : "&") + "returnurl=" + (_profileurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:ProfileUrl", "")))
{
_profileurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:ProfileUrl", "");
_profileurl += (!_profileurl.Contains("?") ? "?" : "&") + "returnurl=" + (_profileurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
_profileurl = NavigateUrl("profile", "returnurl=" + _returnurl);
}
}
}
}

View File

@@ -16,8 +16,8 @@ namespace Oqtane.Themes.OqtaneTheme
ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client",
Resources = new List<Resource>()
{
// obtained from https://cdnjs.com/libraries
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", "anonymous"),
// obtained from https://cdnjs.com/libraries/bootswatch
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.7/cyborg/bootstrap.min.css", "sha512-/LQFzDeXqysGQ/POl5YOEjgVZH1BmqDHvshhnFIChf50bMGQ470qhUrsecD9MRCUwzwqRoshwAbmA2oTW4I6Yg==", "anonymous"),
new Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}

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>
@@ -92,7 +92,7 @@
settings = SettingService.MergeSettings(PageState.Site.Settings, settings);
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "-");
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "-");
_footer = SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "-");
_footer = SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "-");
}
await Task.Yield();
}

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