Compare commits

..

247 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
164 changed files with 2940 additions and 1518 deletions

View File

@@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISyncService, SyncService>(); services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>(); services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>(); services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IOutputCacheService, OutputCacheService>(); services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers // providers

View File

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

View File

@@ -53,7 +53,7 @@ else
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td> <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><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><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>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td> <td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row> </Row>

View File

@@ -56,7 +56,7 @@
<input id="starting" type="date" class="form-control" @bind="@_startDate" /> <input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div> </div>
<div class="col"> <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> </div>
</div> </div>
@@ -69,7 +69,7 @@
<input id="ending" type="date" class="form-control" @bind="@_endDate" /> <input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div> </div>
<div class="col"> <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> </div>
</div> </div>
@@ -82,7 +82,7 @@
<input id="next" type="date" class="form-control" @bind="@_nextDate" /> <input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div> </div>
<div class="col"> <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> </div>
</div> </div>
@@ -132,13 +132,13 @@
_isEnabled = job.IsEnabled.ToString(); _isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString(); _interval = job.Interval.ToString();
_frequency = job.Frequency; _frequency = job.Frequency;
_startDate = Utilities.UtcAsLocalDate(job.StartDate); _startDate = UtcToLocal(job.StartDate);
_startTime = Utilities.UtcAsLocalDateTime(job.StartDate); _startTime = UtcToLocal(job.StartDate);
_endDate = Utilities.UtcAsLocalDate(job.EndDate); _endDate = UtcToLocal(job.EndDate);
_endTime = Utilities.UtcAsLocalDateTime(job.EndDate); _endTime = UtcToLocal(job.EndDate);
_retentionHistory = job.RetentionHistory.ToString(); _retentionHistory = job.RetentionHistory.ToString();
_nextDate = Utilities.UtcAsLocalDate(job.NextExecution); _nextDate = UtcToLocal(job.NextExecution);
_nextTime = Utilities.UtcAsLocalDateTime(job.NextExecution); _nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
@@ -176,10 +176,18 @@
{ {
job.Interval = int.Parse(_interval); job.Interval = int.Parse(_interval);
} }
job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime); job.StartDate = _startDate.HasValue && _startTime.HasValue
job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime); ? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
job.RetentionHistory = int.Parse(_retentionHistory); : null;
job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime);
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 try
{ {
@@ -198,5 +206,4 @@
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
} }
} }
} }

View File

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

View File

@@ -23,8 +23,8 @@ else
<Row> <Row>
<td>@context.Job.Name</td> <td>@context.Job.Name</td>
<td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td> <td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td>
<td>@context.StartDate</td> <td>@UtcToLocal(context.StartDate)</td>
<td>@context.FinishDate</td> <td>@UtcToLocal(context.FinishDate)</td>
</Row> </Row>
<Detail> <Detail>
<td colspan="4">@((MarkupString)context.Notes)</td> <td colspan="4">@((MarkupString)context.Notes)</td>

View File

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

View File

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

View File

@@ -64,7 +64,7 @@ else
</Header> </Header>
<Row> <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)"><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.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td> <td class="@GetClass(context.Function)">@context.Feature</td>
<td class="@GetClass(context.Function)">@context.Function</td> <td class="@GetClass(context.Function)">@context.Function</td>

View File

@@ -17,8 +17,8 @@ else
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<div class="col-sm-6"> <div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" /> <ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ps-2" /> <ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button> <button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))"> <select class="form-select" @onchange="(e => CategoryChanged(e))">

View File

@@ -5,24 +5,57 @@
@inject IStringLocalizer<Export> Localizer @inject IStringLocalizer<Export> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container"> <TabStrip>
<div class="row mb-1 align-items-center"> <TabPanel Name="Content" Heading="Content" ResourceKey="Content">
<Label Class="col-sm-3" For="content" HelpText="The Exported Module Content" ResourceKey="Content">Content: </Label> <div class="container">
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea> <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 />
</div> <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 { @code {
private string _content = string.Empty; private string _content = string.Empty;
private FileManager _filemanager;
private string _filename = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content"; public override string Title => "Export Content";
private async Task ExportModule() protected override void OnInitialized()
{
_filename = Utilities.GetFriendlyUrl(ModuleState.Title);
}
private async Task ExportText()
{ {
try try
{ {
@@ -35,4 +68,34 @@
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); 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 @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IModuleService ModuleService @inject IModuleService ModuleService
@inject IFileService FileService
@inject IStringLocalizer<Import> Localizer @inject IStringLocalizer<Import> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea> <textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea>
</div> </div>
</div> </div>
</div> </div>
<br />
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button> <button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</form> </form>
@@ -28,6 +35,12 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content"; 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() private async Task ImportModule()
{ {
validated = true; validated = true;

View File

@@ -97,6 +97,23 @@
</div> </div>
</div> </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>
<TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions"> <TabPanel Name="Permissions" Heading="Permissions" ResourceKey="Permissions">
@@ -144,6 +161,8 @@
private string _pane; private string _pane;
private string _containerType; private string _containerType;
private string _allPages = "false"; private string _allPages = "false";
private string _header = "";
private string _footer = "";
private string _permissionNames = ""; private string _permissionNames = "";
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private string _pageId; private string _pageId;
@@ -167,37 +186,47 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
SetModuleTitle(Localizer["ModuleSettings.Title"]); SetModuleTitle(Localizer["ModuleSettings.Title"]);
_title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"]; _moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); _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); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (ModuleState.ModuleDefinition != null) var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
{
_module = ModuleState.ModuleDefinition.Name;
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
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 // module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType); _moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.SettingsType);
} }
else else
{ {
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module ) // 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) if (_moduleSettingsType != null)
{ {
@@ -218,7 +247,7 @@
} }
else 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))); var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
@@ -270,10 +299,12 @@
{ {
pagemodule.ContainerType = string.Empty; pagemodule.ContainerType = string.Empty;
} }
pagemodule.Header = _header;
pagemodule.Footer = _footer;
await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState; var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
module.AllPages = bool.Parse(_allPages); module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId; module.PageModuleId = ModuleState.PageModuleId;
module.PermissionList = _permissionGrid.GetPermissionList(); module.PermissionList = _permissionGrid.GetPermissionList();

View File

@@ -30,16 +30,16 @@
<div class="row mb-1 align-items-center"> <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> <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"> <div class="col-sm-9">
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required> <select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option> <option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pages) @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> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -217,6 +217,9 @@
</div> </div>
</Section> </Section>
<br /> <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 /> <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>
</TabPanel> </TabPanel>
@@ -225,15 +228,28 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </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> </div>
</TabPanel> </TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules"> <TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
<Pager Items="_pageModules"> <Pager Items="_pageModules">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th> <th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th> <th>@Localizer["ModuleDefinition"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td> <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"> <TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent @_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> </TabPanel>
<br />
} }
</TabStrip> </TabStrip>
} }
@@ -299,19 +317,21 @@
</div> </div>
</div> </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> </TabPanel>
@if (_themeSettingsType != null) @if (_themeSettingsType != null)
{ {
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings"> <TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@_themeSettingsComponent @_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> </TabPanel>
<br />
} }
</TabStrip> </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> </form>
} }
@@ -348,6 +368,7 @@
private string _bodycontent; private string _bodycontent;
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private string _updatemodulepermissions;
private List<Module> _pageModules; private List<Module> _pageModules;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@@ -436,6 +457,7 @@
// permissions // permissions
_permissions = _page.PermissionList; _permissions = _page.PermissionList;
_updatemodulepermissions = "True";
// page modules // page modules
var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId);
@@ -651,6 +673,7 @@
if (_page.UserId == null) if (_page.UserId == null)
{ {
_page.PermissionList = _permissionGrid.GetPermissionList(); _page.PermissionList = _permissionGrid.GetPermissionList();
_page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions);
} }
_page = await PageService.UpdatePageAsync(_page); _page = await PageService.UpdatePageAsync(_page);

View File

@@ -6,77 +6,95 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService @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" /> if (PageState.User != null)
} {
else <ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
{ }
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" /> else
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> {
<div class="container"> <ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="row mb-1 align-items-center"> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<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="container">
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required /> <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> <div class="row mb-1 align-items-center">
<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>
<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="col-sm-9"> <div class="input-group">
<div class="input-group"> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<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>
<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>
</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 /> <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 /> <br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink> <NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
} }
</form> </form>
}
} }
} }
} else
else {
{ <ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" /> }
} }
@code { @code {
private bool _initialized = false;
private List<Models.TimeZone> _timezones;
private string _passwordrequirements; private string _passwordrequirements;
private string _username = string.Empty; private string _username = string.Empty;
private ElementReference form; private ElementReference form;
@@ -87,6 +105,7 @@ else
private string _confirm = string.Empty; private string _confirm = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private bool _userCreated = false; private bool _userCreated = false;
private bool _allowsitelogin = true; private bool _allowsitelogin = true;
@@ -96,6 +115,9 @@ else
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
} }
protected override void OnParametersSet() protected override void OnParametersSet()
@@ -124,6 +146,7 @@ else
Password = _password, Password = _password,
Email = _email, Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname), DisplayName = (_displayname == string.Empty ? _username : _displayname),
TimeZoneId = _timezoneid,
PhotoFileId = null PhotoFileId = null
}; };
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);

View File

@@ -46,7 +46,7 @@
<Row> <Row>
<div class="search-item mb-2"> <div class="search-item mb-2">
<h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4> <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> </div>
</Row> </Row>
</Pager> </Pager>

View File

@@ -10,6 +10,7 @@
@inject IAliasService AliasService @inject IAliasService AliasService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@@ -41,6 +42,18 @@
</select> </select>
</div> </div>
</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"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label> <Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -133,7 +146,7 @@
<div class="row mb-1 align-items-center"> <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> <Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@_imageFiles" @ref="_logofilemanager" /> <FileManager FileId="@_logofileid" Filter="@_imagefiles" @ref="_logofilemanager" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -181,80 +194,125 @@
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings"> <Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col-sm-3"> <Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SmtpEnabled">Enabled? </Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br /> <select id="smtpenabled" class="form-select" value="@_smtpenabled" @onchange="(e => SMTPEnabledChanged(e))">
</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" >
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> @if (_smtpenabled == "True")
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label> {
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/> <div class="col-sm-3">
</div> </div>
</div> <div class="col-sm-9">
<div class="row mb-1 align-items-center"> <strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
<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>
</div> <div class="row mb-1 align-items-center">
<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>
<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">
<div class="col-sm-9"> <input id="host" class="form-control" @bind="@_smtphost" />
<input id="sender" class="form-control" @bind="@_smtpsender" /> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<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>
<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">
<div class="col-sm-9"> <input id="port" class="form-control" @bind="@_smtpport" />
<select id="relay" class="form-select" @bind="@_smtprelay" required> </div>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<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>
<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">
<div class="col-sm-9"> <select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<select id="smtpenabled" class="form-select" @bind="@_smtpenabled"> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="False">@SharedLocalizer["No"]</option>
<option value="False">@SharedLocalizer["No"]</option> </select>
</select> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<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>
<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">
<div class="col-sm-9"> <select id="smtpauthentication" class="form-select" value="@_smtpauthentication" @onchange="(e => SMTPAuthenticationChanged(e))">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" /> <option value="Basic">@Localizer["Basic"]</option>
<option value="OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div> </div>
</div> @if (_smtpauthentication == "Basic")
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button> {
<br /><br /> <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> </div>
</Section> </Section>
<Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings"> <Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings">
@@ -416,9 +474,11 @@
private List<ThemeControl> _themes = new List<ThemeControl>(); private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>(); private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pages; private List<Page> _pages;
private List<Models.TimeZone> _timezones;
private string _name = string.Empty; private string _name = string.Empty;
private string _homepageid = "-"; private string _homepageid = "-";
private string _timezoneid = string.Empty;
private string _isdeleted; private string _isdeleted;
private string _sitemap = ""; private string _sitemap = "";
private string _siteguid = ""; private string _siteguid = "";
@@ -435,21 +495,28 @@
private Dictionary<string, string> _textEditors = new Dictionary<string, string>(); private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = ""; private string _textEditor = "";
private string _imageFiles = string.Empty; private string _imagefiles = string.Empty;
private string _headcontent = string.Empty; private string _headcontent = string.Empty;
private string _bodycontent = string.Empty; private string _bodycontent = string.Empty;
private string _smtpenabled = "False";
private string _smtpauthentication = "Basic";
private string _smtphost = string.Empty; private string _smtphost = string.Empty;
private string _smtpport = string.Empty; private string _smtpport = string.Empty;
private string _smtpssl = "False"; private string _smtpssl = "True";
private string _smtpusername = string.Empty; private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty; private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password"; private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty; 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 _smtpsender = string.Empty;
private string _smtprelay = "False"; private string _smtprelay = "False";
private string _smtpenabled = "True";
private int _retention = 30; private int _retention = 30;
private string _pwaisenabled; private string _pwaisenabled;
@@ -493,11 +560,13 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null) if (site != null)
{ {
_timezones = TimeZoneService.GetTimeZones();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
_name = site.Name; _name = site.Name;
_timezoneid = site.TimeZoneId;
if (site.HomePageId != null) if (site.HomePageId != null)
{ {
_homepageid = site.HomePageId.Value.ToString(); _homepageid = site.HomePageId.Value.ToString();
@@ -531,23 +600,29 @@
_textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName)); _textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName));
} }
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor); _textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); _imagefiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
// page content // page content
_headcontent = site.HeadContent; _headcontent = site.HeadContent;
_bodycontent = site.BodyContent; _bodycontent = site.BodyContent;
// SMTP // SMTP
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False");
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty); _smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty); _smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpauthentication = SettingService.GetSetting(settings, "SMTPAuthentication", "Basic");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"]; _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); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False"); _smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
// PWA // PWA
@@ -650,6 +725,7 @@
if (site != null) if (site != null)
{ {
site.Name = _name; site.Name = _name;
site.TimeZoneId = _timezoneid;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
@@ -727,8 +803,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, 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, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, 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, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true); settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true); settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
@@ -795,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() private async Task SendEmail()
{ {
if (_smtphost != "" && _smtpport != "" && _smtpsender != "") if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
@@ -805,8 +926,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, 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, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, 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, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");
@@ -827,20 +953,6 @@
} }
} }
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases() private async Task GetAliases()
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@@ -15,8 +15,8 @@
else else
{ {
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" /> <ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ps-2" /> <ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button> <button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<Pager Items="@_themes"> <Pager Items="@_themes">
<Header> <Header>

View File

@@ -15,7 +15,7 @@
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <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 result in a better experience in some environments." ResourceKey="Backup">Backup Files? </Label> <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"> <div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup"> <select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
@@ -24,10 +24,15 @@
</div> </div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button> <br />
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button> @if (!_downloaded)
<br /><br /> {
<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>
}
else
{
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
}
} }
else else
{ {
@@ -43,7 +48,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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 result in a better experience in some environments." ResourceKey="Backup">Backup Files? </Label> <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"> <div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup"> <select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
@@ -52,15 +57,15 @@
</div> </div>
</div> </div>
</div> </div>
<br />
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button> <button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
<br /><br />
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
} }
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private bool _downloaded = false;
private Package _package; private Package _package;
private bool _upgradeavailable = false; private bool _upgradeavailable = false;
private string _backup = "True"; private string _backup = "True";
@@ -125,6 +130,7 @@
ShowProgressIndicator(); ShowProgressIndicator();
await PackageService.DownloadPackageAsync(packageid, version); await PackageService.DownloadPackageAsync(packageid, version);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version); await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version);
_downloaded = true;
HideProgressIndicator(); HideProgressIndicator();
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success); AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
} }

View File

@@ -8,13 +8,16 @@
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <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"> <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> </div>
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required /> <input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div> </div>
@@ -26,64 +29,80 @@
</form> </form>
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _url = string.Empty; private string _url = string.Empty;
private string _mappedurl = string.Empty; private string _mappedurl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private async Task SaveUrlMapping() private async Task SaveUrlMapping()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
if (_url != _mappedurl) if (_url != _mappedurl)
{ {
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url; _url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url; _url = (!_url.StartsWith("http")) ? url + _url : _url;
if (_url.StartsWith(url)) _mappedurl = _mappedurl.Replace(url, "");
{ _mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl;
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;
try if (_url.StartsWith(url))
{ {
urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping); var urlmapping = new UrlMapping();
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping); urlmapping.SiteId = PageState.Site.SiteId;
NavigationManager.NavigateTo(NavigateUrl()); urlmapping.Url = new Route(_url, PageState.Alias.Path).PagePath;
} urlmapping.MappedUrl = _mappedurl;
catch (Exception ex) urlmapping.Requests = 0;
{ urlmapping.CreatedOn = DateTime.UtcNow;
await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message); urlmapping.RequestedOn = DateTime.UtcNow;
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
} try
} {
else urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping);
{ await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning); NavigationManager.NavigateTo(NavigateUrl());
} }
} catch (Exception ex)
else {
{ 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); AddModuleMessage(Localizer["Message.DuplicateUrlMapping"], MessageType.Warning);
} }
} }
else else
{ {
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); 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> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" readonly /> <input id="url" class="form-control" @bind="@_url" maxlength="500" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required /> <input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div> </div>
@@ -67,8 +67,11 @@
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); 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); var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid);
urlmapping.MappedUrl = _mappedurl.Replace(url, ""); urlmapping.MappedUrl = _mappedurl;
urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping); urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping); await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

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

View File

@@ -9,6 +9,7 @@
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IFileService FileService @inject IFileService FileService
@inject IFolderService FolderService @inject IFolderService FolderService
@inject ITimeZoneService TimeZoneService
@inject IJSRuntime jsRuntime @inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@@ -16,9 +17,9 @@
@if (_initialized) @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 else
{ {
@@ -31,7 +32,7 @@
<div class="row mb-1 align-items-center"> <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> <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"> <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> </div>
<div class="row mb-1 align-items-center"> <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> <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="col-sm-9">
<div class="input-group"> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@if (allowtwofactor) @if (_allowtwofactor)
{ {
<div class="row mb-1 align-items-center"> <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> <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"> <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="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
@@ -67,19 +68,31 @@
<div class="row mb-1 align-items-center"> <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> <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"> <div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label> <Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" /> <input id="displayname" class="form-control" @bind="@_displayname" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <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> </div>
</div> </div>
@@ -91,17 +104,17 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
@p.Category @p.Category
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@@ -150,12 +163,12 @@
@if (p.IsRequired) @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" <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 else
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" autocomplete="@p.Autocomplete" <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 else
@@ -163,12 +176,12 @@
@if (p.IsRequired) @if (p.IsRequired)
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" <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 else
{ {
<input id="@p.Name" class="form-control" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" <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) @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" <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 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" <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 else
@@ -192,12 +205,12 @@
@if (p.IsRequired) @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))" <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 else
{ {
<textarea id="@p.Name" class="form-control" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" <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> <option value="from">@Localizer["Items.Sent"]</option>
</select> </select>
<br /> <br />
@if (filter == "to") @if (_filter == "to")
{ {
@if (notifications.Any()) @if (_notifications.Any())
{ {
<Pager Items="@notifications"> <Pager Items="@_notifications">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<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("\n", "");
context.Body = context.Body.Replace("\r", ""); 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) @if (context.IsRead)
{ {
@notificationSummary @_notificationSummary
} }
else else
{ {
<b>@notificationSummary</b> <b>@_notificationSummary</b>
} }
</td> </td>
</Detail> </Detail>
@@ -285,9 +298,9 @@
} }
else else
{ {
@if (notifications.Any()) @if (_notifications.Any())
{ {
<Pager Items="@notifications"> <Pager Items="@_notifications">
<Header> <Header>
<th style="width: 1px;"></th> <th style="width: 1px;"></th>
<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("\n", "");
context.Body = context.Body.Replace("\r", ""); 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) @if (context.IsRead)
{ {
@notificationSummary @_notificationSummary
} }
else else
{ {
<b>@notificationSummary</b> <b>@_notificationSummary</b>
} }
</td> </td>
</Detail> </Detail>
@@ -356,27 +369,30 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private string _passwordrequirements; private string _passwordrequirements;
private string username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password"; private string _passwordtype = "password";
private string _togglepassword = string.Empty; private string _togglepassword = string.Empty;
private string confirm = string.Empty; private string _confirm = string.Empty;
private bool allowtwofactor = false; private bool _allowtwofactor = false;
private string twofactor = "False"; private string _twofactor = "False";
private string email = string.Empty; private string _email = string.Empty;
private string displayname = string.Empty; private string _displayname = string.Empty;
private FileManager filemanager; private FileManager _filemanager;
private int folderid = -1; private int _folderid = -1;
private int photofileid = -1; private List<Models.TimeZone> _timezones;
private File photo = null; private string _timezoneid = string.Empty;
private string _ImageFiles = string.Empty; private int _photofileid = -1;
private List<Profile> profiles; private File _photo = null;
private Dictionary<string, string> userSettings; private string _imagefiles = string.Empty;
private string category = string.Empty;
private string filter = "to"; private List<Profile> _profiles;
private List<Notification> notifications; private Dictionary<string, string> _userSettings;
private string notificationSummary = string.Empty; private string _category = string.Empty;
private string _filter = "to";
private List<Notification> _notifications;
private string _notificationSummary = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
@@ -386,43 +402,40 @@
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); _allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_timezones = TimeZoneService.GetTimeZones();
if (PageState.User != null) if (PageState.User != null)
{ {
username = PageState.User.Username; _username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString(); _twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email; _email = PageState.User.Email;
displayname = PageState.User.DisplayName; _displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
if (string.IsNullOrEmpty(email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
// get user folder // get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null) if (folder != null)
{ {
folderid = folder.FolderId; _folderid = folder.FolderId;
} }
if (PageState.User.PhotoFileId != null) if (PageState.User.PhotoFileId != null)
{ {
photofileid = PageState.User.PhotoFileId.Value; _photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); _photo = await FileService.GetFileAsync(_photofileid);
} }
else else
{ {
photofileid = -1; _photofileid = -1;
photo = null; _photo = null;
} }
userSettings = PageState.User.Settings; _userSettings = PageState.User.Settings;
var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_ImageFiles = SettingService.GetSetting(userSettings, "ImageFiles", Constants.ImageFiles); _imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles);
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles;
await LoadNotificationsAsync(); await LoadNotificationsAsync();
@@ -442,13 +455,13 @@
private async Task LoadNotificationsAsync() private async Task LoadNotificationsAsync()
{ {
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); _notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); _notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@@ -460,38 +473,39 @@
{ {
try try
{ {
if (username != string.Empty && email != string.Empty) if (_username != string.Empty && _email != string.Empty)
{ {
if (_password == confirm) if (_password == _confirm)
{ {
if (ValidateProfiles()) if (ValidateProfiles())
{ {
var user = PageState.User; var user = PageState.User;
user.Username = username; user.Username = _username;
user.Password = _password; user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor); user.TwoFactorRequired = bool.Parse(_twofactor);
user.Email = email; user.Email = _email;
user.DisplayName = (displayname == string.Empty ? username : displayname); user.DisplayName = (_displayname == string.Empty ? _username : _displayname);
user.PhotoFileId = filemanager.GetFileId(); user.TimeZoneId = _timezoneid;
user.PhotoFileId = _filemanager.GetFileId();
if (user.PhotoFileId == -1) if (user.PhotoFileId == -1)
{ {
user.PhotoFileId = null; user.PhotoFileId = null;
} }
if (user.PhotoFileId != null) if (user.PhotoFileId != null)
{ {
photofileid = user.PhotoFileId.Value; _photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid); _photo = await FileService.GetFileAsync(_photofileid);
} }
else else
{ {
photofileid = -1; _photofileid = -1;
photo = null; _photo = null;
} }
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(userSettings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(_userSettings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved"); await logger.LogInformation("User Profile Saved");
if (!string.IsNullOrEmpty(PageState.ReturnUrl)) if (!string.IsNullOrEmpty(PageState.ReturnUrl))
@@ -557,12 +571,12 @@
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) 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)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@@ -594,7 +608,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value); _userSettings = SettingService.SetSetting(_userSettings, SettingName, value);
} }
private async Task Delete(Notification Notification) private async Task Delete(Notification Notification)
@@ -624,7 +638,7 @@
private async void FilterChanged(ChangeEventArgs e) private async void FilterChanged(ChangeEventArgs e)
{ {
filter = (string)e.Value; _filter = (string)e.Value;
await LoadNotificationsAsync(); await LoadNotificationsAsync();
StateHasChanged(); StateHasChanged();
} }
@@ -634,7 +648,7 @@
try try
{ {
ShowProgressIndicator(); ShowProgressIndicator();
foreach(var Notification in notifications) foreach(var Notification in _notifications)
{ {
if (!Notification.IsDeleted) if (!Notification.IsDeleted)
{ {

View File

@@ -5,6 +5,7 @@
@inject IUserService UserService @inject IUserService UserService
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Add> Localizer @inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -12,7 +13,7 @@
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity"> <TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null) @if (_profiles != null)
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -27,12 +28,33 @@
<input id="email" class="form-control" @bind="@_email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</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"> <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="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" /> <input id="displayname" class="form-control" @bind="@_displayname" />
</div> </div>
</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"> <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> <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"> <div class="col-sm-9">
@@ -48,20 +70,20 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
<strong>@p.Category</strong> <strong>@p.Category</strong>
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9"> <div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options)) @if (!string.IsNullOrEmpty(p.Options))
{ {
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))"> <select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) @foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
@@ -103,14 +125,17 @@
@code { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True"; private string _notify = "True";
private List<Profile> profiles; private List<Profile> _profiles;
private Dictionary<string, string> settings; private Dictionary<string, string> _settings;
private string category = string.Empty; private string _category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@@ -118,8 +143,10 @@
{ {
try try
{ {
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _timezones = TimeZoneService.GetTimeZones();
settings = new Dictionary<string, string>(); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true; _initialized = true;
} }
catch (Exception ex) catch (Exception ex)
@@ -131,7 +158,7 @@
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue); string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@@ -152,7 +179,9 @@
user.Username = _username; user.Username = _username;
user.Password = ""; // will be auto generated user.Password = ""; // will be auto generated
user.Email = _email; user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null; user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify); user.SuppressNotification = !bool.Parse(_notify);
@@ -160,7 +189,7 @@
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Created {User}", user); await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@@ -185,12 +214,12 @@
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) 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)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@@ -217,6 +246,6 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; 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 IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IFileService FileService @inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -17,13 +18,13 @@
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" /> <ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <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"> <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> </div>
<div class="row mb-1 align-items-center"> <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="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
@@ -32,32 +33,53 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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="col-sm-9">
<div class="input-group"> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <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"> <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>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<div class="row mb-1 align-items-center"> <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"> <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="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
@@ -65,15 +87,15 @@
</div> </div>
} }
<div class="row mb-1 align-items-center"> <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"> <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> </div>
<div class="row mb-1 align-items-center"> <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"> <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> </div>
</div> </div>
@@ -81,15 +103,15 @@
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles) @foreach (Profile profile in _profiles)
{ {
var p = profile; var p = profile;
if (p.Category != category) if (p.Category != _category)
{ {
<div class="col text-center pb-2"> <div class="col text-center pb-2">
<strong>@p.Category</strong> <strong>@p.Category</strong>
</div> </div>
category = p.Category; _category = p.Category;
} }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label> <Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
@@ -128,47 +150,50 @@
</div> </div>
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <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> <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 /> <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 { @code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false; private bool _initialized = false;
private string _passwordrequirements; private string _passwordrequirements;
private int userid; private int _userid;
private string username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _passwordtype = "password"; private string _passwordtype = "password";
private string _togglepassword = string.Empty; private string _togglepassword = string.Empty;
private string confirm = string.Empty; private string _confirm = string.Empty;
private string email = string.Empty; private string _email = string.Empty;
private string displayname = string.Empty; private string _confirmed = string.Empty;
private string isdeleted; private string _displayname = string.Empty;
private string lastlogin; private string _timezoneid = string.Empty;
private string lastipaddress; private string _isdeleted;
private bool ishost = false; private string _lastlogin;
private string _lastipaddress;
private bool _ishost = false;
private List<Profile> profiles; private List<Profile> _profiles;
private Dictionary<string, string> userSettings; private Dictionary<string, string> _settings;
private string category = string.Empty; private string _category = string.Empty;
private string createdby; private string _createdby;
private DateTime createdon; private DateTime _createdon;
private string modifiedby; private string _modifiedby;
private DateTime modifiedon; private DateTime _modifiedon;
private string deletedby; private string _deletedby;
private DateTime? deletedon; private DateTime? _deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@@ -178,29 +203,32 @@
{ {
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _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)) if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{ {
userid = UserId; _userid = UserId;
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
username = user.Username; _username = user.Username;
email = user.Email; _email = user.Email;
displayname = user.DisplayName; _confirmed = user.EmailConfirmed.ToString();
isdeleted = user.IsDeleted.ToString(); _displayname = user.DisplayName;
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); _timezoneid = PageState.User.TimeZoneId;
lastipaddress = user.LastIPAddress; _isdeleted = user.IsDeleted.ToString();
ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); _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; _settings = user.Settings;
createdby = user.CreatedBy; _createdby = user.CreatedBy;
createdon = user.CreatedOn; _createdon = user.CreatedOn;
modifiedby = user.ModifiedBy; _modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn; _modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy; _deletedby = user.DeletedBy;
deletedon = user.DeletedOn; _deletedon = user.DeletedOn;
} }
} }
@@ -208,14 +236,14 @@
} }
catch (Exception ex) 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); AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
} }
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
{ {
string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); string value = SettingService.GetSetting(_settings, SettingName, DefaultValue);
if (value.Contains("]")) if (value.Contains("]"))
{ {
value = value.Substring(value.IndexOf("]") + 1); value = value.Substring(value.IndexOf("]") + 1);
@@ -227,27 +255,29 @@
{ {
try try
{ {
if (username != string.Empty && email != string.Empty) if (_username != string.Empty && _email != string.Empty)
{ {
if (_password == confirm) if (_password == _confirm)
{ {
if (ValidateProfiles()) 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.SiteId = PageState.Site.SiteId;
user.Username = username; user.Username = _username;
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) 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); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
{ {
await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId); await SettingService.UpdateUserSettingsAsync(_settings, user.UserId);
await logger.LogInformation("User Saved {User}", user); await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
@@ -269,7 +299,7 @@
} }
catch (Exception ex) 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); AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
} }
} }
@@ -278,17 +308,17 @@
{ {
try 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 // post back to the server so that the cookies are set correctly
var interop = new Interop(JSRuntime); 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/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
catch (Exception ex) 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); AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
} }
} }
@@ -297,9 +327,9 @@
{ {
try 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 UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Permanently Deleted {User}", user); await logger.LogInformation("User Permanently Deleted {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
@@ -307,19 +337,19 @@
} }
catch (Exception ex) 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); AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
} }
} }
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in _profiles)
{ {
var value = GetProfileValue(profile.Name, string.Empty); var value = GetProfileValue(profile.Name, string.Empty);
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) 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)) if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
@@ -346,7 +376,7 @@
private void ProfileChanged(ChangeEventArgs e, string SettingName) private void ProfileChanged(ChangeEventArgs e, string SettingName)
{ {
var value = (string)e.Value; var value = (string)e.Value;
userSettings = SettingService.SetSetting(userSettings, SettingName, value); _settings = SettingService.SetSetting(_settings, SettingName, value);
} }
private void TogglePassword() private void TogglePassword()

View File

@@ -43,7 +43,7 @@ else
<td>@context.User.Username</td> <td>@context.User.Username</td>
<td>@context.User.DisplayName</td> <td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</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> <td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", UtcToLocal(context.User.LastLoginOn)) : "")</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
@@ -59,31 +59,34 @@ else
</select> </select>
</div> </div>
</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)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center"> <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> <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"> <div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor"> <select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option> <option value="false">@Localizer["Disabled"]</option>
@@ -432,6 +435,15 @@ else
</select> </select>
</div> </div>
</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>
@@ -485,7 +497,9 @@ else
private List<UserRole> users; private List<UserRole> users;
private string _allowregistration; private string _allowregistration;
private string _allowsitelogin; private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _twofactor; private string _twofactor;
private string _cookiename; private string _cookiename;
private string _cookieexpiration; private string _cookieexpiration;
@@ -533,6 +547,7 @@ else
private string _createusers; private string _createusers;
private string _verifyusers; private string _verifyusers;
private string _allowhostrole; private string _allowhostrole;
private string _allowsitelogin;
private string _secret; private string _secret;
private string _secrettype = "password"; private string _secrettype = "password";
@@ -553,7 +568,9 @@ else
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); _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)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
@@ -616,6 +633,7 @@ else
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
_allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false"); _allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false");
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
} }
private async Task LoadUsersAsync(bool load) private async Task LoadUsersAsync(bool load)
@@ -673,10 +691,12 @@ else
await SiteService.UpdateSiteAsync(site); await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) 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:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
@@ -720,6 +740,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, 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:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);

View File

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

View File

@@ -53,8 +53,8 @@ else
</td> </td>
<td>@context.Language</td> <td>@context.Language</td>
<td>@context.Visits</td> <td>@context.Visits</td>
<td>@context.VisitedOn</td> <td>@UtcToLocal(context.VisitedOn)</td>
<td>@context.CreatedOn</td> <td>@UtcToLocal(context.CreatedOn)</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>

View File

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

View File

@@ -157,6 +157,9 @@
[Parameter] [Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false 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] [Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
@@ -408,7 +411,7 @@
} }
// upload files // 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 // reset progress indicators
if (ShowProgress) if (ShowProgress)

View File

@@ -9,62 +9,26 @@
@if (_permissions != null) @if (_permissions != null)
{ {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table class="table table-borderless"> <table class="table table-borderless">
<tbody> <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)
{
<tr> <tr>
<td>@role.Name</td> <th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames) @foreach (var permissionname in _permissionnames)
{ {
<td style="text-align: center;"> <th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
<TriStateCheckBox Value=@GetPermissionValue(permissionname, role.Name, -1) Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td>
} }
</tr> </tr>
} @foreach (Role role in _roles)
</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> <tr>
<td>@user.DisplayName (@user.Username)</td> <td>@role.Name</td>
@foreach (var permissionname in _permissionnames) @foreach (var permissionname in _permissionnames)
{ {
<td style="text-align: center; width: 1px;"> <td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, "", user.UserId) Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" /> <TriStateCheckBox Value="@GetPermissionValue(permissionname, role.Name, -1)" Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td> </td>
} }
</tr> </tr>
@@ -72,200 +36,242 @@
</tbody> </tbody>
</table> </table>
<br /> <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> </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 { @code {
private List<string> _permissionnames; private List<string> _permissionnames;
private List<Permission> _permissions; private List<Permission> _permissions;
private List<Role> _roles; private List<Role> _roles;
private List<User> _users = new List<User>(); private List<User> _users = new List<User>();
private AutoComplete _user; private AutoComplete _user;
private string _message = string.Empty; private string _message = string.Empty;
[Parameter] [Parameter]
public string EntityName { get; set; } public string EntityName { get; set; }
[Parameter] [Parameter]
public string PermissionNames { get; set; } public string PermissionNames { get; set; }
[Parameter] [Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter] [Parameter]
public List<Permission> PermissionList { get; set; } public List<Permission> PermissionList { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!string.IsNullOrEmpty(Permissions)) if (!string.IsNullOrEmpty(Permissions))
{ {
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions); PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
} }
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true); _roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) _roles.RemoveAll(item => item.Name == RoleNames.Host); // remove host role
{
_roles.RemoveAll(item => item.Name == RoleNames.Host);
}
// get permission names // get permission names
if (string.IsNullOrEmpty(PermissionNames)) if (string.IsNullOrEmpty(PermissionNames))
{ {
_permissionnames = new List<string>(); _permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View); _permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit); _permissionnames.Add(Shared.PermissionNames.Edit);
} }
else else
{ {
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); _permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
} }
// initialize permissions // initialize permissions
_permissions = new List<Permission>(); _permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any()) if (PermissionList != null && PermissionList.Any())
{ {
foreach (var permission in PermissionList) foreach (var permission in PermissionList)
{ {
_permissions.Add(permission); _permissions.Add(permission);
if (permission.UserId != null) if (permission.UserId != null)
{ {
if (!_users.Any(item => item.UserId == permission.UserId.Value)) if (!_users.Any(item => item.UserId == permission.UserId.Value))
{ {
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId)); _users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
} }
} }
} }
} }
else else
{ {
foreach (string permissionname in _permissionnames) foreach (string permissionname in _permissionnames)
{ {
// permission names can be in the form of "EntityName:PermissionName:Roles" // permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":")) if (permissionname.Contains(":"))
{ {
var segments = permissionname.Split(':'); var segments = permissionname.Split(':');
if (segments.Length == 3) if (segments.Length == 3)
{ {
foreach (var role in segments[2].Split(';')) foreach (var role in segments[2].Split(';'))
{ {
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true)); _permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
} }
// ensure admin access // ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin)) 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)); _permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
} }
} }
} }
else else
{ {
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true)); _permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
} }
} }
} }
} }
private string GetPermissionName(string permissionName) private string GetPermissionName(string permissionName)
{ {
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName; return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
} }
private string GetEntityName(string permissionName) private string GetEntityName(string permissionName)
{ {
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName; return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
} }
private string DisplayPermissionName(string permissionName) private string DisplayPermissionName(string permissionName)
{ {
var name = Localizer[GetPermissionName(permissionName)].ToString(); var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString(); name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name; return name;
} }
private bool? GetPermissionValue(string permissionName, string roleName, int userId) private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{ {
bool? isauthorized = null; bool? isauthorized = null;
if (roleName != "") if (roleName != "")
{ {
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName); var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null) if (permission != null)
{ {
isauthorized = permission.IsAuthorized; isauthorized = permission.IsAuthorized;
} }
} }
else else
{ {
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId); var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null) if (permission != null)
{ {
isauthorized = permission.IsAuthorized; isauthorized = permission.IsAuthorized;
} }
} }
return isauthorized; return isauthorized;
} }
private bool GetPermissionDisabled(string permissionName, string roleName) private bool GetPermissionDisabled(string permissionName, string roleName)
{ {
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) var disabled = false;
{
return true;
}
else
{
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private void PermissionChanged(bool? value, string permissionName, string roleName, int userId) // administrator role permissions can only be changed by a host
{ if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
if (roleName != "") {
{ disabled = true;
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName); }
if (permission != null)
{ // API permissions can only be changed by an administrator
_permissions.Remove(permission); if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
} {
if (value != null) disabled = true;
{ }
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
} return disabled;
} }
else
{ private bool? PermissionChanged(bool? value, string permissionName, string roleName, int userId)
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId); {
if (permission != null) if (roleName != "")
{ {
_permissions.Remove(permission); var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
} if (permission != null)
if (value != null) {
{ _permissions.Remove(permission);
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value)); }
}
} // 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) private async Task<Dictionary<string, string>> GetUsers(string filter)
{ {
@@ -305,29 +311,20 @@
private void ValidatePermissions() 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)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
// remove deny administrators and host users // remove host role permissions
permissions = _permissions.Where(item => !item.IsAuthorized && var permissions = _permissions.Where(item => item.RoleName == RoleNames.Host).ToList();
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList(); foreach (var permission in permissions)
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); if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && item.RoleName == RoleNames.Admin))
}
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)))
{ {
_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; } public bool Disabled { get; set; }
[Parameter] [Parameter]
public Action<bool?> OnChange { get; set; } public Func<bool?, bool?> OnChange { get; set; }
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -41,27 +41,35 @@
break; break;
} }
_value = OnChange(_value);
SetImage(); SetImage();
OnChange(_value);
} }
} }
private void SetImage() private void SetImage()
{ {
switch (_value) if (!Disabled)
{ {
case true: switch (_value)
_src = "images/checked.png"; {
_title = Localizer["PermissionGranted"]; case true:
break; _src = "images/checked.png";
case false: _title = Localizer["PermissionGranted"];
_src = "images/unchecked.png"; break;
_title = Localizer["PermissionDenied"]; case false:
break; _src = "images/unchecked.png";
case null: _title = Localizer["PermissionDenied"];
_src = "images/null.png"; break;
_title = string.Empty; case null:
break; _src = "images/null.png";
_title = string.Empty;
break;
}
}
else
{
_src = "images/disabled.png";
_title = Localizer["PermissionDisabled"];
} }
StateHasChanged(); StateHasChanged();

View File

@@ -1,23 +1,23 @@
using Microsoft.AspNetCore.Components;
using Oqtane.Shared;
using Oqtane.Models;
using System.Threading.Tasks;
using Oqtane.Services;
using System; using System;
using Oqtane.Enums;
using Oqtane.UI;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
using System.Dynamic; using System.Dynamic;
using System.Reflection; 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 namespace Oqtane.Modules
{ {
public abstract class ModuleBase : ComponentBase, IModuleControl public abstract class ModuleBase : ComponentBase, IModuleControl
{ {
private Logger _logger; private Logger _logger;
private string _urlparametersstate; private string _urlparametersstate = string.Empty;
private Dictionary<string, string> _urlparameters; private Dictionary<string, string> _urlparameters;
private bool _scriptsloaded = false; private bool _scriptsloaded = false;
@@ -62,7 +62,7 @@ namespace Oqtane.Modules
public Dictionary<string, string> UrlParameters { public Dictionary<string, string> UrlParameters {
get get
{ {
if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters) if (string.IsNullOrEmpty(_urlparametersstate) || _urlparametersstate != PageState.UrlParameters)
{ {
_urlparametersstate = PageState.UrlParameters; _urlparametersstate = PageState.UrlParameters;
_urlparameters = GetUrlParameters(UrlParametersTemplate); _urlparameters = GetUrlParameters(UrlParametersTemplate);
@@ -79,18 +79,21 @@ namespace Oqtane.Modules
{ {
List<Resource> resources = null; List<Resource> resources = null;
var type = GetType(); 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 // ModuleBase
else // modulecontrolbase
{
if (Resources != null)
{ {
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()) if (resources != null && resources.Any())
@@ -421,70 +424,121 @@ namespace Oqtane.Modules
public string ReplaceTokens(string content, object obj) public string ReplaceTokens(string content, object obj)
{ {
var tokens = new List<string>(); // Using StringBuilder avoids the performance penalty of repeated string allocations
var pos = content.IndexOf("["); // that occur with string.Replace or string concatenation inside loops.
if (pos != -1) var sb = new StringBuilder();
{ var cache = new Dictionary<string, string>(); // Cache to store resolved tokens
if (content.IndexOf("]", pos) != -1) int index = 0;
{
var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1);
if (token.Contains(":"))
{
tokens.Add(token.Substring(1, token.Length - 2));
}
}
pos = content.IndexOf("[", pos + 1);
}
if (tokens.Count != 0)
{
foreach (string token in tokens)
{
var segments = token.Split(":");
if (segments.Length >= 2 && segments.Length <= 3)
{
var objectName = string.Join(":", segments, 0, segments.Length - 1);
var propertyName = segments[segments.Length - 1];
var propertyValue = "";
switch (objectName) // 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++)
{ {
case "ModuleState": var type = current.GetType();
propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString(); var prop = type.GetProperty(segments[i]);
break; current = prop?.GetValue(current);
case "PageState":
propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString();
break;
case "PageState:Alias":
propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString();
break;
case "PageState:Site":
propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString();
break;
case "PageState:Page":
propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString();
break;
case "PageState:User":
propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString();
break;
case "PageState:Route":
propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString();
break;
default:
if (obj != null && obj.GetType().Name == objectName)
{
propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString();
}
break;
}
if (propertyValue != null)
{
content = content.Replace("[" + token + "]", propertyValue);
} }
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 content;
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 // logging methods

View File

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

View File

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

View File

@@ -121,10 +121,10 @@
<value>Forgot Password</value> <value>Forgot Password</value>
</data> </data>
<data name="Success.Account.Verified" xml:space="preserve"> <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>
<data name="Message.Account.NotVerified" xml:space="preserve"> <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>
<data name="Success.Account.Linked" xml:space="preserve"> <data name="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value> <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> <value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data> </data>
<data name="Error.Login.Fail" xml:space="preserve"> <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>
<data name="Message.Required.UserInfo" xml:space="preserve"> <data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value> <value>Please Provide All Required Fields</value>

View File

@@ -160,12 +160,12 @@
<value>Enabled?</value> <value>Enabled?</value>
</data> </data>
<data name="Synchronize" xml:space="preserve"> <data name="Synchronize" xml:space="preserve">
<value>Synchronize</value> <value>Check For Updates</value>
</data> </data>
<data name="Success.Module.Synchronize" xml:space="preserve"> <data name="Success.Module.Synchronize" xml:space="preserve">
<value>Modules Have Been Successfully Synchronized With The Marketplace</value> <value>Module Information Has Been Retrieved From The Marketplace</value>
</data> </data>
<data name="Error.Module.Synchronize" xml:space="preserve"> <data name="Error.Module.Synchronize" xml:space="preserve">
<value>Error Synchronizing Modules With The Marketplace</value> <value>Error Retrieving Module Information From The Marketplace</value>
</data> </data>
</root> </root>

View File

@@ -121,7 +121,7 @@
<value>Export</value> <value>Export</value>
</data> </data>
<data name="Content.HelpText" xml:space="preserve"> <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>
<data name="Content.Text" xml:space="preserve"> <data name="Content.Text" xml:space="preserve">
<value>Content: </value> <value>Content: </value>
@@ -135,4 +135,25 @@
<data name="Export Content" xml:space="preserve"> <data name="Export Content" xml:space="preserve">
<value>Export Content</value> <value>Export Content</value>
</data> </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> </root>

View File

@@ -189,4 +189,19 @@
<data name="ModuleSettings.Title" xml:space="preserve"> <data name="ModuleSettings.Title" xml:space="preserve">
<value>Module Settings</value> <value>Module Settings</value>
</data> </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> </root>

View File

@@ -303,4 +303,10 @@
<data name="PersonalizedUrlPath.HelpText" xml:space="preserve"> <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> <value>Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash.</value>
</data> </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> </root>

View File

@@ -180,4 +180,10 @@
<data name="Login" xml:space="preserve"> <data name="Login" xml:space="preserve">
<value>Already have account? Login now.</value> <value>Already have account? Login now.</value>
</data> </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> </root>

View File

@@ -192,7 +192,7 @@
<data name="Port.HelpText" xml:space="preserve"> <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> <value>Enter the port number for the SMTP server. Please note this field is required if you provide a host name.</value>
</data> </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> <value>Specify if SSL is required for your SMTP server</value>
</data> </data>
<data name="SmtpUsername.HelpText" xml:space="preserve"> <data name="SmtpUsername.HelpText" xml:space="preserve">
@@ -202,7 +202,7 @@
<value>Enter the password for your SMTP account</value> <value>Enter the password for your SMTP account</value>
</data> </data>
<data name="SmtpSender.HelpText" xml:space="preserve"> <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>
<data name="EnablePWA.HelpText" xml:space="preserve"> <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> <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"> <data name="Port.Text" xml:space="preserve">
<value>Port: </value> <value>Port: </value>
</data> </data>
<data name="UseSsl.Text" xml:space="preserve"> <data name="SmtpSSL.Text" xml:space="preserve">
<value>SSL Enabled: </value> <value>SSL Required: </value>
</data> </data>
<data name="SmtpUsername.Text" xml:space="preserve"> <data name="SmtpUsername.Text" xml:space="preserve">
<value>Username: </value> <value>Username: </value>
@@ -372,10 +372,10 @@
<data name="PageContent.Heading" xml:space="preserve"> <data name="PageContent.Heading" xml:space="preserve">
<value>Page Content</value> <value>Page Content</value>
</data> </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> <value>Specify if SMTP is enabled for this site</value>
</data> </data>
<data name="SMTPEnabled.Text" xml:space="preserve"> <data name="SmtpEnabled.Text" xml:space="preserve">
<value>Enabled?</value> <value>Enabled?</value>
</data> </data>
<data name="Version.HelpText" xml:space="preserve"> <data name="Version.HelpText" xml:space="preserve">
@@ -447,4 +447,46 @@
<data name="Success.SiteMap.CacheEvicted" xml:space="preserve"> <data name="Success.SiteMap.CacheEvicted" xml:space="preserve">
<value>Site Map Cache Cleared</value> <value>Site Map Cache Cleared</value>
</data> </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> </root>

View File

@@ -160,12 +160,12 @@
<value>Assign</value> <value>Assign</value>
</data> </data>
<data name="Synchronize" xml:space="preserve"> <data name="Synchronize" xml:space="preserve">
<value>Synchronize</value> <value>Check For Updates</value>
</data> </data>
<data name="Success.Theme.Synchronize" xml:space="preserve"> <data name="Success.Theme.Synchronize" xml:space="preserve">
<value>Themes Have Been Successfully Synchronized With The Marketplace</value> <value>Theme Information Has Been Retrieved From The Marketplace</value>
</data> </data>
<data name="Error.Theme.Synchronize" xml:space="preserve"> <data name="Error.Theme.Synchronize" xml:space="preserve">
<value>Error Synchronizing Themes With The Marketplace</value> <value>Error Retrieving Theme Information From The Marketplace</value>
</data> </data>
</root> </root>

View File

@@ -124,7 +124,7 @@
<value>Error Downloading Framework Package</value> <value>Error Downloading Framework Package</value>
</data> </data>
<data name="Framework.HelpText" xml:space="preserve"> <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>
<data name="Framework.Text" xml:space="preserve"> <data name="Framework.Text" xml:space="preserve">
<value>Framework: </value> <value>Framework: </value>
@@ -144,9 +144,6 @@
<data name="Message.Text" xml:space="preserve"> <data name="Message.Text" xml:space="preserve">
<value>Framework Is Already Up To Date</value> <value>Framework Is Already Up To Date</value>
</data> </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"> <data name="Localhost.Text" xml:space="preserve">
<value>You Cannot Perform A System Update In A Development Environment</value> <value>You Cannot Perform A System Update In A Development Environment</value>
</data> </data>
@@ -157,6 +154,6 @@
<value>Backup Files?</value> <value>Backup Files?</value>
</data> </data>
<data name="Backup.HelpText" xml:space="preserve"> <data name="Backup.HelpText" xml:space="preserve">
<value>Specify if you want to backup files during the upgrade process. Disabling this option will result in a better experience in some environments.</value> <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> </data>
</root> </root>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@@ -121,10 +121,10 @@
<value>Redirect To:</value> <value>Redirect To:</value>
</data> </data>
<data name="MappedUrl.HelpText" xml:space="preserve"> <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>
<data name="Url.HelpText" xml:space="preserve"> <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>
<data name="Url.Text" xml:space="preserve"> <data name="Url.Text" xml:space="preserve">
<value>Url:</value> <value>Url:</value>
@@ -141,4 +141,7 @@
<data name="Message.SaveUrlMapping" xml:space="preserve"> <data name="Message.SaveUrlMapping" xml:space="preserve">
<value>The Url must belong to the current site</value> <value>The Url must belong to the current site</value>
</data> </data>
<data name="Generate" xml:space="preserve">
<value>Generate</value>
</data>
</root> </root>

View File

@@ -121,10 +121,10 @@
<value>Redirect To:</value> <value>Redirect To:</value>
</data> </data>
<data name="MappedUrl.HelpText" xml:space="preserve"> <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>
<data name="Url.HelpText" xml:space="preserve"> <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>
<data name="Url.Text" xml:space="preserve"> <data name="Url.Text" xml:space="preserve">
<value>Url:</value> <value>Url:</value>

View File

@@ -147,9 +147,6 @@
<data name="Message.User.NoLogIn" xml:space="preserve"> <data name="Message.User.NoLogIn" xml:space="preserve">
<value>Current User Is Not Logged In</value> <value>Current User Is Not Logged In</value>
</data> </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"> <data name="Error.Profile.Load" xml:space="preserve">
<value>Error Loading User Profile</value> <value>Error Loading User Profile</value>
</data> </data>
@@ -246,4 +243,10 @@
<data name="Logout Everywhere" xml:space="preserve"> <data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value> <value>Logout Everywhere</value>
</data> </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> </root>

View File

@@ -156,4 +156,16 @@
<data name="Notify.Text" xml:space="preserve"> <data name="Notify.Text" xml:space="preserve">
<value>Notify?</value> <value>Notify?</value>
</data> </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> </root>

View File

@@ -210,4 +210,16 @@
<data name="Error.User.Impersonate" xml:space="preserve"> <data name="Error.User.Impersonate" xml:space="preserve">
<value>Unable To Impersonate User</value> <value>Unable To Impersonate User</value>
</data> </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> </root>

View File

@@ -132,6 +132,18 @@
<data name="AllowRegistration.Text" xml:space="preserve"> <data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow Registration? </value> <value>Allow Registration? </value>
</data> </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"> <data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value> <value>Error Saving Settings</value>
</data> </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> <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>
<data name="AllowSiteLogin.Text" xml:space="preserve"> <data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Login?</value> <value>Allow Local Login?</value>
</data> </data>
<data name="Authority.HelpText" xml:space="preserve"> <data name="Authority.HelpText" xml:space="preserve">
<value>The authority url or issuer url associated with the identity provider</value> <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> <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>
<data name="TwoFactor.Text" xml:space="preserve"> <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>
<data name="Disabled" xml:space="preserve"> <data name="Disabled" xml:space="preserve">
<value>Disabled</value> <value>Disabled</value>
@@ -490,7 +508,7 @@
<value>Info</value> <value>Info</value>
</data> </data>
<data name="OAuth2" xml:space="preserve"> <data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0</value> <value>OAuth 2.0 (OAuth2)</value>
</data> </data>
<data name="OIDC" xml:space="preserve"> <data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value> <value>OpenID Connect (OIDC)</value>

View File

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

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

@@ -42,14 +42,6 @@ namespace Oqtane.Services
return await PutJsonAsync<Folder>($"{ApiUrl}/{folder.FolderId}", folder); 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) public async Task DeleteFolderAsync(int folderId)
{ {
await DeleteAsync($"{ApiUrl}/{folderId}"); await DeleteAsync($"{ApiUrl}/{folderId}");

View File

@@ -39,15 +39,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<Folder> UpdateFolderAsync(Folder folder); 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> /// <summary>
/// Delete a <see cref="Folder"/> /// Delete a <see cref="Folder"/>
/// </summary> /// </summary>

View File

@@ -56,7 +56,18 @@ namespace Oqtane.Services
/// Exports a given module /// Exports a given module
/// </summary> /// </summary>
/// <param name="moduleId"></param> /// <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); 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

@@ -256,7 +256,7 @@ namespace Oqtane.Services
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPrivate); 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)] [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

@@ -47,8 +47,13 @@ namespace Oqtane.Services
} }
public async Task<string> ExportModuleAsync(int moduleId, int pageId) public async Task<string> ExportModuleAsync(int moduleId, int pageId)
{ {
return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={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

@@ -266,27 +266,25 @@ namespace Oqtane.Services
return settings; 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>(); foreach (var setting in overwriteSettings)
}
if (settings2 != null)
{
foreach (var setting in settings2)
{ {
if (settings1.ContainsKey(setting.Key)) if (settings.ContainsKey(setting.Key))
{ {
settings1[setting.Key] = setting.Value; settings[setting.Key] = setting.Value;
} }
else 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)] [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

@@ -37,7 +37,7 @@
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // 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 Stylesheet(ThemePath() + "Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}; };

View File

@@ -20,6 +20,7 @@
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries // 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 _pageState = new PageState
{ {
Alias = PageState.Alias, 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 // 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 _pageState = new PageState
{ {
Alias = PageState.Alias, Alias = PageState.Alias,

View File

@@ -353,7 +353,7 @@
module.PageId = PageState.Page.PageId; module.PageId = PageState.Page.PageId;
module.ModuleDefinitionName = _moduleDefinitionName; module.ModuleDefinitionName = _moduleDefinitionName;
module.AllPages = false; module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId); module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName);
module = await ModuleService.AddModuleAsync(module); module = await ModuleService.AddModuleAsync(module);
newModuleId = module.ModuleId; newModuleId = module.ModuleId;
@@ -365,7 +365,7 @@
module.SiteId = PageState.Page.SiteId; module.SiteId = PageState.Page.SiteId;
module.PageId = PageState.Page.PageId; module.PageId = PageState.Page.PageId;
module.AllPages = false; module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId); module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName);
module = await ModuleService.AddModuleAsync(module); module = await ModuleService.AddModuleAsync(module);
var moduleContent = await ModuleService.ExportModuleAsync(int.Parse(_moduleId), PageState.Page.PageId); 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>(); var permissions = new List<Permission>();
// set module view permissions
if (_visibility == "view") if (_visibility == "view")
{ {
// set module view permissions to page view permissions // set module view permissions to page view permissions
@@ -443,8 +445,22 @@
// set module view permissions to page edit permissions // set module view permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit); 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; return permissions;
} }

View File

@@ -103,6 +103,9 @@
{ {
var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty); var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty);
_enabled = !string.IsNullOrEmpty(cookieConsentSetting); _enabled = !string.IsNullOrEmpty(cookieConsentSetting);
if (!_enabled) return;
_optout = cookieConsentSetting == "optout"; _optout = cookieConsentSetting == "optout";
_actioned = await CookieConsentService.IsActionedAsync(); _actioned = await CookieConsentService.IsActionedAsync();
@@ -164,4 +167,4 @@
StateHasChanged(); StateHasChanged();
} }
} }
} }

View File

@@ -1,6 +1,7 @@
@namespace Oqtane.Themes.Controls @namespace Oqtane.Themes.Controls
@using System.Net @using System.Net
@inherits ThemeControlBase @inherits ThemeControlBase
@inject ISettingService SettingService
@inject IStringLocalizer<UserProfile> Localizer @inject IStringLocalizer<UserProfile> Localizer
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@@ -51,20 +52,38 @@
if (!string.IsNullOrEmpty(RegisterUrl)) if (!string.IsNullOrEmpty(RegisterUrl))
{ {
_registerurl = RegisterUrl + "?returnurl=" + (RegisterUrl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl); _registerurl = RegisterUrl;
_registerurl += (!_registerurl.Contains("?") ? "?" : "&") + "returnurl=" + (_registerurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
} }
else else
{ {
_registerurl = NavigateUrl("register", "returnurl=" + _returnurl); if (!string.IsNullOrEmpty(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "")))
{
_registerurl = SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:RegisterUrl", "");
_registerurl += (!_registerurl.Contains("?") ? "?" : "&") + "returnurl=" + (_registerurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
}
else
{
_registerurl = NavigateUrl("register", "returnurl=" + _returnurl);
}
} }
if (!string.IsNullOrEmpty(ProfileUrl)) if (!string.IsNullOrEmpty(ProfileUrl))
{ {
_registerurl = ProfileUrl + "?returnurl=" + (ProfileUrl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl); _profileurl = ProfileUrl;
_profileurl += (!_profileurl.Contains("?") ? "?" : "&") + "returnurl=" + (_profileurl.Contains("://") ? WebUtility.UrlEncode(PageState.Route.RootUrl) + _returnurl : _returnurl);
} }
else else
{ {
_registerurl = NavigateUrl("profile", "returnurl=" + _returnurl); 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", ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client",
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // obtained from https://cdnjs.com/libraries/bootswatch
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", "anonymous"), 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 Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
} }

View File

@@ -43,18 +43,21 @@ namespace Oqtane.Themes
{ {
List<Resource> resources = null; List<Resource> resources = null;
var type = GetType(); var type = GetType();
if (type.BaseType == typeof(ThemeBase)) if (type.IsSubclassOf(typeof(ThemeBase)))
{ {
if (PageState.Page.Resources != null) if (type.IsSubclassOf(typeof(ThemeControlBase)) || type.IsSubclassOf(typeof(ContainerBase)))
{ {
resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Page && item.Namespace == type.Namespace).ToList(); if (Resources != null)
{
resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList();
}
} }
} else // ThemeBase
else // themecontrolbase, containerbase
{
if (Resources != null)
{ {
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.Page && item.Namespace == type.Namespace).ToList();
}
} }
} }
if (resources != null && resources.Any()) if (resources != null && resources.Any())

View File

@@ -0,0 +1,14 @@
namespace Oqtane
{
/// <summary>
/// Dummy class used to collect shared resource strings for this application
/// </summary>
/// <remarks>
/// This class is mostly used with IStringLocalizer and IHtmlLocalizer interfaces.
/// The class must reside at the project root.
/// </remarks>
public class TimeZoneResources
{
}
}

View File

@@ -6,7 +6,7 @@
@if (ComponentType != null && _visible) @if (ComponentType != null && _visible)
{ {
<a id="@ModuleState.PageModuleId.ToString()"></a> <a id="@ModuleState.PageModuleId.ToString()"></a>
<CascadingValue Value="@ModuleState"> <CascadingValue Value="@ModuleState" IsFixed="true">
@if (_useadminborder) @if (_useadminborder)
{ {
<div class="app-pane-admin-border"> <div class="app-pane-admin-border">
@@ -31,6 +31,9 @@
[Parameter] [Parameter]
public Module ModuleState { get; set; } public Module ModuleState { get; set; }
[Parameter]
public string ContainerType { get; set; }
protected override bool ShouldRender() protected override bool ShouldRender()
{ {
return PageState?.RenderId == ModuleState?.RenderId; return PageState?.RenderId == ModuleState?.RenderId;
@@ -44,6 +47,10 @@
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
string container = ModuleState.ContainerType; string container = ModuleState.ContainerType;
if (!string.IsNullOrEmpty(ContainerType))
{
container = ContainerType;
}
if (PageState.ModuleId != -1 && PageState.Route.Action != "" && ModuleState.UseAdminContainer) if (PageState.ModuleId != -1 && PageState.Route.Action != "" && ModuleState.UseAdminContainer)
{ {
container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer; container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer;

View File

@@ -224,17 +224,17 @@ namespace Oqtane.UI
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt) public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
{ {
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1); UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1, false);
return Task.CompletedTask; return Task.CompletedTask;
} }
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default) public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, bool anonymizeuploadfilenames, CancellationToken cancellationToken = default)
{ {
try try
{ {
return _jsRuntime.InvokeAsync<bool>( return _jsRuntime.InvokeAsync<bool>(
"Oqtane.Interop.uploadFiles", cancellationToken, "Oqtane.Interop.uploadFiles", cancellationToken,
posturl, folder, id, antiforgerytoken, jwt, chunksize); posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames);
} }
catch catch
{ {

View File

@@ -1,18 +1,27 @@
@namespace Oqtane.UI @namespace Oqtane.UI
@inject SiteState SiteState @inject SiteState SiteState
@if (PageState.ModuleId == -1)
{
@((MarkupString)ModuleState.Header)
}
@if (_comment != null) @if (_comment != null)
{ {
@((MarkupString)_comment) @((MarkupString)_comment)
@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static) @if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static)
{ {
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" /> <RenderModeBoundary ModuleState="@ModuleState" PageState="@_pageState" SiteState="@SiteState" />
} }
else else
{ {
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, _prerender)" /> <RenderModeBoundary ModuleState="@ModuleState" PageState="@_pageState" SiteState="@SiteState" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, _prerender)" />
} }
} }
@if (PageState.ModuleId == -1)
{
@((MarkupString)ModuleState.Footer)
}
@code { @code {
[CascadingParameter] [CascadingParameter]
@@ -23,6 +32,7 @@
private bool _prerender; private bool _prerender;
private string _comment; private string _comment;
private PageState _pageState;
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
@@ -39,11 +49,12 @@
} }
_comment += " -->"; _comment += " -->";
_pageState = PageState.Clone();
if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive) if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive)
{ {
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries // trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// please note that this performance optimization results in the PageState.Pages property not being available for use in Interactive components // please note that this performance optimization results in the PageState.Pages property not being available for use in downstream Interactive components
PageState.Site.Pages = new List<Page>(); _pageState.Site.Pages = new List<Page>();
} }
} }

View File

@@ -37,5 +37,34 @@ namespace Oqtane.UI
{ {
get { return Site?.Languages; } get { return Site?.Languages; }
} }
public PageState Clone()
{
return new PageState
{
Alias = Alias,
Site = Site,
Page = Page,
Modules = Modules,
User = User,
Uri = Uri,
Route = Route,
QueryString = QueryString,
UrlParameters = UrlParameters,
ModuleId = ModuleId,
Action = Action,
EditMode = EditMode,
LastSyncDate = LastSyncDate,
RenderMode = RenderMode,
Runtime = Runtime,
VisitorId = VisitorId,
RemoteIPAddress = RemoteIPAddress,
ReturnUrl = ReturnUrl,
IsInternalNavigation = IsInternalNavigation,
RenderId = RenderId,
Refresh = Refresh,
AllowCookies = AllowCookies
};
}
} }
} }

View File

@@ -26,10 +26,13 @@ else
[Parameter] [Parameter]
public string Name { get; set; } public string Name { get; set; }
[Parameter]
public string ContainerType { get; set; }
RenderFragment DynamicComponent { get; set; } RenderFragment DynamicComponent { get; set; }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction)
{ {
_useadminborder = true; _useadminborder = true;
@@ -45,12 +48,6 @@ else
{ {
foreach (Module module in PageState.Modules) foreach (Module module in PageState.Modules)
{ {
// set renderid - this allows the framework to determine which components should be rendered when PageState changes
if (module.RenderId != PageState.RenderId)
{
module.RenderId = PageState.RenderId;
}
var pane = module.Pane; var pane = module.Pane;
if (module.ModuleId == PageState.ModuleId && PageState.Action != Constants.DefaultAction) if (module.ModuleId == PageState.ModuleId && PageState.Action != Constants.DefaultAction)
{ {
@@ -101,7 +98,7 @@ else
if (authorized) if (authorized)
{ {
CreateComponent(builder, module, module.PageModuleId); CreateComponent(builder, module);
} }
} }
} }
@@ -112,7 +109,7 @@ else
// check if user is authorized to view module // check if user is authorized to view module
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList))
{ {
CreateComponent(builder, module, -1); CreateComponent(builder, module);
} }
} }
} }
@@ -121,14 +118,12 @@ else
}; };
} }
private void CreateComponent(RenderTreeBuilder builder, Module module, int key) private void CreateComponent(RenderTreeBuilder builder, Module module)
{ {
builder.OpenComponent(0, typeof(ContainerBuilder)); builder.OpenComponent(0, typeof(ContainerBuilder));
builder.AddAttribute(1, "ModuleState", module); builder.AddAttribute(1, "ModuleState", module);
if (key != -1) builder.AddAttribute(2, "ContainerType", ContainerType);
{ builder.SetKey(module.PageModuleId);
builder.SetKey(module.PageModuleId);
}
builder.CloseComponent(); builder.CloseComponent();
} }
} }

View File

@@ -4,8 +4,8 @@
@inject ILogService LoggingService @inject ILogService LoggingService
@inherits ErrorBoundary @inherits ErrorBoundary
<CascadingValue Value="@PageState"> <CascadingValue Value="@PageState" IsFixed="true">
<CascadingValue Value="@ModuleState"> <CascadingValue Value="@ModuleState" IsFixed="true">
@if (CurrentException is null) @if (CurrentException is null)
{ {
@if (ModuleType != null) @if (ModuleType != null)

View File

@@ -48,12 +48,18 @@
private bool _initialized = false; private bool _initialized = false;
private bool _installed = false; private bool _installed = false;
private string _display = "display: none;"; private string _display = "";
private PageState _pageState { get; set; } private PageState _pageState { get; set; }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (PageState != null && PageState.RenderMode == RenderModes.Interactive && PageState.Site.Prerender)
{
// prevents flash on initial interactive page load when using prerendering
_display = "display: none;";
}
SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AntiForgeryToken = AntiForgeryToken;
SiteState.AuthorizationToken = AuthorizationToken; SiteState.AuthorizationToken = AuthorizationToken;
SiteState.Platform = Platform; SiteState.Platform = Platform;
@@ -61,7 +67,7 @@
if (Runtime == Runtimes.Hybrid) if (Runtime == Runtimes.Hybrid)
{ {
var installation = await InstallationService.IsInstalled(); var installation = await InstallationService.IsInstalled();
_installed = installation.Success; _installed = installation.Success;
if (installation.Alias != null) if (installation.Alias != null)
{ {
@@ -73,8 +79,8 @@
if (PageState != null) if (PageState != null)
{ {
_pageState = PageState; _pageState = PageState;
SiteState.Alias = PageState.Alias; SiteState.Alias = _pageState.Alias;
SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : ""; SiteState.RemoteIPAddress = _pageState.RemoteIPAddress;
_installed = true; _installed = true;
} }
} }
@@ -85,9 +91,7 @@
{ {
if (firstRender) if (firstRender)
{ {
// prevents flash on initial interactive page load
_display = ""; _display = "";
StateHasChanged();
} }
} }

View File

@@ -71,7 +71,7 @@
{ {
if (PageState == null || PageState.Refresh) if (PageState == null || PageState.Refresh)
{ {
await Refresh(); await Refresh(false);
} }
} }
@@ -79,7 +79,7 @@
{ {
_absoluteUri = args.Location; _absoluteUri = args.Location;
_isInternalNavigation = true; _isInternalNavigation = true;
await Refresh(); await Refresh(true);
} }
Task IHandleAfterRender.OnAfterRenderAsync() Task IHandleAfterRender.OnAfterRenderAsync()
@@ -93,7 +93,7 @@
} }
[SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
private async Task Refresh() private async Task Refresh(bool locationChanged)
{ {
Site site = null; Site site = null;
Page page = null; Page page = null;
@@ -103,6 +103,7 @@
var refresh = false; var refresh = false;
var lastsyncdate = DateTime.MinValue; var lastsyncdate = DateTime.MinValue;
var visitorId = -1; var visitorId = -1;
var renderid = Guid.Empty;
_error = ""; _error = "";
Route route = new Route(_absoluteUri, SiteState.Alias.Path); Route route = new Route(_absoluteUri, SiteState.Alias.Path);
@@ -288,11 +289,21 @@
modules = PageState.Modules; modules = PageState.Modules;
} }
// renderid allows the framework to determine which module components should be rendered on a page
if (PageState == null || locationChanged)
{
renderid = Guid.NewGuid();
}
else
{
renderid = PageState.RenderId;
}
// load additional metadata for current page // load additional metadata for current page
page = ProcessPage(page, site, user, SiteState.Alias, action); page = ProcessPage(page, site, user, SiteState.Alias, action);
// load additional metadata for modules // load additional metadata for modules
(page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias); (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias, renderid);
//cookie consent //cookie consent
var _allowCookies = PageState?.AllowCookies; var _allowCookies = PageState?.AllowCookies;
@@ -324,7 +335,7 @@
RemoteIPAddress = SiteState.RemoteIPAddress, RemoteIPAddress = SiteState.RemoteIPAddress,
ReturnUrl = returnurl, ReturnUrl = returnurl,
IsInternalNavigation = _isInternalNavigation, IsInternalNavigation = _isInternalNavigation,
RenderId = Guid.NewGuid(), RenderId = renderid,
Refresh = false, Refresh = false,
AllowCookies = _allowCookies.GetValueOrDefault(true) AllowCookies = _allowCookies.GetValueOrDefault(true)
}; };
@@ -343,7 +354,7 @@
var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath); var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{ {
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query; var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + (!urlMapping.MappedUrl.StartsWith("/") ? "/" : "") + urlMapping.MappedUrl + ((!urlMapping.MappedUrl.Contains("?")) ? route.Query : "");
NavigationManager.NavigateTo(url, false); NavigationManager.NavigateTo(url, false);
return; return;
} }
@@ -447,7 +458,7 @@
return page; return page;
} }
private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias) private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias, Guid renderid)
{ {
var paneindex = new Dictionary<string, int>(); var paneindex = new Dictionary<string, int>();
@@ -592,6 +603,8 @@
{ {
module.ContainerType = defaultcontainertype; module.ContainerType = defaultcontainertype;
} }
module.RenderId = renderid;
} }
foreach (Module module in modules.Where(item => item.PageId == page.PageId)) foreach (Module module in modules.Where(item => item.PageId == page.PageId))

View File

@@ -20,13 +20,6 @@
return; return;
} }
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
// set page title // set page title
if (!string.IsNullOrEmpty(PageState.Page.Title)) if (!string.IsNullOrEmpty(PageState.Page.Title))
{ {

View File

@@ -75,13 +75,9 @@ namespace Oqtane.Database.MySQL
return dr; return dr;
} }
public override string RewriteName(string name, bool isQuery) public override string DelimitName(string name)
{ {
if (name.ToLower() == "rows" && isQuery) return $"`{name}`";
{
name = $"`{name}`"; // escape reserved word in SQL query
}
return name;
} }
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>6.1.2</Version> <Version>6.1.4</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySql.Data" Version="9.2.0" /> <PackageReference Include="MySql.Data" Version="9.4.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup> </ItemGroup>

View File

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

View File

@@ -87,9 +87,9 @@ namespace Oqtane.Database.PostgreSQL
return _rewriter.RewriteName(name); return _rewriter.RewriteName(name);
} }
public override string RewriteName(string name, bool isQuery) public override string DelimitName(string name)
{ {
return _rewriter.RewriteName(name); return $"\"{name}\"";
} }
public override string RewriteValue(string value, string type) public override string RewriteValue(string value, string type)

View File

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

View File

@@ -46,6 +46,11 @@ namespace Oqtane.Database.SqlServer
} }
} }
public override string DelimitName(string name)
{
return $"[{name}]";
}
public override int ExecuteNonQuery(string connectionString, string query) public override int ExecuteNonQuery(string connectionString, string query)
{ {
var conn = new SqlConnection(FormatConnectionString(connectionString)); var conn = new SqlConnection(FormatConnectionString(connectionString));

View File

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

View File

@@ -84,6 +84,11 @@ namespace Oqtane.Database.Sqlite
return dr; return dr;
} }
public override string DelimitName(string name)
{
return $"\"{name}\"";
}
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{ {
return optionsBuilder.UseSqlite(connectionString) return optionsBuilder.UseSqlite(connectionString)

View File

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

View File

@@ -75,6 +75,10 @@ app {
color: gray; color: gray;
} }
.app-moduleactions .dropdown-menu {
z-index: 9999;
}
.app-moduleactions .dropdown-submenu { .app-moduleactions .dropdown-submenu {
position: relative; position: relative;
} }
@@ -235,18 +239,19 @@ app {
.app-form-inline { .app-form-inline {
display: inline; display: inline;
} }
.app-search{
.app-search {
display: inline-block; display: inline-block;
position: relative; position: relative;
} }
.app-search input + button{ .app-search input + button {
background: none; background: none;
border: none; border: none;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
} }
.app-search input + button .oi{ .app-search input + button .oi {
top: 0; top: 0;
} }
.app-search-noinput { .app-search-noinput {
@@ -270,4 +275,14 @@ app {
.app-logo .navbar-brand { .app-logo .navbar-brand {
padding: 5px 20px 5px 20px; padding: 5px 20px 5px 20px;
} }
/* cookie consent */
.gdpr-consent-bar .btn-show {
bottom: -3px;
left: 5px;
}
.gdpr-consent-bar .btn-hide {
top: 0;
right: 5px;
}

View File

@@ -311,7 +311,7 @@ Oqtane.Interop = {
} }
return files; return files;
}, },
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) {
var success = true; var success = true;
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
@@ -344,16 +344,22 @@ Oqtane.Interop = {
const totalParts = Math.ceil(file.size / chunkSize); const totalParts = Math.ceil(file.size / chunkSize);
let partCount = 0; let partCount = 0;
let filename = file.name;
if (anonymizeuploadfilenames) {
filename = crypto.randomUUID() + '.' + filename.split('.').pop();
}
const uploadPart = () => { const uploadPart = () => {
const start = partCount * chunkSize; const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size); const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); const chunk = file.slice(start, end);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let formdata = new FormData(); let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken); formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder); formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name); formdata.append('formfile', chunk, filename);
var credentials = 'same-origin'; var credentials = 'same-origin';
var headers = new Headers(); var headers = new Headers();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,8 @@ nuget.exe pack Oqtane.Framework.nuspec
del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" > NUL del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" > NUL
rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish"
dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release
del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\Content" > NUL
rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\Content"
del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" > NUL del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" > NUL
rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content"
setlocal ENABLEDELAYEDEXPANSION setlocal ENABLEDELAYEDEXPANSION

View File

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

View File

@@ -297,7 +297,7 @@
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{ {
// redirect to mapped url // redirect to mapped url
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl; var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + (!urlMapping.MappedUrl.StartsWith("/") ? "/" : "") + urlMapping.MappedUrl + ((!urlMapping.MappedUrl.Contains("?")) ? route.Query : "");
NavigationManager.NavigateTo(url, true); NavigationManager.NavigateTo(url, true);
} }
else // no url mapping exists else // no url mapping exists
@@ -345,6 +345,7 @@
DateTime expiry = DateTime.MinValue; DateTime expiry = DateTime.MinValue;
if (visitorCookieValue != null && visitorCookieValue.Contains("|")) if (visitorCookieValue != null && visitorCookieValue.Contains("|"))
{ {
// visitor cookies contain the visitor id and an expiry date separated by a pipe symbol
var values = visitorCookieValue.Split('|'); var values = visitorCookieValue.Split('|');
int.TryParse(values[0], out _visitorId); int.TryParse(values[0], out _visitorId);
DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry); DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry);

View File

@@ -22,7 +22,6 @@ using Microsoft.AspNetCore.Cors;
using System.IO.Compression; using System.IO.Compression;
using Oqtane.Services; using Oqtane.Services;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@@ -445,9 +444,14 @@ namespace Oqtane.Controllers
} }
// ensure filename is valid // ensure filename is valid
if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName)) string fileName = formfile.FileName;
if (Path.GetExtension(fileName).Contains(':'))
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); fileName = fileName.Substring(0, fileName.LastIndexOf(':')); // remove invalid suffix from extension
}
if (!fileName.IsPathOrFileValid() || !HasValidFileExtension(fileName))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", fileName);
return StatusCode((int)HttpStatusCode.Forbidden); return StatusCode((int)HttpStatusCode.Forbidden);
} }
@@ -459,8 +463,8 @@ namespace Oqtane.Controllers
return StatusCode((int)HttpStatusCode.Forbidden); return StatusCode((int)HttpStatusCode.Forbidden);
} }
// create file name using header values // create file name using header part values
string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); fileName += ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000");
string folderPath = ""; string folderPath = "";
try try
@@ -533,13 +537,13 @@ namespace Oqtane.Controllers
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999" string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1)); int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = Path.GetFileNameWithoutExtension(filename); // base filename filename = Path.GetFileNameWithoutExtension(filename); // base filename including original file extension
string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist (note that file parts can arrive out of order) // if all of the file parts exist (note that file parts can arrive out of order)
if (fileparts.Length == totalparts && CanAccessFiles(fileparts)) if (fileparts.Length == totalparts && CanAccessFiles(fileparts))
{ {
// merge file parts into temp file (in case another user is trying to get the file) // merge file parts into temp file (in case another user is trying to read the file)
bool success = true; bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{ {
@@ -560,25 +564,22 @@ namespace Oqtane.Controllers
} }
// clean up file parts // clean up file parts
foreach (var file in Directory.GetFiles(folder, "*" + token + "*")) foreach (var file in fileparts)
{ {
if (fileparts.Contains(file)) try
{ {
try System.IO.File.Delete(file);
{ }
System.IO.File.Delete(file); catch
} {
catch // unable to delete part - ignore
{
// unable to delete part - ignore
}
} }
} }
// rename temp file // rename temp file
if (success) if (success)
{ {
// remove file if it already exists (as well as any thumbnails which may exist) // remove existing file (as well as any thumbnails)
foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*")) foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*"))
{ {
if (Path.GetExtension(file) != ".tmp") if (Path.GetExtension(file) != ".tmp")

View File

@@ -20,14 +20,16 @@ namespace Oqtane.Controllers
{ {
private readonly IFolderRepository _folders; private readonly IFolderRepository _folders;
private readonly IUserPermissions _userPermissions; private readonly IUserPermissions _userPermissions;
private readonly IFileRepository _files;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
public FolderController(IFolderRepository folders, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) public FolderController(IFolderRepository folders, IUserPermissions userPermissions, IFileRepository files, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{ {
_folders = folders; _folders = folders;
_userPermissions = userPermissions; _userPermissions = userPermissions;
_files = files;
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
@@ -41,7 +43,8 @@ namespace Oqtane.Controllers
int SiteId; int SiteId;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{ {
foreach (Folder folder in _folders.GetFolders(SiteId)) var hierarchy = GetFoldersHierarchy(_folders.GetFolders(SiteId).ToList());
foreach (Folder folder in hierarchy)
{ {
// note that Browse permission is used for this method // note that Browse permission is used for this method
if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList)) if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
@@ -49,7 +52,6 @@ namespace Oqtane.Controllers
folders.Add(folder); folders.Add(folder);
} }
} }
folders = GetFoldersHierarchy(folders);
} }
else else
{ {
@@ -244,34 +246,6 @@ namespace Oqtane.Controllers
return folder; return folder;
} }
// PUT api/<controller>/?siteid=x&folderid=y&parentid=z
[HttpPut]
[Authorize(Roles = RoleNames.Registered)]
public void Put(int siteid, int folderid, int? parentid)
{
if (siteid == _alias.SiteId && _folders.GetFolder(folderid, false) != null && _userPermissions.IsAuthorized(User, siteid, EntityNames.Folder, folderid, PermissionNames.Edit))
{
int order = 1;
List<Folder> folders = _folders.GetFolders(siteid).ToList();
foreach (Folder folder in folders.Where(item => item.ParentId == parentid).OrderBy(item => item.Order))
{
if (folder.Order != order)
{
folder.Order = order;
_folders.UpdateFolder(folder);
_syncManager.AddSyncEvent(_alias, EntityNames.Folder, folder.FolderId, SyncEventActions.Update);
}
order += 2;
}
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Folder Order Updated {SiteId} {FolderId} {ParentId}", siteid, folderid, parentid);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, "Unauthorized Folder Put Attempt {SiteId} {FolderId} {ParentId}", siteid, folderid, parentid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// DELETE api/<controller>/5 // DELETE api/<controller>/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
[Authorize(Roles = RoleNames.Registered)] [Authorize(Roles = RoleNames.Registered)]
@@ -283,12 +257,20 @@ namespace Oqtane.Controllers
var folderPath = _folders.GetFolderPath(folder); var folderPath = _folders.GetFolderPath(folder);
if (Directory.Exists(folderPath)) if (Directory.Exists(folderPath))
{ {
// remove all files from disk (including thumbnails, etc...)
foreach (var filePath in Directory.GetFiles(folderPath)) foreach (var filePath in Directory.GetFiles(folderPath))
{ {
System.IO.File.Delete(filePath); System.IO.File.Delete(filePath);
} }
Directory.Delete(folderPath); Directory.Delete(folderPath);
} }
// remove files from database
foreach (var file in _files.GetFiles(id))
{
_files.DeleteFile(file.FileId);
}
_folders.DeleteFolder(id); _folders.DeleteFolder(id);
_syncManager.AddSyncEvent(_alias, EntityNames.Folder, folder.FolderId, SyncEventActions.Delete); _syncManager.AddSyncEvent(_alias, EntityNames.Folder, folder.FolderId, SyncEventActions.Delete);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Folder Deleted {FolderId}", id); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Folder Deleted {FolderId}", id);
@@ -304,7 +286,6 @@ namespace Oqtane.Controllers
{ {
List<Folder> hierarchy = new List<Folder>(); List<Folder> hierarchy = new List<Folder>();
Action<List<Folder>, Folder> getPath = null; Action<List<Folder>, Folder> getPath = null;
var folders1 = folders;
getPath = (folderList, folder) => getPath = (folderList, folder) =>
{ {
IEnumerable<Folder> children; IEnumerable<Folder> children;
@@ -312,23 +293,23 @@ namespace Oqtane.Controllers
if (folder == null) if (folder == null)
{ {
level = -1; level = -1;
children = folders1.Where(item => item.ParentId == null); children = folders.Where(item => item.ParentId == null);
} }
else else
{ {
level = folder.Level; level = folder.Level;
children = folders1.Where(item => item.ParentId == folder.FolderId); children = folders.Where(item => item.ParentId == folder.FolderId);
} }
foreach (Folder child in children) foreach (Folder child in children)
{ {
child.Level = level + 1; child.Level = level + 1;
child.HasChildren = folders1.Any(item => item.ParentId == child.FolderId); child.HasChildren = folders.Any(item => item.ParentId == child.FolderId);
hierarchy.Add(child); hierarchy.Add(child);
if (getPath != null) getPath(folderList, child); getPath(folderList, child);
} }
}; };
folders = folders.OrderBy(item => item.Order).ToList(); folders = folders.OrderBy(item => item.Name).ToList();
getPath(folders, null); getPath(folders, null);
// add any non-hierarchical items to the end of the list // add any non-hierarchical items to the end of the list

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