Compare commits

..

318 Commits

Author SHA1 Message Date
fec0a02b1c Update README.md 2022-04-04 17:17:12 -04:00
3d93ba1215 Merge pull request #2110 from sbwalker/dev
fix logic issue in url mapping, improve 404 handling, add property change component notifications
2022-04-04 17:16:37 -04:00
042083c0e7 fix logic issue in url mapping, improve 404 handling, add property change component notifications 2022-04-04 17:16:12 -04:00
e0bb7b7faf Merge pull request #2108 from alperenbelgic/patch-1
Broken link fixed in Readme
2022-04-04 15:35:44 -04:00
acc4099ac8 Merge pull request #2109 from sbwalker/dev
dogfooding fixes
2022-04-04 10:54:50 -04:00
683ad8959a dogfooding fixes 2022-04-04 10:53:40 -04:00
a559c771cf Broken link fixed in Readme 2022-04-04 14:37:58 +01:00
8ba0ebf955 Update README.md 2022-04-04 09:08:32 -04:00
d83ec1827a Update README.md 2022-04-02 11:28:04 -04:00
314e49f5e1 Merge pull request #2106 from sbwalker/dev
adopt more of the migrations conventions
2022-04-02 11:25:03 -04:00
412b139796 adopt more of the migrations conventions 2022-04-02 11:24:41 -04:00
9b29487934 Update README.md 2022-04-02 11:12:03 -04:00
95213e41c4 Merge pull request #2105 from sbwalker/dev
replace startswith with equality to handle site subfolders
2022-04-02 11:09:01 -04:00
644ddfd5e1 replace startswith with equality to handle site subfolders 2022-04-02 11:08:38 -04:00
68dd9900c4 Merge pull request #2104 from sbwalker/dev
fix installation CSS issue
2022-04-02 09:29:38 -04:00
6b100cf70b fix installation CSS issue 2022-04-02 09:29:12 -04:00
6f33e5e8a0 Merge pull request #2103 from sbwalker/dev
refactored IUpgradeable to use the migration attribute approach
2022-04-02 09:19:50 -04:00
268e0e72a3 refactored IUpgradeable to use the migration attribute approach 2022-04-02 09:19:30 -04:00
5380b12294 Merge pull request #2102 from sbwalker/dev
allow for multiple upgrade classes
2022-04-01 18:07:13 -04:00
2ba1a95c8d allow for multiple upgrade classes 2022-04-01 18:06:59 -04:00
1ad0ee4a71 Merge pull request #2101 from sbwalker/dev
include theme resources on server page load, add IUpgradeable interface to provide site-based versioning support
2022-04-01 17:57:52 -04:00
fc12903cfd include theme resources on server page load, add IUpgradeable interface to provide site-based versioning support 2022-04-01 17:57:30 -04:00
640d22484d Merge pull request #2099 from leigh-pointer/ExternalMod
Updated Package reference to align with 3.1.0
2022-04-01 08:58:04 -04:00
34dc4d64e6 Merge pull request #2100 from sbwalker/dev
fix issue with the disabled link tags
2022-04-01 08:51:28 -04:00
bbb547efb6 fix issue with the disabled link tags 2022-04-01 08:51:08 -04:00
5b3640e23d Theme Template updated to 3.1.0 2022-04-01 12:56:28 +02:00
0fbbe244d8 Updated Package reference to align with 3.1.0 2022-04-01 12:53:55 +02:00
57def7da0c Merge pull request #2098 from sbwalker/dev
filter deleted pages and modules in the router, provide support for cascading aspect of style sheets, replace ResourceDeclaration concept with ResourceLevel
2022-03-31 21:06:25 -04:00
0fcf1c2732 filter deleted pages and modules in the router, provide support for cascading aspect of style sheets, replace ResourceDeclaration concept with ResourceLevel 2022-03-31 21:05:58 -04:00
c15b6cdf79 Merge pull request #2096 from sbwalker/dev
hide/show secure fields
2022-03-31 09:00:28 -04:00
06e25e04f8 hide/show secure fields 2022-03-31 09:00:13 -04:00
f8e04656cd Merge pull request #2095 from sbwalker/dev
better seperation of concerns
2022-03-31 08:35:29 -04:00
1c8debd894 better seperation of concerns 2022-03-31 08:35:11 -04:00
58d8fcd074 Merge pull request #2094 from sbwalker/dev
cleanup
2022-03-30 22:08:48 -04:00
a70f1ee1e0 cleanup 2022-03-30 22:08:32 -04:00
271ed3cbe2 Merge pull request #2093 from sbwalker/dev
fix registration
2022-03-30 08:10:56 -04:00
8ddaf57e17 fix registration 2022-03-30 08:10:42 -04:00
4f1ead116f Merge pull request #2092 from sbwalker/dev
remote service support via Jwt
2022-03-30 08:07:18 -04:00
3194c5b600 remote service support via Jwt 2022-03-30 08:07:03 -04:00
717f1a9b76 Merge pull request #2089 from sbwalker/dev
jwt changes
2022-03-29 08:39:01 -04:00
b7675a21eb jwt changes 2022-03-29 08:38:46 -04:00
b0d4c0d578 Merge pull request #2088 from sbwalker/dev
jwt improvements
2022-03-29 08:15:27 -04:00
b7a1d2df75 jwt improvements 2022-03-29 08:15:13 -04:00
5d31d33804 Merge pull request #2087 from sbwalker/dev
add Jwt authorization support for for API
2022-03-28 21:52:14 -04:00
a97af42e4b add Jwt authorization support for for API 2022-03-28 21:51:55 -04:00
17c6797afb Merge pull request #2086 from sbwalker/dev
cleanly separate SiteState service for client and server use cases
2022-03-27 21:06:08 -04:00
c8129607e8 cleanly separate SiteState service for client and server use cases 2022-03-27 21:05:44 -04:00
c2dce38bb1 Merge pull request #2085 from sbwalker/dev
fix #2082 - missing SiteId causing password reset notifications to not be created
2022-03-27 20:02:47 -04:00
8b0b7492f5 fix #2082 - missing SiteId causing password reset notifications to not be created 2022-03-27 20:02:19 -04:00
a25cfd87cc Merge pull request #2084 from sbwalker/dev
remove SiteSettings from Alias for better separation of concerns
2022-03-27 19:48:14 -04:00
f9432acf1b remove SiteSettings from Alias for better separation of concerns 2022-03-27 19:47:52 -04:00
c6c468c986 Merge pull request #2081 from sbwalker/dev
factor out auth constants, remove TAlias is Alias is not an extensible type, improve SiteOptions cache clearing, improve principal validation, localization improvements
2022-03-26 17:30:32 -04:00
b92a888583 factor out auth constants, remove TAlias is Alias is not an extensible type, improve SiteOptions cache clearing, improve principal validation, localization improvements 2022-03-26 17:30:06 -04:00
692b4b33fb Merge pull request #2079 from sbwalker/dev
consolidate user creation
2022-03-24 12:33:02 -04:00
79f427e10a consolidate user creation 2022-03-24 12:32:41 -04:00
340b3e7fe8 Merge pull request #2077 from sbwalker/dev
login localization
2022-03-23 17:19:18 -04:00
50a44c9416 login localization 2022-03-23 17:19:02 -04:00
3c41493d8e Merge pull request #2076 from sbwalker/dev
prepare for 3.1 release
2022-03-23 15:04:26 -04:00
4566ea436c prepare for 3.1 release 2022-03-23 15:04:03 -04:00
499bf3bc28 Update README.md 2022-03-23 10:58:44 -04:00
489a321763 Merge pull request #2075 from sbwalker/dev
Add OAuth2 support
2022-03-23 10:52:20 -04:00
9d86d923aa Add OAuth2 support 2022-03-23 10:51:52 -04:00
454529bd6a Merge pull request #2074 from sbwalker/dev
Allow Email Claim Type to be configurable
2022-03-21 16:29:45 -04:00
ca17dd3ca3 Allow Email Claim Type to be configurable 2022-03-21 16:29:28 -04:00
71c7a3de69 Merge pull request #2073 from sbwalker/dev
Add scheme to Redirect Url
2022-03-21 10:50:19 -04:00
76fc689337 Add scheme to Redirect Url 2022-03-21 10:50:01 -04:00
af5d25490a Merge pull request #2072 from sbwalker/dev
OIDC improvements
2022-03-21 10:39:51 -04:00
fb161ae783 OIDC improvements 2022-03-21 10:39:35 -04:00
b92b20e8d2 Merge pull request #2071 from sbwalker/dev
OIDC improvements
2022-03-21 09:12:40 -04:00
4b19059df1 OIDC improvements 2022-03-21 09:12:18 -04:00
baa6ec5cba Merge pull request #2069 from sbwalker/dev
More improvements to OIDC support
2022-03-19 13:42:37 -04:00
1a86b80c61 More improvements to OIDC support 2022-03-19 13:42:19 -04:00
3783da3647 Merge pull request #2067 from sbwalker/dev
OIDC improvements
2022-03-16 17:28:51 -04:00
39dfc00693 OIDC improvements 2022-03-16 17:28:32 -04:00
c7cad20aa7 Merge pull request #2065 from sbwalker/dev
Resolved issue in TenantManager resulting in "No database provider has been configured for this DbContext" error being logged during startup. Added logic to update email address in AspNetUsers when updating a User.
2022-03-16 09:54:39 -04:00
5901365e0e Resolved issue in TenantManager resulting in "No database provider has been configured for this DbContext" error being logged during startup. Added logic to update email address in AspNetUsers when updating a User. 2022-03-16 09:53:59 -04:00
6324aacba1 Merge pull request #2064 from sbwalker/dev
Improve Principal handling for OIDC and resolve Logout issue (caused by AntiForgeryToken)
2022-03-14 22:29:19 -04:00
d51ba8f6dd Improve Principal handling for OIDC and resolve Logout issue (caused by AntiForgeryToken) 2022-03-14 22:28:41 -04:00
9b69e135d9 Merge pull request #2053 from leigh-pointer/FileConstants
Fix for File Upload Failed {Error} .json file #2052
2022-03-13 22:56:50 -04:00
c3218b2f5a Merge pull request #2060 from sbwalker/dev
Added support for per site options and OpenID Connect
2022-03-13 22:56:29 -04:00
9bbbff31f8 Added support for per site options and OpenID Connect 2022-03-13 22:55:52 -04:00
432429026b Fix for File Upload Failed {Error} .json file #2052
Udate to FileUpload Constant 
added extensions json, xml, xslt, rss, html, htm, css
This is an interim fix with plans to make the upload extensions a soft implementation.
2022-03-09 14:27:14 +01:00
a47ecbdea9 Merge pull request #2048 from sbwalker/dev
Added password policy validation in install wizard
2022-03-08 08:20:15 -05:00
f250aff99b Added password policy validation in install wizard 2022-03-08 08:31:18 -05:00
003f14003e Fixed issue with IHostResources not being registered properly 2022-03-07 16:52:40 -05:00
fe4e245cda Merge pull request #2045 from sbwalker/dev
Fixed issue with IHostResources not being registered properly
2022-03-07 16:41:37 -05:00
668da62519 Fix #2032 - Fresh install with Postgres failed with "42703: column "settingname" does not exist POSITION: 43 2022-03-07 12:23:35 -05:00
fd89254d5a fix #2041 - Server restart post module install fails with null exception 2022-03-07 12:19:00 -05:00
b4338c1761 Merge pull request #2043 from sbwalker/dev
Fix #2032 - Fresh install with Postgres failed with "42703: column "settingname" does not exist POSITION: 43
2022-03-07 12:12:44 -05:00
fb3c79617f Merge pull request #2042 from sbwalker/dev
fix #2041 - Server restart post module install fails with null exception
2022-03-07 12:07:58 -05:00
b80fe428ac add show/hide password toggle on Login form 2022-03-04 11:43:54 -05:00
5806563ba4 Merge pull request #2040 from sbwalker/dev
add show/hide password toggle on Login form
2022-03-04 11:32:56 -05:00
5adecc307f Allow user identity password and lockout configuration to be customized. Included additional environment information in System Info. 2022-03-04 10:41:45 -05:00
12d1b5e849 Update README.md 2022-03-04 10:35:07 -05:00
3f2095870d Merge pull request #2039 from sbwalker/dev
Allow user identity password and lockout configuration to be customized. Included additional environment information in System Info.
2022-03-04 10:31:18 -05:00
1481c76a0d Update README.md 2022-03-03 09:19:57 -05:00
1cdc80e09b 2 factor authentication and user account lockout completed 2022-03-03 09:12:37 -05:00
e568aa8320 Merge pull request #2037 from sbwalker/dev
2 factor authentication and user account lockout completed
2022-03-03 09:01:54 -05:00
28629aa836 Adding 2 factor authentication 2022-02-28 16:00:52 -05:00
19f180331b Adding 2 factor authentication 2022-02-28 15:58:49 -05:00
3292f0b545 Merge pull request #2034 from sbwalker/dev
Adding 2 factor authentication
2022-02-28 15:50:04 -05:00
3333bfeeff Merge pull request #2033 from sbwalker/dev
Adding 2 factor authentication
2022-02-28 15:48:02 -05:00
eb1ac3bc9b Added support for User Account Lockout 2022-02-25 16:17:54 -05:00
65ae1a6177 Merge pull request #2031 from sbwalker/dev
Added support for User Account Lockout
2022-02-25 16:07:09 -05:00
0fba385b9e Enhanced Purge Job to include retention policy for Notifications 2022-02-24 12:37:06 -05:00
ee65a54684 Merge pull request #2028 from sbwalker/dev
Enhanced Purge Job to include retention policy for Notifications
2022-02-24 12:26:24 -05:00
82fef82c4f use consistent naming convention for System Update log file 2022-02-24 11:11:15 -05:00
70383a9b9d Merge pull request #2027 from sbwalker/dev
use consistent naming convention for System Update log file
2022-02-24 11:00:33 -05:00
15fdba060c Improvements to System Upgrade to preserve the processing details in a log file in the /Packages folder to improve troubleshooting abilities 2022-02-24 09:55:34 -05:00
c1065dab2d Merge pull request #2026 from sbwalker/dev
Improvements to System Upgrade to preserve the processing details in a log file in the /Packages folder to improve troubleshooting abilities
2022-02-24 09:44:55 -05:00
dfb4afc698 Merge pull request #2020 from leigh-pointer/ImportExportSettings
Fix for Module Settings Import and Export #2019
2022-02-24 08:51:20 -05:00
893b09e7e4 Merge pull request #2025 from sbwalker/dev
Added more constructors for convenience in creating  Notification objects. Refactored to use the new constructors where applicable. Fixed localization key issue in Site Settings and added scroll to top when testing SMTP.
2022-02-24 08:51:10 -05:00
938bcb2b62 Added more constructors for convenience in creating Notification objects. Refactored to use the new constructors where applicable. Fixed localization key issue in Site Settings and added scroll to top when testing SMTP. 2022-02-24 09:01:44 -05:00
ac45f67a21 enhancement to send log notifications to host users 2022-02-23 16:10:24 -05:00
36cd9664b1 Merge pull request #2022 from sbwalker/dev
enhancement to send log notifications to host users
2022-02-23 15:59:41 -05:00
073d330db4 Fix for Module Settings Import and Export #2019
Added the module Settings so they are available for the Import and Export Interface.
2022-02-23 14:33:24 +01:00
9ba356c47e moved AlterStringColumn to IDatabase interface so that it can be overridden in the Sqlite provider rather than requiring conditional logic in the migrations 2022-02-22 14:47:31 -05:00
f2bec9b478 Merge pull request #2017 from sbwalker/dev
moved AlterStringColumn to IDatabase interface so that it can be overridden in the Sqlite provider rather than requiring conditional logic in the migrations
2022-02-22 14:36:58 -05:00
3d0cbdd1a7 expand Url column in Visitor and UrlMapping to accomodate maximum url size of 2048 characters 2022-02-22 10:01:52 -05:00
c5f5bf0287 Merge pull request #2015 from sbwalker/dev
expand Url column in Visitor and UrlMapping to accomodate maximum url size of 2048 characters
2022-02-22 09:51:14 -05:00
99986c1b94 changed IsModule property name to ES6Module for clarity 2022-02-20 08:53:04 -05:00
7d669caa3c Merge pull request #2011 from sbwalker/dev
changed IsModule property name to ES6Module for clarity
2022-02-20 08:42:34 -05:00
b68e3cb10f Add support for ES6 module JavaScript resources 2022-02-19 17:24:41 -05:00
e305c488d4 Merge pull request #2010 from sbwalker/dev
Add support for ES6 module JavaScript resources
2022-02-19 17:14:21 -05:00
a2417bbe56 Merge pull request #2005 from leigh-pointer/BootstrapUpdate
Theme Template Updated
2022-02-18 11:00:58 -05:00
b3967b36c0 Corrected incorrect CDN 2022-02-18 14:52:51 +01:00
5fb33dfee9 Bootstrap reference updated to 5.1.3 2022-02-18 08:38:53 +01:00
c002768e5b Merge pull request #2001 from oqtane/master
Merge pull request #2000 from oqtane/dev
2022-02-15 14:31:40 -05:00
aff33c6a5d Merge pull request #2000 from oqtane/dev
3.0.3 release
2022-02-15 14:31:21 -05:00
a84b497fae Merge pull request #1999 from artmedia/patch-1
typo correction for closing tag
2022-02-15 09:37:39 -05:00
c33d1bcd3c typo correction for closing label
typo correction for closing label
2022-02-15 10:19:58 +01:00
4071e14a7e Update README.md 2022-02-14 16:25:58 -05:00
b5b3f190b7 Merge pull request #1998 from leigh-pointer/ModuleCount
null reference exception still occurring
2022-02-14 13:07:29 -05:00
d43a3e132c null reference exception still occurring
added a '?' operator after the m.ModuleDefinition
2022-02-14 19:06:00 +01:00
a90c21f80a Merge pull request #1997 from sbwalker/dev
prepare for 3.0.3 release
2022-02-11 16:49:21 -05:00
2b768165e5 prepare for 3.0.3 release 2022-02-11 16:59:48 -05:00
e8425ba03a improve performance by reducing database calls in initial client request scenarios 2022-02-11 15:42:34 -05:00
b0a6f402e9 Merge pull request #1996 from sbwalker/dev
improve performance by reducing database calls in initial client request scenarios
2022-02-11 15:32:16 -05:00
02e86a940b Merge pull request #1973 from 2sic-forks/refs
fix #1972 Missing Preprocessor Directives during Runtime Compile
2022-02-10 07:56:52 -05:00
79d03eb43e Merge pull request #1988 from 2sic-forks/content
fix #1987 Nuspec to include 'content'
2022-02-10 07:56:08 -05:00
b564955f85 Merge pull request #1994 from sbwalker/dev
fixed #1989 - installation on SQLite failing due to DropColumn, fixed #1986 - IClientStartup not getting called for External Modules, added ability to correlate new visitors by IP address
2022-02-10 07:55:51 -05:00
5aed64f614 fixed #1989 - installation on SQLite failing due to DropColumn, fixed #1986 - IClientStartup not getting called for External Modules, added ability to correlate new visitors by IP address 2022-02-10 08:05:55 -05:00
a823a4d9b7 remove precompile symbol OQTANE3_0_OR_GREATER as it have the same effect as simple OQTANE 2022-02-08 20:15:50 +01:00
4eed2193f4 Merge branch 'oqtane:dev' into refs 2022-02-08 19:55:34 +01:00
48e7a41af6 fix #1987 Nuspec to include 'content' 2022-02-08 19:38:31 +01:00
bba5caecf7 Merge branch 'oqtane:dev' into content 2022-02-08 19:34:15 +01:00
ede6a45f15 more RichTextEditor refactoring 2022-02-08 07:42:47 -05:00
9c65d23229 Merge pull request #1992 from sbwalker/dev
more RichTextEditor refactoring
2022-02-08 07:32:21 -05:00
5dedfe9295 fix oqtane#1987 Nuspec to include 'content' ('contentFiles') 2022-02-07 14:09:14 +01:00
95d8c368c8 Meta tags should not be HTML encoded 2022-02-06 18:54:09 -05:00
49fbfb8bbc Merge pull request #1985 from sbwalker/dev
Meta tags should not be HTML encoded
2022-02-06 18:43:52 -05:00
aa3d2a5289 Merge pull request #1969 from 2sic-forks/dev
fix #1272 - add support for refs folder in package installation
2022-02-06 12:11:29 -05:00
48ae6df4b7 Merge pull request #1984 from sbwalker/dev
resolved UI error when closing Event Log and Visitor Management, made button class consistent in Recycle Bin, refactored RichTextEditor, made use of ConfigManager consistently throughout framework, added support for deleted Sites, removed reference to Runtime in Startup as it is now set per Site, added versioning to Html/Text, added Meta tag support to Page Management
2022-02-06 12:09:40 -05:00
c635351a12 resolved UI error when closing Event Log and Visitor Management, made button class consistent in Recycle Bin, refactored RichTextEditor, made use of ConfigManager consistently throughout framework, added support for deleted Sites, removed reference to Runtime in Startup as it is now set per Site, added versioning to Html/Text, added Meta tag support to Page Management 2022-02-06 12:19:42 -05:00
d9ff77fd9a fix #1972 Missing Preprocessor Directives during Runtime Compile 2022-01-31 18:27:56 +01:00
e1a7954307 fix #1272 - add support for refs folder in package installation 2022-01-29 06:45:51 +01:00
efe6421133 Merge pull request #1957 from Rodien/dev
Added web.Release.config to include remove WebDAV during the publish stage of a release
2022-01-28 13:00:56 -05:00
79b62f4407 Merge pull request #1968 from sbwalker/dev
improved UX in Event Log by preserving criteria when viewing Details, added RowClass and ColumnClass parameters to Pager component, added initial-scale=1.0 to viewport specification in _host, added default visitor tracking filter, fixed "The given key 'level' was not present in the dictionary" issue in Visitor Management - Details by ensuring data was fully loaded
2022-01-27 18:02:20 -05:00
9d17804ac7 improved UX in Event Log by preserving criteria when viewing Details, added RowClass and ColumnClass parameters to Pager component, added initial-scale=1.0 to viewport specification in _host, added default visitor tracking filter, fixed "The given key 'level' was not present in the dictionary" issue in Visitor Management - Details by ensuring data was fully loaded 2022-01-27 18:12:04 -05:00
5986355504 Merge pull request #1965 from leigh-pointer/ModuleCount#1963
Fix for Version 3 module definitions error #1963
2022-01-25 13:38:58 -05:00
192e6fde92 Fix for Version 3 module definitions error #1963
The code was assuming that the ModuleDefinitionId exists in the PageState.Modules collection. Instead of using .Count()  code now uses .FirstOrDefault() != null
2022-01-25 06:23:07 +01:00
ad090e62cc enhance Pager to support pure responsive Grid format (Columns = 0) 2022-01-23 10:40:41 -05:00
5c072fea62 Merge pull request #1960 from sbwalker/dev
enhance Pager to support pure responsive Grid format (Columns = 0)
2022-01-23 10:30:54 -05:00
fd01a40810 Merge pull request #1958 from leigh-pointer/AddUserId
Add the Username to the User And Roles display.
2022-01-22 19:25:20 -05:00
7b9a83a273 Merge pull request #1959 from sbwalker/dev
added router support for url fragments, added language attribute to HTML document tag to improve validation, fixed Theme Settings so they can only be invoked via the Control Panel, added support for webp image files
2022-01-22 19:24:51 -05:00
f964e0e502 added router support for url fragments, added language attribute to HTML document tag to improve validation, fixed Theme Settings so they can only be invoked via the Control Panel, added support for webp image files 2022-01-22 19:34:30 -05:00
22acb7c74b Comments cleanup
Code cleanup. I removed the unnecessary comments.

This code will remove WebDAV during the publish stage of a release.
2022-01-21 11:58:24 +01:00
6a99e81e75 Add the Username to the display.
When looking through Users and Roles it would seem ideal to also show the username.
2022-01-21 09:36:32 +01:00
93b6de1caf Added web.Release.config to include remove WebDAV during the publish stage of a release 2022-01-21 02:32:48 +01:00
1fbab5db2b Merge pull request #1954 from sbwalker/dev
enhance Pager component with OnPageChanged event and implement in Visitor Management, allow PermissionGrid component to support Host role, fix unhandled exception in RichTextEditor component related to rerendering, make Quill resource declarations forward compatible, update Blazor theme to Boostrap 5.1.3, add missing RemoteIPAddress parameter in _Host app component, include logic to enable bypass of non-default alias redirection
2022-01-19 17:40:01 -05:00
950e852dee Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2022-01-19 17:47:39 -05:00
826898e3fe enhance Pager component with OnPageChanged event and implement in Visitor Management, allow PermissionGrid component to support Host role, fix unhandled exception in RichTextEditor component related to rerendering, make Quill resource declarations forward compatible, update Blazor theme to Boostrap 5.1.3, add missing RemoteIPAddress parameter in _Host app component, include logic to enable bypass of non-default alias redirection 2022-01-19 17:47:27 -05:00
1268149d83 Merge pull request #1944 from oqtane/master
Merge pull request #1943 from oqtane/dev
2022-01-16 11:06:34 -05:00
908299970f Merge pull request #1943 from oqtane/dev
3.0.2 release
2022-01-16 11:05:56 -05:00
861dde8627 Update README.md 2022-01-15 12:59:50 -05:00
cc9802a0d8 use PageState.Uri rather than creating a new Uri object 2022-01-15 12:58:47 -05:00
69d1f3aa53 Merge pull request #1940 from sbwalker/dev
use PageState.Uri rather than creating a new Uri object
2022-01-15 12:49:00 -05:00
ea4587d842 Update README.md 2022-01-15 12:38:23 -05:00
fb4c95f945 Update README.md 2022-01-15 12:37:52 -05:00
95a27af5f2 Update README.md 2022-01-15 12:34:34 -05:00
9d7b25ade6 Merge pull request #1939 from sbwalker/dev
improvement for updating private/public Settings
2022-01-15 09:34:56 -05:00
3a8f4199cd improvement for updating private/public Settings 2022-01-15 09:44:36 -05:00
11002efc02 hide deleted pages in Admin Dashboard, impove Settings API by replacing IsPublic with IsPrivate, isolate Setting updates to not affect PageState, make Pager horizintally scrollable on narrow viewports, improve LocalizableComponent to support embedded controls 2022-01-14 13:26:24 -05:00
367c1c3568 Merge pull request #1938 from sbwalker/dev
hide deleted pages in Admin Dashboard, impove Settings API by replacing IsPublic with IsPrivate, isolate Setting updates to not affect PageState, make Pager horizintally scrollable on narrow viewports, improve LocalizableComponent to support embedded controls
2022-01-14 13:16:42 -05:00
9e04230d99 added interop method for setting scroll position, persisted RemoteIPAddress in PageState so it is available on Blazor Server, added support for forwarded headers from load balancers and proxy servers, replaced DateTime.Now references DateTimeUtcNow for consistency, fixed issue where upgrade logic was being executed for prior version 2022-01-13 07:18:37 -05:00
21304db7c9 Merge pull request #1936 from sbwalker/dev
added interop method for setting scroll position, persisted RemoteIPAddress in PageState so it is available on Blazor Server, added support for forwarded headers from load balancers and proxy servers, replaced DateTime.Now references DateTimeUtcNow for consistency, fixed issue where upgrade logic was being executed for prior version
2022-01-13 07:10:15 -05:00
f4f6e98045 Merge pull request #1935 from leigh-pointer/DeadResxKey
Empty Resx Value on Page Edit
2022-01-12 16:13:00 -05:00
ad41eff38a Empty Resx Value on Page Edit 2022-01-12 20:42:59 +01:00
dbd6cc4148 Merge pull request #1934 from oqtane/revert-1931-dev
Revert "Fixed first render js bug"
2022-01-12 14:09:08 -05:00
dda71e5ccd Revert "Fixed first render js bug" 2022-01-12 14:07:50 -05:00
cfe8059176 Merge pull request #1931 from zzmzaizai/dev
Fixed first render js bug
2022-01-12 13:57:13 -05:00
8b00784ecc Merge pull request #1933 from sbwalker/dev
fix z-index for Blazor theme on mobile
2022-01-12 07:50:44 -05:00
9bcc6bbad0 fix z-index for Blazor theme on mobile 2022-01-12 08:00:25 -05:00
ce7995966d Fixed first render js bug
Solve the problem that when the page is rendered for the first time and JS is executed, the reference to the JS file has not been successful, and the page is abnormally wrong
2022-01-12 10:36:10 +08:00
cea5f86df4 prepare for 3.0.2 release 2022-01-11 17:46:36 -05:00
0912253b1b Merge pull request #1930 from sbwalker/dev
prepare for 3.0.2 release
2022-01-11 17:36:54 -05:00
5aecc4be03 remove invalid app tag, fix page title not being set on first render 2022-01-11 15:07:54 -05:00
e09178c14c Merge pull request #1927 from sbwalker/dev
remove invalid app tag, fix page title not being set on first render
2022-01-11 14:58:13 -05:00
477ded6a4a Merge pull request #1921 from zzmzaizai/dev
Fixed first render css bug
2022-01-11 14:49:57 -05:00
311c48becb Merge pull request #1926 from sbwalker/dev
improve UX of password reset
2022-01-11 10:48:18 -05:00
ec924a7ddf improve UX of password reset 2022-01-11 10:57:58 -05:00
e39416a786 Update README.md 2022-01-11 09:14:43 -05:00
51b356cc0e enhanced scheduler to support one-time jobs, fixed pager component so that top/bottom have consistent UX, fixed Blazor theme z-index issues caused by input-group in Bootstrap 5, improved password reset instructions in email notification 2022-01-10 19:58:58 -05:00
66b13bdb8b Merge pull request #1925 from sbwalker/dev
enhanced scheduler to support one-time jobs, fixed pager component so that top/bottom have consistent UX, fixed Blazor theme z-index issues caused by input-group in Bootstrap 5, improved password reset instructions in email notification
2022-01-10 19:49:25 -05:00
4ade58da01 Fixed first render css bug
Fixed the bug that CSS could not be render when the module was loaded for the first
2022-01-10 16:06:48 +08:00
efcfc0783c Merge pull request #1917 from leigh-pointer/LabelCSS
Fix for  #1914 Label Control appending Class to LabelClass
2022-01-08 14:03:28 -05:00
aa22db7fe5 Merge pull request #1916 from sbwalker/dev
add error handling in purge job
2022-01-08 10:12:27 -05:00
5e0f008b65 add error handling in purge job 2022-01-08 10:22:05 -05:00
eaf840e1da improvements to purge job 2022-01-08 10:17:10 -05:00
fc9e47778b Fix for #1914 Label Control appending Class to LabelClass
Modified so that the Class parameter is not constantly appended when a new Class is applied.
2022-01-08 16:12:27 +01:00
35edf78aed Merge pull request #1915 from sbwalker/dev
improvements to purge job
2022-01-08 10:07:36 -05:00
07718f0449 add option to Control Panel to specify module visibility 2022-01-08 08:44:18 -05:00
6759156519 Merge pull request #1913 from sbwalker/dev
add option to Control Panel to specify module visibility
2022-01-08 08:34:45 -05:00
e2688e6feb include purge job for maintaining event logs and visitor logs 2022-01-07 23:30:29 -05:00
65ba6423b1 Merge pull request #1912 from sbwalker/dev
include purge job for maintaining event logs and visitor logs
2022-01-07 23:21:06 -05:00
a2f8fe3694 convention shortcut to suppress title in container 2022-01-06 17:24:18 -05:00
5273a17ab6 Merge pull request #1911 from sbwalker/dev
convention shortcut to suppress title in container
2022-01-06 17:14:45 -05:00
f7c1e7b706 alias management improvements 2022-01-06 13:37:29 -05:00
45bbc4c681 Merge pull request #1910 from sbwalker/dev
alias management improvements
2022-01-06 13:27:51 -05:00
6af5682548 increment copyright date to 2022, allow scheduled jobs to support weekly interval, improve dynamic image generation, add defensive logic to router 2022-01-05 14:28:42 -05:00
24ed06626d Merge pull request #1909 from sbwalker/dev
increment copyright date to 2022, allow scheduled jobs to support weekly interval, improve dynamic image generation, add defensive logic to router
2022-01-05 14:19:16 -05:00
eeff4af167 make Url Mappings relative rather than absolute 2022-01-03 10:56:13 -05:00
17f46afe14 Merge pull request #1903 from sbwalker/dev
make Url Mappings relative rather than absolute
2022-01-03 10:46:42 -05:00
224618cf21 improve Scheduled Job start/stop user experience, utilize start time when setting next job execution 2022-01-02 21:01:55 -05:00
ea93ab2a83 Merge pull request #1902 from sbwalker/dev
improve Scheduled Job start/stop user experience, utilize start time when setting next job execution
2022-01-02 20:53:00 -05:00
b9f7c39550 improve capture of request attributes 2021-12-30 14:13:58 -05:00
86b4b8e43a Merge pull request #1901 from sbwalker/dev
improve capture of request attributes
2021-12-30 14:04:27 -05:00
f54d07548e separate PWA service worker script from manifest script 2021-12-23 09:46:03 -05:00
037db8a3e4 Merge pull request #1898 from sbwalker/dev
separate PWA service worker script from manifest script
2021-12-23 09:36:43 -05:00
8f00e85abd Merge pull request #1897 from leigh-pointer/MissingResx
Missing Resx Keys
2021-12-23 07:50:35 -05:00
9ccc4c4059 Missing Resx Keys
Added missing Keys for Login and Visitor
2021-12-23 11:36:20 +01:00
8408f98693 Merge pull request #1896 from sbwalker/dev
encode PWA Script
2021-12-22 15:43:13 -05:00
cde271fd5b encode PWA Script 2021-12-22 15:52:31 -05:00
c21a097fd2 added support for default alias specification, alias auto registration, alias redirect, alias line break delimiters 2021-12-22 15:43:59 -05:00
83c32d4963 Merge pull request #1895 from sbwalker/dev
added support for default alias specification, alias auto registration, alias redirect, alias line break delimiters
2021-12-22 15:34:49 -05:00
22c2d56da0 imrove custom entity support in settings 2021-12-20 07:58:15 -05:00
bd8d6e0480 Merge pull request #1886 from sbwalker/dev
imrove custom entity support in settings
2021-12-20 07:49:10 -05:00
825eb700b1 Merge pull request #1885 from chlupac/SearchUserFix
Search user work again
2021-12-20 07:46:06 -05:00
e59ee70f88 Search user work again 2021-12-20 13:06:33 +01:00
1173a29ed5 Merge pull request #1884 from sbwalker/dev
Add support for IsPublic to all Setting types, enable Url Mapping for internal links
2021-12-18 10:26:26 -05:00
6a2ff369ea Add support for IsPublic to all Setting types, enable Url Mapping for internal links 2021-12-18 10:35:22 -05:00
e22606ae79 Merge pull request #1882 from leigh-pointer/#1880GetModuleDefinitionSettings
#1880 Issue with new SettingService
2021-12-16 15:37:22 -05:00
bf56c2a9fa Merge pull request #1883 from leigh-pointer/RichTextContent
Rework to #1848 RawHTML not being saved
2021-12-16 15:37:14 -05:00
6567b55ea3 Removed RichTextEditor OnInitialized
Redundant procedure call.
2021-12-16 20:11:07 +01:00
20e90c0de4 Rework to #1848 RawHTML not being saved
Restructured the execution of code.
RawHTML now works as it did in previous versions as well as the new functionality.
2021-12-16 20:07:40 +01:00
2892d5ec6f Update README.md 2021-12-16 09:38:10 -05:00
e034811e92 #1880 Issue with new SettingService
Needed to update the SettingService to apply the IsPublic field when updating settings for the ModuleDefinition.
2021-12-15 18:54:26 +01:00
ee18bbd145 Merge pull request #1881 from sbwalker/dev
add logging for the logout event to the UI component, relocate module setting deletion to repository
2021-12-15 10:24:17 -05:00
e3ebbde767 add logging for the logout event to the UI component, relocate module setting deletion to repository 2021-12-15 10:33:12 -05:00
6a57980439 Merge pull request #1879 from leigh-pointer/#1877#1878-ModuleDelete
1877 Module data not being delete when recycle bin is purged
2021-12-15 10:14:38 -05:00
765760f3a5 Fix for #1877 #1878 Module data not being deleted
Fixed the permissions validation and added functionality to remove all the settings for the deleted module.
2021-12-15 08:26:00 +01:00
ab1ac7c995 Merge pull request #1875 from oqtane/master
Merge pull request #1874 from oqtane/dev
2021-12-12 20:46:54 -05:00
99b0c9c079 Merge pull request #1874 from oqtane/dev
3.0.1 release
2021-12-12 20:46:28 -05:00
e99ab431e1 Merge pull request #1872 from sbwalker/dev
create url mapping when page path changes
2021-12-12 09:50:33 -05:00
1e1aaaccca create url mapping when page path changes 2021-12-12 09:59:33 -05:00
318d6afb0e fix url mapping resx issue 2021-12-11 09:39:03 -05:00
ba353857eb Merge pull request #1869 from sbwalker/dev
fix url mapping resx issue
2021-12-11 09:30:19 -05:00
1fd42f343d Merge pull request #1867 from leigh-pointer/MissingRes-3.0.1
Missing resource Keys for URL mapping and Visitors
2021-12-11 09:25:45 -05:00
ec9686cfb8 Merge branch 'dev' into MissingRes-3.0.1 2021-12-11 09:25:03 -05:00
a1bff809f3 Merge pull request #1868 from sbwalker/dev
visitor improvements
2021-12-11 09:21:26 -05:00
76fe155c0a visitor improvements 2021-12-11 09:30:05 -05:00
0b0254aed9 Updated Resx file 2021-12-11 14:24:08 +01:00
9258c3849b Went through each Framework module updating Resources
New English resources added
2021-12-11 13:43:22 +01:00
d530f30bc9 Missing Res Keys
Added missing resource keys
2021-12-11 11:54:25 +01:00
7d8bbac04f remove quill 1.3.6 assets 2021-12-10 14:19:32 -05:00
7b0c0c3e17 prepare for 3.0.1 2021-12-10 14:16:16 -05:00
298c3097f7 Merge pull request #1866 from sbwalker/dev
remove quill 1.3.6 assets
2021-12-10 14:10:59 -05:00
3a3f221418 Merge pull request #1865 from sbwalker/dev
prepare for 3.0.1
2021-12-10 14:07:30 -05:00
78110791e1 Merge pull request #1863 from leigh-pointer/InstallManagerDelete
Fix for Installed packages not being removed correctly
2021-12-10 10:11:31 -05:00
95d4c3d0d5 Merge pull request #1864 from sbwalker/dev
adjust permissions for new settings
2021-12-10 10:11:16 -05:00
e95b49ba8f adjust permissions for new settings 2021-12-10 10:20:03 -05:00
92ccb7e463 Fix for Installed packages not being removed correctly
When a package is remove in some instance the system complains that a file still exists in the deleting directory but there is not file.
Added true parameter to the Directory delete for force the removal. 
Directory.Delete(Path.GetDirectoryName(filepath), true);
2021-12-10 16:06:12 +01:00
2f34bf69e3 moduledefinition settings and host settings 2021-12-09 15:50:00 -05:00
d093c03d92 Merge pull request #1862 from sbwalker/dev
moduledefinition settings and host settings
2021-12-09 15:41:19 -05:00
d1ade8789b Merge pull request #1832 from leigh-pointer/ModuleDefinitionSettings
Settings for ModuleDefinitions #1829
2021-12-09 13:35:54 -05:00
1291eb5b7c Merge pull request #1861 from sbwalker/dev
added support for url mapping and viitors
2021-12-09 08:40:15 -05:00
9c32937c83 added support for url mapping and viitors 2021-12-09 08:48:56 -05:00
1ec28e9825 Merge pull request #1855 from svendu/fix_postgres_installation
Make IsPublic of type bool to make PostgreSQL happy
2021-12-08 12:54:30 -05:00
86fce898e5 Merge pull request #1853 from leigh-pointer/PagerBoth
Update the ToolBar position on the Pager Component
2021-12-08 12:54:10 -05:00
bbee87f7df Make IsPublic of type bool 2021-12-07 12:07:26 +01:00
811ddb9b44 Update the ToolBar position on the Pager Component
Add the option "Both" to display the toolbar at the top and bottom of the pager.  Nice if the Pager is displaying large sets of data.
2021-12-06 19:18:07 +01:00
de798da074 Merge pull request #1848 from leigh-pointer/RichTextContentRefresh
Fix #1837 RichTextEditor Content not re-Rendering
2021-12-03 09:45:59 -05:00
65d468be33 Fix #1837 RichTextEditor Content not re-Rendering
Change to the OnAfterRenderAsync method and changed OnInitialized to OnParametersSet
2021-12-03 06:31:45 +01:00
9664ff67f3 Merge pull request #1842 from leigh-pointer/QuillEditor1.3.7-SecurityUpdate
Quill Security related bug fixes.
2021-12-02 16:24:49 -05:00
99f73cf31e Merge pull request #1846 from sbwalker/dev
Additional properties added to Route model to improve abstraction, modified Site Settings to support settings moved to the server.
2021-12-02 16:24:39 -05:00
a216e2b92e Additional properties added to Route model to improve abstraction, modified Site Settings to support settings moved to the server. 2021-12-02 16:33:16 -05:00
9dfd9ad519 Quill Security related bug fixes.
Upgraded Quill references to 1.3.7
Tabnabbing vulnerability in snow theme #2438
https://github.com/quilljs/quill/issues/2438

https://github.com/quilljs/quill/releases/tag/v1.3.7
2021-12-02 09:56:55 +01:00
97133510a7 Update README.md 2021-12-01 09:11:21 -05:00
43d166fb7d Route parsing abstraction and optimization, site router performance improvements, migrate site-based concepts (favicon, PWA support) to server for performance and prerendering benefits, move ThemeBuilder interop logic to OnAfterRenderAsync, upgrade SqlClient to release version, update installer to Bootstrap 5.1.3 2021-12-01 08:22:59 -05:00
2f8a580854 Merge pull request #1840 from sbwalker/dev
Route parsing abstraction and optimization, site router performance improvements, migrate site-based concepts (favicon, PWA support) to server for performance and prerendering benefits, move ThemeBuilder interop logic to OnAfterRenderAsync, upgrade SqlClient to release version, update installer to Bootstrap 5.1.3
2021-12-01 08:14:46 -05:00
a21a2ab3bb Settings for ModuleDefinitions #1829
Add Update settings for the ModuleDefinition
2021-11-24 16:06:52 +01:00
03106526e9 Enhance the default site template with a Develop page that makes the Module Creator more discoverable for new users 2021-11-24 09:07:43 -05:00
a9ac3917b3 Merge pull request #1831 from sbwalker/dev
Enhance the default site template with a Develop page that makes the Module Creator more discoverable for new users
2021-11-24 08:59:54 -05:00
53ff491efd Assorted enhancements 2021-11-24 08:08:39 -05:00
1feb6ec452 Merge pull request #1830 from sbwalker/dev
Assorted enhancements
2021-11-24 08:00:49 -05:00
df00f53e54 Merge pull request #1823 from hishamco/tab-panel-localizer
Fix heading localization in TabPanel
2021-11-22 16:03:50 -05:00
be32af7588 Merge pull request #1827 from sbwalker/dev
refactored ErrorBoundary implementation to support logging
2021-11-22 16:03:17 -05:00
19be77ed49 refactored ErrorBoundary implementation to support logging 2021-11-22 16:11:44 -05:00
1c43c095bc Fix heading localization in TabPanel 2021-11-20 09:47:49 +03:00
59850f4869 Merge pull request #1815 from chlupac/ErrorBoundary
Implementing ErrorBoundary in ModuleInstance component
2021-11-18 16:14:35 -05:00
804b61ff95 Merge pull request #1820 from hishamco/swagger
Handle SchemaId in Swagger
2021-11-18 08:54:57 -05:00
d431c607ba Handle SchemaId in Swagger 2021-11-18 14:50:17 +03:00
b87b0489e9 Merge pull request #1812 from leigh-pointer/PageModules
Modification to Page Management component
2021-11-17 08:52:31 -05:00
2e593d44ee Merge pull request #1813 from leigh-pointer/ModuleDefinitionsInUse
Modification to Module Management
2021-11-17 08:49:00 -05:00
71354464e3 Merge pull request #1810 from leigh-pointer/ControlPanel
Page management buttons resizing
2021-11-17 08:44:39 -05:00
c48b4788c6 Merge pull request #1805 from chlupac/AliasFix
Fix - site with default alias (*) edit fail
2021-11-17 08:44:05 -05:00
fe9a7333ed Merge pull request #1798 from leigh-pointer/BreadCrumbs
Fix for #1797 Breadcrumbs render clickable
2021-11-17 08:43:55 -05:00
16d9e06db2 Merge pull request #1793 from 2sic-forks/dev
Add many PrivateApi attributes to hide unimportant stuff in docs
2021-11-17 08:43:35 -05:00
b40ee19735 ErrorBoundary 2021-11-17 11:22:24 +01:00
d5b0356625 Modification to Module Management
The component now reports back if the module is in use.  This will assist in housekeeping and removal of unused modules.
2021-11-16 00:37:57 +01:00
5ca77c3f64 Modification to Page Management component
Add a new tabpane that lists all the module on that page.  From here you are able to modify the module settings and or delete the module from a page.  Delete will send the module to the recycle bin.
2021-11-15 23:26:20 +01:00
54e9307795 Page management buttons resizing
When the language is changed, in this instance Dutch the buttons are not resized to fit the caption.  This small fix rectifies this.
2021-11-15 21:14:40 +01:00
60d7e45048 Fix - site with default alias (*) edit fail 2021-11-14 10:22:01 +01:00
931559cca8 Update README.md 2021-11-12 09:42:34 -05:00
2567c2937d Fix for #1797 Breadcrumbs render clickable
This fixes the issue when the page property IsClickable is set to false the breadcrum for the page is not clickable.
2021-11-12 07:07:15 +01:00
5b8e6d4df6 Add many PrivateApi attributes to hide unimportant stuff in docs 2021-11-11 20:01:55 +01:00
087c053bd5 Merge pull request #1791 from oqtane/master
Merge pull request #1790 from oqtane/dev
2021-11-11 10:13:49 -05:00
7e699136d7 Merge pull request #1790 from oqtane/dev
version 3.0.0 release
2021-11-11 10:13:00 -05:00
298 changed files with 16757 additions and 6414 deletions

View File

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

View File

@ -15,7 +15,7 @@
<div style="@_display"> <div style="@_display">
<CascadingAuthenticationState> <CascadingAuthenticationState>
<CascadingValue Value="@PageState"> <CascadingValue Value="@PageState">
<SiteRouter Runtime="@Runtime" RenderMode="@RenderMode" OnStateChange="@ChangeState" /> <SiteRouter Runtime="@Runtime" RenderMode="@RenderMode" VisitorId="@VisitorId" OnStateChange="@ChangeState" />
</CascadingValue> </CascadingValue>
</CascadingAuthenticationState> </CascadingAuthenticationState>
</div> </div>
@ -30,35 +30,41 @@
} }
@code { @code {
[Parameter] [Parameter]
public string AntiForgeryToken { get; set; } public string AntiForgeryToken { get; set; }
[Parameter] [Parameter]
public string Runtime { get; set; } public string Runtime { get; set; }
[Parameter] [Parameter]
public string RenderMode { get; set; } public string RenderMode { get; set; }
private bool _initialized = false; [Parameter]
private string _display = "display: none;"; public int VisitorId { get; set; }
private Installation _installation = new Installation { Success = false, Message = "" };
private PageState PageState { get; set; } [Parameter]
public string RemoteIPAddress { get; set; }
protected override async Task OnParametersSetAsync() [Parameter]
{ public string AuthorizationToken { get; set; }
SiteState.AntiForgeryToken = AntiForgeryToken;
InstallationService.SetAntiForgeryTokenHeader(AntiForgeryToken); private bool _initialized = false;
private string _display = "display: none;";
private Installation _installation = new Installation { Success = false, Message = "" };
private PageState PageState { get; set; }
protected override async Task OnParametersSetAsync()
{
SiteState.RemoteIPAddress = RemoteIPAddress;
SiteState.AntiForgeryToken = AntiForgeryToken;
SiteState.AuthorizationToken = AuthorizationToken;
_installation = await InstallationService.IsInstalled(); _installation = await InstallationService.IsInstalled();
if (_installation.Alias != null) if (_installation.Alias != null)
{ {
SiteState.Alias = _installation.Alias; SiteState.Alias = _installation.Alias;
} }
else
{
_installation.Message = "Site Not Configured Correctly - No Matching Alias Exists For Host Name";
}
_initialized = true; _initialized = true;
} }

View File

@ -46,6 +46,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ILocalizationService, LocalizationService>(); services.AddScoped<ILocalizationService, LocalizationService>();
services.AddScoped<ILanguageService, LanguageService>(); services.AddScoped<ILanguageService, LanguageService>();
services.AddScoped<IDatabaseService, DatabaseService>(); services.AddScoped<IDatabaseService, DatabaseService>();
services.AddScoped<IUrlMappingService, UrlMappingService>();
services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>(); services.AddScoped<ISyncService, SyncService>();
return services; return services;

View File

@ -121,90 +121,97 @@
{ {
_databaseName = "LocalDB"; _databaseName = "LocalDB";
} }
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
private void DatabaseChanged(ChangeEventArgs eventArgs) private void DatabaseChanged(ChangeEventArgs eventArgs)
{ {
try try
{ {
_databaseName = (string)eventArgs.Value; _databaseName = (string)eventArgs.Value;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
{ {
_message = Localizer["Error.DbConfig.Load"]; _message = Localizer["Error.DbConfig.Load"];
} }
} }
private void LoadDatabaseConfigComponent() private void LoadDatabaseConfigComponent()
{ {
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (database != null) if (database != null)
{ {
_databaseConfigType = Type.GetType(database.ControlType); _databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder => DatabaseConfigComponent = builder =>
{ {
builder.OpenComponent(0, _databaseConfigType); builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/css/bootstrap.min.css", "text/css", "sha512-usVBAd66/NpVNfBge19gws2j6JZinnca12rAe2l+d+QkLU9fiG02O1X8Q6hepIpr/EYKZvKx/I9WsnujJuOmBA==", "anonymous", ""); await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", "");
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/js/bootstrap.min.js", "sha512-a6ctI6w1kg3J4dSjknHj3aWLEbjitAXAjLDRUxo2wyYmDFRcz2RJuQr5M3Kt8O/TtUSp8n2rAyaXYy1sjoKmrQ==", "anonymous", "", "head", ""); await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head");
} }
} }
private async Task Install() private async Task Install()
{ {
var connectionString = String.Empty; var connectionString = String.Empty;
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
connectionString = databaseConfigControl.GetConnectionString(); connectionString = databaseConfigControl.GetConnectionString();
} }
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
{ {
_loadingDisplay = ""; if (await UserService.ValidatePasswordAsync(_hostPassword))
StateHasChanged(); {
_loadingDisplay = "";
StateHasChanged();
Uri uri = new Uri(NavigationManager.Uri); Uri uri = new Uri(NavigationManager.Uri);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var config = new InstallConfig var config = new InstallConfig
{ {
DatabaseType = database.DBType, DatabaseType = database.DBType,
ConnectionString = connectionString, ConnectionString = connectionString,
Aliases = uri.Authority, Aliases = uri.Authority,
HostUsername = _hostUsername, HostUsername = _hostUsername,
HostPassword = _hostPassword, HostPassword = _hostPassword,
HostEmail = _hostEmail, HostEmail = _hostEmail,
HostName = _hostUsername, HostName = _hostUsername,
TenantName = TenantNames.Master, TenantName = TenantNames.Master,
IsNewTenant = true, IsNewTenant = true,
SiteName = Constants.DefaultSite, SiteName = Constants.DefaultSite,
Register = _register Register = _register
}; };
var installation = await InstallationService.Install(config); var installation = await InstallationService.Install(config);
if (installation.Success) if (installation.Success)
{ {
NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true);
} }
else else
{ {
_message = installation.Message; _message = installation.Message;
_loadingDisplay = "display: none;"; _loadingDisplay = "display: none;";
} }
}
else
{
_message = Localizer["Message.Password.Invalid"];
}
} }
else else
{ {

View File

@ -36,7 +36,9 @@
<option value="m">@Localizer["Minute(s)"]</option> <option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option> <option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option> <option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option> <option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select> </select>
</div> </div>
</div> </div>
@ -95,82 +97,89 @@
</form> </form>
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private int _jobId; private int _jobId;
private string _name = string.Empty; private string _name = string.Empty;
private string _jobType = string.Empty; private string _jobType = string.Empty;
private string _isEnabled = "True"; private string _isEnabled = "True";
private string _interval = string.Empty; private string _interval = string.Empty;
private string _frequency = string.Empty; private string _frequency = string.Empty;
private DateTime? _startDate = null; private DateTime? _startDate = null;
private string _startTime = string.Empty; private string _startTime = string.Empty;
private DateTime? _endDate = null; private DateTime? _endDate = null;
private string _endTime = string.Empty; private string _endTime = string.Empty;
private string _retentionHistory = string.Empty; private string _retentionHistory = string.Empty;
private DateTime? _nextDate = null; private DateTime? _nextDate = null;
private string _nextTime = string.Empty; private string _nextTime = 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;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_jobId = Int32.Parse(PageState.QueryString["id"]); _jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId); Job job = await JobService.GetJobAsync(_jobId);
if (job != null) if (job != null)
{ {
_name = job.Name; _name = job.Name;
_jobType = job.JobType; _jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString(); _isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString(); _interval = job.Interval.ToString();
_frequency = job.Frequency; _frequency = job.Frequency;
_startDate = job.StartDate; _startDate = job.StartDate;
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0) if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{ {
_startTime = job.StartDate.Value.ToString("HH:mm"); _startTime = job.StartDate.Value.ToString("HH:mm");
} }
_endDate = job.EndDate; _endDate = job.EndDate;
if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0) if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0)
{ {
_endTime = job.EndDate.Value.ToString("HH:mm"); _endTime = job.EndDate.Value.ToString("HH:mm");
} }
_retentionHistory = job.RetentionHistory.ToString(); _retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution; _nextDate = job.NextExecution;
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0) if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{ {
_nextTime = job.NextExecution.Value.ToString("HH:mm"); _nextTime = job.NextExecution.Value.ToString("HH:mm");
} }
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn; modifiedon = job.ModifiedOn;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message); await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
} }
} }
private async Task SaveJob() private async Task SaveJob()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
var job = await JobService.GetJobAsync(_jobId); var job = await JobService.GetJobAsync(_jobId);
job.Name = _name; job.Name = _name;
job.JobType = _jobType; job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled); job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency; job.Frequency = _frequency;
job.Interval = int.Parse(_interval); if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate; job.StartDate = _startDate;
if (job.StartDate != null) if (job.StartDate != null)
{ {

View File

@ -36,75 +36,75 @@ else
<td>@context.NextExecution</td> <td>@context.NextExecution</td>
<td> <td>
@if (context.IsStarted) @if (context.IsStarted)
{ {
<button type="button" class="btn btn-danger" @onclick="(async () => await StopJob(context.JobId))">@Localizer["Stop"]</button> <button type="button" class="btn btn-danger" @onclick="(async () => await StopJob(context.JobId))">@Localizer["Stop"]</button>
} }
else else
{ {
<button type="button" class="btn btn-success" @onclick="(async () => await StartJob(context.JobId))">@Localizer["Start"]</button> <button type="button" class="btn btn-success" @onclick="(async () => await StartJob(context.JobId))">@Localizer["Start"]</button>
} }
</td> </td>
</Row> </Row>
</Pager> </Pager>
} }
@code { @code {
private List<Job> _jobs; private List<Job> _jobs;
public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } }
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_jobs = await JobService.GetJobsAsync(); _jobs = await JobService.GetJobsAsync();
} }
private string DisplayStatus(bool isEnabled, bool isExecuting) private string DisplayStatus(bool isEnabled, bool isExecuting)
{ {
var status = string.Empty; var status = string.Empty;
if (!isEnabled) if (!isEnabled)
{ {
status = Localizer["Disabled"]; status = Localizer["Disabled"];
} }
else else
{ {
if (isExecuting) if (isExecuting)
{ {
status = Localizer["Executing"]; status = Localizer["Executing"];
} }
else else
{ {
status = Localizer["Idle"]; status = Localizer["Idle"];
} }
} }
return status; return status;
} }
private string DisplayFrequency(int interval, string frequency) private string DisplayFrequency(int interval, string frequency)
{ {
var result = $"{Localizer["Every"]} {interval.ToString()} "; var result = "";
switch (frequency) switch (frequency)
{ {
case "m": case "m":
result += Localizer["Minute"]; result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Minute"];
break; break;
case "H": case "H":
result += Localizer["Hour"]; result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Hour"];
break; break;
case "d": case "d":
result += Localizer["Day"]; result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Day"];
break; break;
case "M": case "w":
result += Localizer["Month"]; result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Week"];
break; break;
} case "M":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Month"];
if (interval > 1) break;
{ case "O":
result += Localizer["s"]; result = Localizer["Once"];
} break;
}
return result; return result;
} }
@ -114,6 +114,7 @@ else
{ {
await JobService.DeleteJobAsync(job.JobId); await JobService.DeleteJobAsync(job.JobId);
await logger.LogInformation("Job Deleted {Job}", job); await logger.LogInformation("Job Deleted {Job}", job);
_jobs = await JobService.GetJobsAsync();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@ -125,12 +126,36 @@ else
private async Task StartJob(int jobId) private async Task StartJob(int jobId)
{ {
await JobService.StartJobAsync(jobId); try
{
await JobService.StartJobAsync(jobId);
await logger.LogInformation("Job Started {JobId}", jobId);
AddModuleMessage(Localizer["Message.Job.Start"], MessageType.Success);
_jobs = await JobService.GetJobsAsync();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Starting Job {JobId} {Error}", jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Start"], MessageType.Error);
}
} }
private async Task StopJob(int jobId) private async Task StopJob(int jobId)
{ {
await JobService.StopJobAsync(jobId); try
{
await JobService.StopJobAsync(jobId);
await logger.LogInformation("Job Stopped {JobId}", jobId);
AddModuleMessage(Localizer["Message.Job.Stop"], MessageType.Success);
_jobs = await JobService.GetJobsAsync();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Stopping Job {JobId} {Error}", jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Stop"], MessageType.Error);
}
} }
private async Task Refresh() private async Task Refresh()

View File

@ -113,7 +113,7 @@ else
<TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host"> <TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host">
<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="Upload one or more translations. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Language: </Label> <Label Class="col-sm-3" HelpText="Upload one or more translations. Once they are uploaded click Install to complete the installation." ResourceKey="LanguageUpload">Language: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" /> <FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" />
</div> </div>
@ -350,7 +350,7 @@ else
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360);
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); NavigationManager.NavigateTo(NavigationManager.Uri, true);
} }
} }
} }

View File

@ -7,10 +7,6 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_message != string.Empty)
{
<ModuleMessage Message="@_message" Type="@_type" />
}
<AuthorizeView Roles="@RoleNames.Registered"> <AuthorizeView Roles="@RoleNames.Registered">
<Authorizing> <Authorizing>
<text>...</text> <text>...</text>
@ -19,185 +15,285 @@
<div>@Localizer["Info.SignedIn"]</div> <div>@Localizer["Info.SignedIn"]</div>
</Authorized> </Authorized>
<NotAuthorized> <NotAuthorized>
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate> @if (!twofactor)
<div class="container Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))"> {
<div class="form-group"> <form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<label for="Username" class="control-label">@SharedLocalizer["Username"] </label> <div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
<input type="text" @ref="username" name="Username" class="form-control username" placeholder="Username" @bind="@_username" id="Username" required /> @if (_allowexternallogin)
</div> {
<div class="form-group"> <button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<label for="Password" class="control-label">@SharedLocalizer["Password"] </label> <br /><br />
<input type="password" name="Password" class="form-control password" placeholder="Password" @bind="@_password" id="Password" required /> }
</div> @if (_allowsitelogin)
<div class="form-group"> {
<div class="form-check form-check-inline"> <div class="form-group">
<label class="form-check-label" for="Remember">@Localizer["RememberMe"]</label>&nbsp; <Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input type="checkbox" class="form-check-input" name="Remember" @bind="@_remember" id="Remember" /> <input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div> </div>
</div> <div class="form-group mt-2">
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button> <Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <div class="input-group">
<br /><br /> <input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div> </div>
</form> </div>
</NotAuthorized> <div class="form-group mt-2">
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
}
</div>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
</NotAuthorized>
</AuthorizeView> </AuthorizeView>
@code { @code {
private string _returnUrl = string.Empty; private bool _allowsitelogin = true;
private string _message = string.Empty; private bool _allowexternallogin = false;
private MessageType _type = MessageType.Info; private ElementReference login;
private string _username = string.Empty; private bool validated = false;
private string _password = string.Empty; private bool twofactor = false;
private bool _remember = false; private string _username = string.Empty;
private bool validated = false; private ElementReference username;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private bool _remember = false;
private string _code = string.Empty;
private ElementReference login; private string _returnUrl = string.Empty;
private ElementReference username;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
}; };
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (PageState.QueryString.ContainsKey("returnurl")) try
{ {
_returnUrl = PageState.QueryString["returnurl"]; _togglepassword = Localizer["ShowPassword"];
}
if (PageState.QueryString.ContainsKey("name")) if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{ {
_username = PageState.QueryString["name"]; _allowsitelogin = bool.Parse(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]);
} }
if (PageState.QueryString.ContainsKey("token")) if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"]))
{ {
var user = new User(); _allowexternallogin = true;
user.SiteId = PageState.Site.SiteId; }
user.Username = _username;
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null) if (PageState.QueryString.ContainsKey("returnurl"))
{ {
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); _returnUrl = PageState.QueryString["returnurl"];
_message = Localizer["Success.Account.Verified"]; }
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
_message = Localizer["Message.Account.NotVerfied"];
_type = MessageType.Warning;
}
}
}
protected override async Task OnAfterRenderAsync(bool firstRender) if (PageState.QueryString.ContainsKey("name"))
{ {
if (firstRender) _username = PageState.QueryString["name"];
{ }
if(PageState.User == null)
{
await username.FocusAsync();
}
}
}
private async Task Login() if (PageState.QueryString.ContainsKey("token"))
{ {
validated = true; var user = new User();
var interop = new Interop(JSRuntime); user.SiteId = PageState.Site.SiteId;
if (await interop.FormValid(login)) user.Username = _username;
{ user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = _password;
user = await UserService.LoginUserAsync(user, false, false);
if (user.IsAuthenticated) if (user != null)
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
// server-side Blazor needs to post to the Login page so that the cookies are set correctly AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; }
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); else
await interop.SubmitForm(url, fields); {
} await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
else AddModuleMessage(Localizer["Message.Account.NotVerfied"], MessageType.Warning);
{ }
await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username); }
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); }
} catch (Exception ex)
} {
else await logger.LogError(ex, "Error Loading Login {Error}", ex.Message);
{ AddModuleMessage(Localizer["Error.LoadLogin"], MessageType.Error);
// client-side Blazor }
var user = new User(); }
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = _password;
user = await UserService.LoginUserAsync(user, true, _remember);
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
else
{
await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
private void Cancel() protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
NavigationManager.NavigateTo(_returnUrl); if (firstRender && PageState.User == null)
} {
await username.FocusAsync();
}
}
private async Task Forgot() private async Task Login()
{ {
if (_username != string.Empty) try
{ {
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId); validated = true;
if (user != null) var interop = new Interop(JSRuntime);
{ if (await interop.FormValid(login))
await UserService.ForgotPasswordAsync(user); {
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username); var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password};
_message = "Please Check The Email Address Associated To Your User Account For A Password Reset Notification";
}
else
{
_message = "User Does Not Exist";
_type = MessageType.Warning;
}
}
else
{
_message = "Please Enter The Username Related To Your Account And Then Select The Forgot Password Option Again";
}
StateHasChanged(); if (!twofactor)
} {
user = await UserService.LoginUserAsync(user, false, false);
}
else
{
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
{
// server-side Blazor needs to post to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
else
{
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
}
else
{
if (user.TwoFactorRequired)
{
twofactor = true;
validated = false;
AddModuleMessage(Localizer["Message.TwoFactor"], MessageType.Info);
}
else
{
if (!twofactor)
{
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
else
{
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
}
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Performing Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Login"], MessageType.Error);
}
}
private void Cancel()
{
NavigationManager.NavigateTo(_returnUrl);
}
private async Task Forgot()
{
try
{
if (_username != string.Empty)
{
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
if (user != null)
{
await UserService.ForgotPasswordAsync(user);
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
AddModuleMessage(Localizer["Message.ForgotUser"], MessageType.Info);
}
else
{
AddModuleMessage(Localizer["Message.UserDoesNotExist"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.ForgotPassword"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Resetting Password {Error}", ex.Message);
AddModuleMessage(Localizer["Error.ResetPassword"], MessageType.Error);
}
}
private void Reset()
{
twofactor = false;
_username = "";
_password = "";
ClearModuleMessage();
StateHasChanged();
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
await Login();
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = Localizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = Localizer["ShowPassword"];
}
}
private void ExternalLogin()
{
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + _returnUrl), true);
}
private async Task KeyPressed(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
await Login();
}
}
} }

View File

@ -9,246 +9,195 @@
@inject IStringLocalizer<Detail> Localizer @inject IStringLocalizer<Detail> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container"> @if (_initialized)
<div class="row mb-1 align-items-center"> {
<div class="container">
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="dateTime" HelpText="The date and time of this log" ResourceKey="DateTime">Date/Time: </Label>
</div> <div class="col-sm-9">
</div> <input id="dateTime" class="form-control" @bind="@_logDate" readonly />
<div class="row mb-1 align-items-center"> </div>
</div>
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="level" HelpText="The level of this log" ResourceKey="Level">Level: </Label>
</div> <div class="col-sm-9">
</div> <input id="level" class="form-control" @bind="@_level" readonly />
<div class="row mb-1 align-items-center"> </div>
</div>
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="feature" HelpText="The feature that was affected" ResourceKey="Feature">Feature: </Label>
</div> <div class="col-sm-9">
</div> <input id="feature" class="form-control" @bind="@_feature" readonly />
<div class="row mb-1 align-items-center"> </div>
</div>
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="function" HelpText="The function that was performed" ResourceKey="Function">Function: </Label>
</div> <div class="col-sm-9">
</div> <input id="function" class="form-control" @bind="@_function" readonly />
<div class="row mb-1 align-items-center"> </div>
</div>
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="category" HelpText="The categories that were affected" ResourceKey="Category">Category: </Label>
</div> <div class="col-sm-9">
</div> <input id="category" class="form-control" @bind="@_category" readonly />
<div class="row mb-1 align-items-center"> </div>
</div>
<div class="col-sm-9"> @if (_pageName != string.Empty)
{
</div> <div class="row mb-1 align-items-center">
</div> <Label Class="col-sm-3" For="page" HelpText="The page that was affected" ResourceKey="Page">Page: </Label>
<div class="row mb-1 align-items-center"> <div class="col-sm-9">
<input id="page" class="form-control" @bind="@_pageName" readonly />
<div class="col-sm-9"> </div>
</div>
</div> }
</div> @if (_moduleTitle != string.Empty)
<div class="row mb-1 align-items-center"> {
<div class="row mb-1 align-items-center">
<div class="col-sm-9"> <Label Class="col-sm-3" For="module" HelpText="The module that was affected" ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
</div> <input id="module" class="form-control" @bind="@_moduleTitle" readonly />
</div> </div>
<div class="row mb-1 align-items-center"> </div>
}
<div class="col-sm-9"> @if (_username != string.Empty)
{
</div> <div class="row mb-1 align-items-center">
</div> <Label Class="col-sm-3" For="user" HelpText="The user that caused this log" ResourceKey="User">User: </Label>
<div class="row mb-1 align-items-center"> <div class="col-sm-9">
<input id="user" class="form-control" @bind="@_username" readonly />
<div class="col-sm-9"> </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="url" HelpText="The url the log comes from" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<div class="col-sm-9"> <input id="url" class="form-control" @bind="@_url" readonly />
</div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="template" HelpText="What the log is about" ResourceKey="Template">Template: </Label>
</div> <div class="col-sm-9">
<div class="row mb-1 align-items-center"> <input id="template" class="form-control" @bind="@_template" readonly />
<Label Class="col-sm-3" For="dateTime" HelpText="The date and time of this log" ResourceKey="DateTime">Date/Time: </Label> </div>
<div class="col-sm-9"> </div>
<input id="dateTime" class="form-control" @bind="@_logDate" readonly /> <div class="row mb-1 align-items-center">
</div> <Label Class="col-sm-3" For="message" HelpText="The message that the system generated" ResourceKey="Message">Message: </Label>
</div> <div class="col-sm-9">
<div class="row mb-1 align-items-center"> <textarea id="message" class="form-control" @bind="@_message" rows="5" readonly></textarea>
<Label Class="col-sm-3" For="level" HelpText="The level of this log" ResourceKey="Level">Level: </Label> </div>
<div class="col-sm-9"> </div>
<input id="level" class="form-control" @bind="@_level" readonly /> @if (!string.IsNullOrEmpty(_exception))
</div> {
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="exception" HelpText="The exceptions generated by the system" ResourceKey="Exception">Exception: </Label>
<Label Class="col-sm-3" For="feature" HelpText="The feature that was affected" ResourceKey="Feature">Feature: </Label> <div class="col-sm-9">
<div class="col-sm-9">
<input id="feature" class="form-control" @bind="@_feature" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="function" HelpText="The function that was performed" ResourceKey="Function">Function: </Label>
<div class="col-sm-9">
<input id="function" class="form-control" @bind="@_function" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="category" HelpText="The categories that were affected" ResourceKey="Category">Category: </Label>
<div class="col-sm-9">
<input id="category" class="form-control" @bind="@_category" readonly />
</div>
</div>
@if (_pageName != string.Empty)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="page" HelpText="The page that was affected" ResourceKey="Page">Page: </Label>
<div class="col-sm-9">
<input id="page" class="form-control" @bind="@_pageName" readonly />
</div>
</div>
}
@if (_moduleTitle != string.Empty)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="The module that was affected" ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_moduleTitle" readonly />
</div>
</div>
}
@if (_username != string.Empty)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="The user that caused this log" ResourceKey="User">User: </Label>
<div class="col-sm-9">
<input id="user" class="form-control" @bind="@_username" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The url the log comes from" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="template" HelpText="What the log is about" ResourceKey="Template">Template: </Label>
<div class="col-sm-9">
<input id="template" class="form-control" @bind="@_template" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="message" HelpText="The message that the system generated" ResourceKey="Message">Message: </Label>
<div class="col-sm-9">
<textarea id="message" class="form-control" @bind="@_message" rows="5" readonly></textarea>
</div>
</div>
@if (!string.IsNullOrEmpty(_exception))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="exception" HelpText="The exceptions generated by the system" ResourceKey="Exception">Exception: </Label>
<div class="col-sm-9">
<textarea id="exception" class="form-control" @bind="@_exception" rows="5" readonly></textarea> <textarea id="exception" class="form-control" @bind="@_exception" rows="5" readonly></textarea>
</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="properties" HelpText="The properties that were affected" ResourceKey="Properties">Properties: </Label> <Label Class="col-sm-3" For="properties" HelpText="The properties that were affected" ResourceKey="Properties">Properties: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="properties" class="form-control" @bind="@_properties" rows="5" readonly></textarea> <textarea id="properties" class="form-control" @bind="@_properties" rows="5" readonly></textarea>
</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="server" HelpText="The server that was affected" ResourceKey="Server">Server: </Label> <Label Class="col-sm-3" For="server" HelpText="The server that was affected" ResourceKey="Server">Server: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="server" class="form-control" @bind="@_server" readonly /> <input id="server" class="form-control" @bind="@_server" readonly />
</div> </div>
</div> </div>
</div>
}
<NavLink class="btn btn-secondary" href="@CloseUrl()">@SharedLocalizer["Cancel"]</NavLink>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> @code {
private bool _initialized = false;
private int _logId;
private string _logDate = string.Empty;
private string _level = string.Empty;
private string _feature = string.Empty;
private string _function = string.Empty;
private string _category = string.Empty;
private string _pageName = string.Empty;
private string _moduleTitle = string.Empty;
private string _username = string.Empty;
private string _url = string.Empty;
private string _template = string.Empty;
private string _message = string.Empty;
private string _exception = string.Empty;
private string _properties = string.Empty;
private string _server = string.Empty;
@code { public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private int _logId;
private string _logDate = string.Empty;
private string _level = string.Empty;
private string _feature = string.Empty;
private string _function = string.Empty;
private string _category = string.Empty;
private string _pageName = string.Empty;
private string _moduleTitle = string.Empty;
private string _username = string.Empty;
private string _url = string.Empty;
private string _template = string.Empty;
private string _message = string.Empty;
private string _exception = string.Empty;
private string _properties = string.Empty;
private string _server = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; protected override async Task OnInitializedAsync()
{
protected override async Task OnInitializedAsync() try
{ {
try _logId = Int32.Parse(PageState.QueryString["id"]);
var log = await LogService.GetLogAsync(_logId);
if (log != null)
{ {
_logId = Int32.Parse(PageState.QueryString["id"]); _logDate = log.LogDate.ToString(CultureInfo.CurrentCulture);
var log = await LogService.GetLogAsync(_logId); _level = log.Level;
if (log != null) _feature = log.Feature;
_function = log.Function;
_category = log.Category;
if (log.PageId != null)
{ {
_logDate = log.LogDate.ToString(CultureInfo.CurrentCulture); var page = await PageService.GetPageAsync(log.PageId.Value);
_level = log.Level; if (page != null)
_feature = log.Feature;
_function = log.Function;
_category = log.Category;
if (log.PageId != null)
{ {
var page = await PageService.GetPageAsync(log.PageId.Value); _pageName = page.Name;
if (page != null)
{
_pageName = page.Name;
}
} }
if (log.PageId != null && log.ModuleId != null)
{
var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value);
if (pagemodule != null)
{
_moduleTitle = pagemodule.Title;
}
}
if (log.UserId != null)
{
var user = await UserService.GetUserAsync(log.UserId.Value, PageState.Site.SiteId);
if (user != null)
{
_username = user.Username;
}
}
_url = log.Url;
_template = log.MessageTemplate;
_message = log.Message;
_exception = log.Exception;
_properties = log.Properties;
_server = log.Server;
} }
}
catch (Exception ex) if (log.PageId != null && log.ModuleId != null && log.ModuleId != -1)
{ {
await logger.LogError(ex, "Error Loading Log {LogId} {Error}", _logId, ex.Message); var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error); if (pagemodule != null)
{
_moduleTitle = pagemodule.Title;
}
}
if (log.UserId != null)
{
var user = await UserService.GetUserAsync(log.UserId.Value, PageState.Site.SiteId);
if (user != null)
{
_username = user.Username;
}
}
_url = log.Url;
_template = log.MessageTemplate;
_message = log.Message;
_exception = log.Exception;
_properties = log.Properties;
_server = log.Server;
_initialized = true;
} }
} }
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Log {LogId} {Error}", _logId, ex.Message);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
}
} }
private string CloseUrl()
{
if (!PageState.QueryString.ContainsKey("level"))
{
return NavigateUrl();
}
else
{
return NavigateUrl(PageState.Page.Path, "level=" + PageState.QueryString["level"] + "&function=" + PageState.QueryString["function"] + "&rows=" + PageState.QueryString["rows"] + "&page=" + PageState.QueryString["page"]);
}
}
}

View File

@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.Logs @namespace Oqtane.Modules.Admin.Logs
@inherits ModuleBase @inherits ModuleBase
@inject ILogService LogService @inject ILogService LogService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -10,73 +11,92 @@
} }
else else
{ {
<div class="container g-0"> <TabStrip>
<div class="row mb-1 align-items-center"> <TabPanel Name="Events" Heading="Events" ResourceKey="Events">
<div class="col-sm-4"> <div class="container g-0">
<Label For="level" HelpText="Select the log level for event log items" ResourceKey="Level">Level: </Label><br /><br /> <div class="row mb-1 align-items-center">
<select id="level" class="form-select" @onchange="(e => LevelChanged(e))"> <div class="col-sm-4">
<option value="-">&lt;@Localizer["AllLevels"]&gt;</option> <Label For="level" HelpText="Select the log level for event log items" ResourceKey="Level">Level: </Label><br /><br />
<option value="Trace">@Localizer["Trace"]</option> <select id="level" class="form-select" value="@_level" @onchange="(e => LevelChanged(e))">
<option value="Debug">@Localizer["Debug"]</option> <option value="-">&lt;@Localizer["AllLevels"]&gt;</option>
<option value="Information">@Localizer["Information"]</option> <option value="Trace">@Localizer["Trace"]</option>
<option value="Warning">@Localizer["Warning"]</option> <option value="Debug">@Localizer["Debug"]</option>
<option value="Error">@Localizer["Error"]</option> <option value="Information">@Localizer["Information"]</option>
<option value="Critical">@Localizer["Critical"]</option> <option value="Warning">@Localizer["Warning"]</option>
</select> <option value="Error">@Localizer["Error"]</option>
</div> <option value="Critical">@Localizer["Critical"]</option>
<div class="col-sm-4"> </select>
<Label For="function" HelpText="Select the function for event log items" ResourceKey="Function">Function: </Label><br /><br /> </div>
<select id="function" class="form-select" @onchange="(e => FunctionChanged(e))"> <div class="col-sm-4">
<option value="-">&lt;@Localizer["AllFunctions"]&gt;</option> <Label For="function" HelpText="Select the function for event log items" ResourceKey="Function">Function: </Label><br /><br />
<option value="Create">@Localizer["Create"]</option> <select id="function" class="form-select" value="@_function" @onchange="(e => FunctionChanged(e))">
<option value="Read">@Localizer["Read"]</option> <option value="-">&lt;@Localizer["AllFunctions"]&gt;</option>
<option value="Update">@SharedLocalizer["Update"]</option> <option value="Create">@Localizer["Create"]</option>
<option value="Delete">@SharedLocalizer["Delete"]</option> <option value="Read">@Localizer["Read"]</option>
<option value="Security">@Localizer["Security"]</option> <option value="Update">@SharedLocalizer["Update"]</option>
<option value="Other">@Localizer["Other"]</option> <option value="Delete">@SharedLocalizer["Delete"]</option>
</select> <option value="Security">@Localizer["Security"]</option>
</div> <option value="Other">@Localizer["Other"]</option>
<div class="col-sm-4"> </select>
<Label For="rows" HelpText="Select the maximum number of event log items to review. Please note that if you choose more than 10 items the information will be split into pages." ResourceKey="Rows">Maximum Items: </Label><br /><br /> </div>
<select id="rows" class="form-select" @onchange="(e => RowsChanged(e))"> <div class="col-sm-4">
<option value="10">10</option> <Label For="rows" HelpText="Select the maximum number of event log items to review. Please note that if you choose more than 10 items the information will be split into pages." ResourceKey="Rows">Maximum Items: </Label><br /><br />
<option value="50">50</option> <select id="rows" class="form-select" value="@_rows" @onchange="(e => RowsChanged(e))">
<option value="100">100</option> <option value="10">10</option>
</select> <option value="50">50</option>
</div> <option value="100">100</option>
</div> </select>
</div> </div>
</div>
</div>
<br />
@if (_logs.Any()) @if (_logs.Any())
{ {
<Pager Items="@_logs"> <Pager Items="@_logs" CurrentPage="@_page.ToString()" OnPageChange="OnPageChange">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Date"]</th> <th>@Localizer["Date"]</th>
<th>@Localizer["Level"]</th> <th>@Localizer["Level"]</th>
<th>@Localizer["Feature"]</th> <th>@Localizer["Feature"]</th>
<th>@Localizer["Function"]</th> <th>@Localizer["Function"]</th>
</Header> </Header>
<Row> <Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Parameters="@($"id=" + context.LogId.ToString())" ResourceKey="LogDetails" /></td> <td class="@GetClass(context.Function)"><ActionLink Action="Detail" Parameters="@($"id=" + context.LogId.ToString() + "&level=" + _level + "&function=" + _function + "&rows=" + _rows + "&page=" + _page.ToString())" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td> <td class="@GetClass(context.Function)">@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>
</Row> </Row>
</Pager> </Pager>
} }
else else
{ {
<p><em>@Localizer["NoLogs"]</em></p> <p><em>@Localizer["NoLogs"]</em></p>
} }
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of events to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
} }
@code { @code {
private string _level = "-"; private string _level = "-";
private string _function = "-"; private string _function = "-";
private string _rows = "10"; private string _rows = "10";
private int _page = 1;
private List<Log> _logs; private List<Log> _logs;
private string _retention = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -84,7 +104,27 @@ else
{ {
try try
{ {
if (PageState.QueryString.ContainsKey("level"))
{
_level = PageState.QueryString["level"];
}
if (PageState.QueryString.ContainsKey("function"))
{
_function = PageState.QueryString["function"];
}
if (PageState.QueryString.ContainsKey("rows"))
{
_rows = PageState.QueryString["rows"];
}
if (PageState.QueryString.ContainsKey("page") && int.TryParse(PageState.QueryString["page"], out int page))
{
_page = page;
}
await GetLogs(); await GetLogs();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = SettingService.GetSetting(settings, "LogRetention", "30");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -170,4 +210,27 @@ else
} }
return classname; return classname;
} }
private async Task SaveSiteSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "LogRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
private void OnPageChange(int page)
{
_page = page;
}
} }

View File

@ -128,7 +128,7 @@ else
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference }; var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition); moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = ModuleState.Settings; var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName); SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);

View File

@ -1,7 +1,9 @@
using Oqtane.Models; using Oqtane.Documentation;
using Oqtane.Models;
namespace Oqtane.Modules.Admin.ModuleCreator namespace Oqtane.Modules.Admin.ModuleCreator
{ {
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule public class ModuleInfo : IModule
{ {
public ModuleDefinition ModuleDefinition => new ModuleDefinition public ModuleDefinition ModuleDefinition => new ModuleDefinition

View File

@ -22,6 +22,7 @@ else
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
<th>@SharedLocalizer["Version"]</th> <th>@SharedLocalizer["Version"]</th>
<th>@Localizer["InUse"]</th>
<th>@SharedLocalizer["Expires"]</th> <th>@SharedLocalizer["Expires"]</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
</Header> </Header>
@ -35,6 +36,16 @@ else
</td> </td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Version</td> <td>@context.Version</td>
<td>
@if(context.AssemblyName == "Oqtane.Client" || PageState.Modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{
<span>@SharedLocalizer["Yes"]</span>
}
else
{
<span>@SharedLocalizer["No"]</span>
}
</td>
<td> <td>
@((MarkupString)PurchaseLink(context.PackageName)) @((MarkupString)PurchaseLink(context.PackageName))
</td> </td>

View File

@ -95,6 +95,12 @@
<input id="title" class="form-control" @bind="@_title" /> <input id="title" class="form-control" @bind="@_title" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="meta" HelpText="Optionally enter meta tags (in exactly the form you want them to be included in the page output)." ResourceKey="Meta">Meta: </Label>
<div class="col-sm-9">
<textarea id="meta" class="form-control" @bind="@_meta" rows="3"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label> <Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -156,220 +162,222 @@
</form> </form>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private List<Theme> _themeList; private List<Theme> _themeList;
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> _pageList; private List<Page> _pageList;
private string _name; private string _name;
private string _title; private string _title;
private string _path = string.Empty; private string _meta;
private string _parentid = "-1"; private string _path = string.Empty;
private string _insert = ">>"; private string _parentid = "-1";
private List<Page> _children; private string _insert = ">>";
private int _childid = -1; private List<Page> _children;
private string _isnavigation = "True"; private int _childid = -1;
private string _isclickable = "True"; private string _isnavigation = "True";
private string _url; private string _isclickable = "True";
private string _ispersonalizable = "False"; private string _url;
private string _themetype = string.Empty; private string _ispersonalizable = "False";
private string _containertype = string.Empty; private string _themetype = string.Empty;
private string _icon = string.Empty; private string _containertype = string.Empty;
private string _permissions = string.Empty; private string _icon = string.Empty;
private PermissionGrid _permissionGrid; private string _permissions = string.Empty;
private Type _themeSettingsType; private PermissionGrid _permissionGrid;
private object _themeSettings; private Type _themeSettingsType;
private RenderFragment ThemeSettingsComponent { get; set; } private object _themeSettings;
private bool _refresh = false; private RenderFragment ThemeSettingsComponent { get; set; }
private ElementReference form; private bool _refresh = false;
private bool validated = false; private ElementReference form;
private bool validated = false;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList); _themes = ThemeService.GetThemeControls(_themeList);
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_pageList = PageState.Pages; _pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList(); _children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_permissions = string.Empty; _permissions = string.Empty;
ThemeSettings(); ThemeSettings();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Initializing Page {Error}", ex.Message); await logger.LogError(ex, "Error Initializing Page {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Page.Initialize"], MessageType.Error); AddModuleMessage(Localizer["Error.Page.Initialize"], MessageType.Error);
} }
} }
private async void ParentChanged(ChangeEventArgs e) private async void ParentChanged(ChangeEventArgs e)
{ {
try try
{ {
_parentid = (string)e.Value; _parentid = (string)e.Value;
_children = new List<Page>(); _children = new List<Page>();
if (_parentid == "-1") if (_parentid == "-1")
{ {
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null)) foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{ {
_children.Add(p); _children.Add(p);
} }
} }
} }
else else
{ {
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid))) foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{ {
_children.Add(p); _children.Add(p);
} }
} }
} }
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message); await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
} }
} }
private async void ThemeChanged(ChangeEventArgs e) private async void ThemeChanged(ChangeEventArgs e)
{ {
try try
{ {
_themetype = (string)e.Value; _themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = "-"; _containertype = "-";
ThemeSettings(); ThemeSettings();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message); await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Pane.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.Pane.Load"], MessageType.Error);
} }
} }
private void ThemeSettings() private void ThemeSettings()
{ {
_themeSettingsType = null; _themeSettingsType = null;
var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype)));
if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{ {
_themeSettingsType = Type.GetType(theme.ThemeSettingsType); _themeSettingsType = Type.GetType(theme.ThemeSettingsType);
if (_themeSettingsType != null) if (_themeSettingsType != null)
{ {
ThemeSettingsComponent = builder => ThemeSettingsComponent = builder =>
{ {
builder.OpenComponent(0, _themeSettingsType); builder.OpenComponent(0, _themeSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
_refresh = true; _refresh = true;
} }
} }
private async Task SavePage() private async Task SavePage()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
Page page = null; Page page = null;
try try
{ {
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-") if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
{ {
page = new Page(); page = new Page();
page.SiteId = PageState.Page.SiteId; page.SiteId = PageState.Page.SiteId;
page.Name = _name; page.Name = _name;
page.Title = _title; page.Title = _title;
if (string.IsNullOrEmpty(_path)) if (string.IsNullOrEmpty(_path))
{ {
_path = _name; _path = _name;
} }
if (_path.Contains("/")) if (_path.Contains("/"))
{ {
_path = _path.Substring(_path.LastIndexOf("/") + 1); _path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
if (_parentid == "-1") if (_parentid == "-1")
{ {
page.ParentId = null; page.ParentId = null;
page.Path = Utilities.GetFriendlyUrl(_path); page.Path = Utilities.GetFriendlyUrl(_path);
} }
else else
{ {
page.ParentId = Int32.Parse(_parentid); page.ParentId = Int32.Parse(_parentid);
var parent = PageState.Pages.Where(item => item.PageId == page.ParentId).FirstOrDefault(); var parent = PageState.Pages.Where(item => item.PageId == page.ParentId).FirstOrDefault();
if (parent.Path == string.Empty) if (parent.Path == string.Empty)
{ {
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path); page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
} }
else else
{ {
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path); page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
} }
} }
if(PagePathIsDeleted(page.Path, page.SiteId, _pageList)) if(PagePathIsDeleted(page.Path, page.SiteId, _pageList))
{ {
AddModuleMessage(string.Format(Localizer["Message.Page.Deleted"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Message.Page.Deleted"], _path), MessageType.Warning);
return; return;
} }
if (!PagePathIsUnique(page.Path, page.SiteId, _pageList)) if (!PagePathIsUnique(page.Path, page.SiteId, _pageList))
{ {
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning); AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
return; return;
} }
Page child; Page child;
switch (_insert) switch (_insert)
{ {
case "<<": case "<<":
page.Order = 0; page.Order = 0;
break; break;
case "<": case "<":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order - 1; page.Order = child.Order - 1;
break; break;
case ">": case ">":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault(); child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order + 1; page.Order = child.Order + 1;
break; break;
case ">>": case ">>":
page.Order = int.MaxValue; page.Order = int.MaxValue;
break; break;
} }
page.IsNavigation = (_isnavigation == null ? true : Boolean.Parse(_isnavigation)); page.IsNavigation = (_isnavigation == null ? true : Boolean.Parse(_isnavigation));
page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable)); page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable));
page.Url = _url; page.Url = _url;
page.ThemeType = (_themetype != "-") ? _themetype : string.Empty; page.ThemeType = (_themetype != "-") ? _themetype : string.Empty;
if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType) if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType)
{ {
page.ThemeType = string.Empty; page.ThemeType = string.Empty;
} }
page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty; page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty;
if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType) if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType)
{ {
page.DefaultContainerType = string.Empty; page.DefaultContainerType = string.Empty;
} }
page.Icon = (_icon == null ? string.Empty : _icon); page.Icon = (_icon == null ? string.Empty : _icon);
page.Permissions = _permissionGrid.GetPermissions(); page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable)); page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable));
page.UserId = null; page.UserId = null;
page.Meta = _meta;
page = await PageService.AddPageAsync(page); page = await PageService.AddPageAsync(page);
await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId); await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId);

View File

@ -3,13 +3,14 @@
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IPageService PageService @inject IPageService PageService
@inject IPageModuleService PageModuleService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> 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>
<TabStrip Refresh="@_refresh"> <TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings"> <TabPanel Name="Settings" ResourceKey="Settings" Heading=@Localizer["Settings.Heading"]>
@if (_themeList != null) @if (_themeList != null)
{ {
<div class="container"> <div class="container">
@ -101,6 +102,12 @@
<input id="title" class="form-control" @bind="@_title" maxlength="200"/> <input id="title" class="form-control" @bind="@_title" maxlength="200"/>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="meta" HelpText="Optionally enter meta tags (in exactly the form you want them to be included in the page output)." ResourceKey="Meta">Meta: </Label>
<div class="col-sm-9">
<textarea id="meta" class="form-control" @bind="@_meta" rows="3"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label> <Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -151,10 +158,27 @@
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" Permissions="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.Page" Permissions="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
}
</TabPanel>
<TabPanel Name="PageModules" Heading="Modules" ResourceKey="PageModules">
@if(_pageModules != null)
{
<Pager Items="_pageModules">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["ModuleTitle"]</th>
<th>@Localizer["ModuleDefinition"]</th>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" Permissions="@context.Permissions" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" Permissions="@context.Permissions" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td>
</Row>
</Pager>
} }
</TabPanel> </TabPanel>
@if (_themeSettingsType != null) @if (_themeSettingsType != null)
@ -170,286 +194,314 @@
</form> </form>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private List<Theme> _themeList; private List<Theme> _themeList;
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> _pageList; private List<Page> _pageList;
private int _pageId; private List<Module> _pageModules;
private string _name; private int _pageId;
private string _title; private string _name;
private string _path; private string _title;
private string _currentparentid; private string _meta;
private string _parentid = "-1"; private string _path;
private string _insert = "="; private string _currentparentid;
private List<Page> _children; private string _parentid = "-1";
private int _childid = -1; private string _insert = "=";
private string _isnavigation; private List<Page> _children;
private string _isclickable; private int _childid = -1;
private string _url; private string _isnavigation;
private string _ispersonalizable; private string _isclickable;
private string _themetype; private string _url;
private string _containertype = "-"; private string _ispersonalizable;
private string _icon; private string _themetype;
private string _permissions = null; private string _containertype = "-";
private string _createdby; private string _icon;
private DateTime _createdon; private string _permissions = null;
private string _modifiedby; private string _createdby;
private DateTime _modifiedon; private DateTime _createdon;
private string _deletedby; private string _modifiedby;
private DateTime? _deletedon; private DateTime _modifiedon;
private PermissionGrid _permissionGrid; private string _deletedby;
private Type _themeSettingsType; private DateTime? _deletedon;
private object _themeSettings; private PermissionGrid _permissionGrid;
private RenderFragment ThemeSettingsComponent { get; set; } private Type _themeSettingsType;
private bool _refresh = false; private object _themeSettings;
private RenderFragment ThemeSettingsComponent { get; set; }
private bool _refresh = false;
protected Page page;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_pageList = PageState.Pages; _pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList(); _children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
_themeList = await ThemeService.GetThemesAsync(); _pageId = Int32.Parse(PageState.QueryString["id"]);
_themes = ThemeService.GetThemeControls(_themeList); page = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId);
_pageId = Int32.Parse(PageState.QueryString["id"]); if (page != null)
var page = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId); {
if (page != null) _name = page.Name;
{ _title = page.Title;
_name = page.Name; _meta = page.Meta;
_title = page.Title; _path = page.Path;
_path = page.Path; _pageModules = PageState.Modules.Where(m => m.PageId == page.PageId).ToList();
if (string.IsNullOrEmpty(_path)) if (string.IsNullOrEmpty(_path))
{ {
_path = "/"; _path = "/";
} }
else else
{ {
if (_path.Contains("/")) if (_path.Contains("/"))
{ {
_path = _path.Substring(_path.LastIndexOf("/") + 1); _path = _path.Substring(_path.LastIndexOf("/") + 1);
} }
} }
if (page.ParentId == null) if (page.ParentId == null)
{ {
_parentid = "-1"; _parentid = "-1";
} }
else else
{ {
_parentid = page.ParentId.ToString(); _parentid = page.ParentId.ToString();
} }
_currentparentid = _parentid; _currentparentid = _parentid;
_isnavigation = page.IsNavigation.ToString(); _isnavigation = page.IsNavigation.ToString();
_isclickable = page.IsClickable.ToString(); _isclickable = page.IsClickable.ToString();
_url = page.Url; _url = page.Url;
_ispersonalizable = page.IsPersonalizable.ToString(); _ispersonalizable = page.IsPersonalizable.ToString();
_themetype = page.ThemeType; _themetype = page.ThemeType;
if (string.IsNullOrEmpty(_themetype)) if (string.IsNullOrEmpty(_themetype))
{ {
_themetype = PageState.Site.DefaultThemeType; _themetype = PageState.Site.DefaultThemeType;
} }
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = page.DefaultContainerType; _containertype = page.DefaultContainerType;
if (string.IsNullOrEmpty(_containertype)) if (string.IsNullOrEmpty(_containertype))
{ {
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
} }
_icon = page.Icon; _icon = page.Icon;
_permissions = page.Permissions; _permissions = page.Permissions;
_createdby = page.CreatedBy; _createdby = page.CreatedBy;
_createdon = page.CreatedOn; _createdon = page.CreatedOn;
_modifiedby = page.ModifiedBy; _modifiedby = page.ModifiedBy;
_modifiedon = page.ModifiedOn; _modifiedon = page.ModifiedOn;
_deletedby = page.DeletedBy; _deletedby = page.DeletedBy;
_deletedon = page.DeletedOn; _deletedon = page.DeletedOn;
ThemeSettings(); ThemeSettings();
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Page {PageId} {Error}", _pageId, ex.Message); await logger.LogError(ex, "Error Loading Page {PageId} {Error}", _pageId, ex.Message);
AddModuleMessage(Localizer["Error.Page.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.Page.Load"], MessageType.Error);
} }
} }
private async void ParentChanged(ChangeEventArgs e) private async Task DeleteModule(Module module)
{ {
try try
{ {
_parentid = (string)e.Value; PageModule pagemodule = await PageModuleService.GetPageModuleAsync(page.PageId, module.ModuleId);
_children = new List<Page>(); pagemodule.IsDeleted = true;
if (_parentid == "-1") await PageModuleService.UpdatePageModuleAsync(pagemodule);
{ await logger.LogInformation(LogFunction.Update,"Module Deleted {Title}", module.Title);
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null)) _pageModules.RemoveAll(item => item.PageModuleId == pagemodule.PageModuleId);
{ StateHasChanged();
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) NavigationManager.NavigateTo(NavigationManager.Uri + "&tab=PageModules");
{ }
_children.Add(p); catch (Exception ex)
} {
} await logger.LogError(ex, "Error Deleting Module {Title} {Error}", module.Title, ex.Message);
} AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
else }
{ }
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{
_children.Add(p);
}
}
}
if (_parentid == _currentparentid)
{
_insert = "=";
}
else
{
_insert = ">>";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
}
}
private async void ThemeChanged(ChangeEventArgs e) private async void ParentChanged(ChangeEventArgs e)
{ {
try try
{ {
_themetype = (string)e.Value; _parentid = (string)e.Value;
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _children = new List<Page>();
_containertype = "-"; if (_parentid == "-1")
ThemeSettings(); {
StateHasChanged(); foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
} {
catch (Exception ex) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{ {
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message); _children.Add(p);
AddModuleMessage(Localizer["Error.Pane.Load"], MessageType.Error); }
} }
} }
else
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{
_children.Add(p);
}
}
}
if (_parentid == _currentparentid)
{
_insert = "=";
}
else
{
_insert = ">>";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
}
}
private void ThemeSettings() private async void ThemeChanged(ChangeEventArgs e)
{ {
_themeSettingsType = null; try
var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); {
if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) _themetype = (string)e.Value;
{ _containers = ThemeService.GetContainerControls(_themeList, _themetype);
_themeSettingsType = Type.GetType(theme.ThemeSettingsType); _containertype = "-";
if (_themeSettingsType != null) ThemeSettings();
{ StateHasChanged();
ThemeSettingsComponent = builder => }
{ catch (Exception ex)
builder.OpenComponent(0, _themeSettingsType); {
builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message);
builder.CloseComponent(); AddModuleMessage(Localizer["Error.Pane.Load"], MessageType.Error);
}; }
} }
_refresh = true;
}
}
private async Task SavePage() private void ThemeSettings()
{ {
validated = true; _themeSettingsType = null;
var interop = new Interop(JSRuntime); if (PageState.QueryString.ContainsKey("cp")) // can only be displayed if invoked from Control Panel
if (await interop.FormValid(form)) {
{ var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype)));
Page page = null; if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
try {
{ _themeSettingsType = Type.GetType(theme.ThemeSettingsType);
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-") if (_themeSettingsType != null)
{ {
page = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId); ThemeSettingsComponent = builder =>
string currentPath = page.Path; {
builder.OpenComponent(0, _themeSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
_refresh = true;
}
}
}
page.Name = _name; private async Task SavePage()
page.Title = _title; {
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
Page page = null;
try
{
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
{
page = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId);
string currentPath = page.Path;
if (string.IsNullOrEmpty(_path)) page.Name = _name;
{ page.Title = _title;
_path = _name;
}
if (_path.Contains("/"))
{
_path = _path.Substring(_path.LastIndexOf("/") + 1);
}
if (_parentid == "-1") if (string.IsNullOrEmpty(_path))
{ {
page.ParentId = null; _path = _name;
page.Path = Utilities.GetFriendlyUrl(_path); }
} if (_path.Contains("/"))
else {
{ _path = _path.Substring(_path.LastIndexOf("/") + 1);
page.ParentId = Int32.Parse(_parentid); }
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
if (!PagePathIsUnique(page.Path, page.SiteId, page.PageId, _pageList)) if (_parentid == "-1")
{ {
AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning); page.ParentId = null;
return; page.Path = Utilities.GetFriendlyUrl(_path);
} }
else
{
page.ParentId = Int32.Parse(_parentid);
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
if (_insert != "=") if (!PagePathIsUnique(page.Path, page.SiteId, page.PageId, _pageList))
{ {
Page child; AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning);
switch (_insert) return;
{ }
case "<<":
page.Order = 0; if (_insert != "=")
break; {
case "<": Page child;
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid); switch (_insert)
if (child != null) page.Order = child.Order - 1; {
break; case "<<":
case ">": page.Order = 0;
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid); break;
if (child != null) page.Order = child.Order + 1; case "<":
break; child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid);
case ">>": if (child != null) page.Order = child.Order - 1;
page.Order = int.MaxValue; break;
break; case ">":
} child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid);
} if (child != null) page.Order = child.Order + 1;
page.IsNavigation = (_isnavigation == null || Boolean.Parse(_isnavigation)); break;
page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable)); case ">>":
page.Url = _url; page.Order = int.MaxValue;
page.ThemeType = (_themetype != "-") ? _themetype : string.Empty; break;
if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType) }
{ }
page.ThemeType = string.Empty; page.IsNavigation = (_isnavigation == null || Boolean.Parse(_isnavigation));
} page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable));
page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty; page.Url = _url;
if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType) page.ThemeType = (_themetype != "-") ? _themetype : string.Empty;
{ if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType)
page.DefaultContainerType = string.Empty; {
} page.ThemeType = string.Empty;
page.Icon = _icon ?? string.Empty; }
page.Permissions = _permissionGrid.GetPermissions(); page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty;
page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable)); if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType)
page.UserId = null; {
page.DefaultContainerType = string.Empty;
}
page.Icon = _icon ?? string.Empty;
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable));
page.UserId = null;
page.Meta = _meta;
page = await PageService.UpdatePageAsync(page); page = await PageService.UpdatePageAsync(page);
await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId); await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId);

View File

@ -13,6 +13,7 @@
<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 style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>

View File

@ -25,7 +25,7 @@
<th>@Localizer["DeletedOn"]</th> <th>@Localizer["DeletedOn"]</th>
</Header> </Header>
<Row> <Row>
<td><button @onclick="@(() => RestorePage(context))" class="btn btn-info" title="Restore">Restore</button></td> <td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">Restore</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td> <td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.DeletedBy</td> <td>@context.DeletedBy</td>
@ -34,9 +34,7 @@
</Pager> </Pager>
@if (_pages.Any()) @if (_pages.Any())
{ {
<div style="text-align:right;"> <br /><ActionDialog Header="Delete All Pages" Message="Are You Sure You Wish To Permanently Delete All Pages?" Action="Delete All Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
<ActionDialog Header="Delete All Pages" Message="Are You Sure You Wish To Permanently Delete All Pages?" Action="Delete All Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
</div>
} }
} }
</TabPanel> </TabPanel>
@ -58,7 +56,7 @@
<th>@Localizer["DeletedOn"]</th> <th>@Localizer["DeletedOn"]</th>
</Header> </Header>
<Row> <Row>
<td><button @onclick="@(() => RestoreModule(context))" class="btn btn-info" title="Restore">@Localizer["Restore"]</button></td> <td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td> <td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@PageState.Pages.Find(item => item.PageId == context.PageId).Name</td> <td>@PageState.Pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td> <td>@context.Title</td>
@ -68,9 +66,7 @@
</Pager> </Pager>
@if (_modules.Any()) @if (_modules.Any())
{ {
<div style="text-align:right;"> <br /><ActionDialog Header="Delete All Modules" Message="Are You Sure You Wish To Permanently Delete All Modules?" Action="Delete All Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
<ActionDialog Header="Delete All Modules" Message="Are You Sure You Wish To Permanently Delete All Modules?" Action="Delete All Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
</div>
} }
} }

View File

@ -90,9 +90,10 @@ else
{ {
SiteId = PageState.Site.SiteId, SiteId = PageState.Site.SiteId,
Username = _username, Username = _username,
DisplayName = (_displayname == string.Empty ? _username : _displayname), Password = _password,
Email = _email, Email = _email,
Password = _password DisplayName = (_displayname == string.Empty ? _username : _displayname),
PhotoFileId = null
}; };
user = await UserService.AddUserAsync(user); user = await UserService.AddUserAsync(user);

View File

@ -7,41 +7,49 @@
<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="form-group"> <div class="row mb-1 align-items-center">
<label for="Username" class="control-label">@SharedLocalizer["Username"] </label> <Label Class="col-sm-3" For="username" HelpText="Your username will be populated from the link you received in the password reset notification" ResourceKey="Username">Username: </Label>
<input type="text" class="form-control" placeholder="Username" @bind="@_username" readonly id="Username" /> <div class="col-sm-9">
<input id="username" type="text" class="form-control" @bind="@_username" readonly />
</div>
</div> </div>
<div class="form-group"> <div class="row mb-1 align-items-center">
<label for="Password" class="control-label">@SharedLocalizer["Password"] </label> <Label Class="col-sm-3" For="password" HelpText="The new password. It must satisfy complexity rules for the site." ResourceKey="Password">Password: </Label>
<input type="password" class="form-control" placeholder="Password" @bind="@_password" id="Password" required /> <div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_password" required />
</div>
</div> </div>
<div class="form-group"> <div class="row mb-1 align-items-center">
<label for="Confirm" class="control-label">@Localizer["Password.Confirm"] </label> <Label Class="col-sm-3" For="confirm" HelpText="Enter the password again. It must exactly match the password entered above." ResourceKey="Confirm">Confirm: </Label>
<input type="password" class="form-control" placeholder="Password" @bind="@_confirm" id="Confirm" required /> <div class="col-sm-9">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" required />
</div>
</div> </div>
<button type="button" class="btn btn-primary" @onclick="Reset">@Localizer["Password.Reset"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div> </div>
<br />
<button type="button" class="btn btn-primary" @onclick="Reset">@Localizer["Password.Reset"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form> </form>
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _password = string.Empty; private string _password = string.Empty;
private string _confirm = string.Empty; private string _confirm = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnInitialized() protected override async Task OnInitializedAsync()
{ {
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token")) if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{ {
_username = PageState.QueryString["name"]; _username = PageState.QueryString["name"];
} }
else else
{ {
NavigationManager.NavigateTo(NavigateUrl(string.Empty)); await logger.LogError(LogFunction.Security, "Invalid Attempt To Access User Password Reset");
NavigationManager.NavigateTo(NavigateUrl("")); // home page
} }
} }

View File

@ -53,12 +53,14 @@ else
<Pager Items="@userroles"> <Pager Items="@userroles">
<Header> <Header>
<th>@Localizer["Users"]</th> <th>@Localizer["Users"]</th>
<th>@SharedLocalizer["Username"]</th>
<th>@Localizer["Effective"]</th> <th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th> <th>@Localizer["Expiry"]</th>
<th>&nbsp;</th> <th>&nbsp;</th>
</Header> </Header>
<Row> <Row>
<td>@context.User.DisplayName</td> <td>@context.User.DisplayName</td>
<td>@context.User.Username</td>
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Admin.Site @namespace Oqtane.Modules.Admin.Site
@inherits ModuleBase @inherits ModuleBase
@using System.Text.RegularExpressions
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ISiteService SiteService @inject ISiteService SiteService
@inject ITenantService TenantService @inject ITenantService TenantService
@ -22,90 +23,64 @@
</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="alias" HelpText="The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas." ResourceKey="Aliases">Aliases: </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">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) <FileManager FileId="@_logofileid" Filter="@Constants.ImageFiles" @ref="_logofilemanager" />
{
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
}
else
{
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" readonly></textarea>
}
</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="allowRegister" HelpText="Do you want the users to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration? </Label> <Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="allowRegister" class="form-select" @bind="@_allowregistration" required> <FileManager FileId="@_faviconfileid" Filter="ico" @ref="_faviconfilemanager" />
<option value="True">@SharedLocalizer["Yes"]</option> </div>
<option value="False">@SharedLocalizer["No"]</option> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the sites default theme" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
<select id="defaultTheme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
<option value="-">&lt;@Localizer["Theme.Select"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select> </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="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Is Deleted? </Label> <Label Class="col-sm-3" For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="isDeleted" class="form-select" @bind="@_isdeleted" required> <select id="defaultContainer" class="form-select" @bind="@_containertype" required>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
<option value="False">@SharedLocalizer["No"]</option> @foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select> </select>
</div> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance"> <Label Class="col-sm-3" For="defaultAdminContainer" HelpText="Select the default admin container for the site" ResourceKey="DefaultAdminContainer">Default Admin Container: </Label>
<div class="container"> <div class="col-sm-9">
<div class="row mb-1 align-items-center"> <select id="defaultAdminContainer" class="form-select" @bind="@_admincontainertype" required>
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label> <option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
<div class="col-sm-9"> <option value="@Constants.DefaultAdminContainer">&lt;@Localizer["DefaultAdminContainer"]&gt;</option>
<FileManager FileId="@_logofileid" Filter="@Constants.ImageFiles" @ref="_logofilemanager" /> @foreach (var container in _containers)
</div> {
</div> <option value="@container.TypeName">@container.Name</option>
<div class="row mb-1 align-items-center"> }
<Label Class="col-sm-3" For="favicon" HelpText="Specify a Favicon" ResourceKey="FavoriteIcon">Favicon: </Label> </select>
<div class="col-sm-9">
<FileManager FileId="@_faviconfileid" Filter="ico" @ref="_faviconfilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultTheme" HelpText="Select the sites default theme" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
<select id="defaultTheme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
<option value="-">&lt;@Localizer["Theme.Select"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
<div class="col-sm-9">
<select id="defaultContainer" class="form-select" @bind="@_containertype" required>
<option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultAdminContainer" HelpText="Select the default admin container for the site" ResourceKey="DefaultAdminContainer">Default Admin Container: </Label>
<div class="col-sm-9">
<select id="defaultAdminContainer" class="form-select" @bind="@_admincontainertype" required>
<option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
<option value="@Constants.DefaultAdminContainer">&lt;@Localizer["DefaultAdminContainer"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</div>
</div> </div>
</div> </div>
</Section> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
<div class="col-sm-9">
<select id="isDeleted" class="form-select" @bind="@_isdeleted" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<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">
@ -145,7 +120,10 @@
<div class="row mb-1 align-items-center"> <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> <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="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_smtppassword" /> <div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword">@_togglesmtppassword</button>
</div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -154,6 +132,12 @@
<input id="sender" class="form-control" @bind="@_smtpsender" /> <input id="sender" class="form-control" @bind="@_smtpsender" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button> <button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br /> <br /><br />
</div> </div>
@ -183,8 +167,29 @@
</div> </div>
</div> </div>
</Section> </Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultalias" HelpText="The default alias for the site. Requests for non-default aliases will be redirected to the default alias." ResourceKey="DefaultAlias">Default Alias: </Label>
<div class="col-sm-9">
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
@foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
{
<option value="@name">@name</option>
}
</select>
</div>
</div>
</div>
</Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting"> <Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -240,372 +245,423 @@
} }
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private bool _initialized = false; private bool _initialized = false;
private List<Theme> _themeList; private List<Theme> _themeList;
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 string _name = string.Empty; private string _name = string.Empty;
private List<Alias> _aliasList; private List<Alias> _aliases;
private string _urls = string.Empty; private string _defaultalias = string.Empty;
private string _runtime = ""; private string _urls = string.Empty;
private string _prerender = ""; private string _runtime = "";
private int _logofileid = -1; private string _prerender = "";
private FileManager _logofilemanager; private int _logofileid = -1;
private int _faviconfileid = -1; private FileManager _logofilemanager;
private FileManager _faviconfilemanager; private int _faviconfileid = -1;
private string _themetype = "-"; private FileManager _faviconfilemanager;
private string _containertype = "-"; private string _themetype = "-";
private string _admincontainertype = "-"; private string _containertype = "-";
private string _allowregistration; private string _admincontainertype = "-";
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 = "False";
private string _smtpusername = string.Empty; private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty; private string _smtppassword = string.Empty;
private string _smtpsender = string.Empty; private string _smtppasswordtype = "password";
private string _pwaisenabled; private string _togglesmtppassword = string.Empty;
private int _pwaappiconfileid = -1; private string _smtpsender = string.Empty;
private FileManager _pwaappiconfilemanager; private string _retention = string.Empty;
private int _pwasplashiconfileid = -1; private string _pwaisenabled;
private FileManager _pwasplashiconfilemanager; private int _pwaappiconfileid = -1;
private string _tenant = string.Empty; private FileManager _pwaappiconfilemanager;
private string _database = string.Empty; private int _pwasplashiconfileid = -1;
private string _connectionstring = string.Empty; private FileManager _pwasplashiconfilemanager;
private string _createdby; private string _tenant = string.Empty;
private DateTime _createdon; private string _database = string.Empty;
private string _modifiedby; private string _connectionstring = string.Empty;
private DateTime _modifiedon; private string _createdby;
private string _deletedby; private DateTime _createdon;
private DateTime? _deletedon; private string _modifiedby;
private string _isdeleted; private DateTime _modifiedon;
private string _deletedby;
private DateTime? _deletedon;
private string _isdeleted;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_themeList = await ThemeService.GetThemesAsync(); _themeList = await ThemeService.GetThemesAsync();
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null) if (site != null)
{ {
_name = site.Name; _name = site.Name;
_runtime = site.Runtime; _runtime = site.Runtime;
_prerender = site.RenderMode.Replace(_runtime, ""); _prerender = site.RenderMode.Replace(_runtime, "");
_allowregistration = site.AllowRegistration.ToString(); _isdeleted = site.IsDeleted.ToString();
_isdeleted = site.IsDeleted.ToString();
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
_aliasList = await AliasService.GetAliasesAsync(); await GetAliases();
foreach (Alias alias in _aliasList.Where(item => item.SiteId == site.SiteId && item.TenantId == site.TenantId).ToList()) }
{
_urls += alias.Name + ",";
}
_urls = _urls.Substring(0, _urls.Length - 1);
} if (site.LogoFileId != null)
{
_logofileid = site.LogoFileId.Value;
}
if (site.LogoFileId != null) if (site.FaviconFileId != null)
{ {
_logofileid = site.LogoFileId.Value; _faviconfileid = site.FaviconFileId.Value;
} }
if (site.FaviconFileId != null) _themes = ThemeService.GetThemeControls(_themeList);
{ _themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme;
_faviconfileid = site.FaviconFileId.Value; _containers = ThemeService.GetContainerControls(_themeList, _themetype);
} _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_themes = ThemeService.GetThemeControls(_themeList); _pwaisenabled = site.PwaIsEnabled.ToString();
_themetype = (!string.IsNullOrEmpty(site.DefaultThemeType)) ? site.DefaultThemeType : Constants.DefaultTheme; if (site.PwaAppIconFileId != null)
_containers = ThemeService.GetContainerControls(_themeList, _themetype); {
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _pwaappiconfileid = site.PwaAppIconFileId.Value;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; }
if (site.PwaSplashIconFileId != null)
{
_pwasplashiconfileid = site.PwaSplashIconFileId.Value;
}
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_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");
_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);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _togglesmtppassword = Localizer["Show"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
_pwaisenabled = site.PwaIsEnabled.ToString(); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var tenants = await TenantService.GetTenantsAsync();
var _databases = await DatabaseService.GetDatabasesAsync();
var tenant = tenants.Find(item => item.TenantId == site.TenantId);
if (tenant != null)
{
_tenant = tenant.Name;
_database = _databases.Find(item => item.DBType == tenant.DBType)?.Name;
_connectionstring = tenant.DBConnectionString;
}
}
if (site.PwaAppIconFileId != null) _createdby = site.CreatedBy;
{ _createdon = site.CreatedOn;
_pwaappiconfileid = site.PwaAppIconFileId.Value; _modifiedby = site.ModifiedBy;
} _modifiedon = site.ModifiedOn;
_deletedby = site.DeletedBy;
_deletedon = site.DeletedOn;
if (site.PwaSplashIconFileId != null) _initialized = true;
{ }
_pwasplashiconfileid = site.PwaSplashIconFileId.Value; }
} catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Site {SiteId} {Error}", PageState.Site.SiteId, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
_pwaisenabled = site.PwaIsEnabled.ToString(); private async void ThemeChanged(ChangeEventArgs e)
if (site.PwaAppIconFileId != null) {
{ try
_pwaappiconfileid = site.PwaAppIconFileId.Value; {
} _themetype = (string)e.Value;
if (site.PwaSplashIconFileId != null) if (_themetype != "-")
{ {
_pwasplashiconfileid = site.PwaSplashIconFileId.Value; _containers = ThemeService.GetContainerControls(_themeList, _themetype);
} }
else
{
_containers = new List<ThemeControl>();
}
_containertype = "-";
_admincontainertype = Constants.DefaultAdminContainer;
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadPane"], MessageType.Error);
}
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) private async Task SaveSite()
{ {
var tenants = await TenantService.GetTenantsAsync(); validated = true;
var _databases = await DatabaseService.GetDatabasesAsync(); var interop = new Interop(JSRuntime);
var tenant = tenants.Find(item => item.TenantId == site.TenantId); if (await interop.FormValid(form))
if (tenant != null) {
{ try
_tenant = tenant.Name; {
_database = _databases.Find(item => item.DBType == tenant.DBType)?.Name; if (_name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-")
_connectionstring = tenant.DBConnectionString; {
} var unique = true;
} if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_urls = Regex.Replace(_urls, @"\r\n?|\n", ","); // convert line breaks to commas
var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
{
var alias = aliases.Where(item => item.Name == name).FirstOrDefault();
if (alias != null && unique)
{
unique = (alias.TenantId == PageState.Site.TenantId && alias.SiteId == PageState.Site.SiteId);
}
}
if (unique && string.IsNullOrEmpty(_defaultalias)) unique = false;
}
_createdby = site.CreatedBy; if (unique)
_createdon = site.CreatedOn; {
_modifiedby = site.ModifiedBy; var site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
_modifiedon = site.ModifiedOn; if (site != null)
_deletedby = site.DeletedBy; {
_deletedon = site.DeletedOn; bool refresh = false;
bool reload = false;
_initialized = true; site.Name = _name;
} if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
} {
catch (Exception ex) if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender)
{ {
await logger.LogError(ex, "Error Loading Site {SiteId} {Error}", PageState.Site.SiteId, ex.Message); site.Runtime = _runtime;
AddModuleMessage(ex.Message, MessageType.Error); site.RenderMode = _runtime + _prerender;
} reload = true; // needs to be reloaded on server
} }
}
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
private async void ThemeChanged(ChangeEventArgs e) site.LogoFileId = null;
{ var logofileid = _logofilemanager.GetFileId();
try if (logofileid != -1)
{ {
_themetype = (string)e.Value; site.LogoFileId = logofileid;
if (_themetype != "-") }
{ int? faviconFieldId = _faviconfilemanager.GetFileId();
_containers = ThemeService.GetContainerControls(_themeList, _themetype); if (faviconFieldId == -1) faviconFieldId = null;
} if (site.FaviconFileId != faviconFieldId)
else {
{ site.FaviconFileId = faviconFieldId;
_containers = new List<ThemeControl>(); reload = true; // needs to be reloaded on server
} }
_containertype = "-"; if (site.DefaultThemeType != _themetype)
_admincontainertype = Constants.DefaultAdminContainer; {
StateHasChanged(); site.DefaultThemeType = _themetype;
} refresh = true; // needs to be refreshed on client
catch (Exception ex) }
{ if (site.DefaultContainerType != _containertype)
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message); {
AddModuleMessage(Localizer["Error.Theme.LoadPane"], MessageType.Error); site.DefaultContainerType = _containertype;
} refresh = true; // needs to be refreshed on client
} }
site.AdminContainerType = _admincontainertype;
private async Task SaveSite() if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{ {
validated = true; site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
var interop = new Interop(JSRuntime); reload = true; // needs to be reloaded on server
if (await interop.FormValid(form)) }
{ int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId();
try if (pwaappiconfileid == -1) pwaappiconfileid = null;
{ if (site.PwaAppIconFileId != pwaappiconfileid)
if (_name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-") {
{ site.PwaAppIconFileId = pwaappiconfileid;
var unique = true; reload = true; // needs to be reloaded on server
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) }
{ int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) if (pwasplashiconfileid == -1) pwasplashiconfileid = null;
{ if (site.PwaSplashIconFileId != pwasplashiconfileid)
if (_aliasList.Exists(item => item.Name == name && item.SiteId != PageState.Alias.SiteId && item.TenantId != PageState.Alias.TenantId)) {
{ site.PwaSplashIconFileId = pwasplashiconfileid;
unique = false; reload = true; // needs to be reloaded on server
} }
}
}
if (unique) site = await SiteService.UpdateSiteAsync(site);
{
var site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
{
bool reload = false;
bool refresh = (site.DefaultThemeType != _themetype || site.DefaultContainerType != _containertype);
site.Name = _name; var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
{ settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender) settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
{ settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
site.Runtime = _runtime; settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
site.RenderMode = _runtime + _prerender; settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
refresh = true; settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
reload = true; await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
}
}
site.AllowRegistration = (_allowregistration == null ? true : Boolean.Parse(_allowregistration));
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
site.LogoFileId = null; if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
var logofileid = _logofilemanager.GetFileId(); {
if (logofileid != -1) var names = _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
{ .Select(sValue => sValue.Trim()).ToArray();
site.LogoFileId = logofileid; foreach (Alias alias in _aliases)
} {
var faviconFieldId = _faviconfilemanager.GetFileId(); if (!names.Contains(alias.Name.Trim()))
if (faviconFieldId != -1) {
{ await AliasService.DeleteAliasAsync(alias.AliasId);
site.FaviconFileId = faviconFieldId; }
} }
site.DefaultThemeType = _themetype;
site.DefaultContainerType = _containertype;
site.AdminContainerType = _admincontainertype;
site.PwaIsEnabled = (_pwaisenabled == null ? true : Boolean.Parse(_pwaisenabled)); foreach (string name in names)
var pwaappiconfileid = _pwaappiconfilemanager.GetFileId(); {
if (pwaappiconfileid != -1) var alias = _aliases.Find(item => item.Name.Trim() == name);
{ if (alias == null)
site.PwaAppIconFileId = pwaappiconfileid; {
} alias = new Alias();
var pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId(); alias.Name = name;
if (pwasplashiconfileid != -1) alias.TenantId = site.TenantId;
{ alias.SiteId = site.SiteId;
site.PwaSplashIconFileId = pwasplashiconfileid; alias.IsDefault = (name == _defaultalias);
} await AliasService.AddAliasAsync(alias);
}
else
{
if (alias.Name != name || alias.IsDefault != (alias.Name.Trim() == _defaultalias))
{
alias.Name = name;
alias.IsDefault = (name == _defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
}
await GetAliases();
}
site = await SiteService.UpdateSiteAsync(site); await logger.LogInformation("Site Settings Saved {Site}", site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); if (refresh || reload)
SettingService.SetSetting(settings, "SMTPHost", _smtphost); {
SettingService.SetSetting(settings, "SMTPPort", _smtpport); NavigationManager.NavigateTo(NavigateUrl(true), reload); // refresh/reload
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl); }
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername); else
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword); {
SettingService.SetSetting(settings, "SMTPSender", _smtpsender); AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await interop.ScrollTo(0, 0, "smooth");
}
}
}
else // deuplicate alias or default alias not specified
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.SiteName"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site {SiteId} {Error}", PageState.Site.SiteId, ex.Message);
AddModuleMessage(Localizer["Error.SaveSite"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) private async Task DeleteSite()
{ {
var names = _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); try
foreach (Alias alias in _aliasList.Where(item => item.SiteId == site.SiteId && item.TenantId == site.TenantId).ToList()) {
{ var sites = await SiteService.GetSitesAsync();
if (!names.Contains(alias.Name)) if (sites.Count > 1)
{ {
await AliasService.DeleteAliasAsync(alias.AliasId); await SiteService.DeleteSiteAsync(PageState.Site.SiteId);
} await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId);
}
foreach (string name in names) var aliases = await AliasService.GetAliasesAsync();
{ foreach (Alias a in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId))
if (!_aliasList.Exists(item => item.Name == name)) {
{ await AliasService.DeleteAliasAsync(a.AliasId);
Alias alias = new Alias(); }
alias.Name = name;
alias.TenantId = site.TenantId;
alias.SiteId = site.SiteId;
await AliasService.AddAliasAsync(alias);
}
}
}
await logger.LogInformation("Site Settings Saved {Site}", site); NavigationManager.NavigateTo(NavigateUrl("admin/sites"));
}
else
{
AddModuleMessage(Localizer["Message.FailAuth.DeleteSite"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Site {SiteId} {Error}", PageState.Site.SiteId, ex.Message);
AddModuleMessage(Localizer["Error.DeleteSite"], MessageType.Error);
}
}
if (refresh) private async Task SendEmail()
{ {
NavigationManager.NavigateTo(NavigateUrl(), reload); // refresh to show new theme or container if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
} {
else try
{ {
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
} SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
} SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
} SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
else SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
{ SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
} await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
} await logger.LogInformation("Site SMTP Settings Saved");
else
{
AddModuleMessage(Localizer["Message.Required.SiteName"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site {SiteId} {Error}", PageState.Site.SiteId, ex.Message);
AddModuleMessage(Localizer["Error.SaveSite"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task DeleteSite() await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
{ AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
try var interop = new Interop(JSRuntime);
{ await interop.ScrollTo(0, 0, "smooth");
var sites = await SiteService.GetSitesAsync(); }
if (sites.Count > 1) catch (Exception ex)
{ {
await SiteService.DeleteSiteAsync(PageState.Site.SiteId); await logger.LogError(ex, "Error Testing SMTP Configuration");
await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId); AddModuleMessage(Localizer["Error.Smtp.TestConfig"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.Smtp"], MessageType.Warning);
}
}
var aliases = await AliasService.GetAliasesAsync(); private async Task GetAliases()
foreach (Alias a in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId)) {
{ _urls = string.Empty;
await AliasService.DeleteAliasAsync(a.AliasId); _defaultalias = string.Empty;
} _aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
foreach (Alias alias in _aliases)
{
_urls += (_urls == string.Empty) ? alias.Name.Trim() : ", " + alias.Name.Trim();
if (alias.IsDefault && string.IsNullOrEmpty(_defaultalias)) _defaultalias = alias.Name.Trim();
}
if (string.IsNullOrEmpty(_defaultalias)) _defaultalias = _aliases.First().Name.Trim();
}
NavigationManager.NavigateTo(NavigateUrl("admin/sites")); private void ToggleSMTPPassword()
} {
else if (_smtppasswordtype == "password")
{ {
AddModuleMessage(Localizer["Message.FailAuth.DeleteSite"], MessageType.Warning); _smtppasswordtype = "text";
} _togglesmtppassword = Localizer["Hide"];
} }
catch (Exception ex) else
{ {
await logger.LogError(ex, "Error Deleting Site {SiteId} {Error}", PageState.Site.SiteId, ex.Message); _smtppasswordtype = "password";
AddModuleMessage(Localizer["Error.DeleteSite"], MessageType.Error); _togglesmtppassword = Localizer["Show"];
} }
} }
private async Task SendEmail()
{
if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
SettingService.SetSetting(settings, "SMTPHost", _smtphost);
SettingService.SetSetting(settings, "SMTPPort", _smtpport);
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl);
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername);
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword);
SettingService.SetSetting(settings, "SMTPSender", _smtpsender);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User.DisplayName, PageState.User.Email, PageState.User.DisplayName, PageState.User.Email, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Testing SMTP Configuration");
AddModuleMessage(Localizer["Error.Smtp.TestConfig"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.required.Smtp"], MessageType.Warning);
}
}
} }

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Admin.Sites @namespace Oqtane.Modules.Admin.Sites
@using Oqtane.Interfaces @using Oqtane.Interfaces
@using System.Text.RegularExpressions
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ITenantService TenantService @inject ITenantService TenantService
@ -282,6 +283,7 @@ else
{ {
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-") if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-")
{ {
_urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>(); var duplicates = new List<string>();
var aliases = await AliasService.GetAliasesAsync(); var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
@ -370,8 +372,7 @@ else
if (installation.Success) if (installation.Success)
{ {
var aliasname = config.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0]; var aliasname = config.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0];
var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + aliasname, true);
NavigationManager.NavigateTo(uri.Scheme + "://" + aliasname, true);
} }
else else
{ {

View File

@ -30,20 +30,16 @@ else
@code { @code {
private List<Alias> _sites; private List<Alias> _sites;
private string _scheme;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
var uri = new Uri(NavigationManager.Uri);
_scheme = uri.Scheme + "://";
var aliases = await AliasService.GetAliasesAsync(); var aliases = await AliasService.GetAliasesAsync();
_sites = new List<Alias>(); _sites = new List<Alias>();
foreach (Alias alias in aliases) foreach (Alias alias in aliases)
{ {
if (!_sites.Exists(item => item.TenantId == alias.TenantId && item.SiteId == alias.SiteId)) if (alias.IsDefault && !_sites.Exists(item => item.TenantId == alias.TenantId && item.SiteId == alias.SiteId))
{ {
_sites.Add(alias); _sites.Add(alias);
} }
@ -52,11 +48,11 @@ else
private void Edit(string name) private void Edit(string name)
{ {
NavigationManager.NavigateTo(_scheme + name + "/admin/site/?reload"); NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name + "/admin/site", true);
} }
private void Browse(string name) private void Browse(string name)
{ {
NavigationManager.NavigateTo(_scheme + name + "/?reload"); NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name, true);
} }
} }

View File

@ -57,22 +57,20 @@ else
{ {
@if (_results.Count > 0) @if (_results.Count > 0)
{ {
<div class="table-responsive"> <Pager Class="table table-bordered" Items="@_results">
<Pager Class="table table-bordered" Items="@_results"> <Header>
<Header> @foreach (KeyValuePair<string, string> kvp in _results.First())
@foreach (KeyValuePair<string, string> kvp in _results.First()) {
{ <th>@kvp.Key</th>
<th>@kvp.Key</th> }
} </Header>
</Header> <Row>
<Row> @foreach (KeyValuePair<string, string> kvp in context)
@foreach (KeyValuePair<string, string> kvp in context) {
{ <td>@kvp.Value</td>
<td>@kvp.Value</td> }
} </Row>
</Row> </Pager>
</Pager>
</div>
} }
else else
{ {

View File

@ -27,17 +27,47 @@
</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="serverpath" HelpText="Server Path" ResourceKey="ServerPath">Server Path: </Label> <Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="serverpath" class="form-control" @bind="@_serverpath" readonly /> <input id="machinename" class="form-control" @bind="@_machinename" 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="servertime" HelpText="Server Time" ResourceKey="ServerTime">Server Time: </Label> <Label Class="col-sm-3" For="ipaddress" HelpText="Server IP Address" ResourceKey="IPAddress">IP Address: </Label>
<div class="col-sm-9">
<input id="ipaddress" class="form-control" @bind="@_ipaddress" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contentrootpath" HelpText="Root Path" ResourceKey="ContentRootPath">Root Path: </Label>
<div class="col-sm-9">
<input id="contentrootpath" class="form-control" @bind="@_contentrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="webrootpath" HelpText="Web Path" ResourceKey="WebRootPath">Web Path: </Label>
<div class="col-sm-9">
<input id="webrootpath" class="form-control" @bind="@_webrootpath" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="servertime" HelpText="Server Date/Time (in UTC)" ResourceKey="ServerTime">Server Date/Time: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="servertime" class="form-control" @bind="@_servertime" readonly /> <input id="servertime" class="form-control" @bind="@_servertime" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tickcount" HelpText="Amount Of Time The Service Has Been Available And Operational" ResourceKey="TickCount">Service Uptime: </Label>
<div class="col-sm-9">
<input id="tickcount" class="form-control" @bind="@_tickcount" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="workingset" HelpText="Memory Allocation Of Service (in MB)" ResourceKey="WorkingSet">Memory Allocation: </Label>
<div class="col-sm-9">
<input id="workingset" class="form-control" @bind="@_workingset" 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="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label> <Label Class="col-sm-3" For="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -69,6 +99,21 @@
<option value="Warning">@Localizer["Warning"]</option> <option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option> <option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option> <option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notificationlevel" HelpText="The Minimum Logging Level For Which Notifications Should Be Sent To Host Users." ResourceKey="NotificationLevel">Notification Level: </Label>
<div class="col-sm-9">
<select id="notificationlevel" class="form-select" @bind="@_notificationlevel">
<option value="Trace">@Localizer["Trace"]</option>
<option value="Debug">@Localizer["Debug"]</option>
<option value="Information">@Localizer["Information"]</option>
<option value="Warning">@Localizer["Warning"]</option>
<option value="Error">@Localizer["Error"]</option>
<option value="Critical">@Localizer["Critical"]</option>
<option value="None">@Localizer["None"]</option>
</select> </select>
</div> </div>
</div> </div>
@ -99,49 +144,66 @@
</TabStrip> </TabStrip>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private string _version = string.Empty; private string _version = string.Empty;
private string _clrversion = string.Empty; private string _clrversion = string.Empty;
private string _osversion = string.Empty; private string _osversion = string.Empty;
private string _serverpath = string.Empty; private string _machinename = string.Empty;
private string _servertime = string.Empty; private string _ipaddress = string.Empty;
private string _installationid = string.Empty; private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty;
private string _servertime = string.Empty;
private string _tickcount = string.Empty;
private string _workingset = string.Empty;
private string _installationid = string.Empty;
private string _detailederrors = string.Empty; private string _detailederrors = string.Empty;
private string _logginglevel = string.Empty; private string _logginglevel = string.Empty;
private string _swagger = string.Empty; private string _notificationlevel = string.Empty;
private string _packageservice = string.Empty; private string _swagger = string.Empty;
private string _packageservice = string.Empty;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_version = Constants.Version; _version = Constants.Version;
Dictionary<string, string> systeminfo = await SystemService.GetSystemInfoAsync(); Dictionary<string, object> systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null) if (systeminfo != null)
{ {
_clrversion = systeminfo["clrversion"]; _clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["osversion"]; _osversion = systeminfo["OSVersion"].ToString();
_serverpath = systeminfo["serverpath"]; _machinename = systeminfo["MachineName"].ToString();
_servertime = systeminfo["servertime"]; _ipaddress = systeminfo["IPAddress"].ToString();
_installationid = systeminfo["installationid"]; _contentrootpath = systeminfo["ContentRootPath"].ToString();
_webrootpath = systeminfo["WebRootPath"].ToString();
_servertime = systeminfo["ServerTime"].ToString() + " UTC";
_tickcount = TimeSpan.FromMilliseconds(Convert.ToInt64(systeminfo["TickCount"].ToString())).ToString();
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
}
_detailederrors = systeminfo["detailederrors"]; systeminfo = await SystemService.GetSystemInfoAsync();
_logginglevel = systeminfo["logginglevel"]; if (systeminfo != null)
_swagger = systeminfo["swagger"]; {
_packageservice = systeminfo["packageservice"]; _installationid = systeminfo["InstallationId"].ToString();
} _detailederrors = systeminfo["DetailedErrors"].ToString();
} _logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString();
_packageservice = systeminfo["PackageService"].ToString();
}
}
private async Task SaveConfig() private async Task SaveConfig()
{ {
try try
{ {
var settings = new Dictionary<string, string>(); var settings = new Dictionary<string, object>();
settings.Add("detailederrors", _detailederrors); settings.Add("DetailedErrors", _detailederrors);
settings.Add("logginglevel", _logginglevel); settings.Add("Logging:LogLevel:Default", _logginglevel);
settings.Add("swagger", _swagger); settings.Add("Logging:LogLevel:Notify", _notificationlevel);
settings.Add("packageservice", _packageservice); settings.Add("UseSwagger", _swagger);
settings.Add("PackageService", _packageservice);
await SystemService.UpdateSystemInfoAsync(settings); await SystemService.UpdateSystemInfoAsync(settings);
AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
} }

View File

@ -0,0 +1,89 @@
@namespace Oqtane.Modules.Admin.UrlMappings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUrlMappingService UrlMappingService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The fully qualified Url for this site" ResourceKey="Url">Url:</Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveUrlMapping">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</div>
</form>
@code {
private ElementReference form;
private bool validated = false;
private string _url = string.Empty;
private string _mappedurl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private async Task SaveUrlMapping()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url != _mappedurl)
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
_url = (_url.StartsWith("/")) ? _url.Substring(1) : _url;
_url = (!_url.StartsWith("http")) ? url + _url : _url;
if (_url.StartsWith(url))
{
var urlmapping = new UrlMapping();
urlmapping.SiteId = PageState.Site.SiteId;
var route = new Route(_url, PageState.Alias.Path);
urlmapping.Url = route.PagePath;
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping.Requests = 0;
urlmapping.CreatedOn = DateTime.UtcNow;
urlmapping.RequestedOn = DateTime.UtcNow;
try
{
urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message);
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.DuplicateUrlMapping"], MessageType.Warning);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -0,0 +1,92 @@
@namespace Oqtane.Modules.Admin.UrlMappings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUrlMappingService UrlMappingService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="A fully qualified Url for this site" ResourceKey="Url">Url:</Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" maxlength="500" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="mappedurl" HelpText="A fully qualified Url where the user will be redirected" ResourceKey="MappedUrl">Redirect To:</Label>
<div class="col-sm-9">
<input id="mappedurl" class="form-control" @bind="@_mappedurl" maxlength="500" required />
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveUrlMapping">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</div>
</form>
@code {
private ElementReference form;
private bool validated = false;
private int _urlmappingid;
private string _url = string.Empty;
private string _mappedurl = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync()
{
try
{
_urlmappingid = Int32.Parse(PageState.QueryString["id"]);
var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid);
if (urlmapping != null)
{
_url = urlmapping.Url;
_mappedurl = urlmapping.MappedUrl;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading UrlMapping {UrlMappingId} {Error}", _urlmappingid, ex.Message);
AddModuleMessage(Localizer["Error.LoadUrlMapping"], MessageType.Error);
}
}
private async Task SaveUrlMapping()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url != _mappedurl)
{
try
{
var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/";
url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : "");
var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid);
urlmapping.MappedUrl = _mappedurl.Replace(url, "");
urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping);
await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving UrlMapping {UrlMappingId} {Error}", _urlmappingid, ex.Message);
AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.DuplicateUrlMapping"], MessageType.Warning);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -0,0 +1,135 @@
@namespace Oqtane.Modules.Admin.UrlMappings
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUrlMappingService UrlMappingService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_urlMappings == null)
{
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<TabStrip>
<TabPanel Name="Urls" Heading="Urls" ResourceKey="Urls">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Add Url Mapping" ResourceKey="AddUrlMapping" />
</div>
<div class="col-sm-6">
<select id="type" class="form-select custom-select" @onchange="(e => MappedChanged(e))">
<option value="true">@Localizer["Mapped"]</option>
<option value="false">@Localizer["Broken"]</option>
</select>
</div>
</div>
</div>
<br/>
<Pager Items="@_urlMappings">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Url"]</th>
<th>@Localizer["Requests"]</th>
<th>@Localizer["Requested"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.UrlMappingId.ToString())" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Url Mapping" Message="@string.Format(Localizer["Confirm.DeleteUrlMapping"], context.Url)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUrlMapping(context))" ResourceKey="DeleteUrlMapping" /></td>
<td>
<a href="@Utilities.TenantUrl(PageState.Alias, context.Url)">@context.Url</a>
@if (_mapped)
{
@((MarkupString)"<br />&gt;&gt;&nbsp;")<a href="@((context.MappedUrl.StartsWith("http") ? context.MappedUrl : Utilities.TenantUrl(PageState.Alias, context.MappedUrl)))">@context.MappedUrl</a>
}
</td>
<td>@context.Requests</td>
<td>@context.RequestedOn</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capturebrokenurls" HelpText="Specify if broken Urls should be captured automatically and saved in Url Mappings" ResourceKey="CaptureBrokenUrls">Capture Broken Urls? </Label>
<div class="col-sm-9">
<select id="capturebrokenurls" class="form-select" @bind="@_capturebrokenurls" >
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
}
@code {
private bool _mapped = true;
private List<UrlMapping> _urlMappings;
private string _capturebrokenurls;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
await GetUrlMappings();
_capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString();
}
private async void MappedChanged(ChangeEventArgs e)
{
try
{
_mapped = bool.Parse(e.Value.ToString());
await GetUrlMappings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On TypeChanged");
}
}
private async Task DeleteUrlMapping(UrlMapping urlMapping)
{
try
{
await UrlMappingService.DeleteUrlMappingAsync(urlMapping.UrlMappingId);
await logger.LogInformation("UrlMapping Deleted {UrlMapping}", urlMapping);
await GetUrlMappings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting UrlMapping {UrlMapping} {Error}", urlMapping, ex.Message);
AddModuleMessage(Localizer["Error.DeleteUrlMapping"], MessageType.Error);
}
}
private async Task GetUrlMappings()
{
_urlMappings = await UrlMappingService.GetUrlMappingsAsync(PageState.Site.SiteId, _mapped);
}
private async Task SaveSiteSettings()
{
try
{
var site = PageState.Site;
site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls);
await SiteService.UpdateSiteAsync(site);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
}

View File

@ -13,7 +13,7 @@
<Label Class="col-sm-3" For="to" HelpText="Enter the username you wish to send a message to" ResourceKey="To">To: </Label> <Label Class="col-sm-3" For="to" HelpText="Enter the username you wish to send a message to" ResourceKey="To">To: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="to" class="form-control" @bind="@username" /> <input id="to" class="form-control" @bind="@username" />
</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="subject" HelpText="Enter the subject of the message" ResourceKey="Subject">Subject: </Label> <Label Class="col-sm-3" For="subject" HelpText="Enter the subject of the message" ResourceKey="Subject">Subject: </Label>
@ -49,7 +49,7 @@
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId); var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, null); var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body);
notification = await NotificationService.AddNotificationAsync(notification); notification = await NotificationService.AddNotificationAsync(notification);
await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId); await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());

View File

@ -12,7 +12,7 @@
@if (PageState.User != null && photo != null) @if (PageState.User != null && photo != null)
{ {
<img src="@ImageUrl(photofileid, 400, 400, "crop")" 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
{ {
@ -41,6 +41,18 @@ else
<input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" /> <input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" />
</div> </div>
</div> </div>
@if (allowtwofactor)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required>
<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="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">
@ -201,104 +213,125 @@ else
} }
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
<br /><br />
@code { @code {
private string username = string.Empty; private string username = string.Empty;
private string password = string.Empty; private string password = string.Empty;
private string confirm = string.Empty; private string confirm = string.Empty;
private string email = string.Empty; private bool allowtwofactor = false;
private string displayname = string.Empty; private string twofactor = "False";
private FileManager filemanager; private string email = string.Empty;
private int folderid = -1; private string displayname = string.Empty;
private int photofileid = -1; private FileManager filemanager;
private File photo = null; private int folderid = -1;
private List<Profile> profiles; private int photofileid = -1;
private Dictionary<string, string> settings; private File photo = null;
private string category = string.Empty; private List<Profile> profiles;
private string filter = "to"; private Dictionary<string, string> settings;
private List<Notification> notifications; private string category = string.Empty;
private string filter = "to";
private List<Notification> notifications;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
if (PageState.User != null) if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{ {
username = PageState.User.Username; allowtwofactor = bool.Parse(PageState.Site.Settings["LoginOptions:TwoFactor"]);
email = PageState.User.Email; }
displayname = PageState.User.DisplayName;
// get user folder if (PageState.User != null)
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); {
if (folder != null) username = PageState.User.Username;
{ twofactor = PageState.User.TwoFactorRequired.ToString();
folderid = folder.FolderId; email = PageState.User.Email;
} displayname = PageState.User.DisplayName;
if (PageState.User.PhotoFileId != null) // get user folder
{ var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
photofileid = PageState.User.PhotoFileId.Value; if (folder != null)
photo = await FileService.GetFileAsync(photofileid); {
} folderid = folder.FolderId;
else }
{
photofileid = -1;
photo = null;
}
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); if (PageState.User.PhotoFileId != null)
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); {
photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
await LoadNotificationsAsync(); profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
} settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
else
{
AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
private async Task LoadNotificationsAsync() await LoadNotificationsAsync();
{ }
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); else
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); {
} AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
private string GetProfileValue(string SettingName, string DefaultValue) private async Task LoadNotificationsAsync()
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId);
notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList();
}
private async Task Save() private string GetProfileValue(string SettingName, string DefaultValue)
{ => SettingService.GetSetting(settings, SettingName, DefaultValue);
try
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
{
var user = PageState.User;
user.Username = username;
user.Password = password;
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
await UserService.UpdateUserAsync(user); private async Task Save()
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); {
await logger.LogInformation("User Profile Saved"); try
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
{
var user = PageState.User;
user.Username = username;
user.Password = password;
user.TwoFactorRequired = bool.Parse(twofactor);
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = null;
}
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
NavigationManager.NavigateTo(NavigateUrl()); await UserService.UpdateUserAsync(user);
} await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
}
else else
{ {
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning); AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);

View File

@ -3,6 +3,7 @@
@inject IUserRoleService UserRoleService @inject IUserRoleService UserRoleService
@inject IUserService UserService @inject IUserService UserService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -14,112 +15,581 @@
} }
else else
{ {
<div class="container"> <TabStrip>
<div class="row mb-1 align-items-center"> <TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<div class="col-sm-4"> <div class="container">
<ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" /> <div class="row mb-1 align-items-center">
</div> <div class="col-sm-4">
<div class="col-sm-4"> <ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" />
<input class="form-control" @bind="@_search" /> </div>
</div> <div class="col-sm-4">
<div class="col-sm-4"> <input class="form-control" @bind="@_search" />
<button class="btn btn-secondary" @onclick="OnSearch">@SharedLocalizer["Search"]</button> </div>
</div> <div class="col-sm-4">
</div> <button type="button" class="btn btn-secondary" @onclick="OnSearch">@SharedLocalizer["Search"]</button>
</div> </div>
<Pager Items="@userroles"> </div>
<Header> </div>
<th style="width: 1px;">&nbsp;</th> <Pager Items="@userroles">
<th style="width: 1px;">&nbsp;</th> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th style="width: 1px;">&nbsp;</th>
</Header> <th style="width: 1px;">&nbsp;</th>
<Row> <th>@SharedLocalizer["Name"]</th>
<td> <th>@SharedLocalizer["Username"]</th>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" /> </Header>
</td> <Row>
<td> <td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" /> <ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" />
</td> </td>
<td> <td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" /> <ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td> </td>
<td>@context.User.DisplayName</td> <td>
</Row> <ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" />
</Pager> </td>
<td>@context.User.DisplayName</td>
<td>@context.User.Username</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowregistration" HelpText="Do you want anonymous visitors to be able to register for an account on the site" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<div class="col-sm-9">
<select id="allowregistration" class="form-select" @bind="@_allowregistration">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site." ResourceKey="AllowSiteLogin">Allow Login?</Label>
<div class="col-sm-9">
<input id="allowsitelogin" class="form-control" value="@SharedLocalizer["Yes"]" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want to allow users to use two factor authentication? Note that the Notification Job in Scheduled Jobs needs to be enabled for this option to work properly." ResourceKey="TwoFactor">Allow Two Factor?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(PageState.Alias.Path))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookietype" HelpText="Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site (this option is only applicable to micro-sites)." ResourceKey="CookieType">Cookie Type:</Label>
<div class="col-sm-9">
<select id="cookietype" class="form-select" @bind="@_cookietype">
<option value="domain">@Localizer["Domain"]</option>
<option value="site">@Localizer["Site"]</option>
</select>
</div>
</div>
}
</Section>
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>@Localizer["Not Specified"]</option>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OpenID Connect"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth 2.0"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="The external login provider name which will be displayed on the login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadataurl" HelpText="The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</div>
</div>
}
@if (_providertype == AuthenticationProviderTypes.OAuth2)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_clientsecrettype" id="clientsecret" class="form-control" @bind="@_clientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleClientSecret">@_toggleclientsecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The type name for the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim Type:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="secret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="secret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="issuer" HelpText="Optionally provide the issuer of the token" ResourceKey="Issuer">Issuer:</Label>
<div class="col-sm-9">
<input id="issuer" class="form-control" @bind="@_issuer" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="audience" HelpText="Optionally provide the audience for the token" ResourceKey="Audience">Audience:</Label>
<div class="col-sm-9">
<input id="audience" class="form-control" @bind="@_audience" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lifetime" HelpText="The number of minutes for which a token should be valid" ResourceKey="Lifetime">Lifetime:</Label>
<div class="col-sm-9">
<input id="lifetime" class="form-control" @bind="@_lifetime" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="token" HelpText="Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future." ResourceKey="Token">Access Token:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="token" class="form-control" @bind="@_token" />
<button type="button" class="btn btn-secondary" @onclick="@CreateToken">@Localizer["CreateToken"]</button>
</div>
</div>
</div>
</Section>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
} }
@code { @code {
private List<UserRole> allroles; private List<UserRole> allroles;
private List<UserRole> userroles; private List<UserRole> userroles;
private string _search; private string _search;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; private string _allowregistration;
private string _allowsitelogin;
private string _twofactor;
private string _cookietype;
protected override async Task OnInitializedAsync() private string _minimumlength;
{ private string _uniquecharacters;
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId); private string _requiredigit;
await LoadSettingsAsync(); private string _requireupper;
userroles = Search(_search); private string _requirelower;
} private string _requirepunctuation;
private string _maximumfailures;
private string _lockoutduration;
private List<UserRole> Search(string search) private string _providertype;
{ private string _providername;
var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))); private string _authority;
private string _metadataurl;
private string _authorizationurl;
private string _tokenurl;
private string _userinfourl;
private string _clientid;
private string _clientsecret;
private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty;
private string _scopes;
private string _pkce;
private string _redirecturl;
private string _emailclaimtype;
private string _domainfilter;
private string _createusers;
if (string.IsNullOrEmpty(_search)) private string _secret;
{ private string _secrettype = "password";
results = results.Where(item => private string _togglesecret = string.Empty;
( private string _issuer;
item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) || private string _audience;
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) || private string _lifetime;
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase) private string _token;
)
);
}
return results.ToList();
}
private async Task OnSearch() public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
{
userroles = Search(_search);
await UpdateSettingsAsync();
}
private async Task DeleteUser(UserRole UserRole) protected override async Task OnInitializedAsync()
{ {
try allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
{ await LoadSettingsAsync();
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); userroles = Search(_search);
if (user != null)
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = Search(_search);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private string settingSearch = "AU-search"; var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookietype = SettingService.GetSetting(settings, "LoginOptions:CookieType", "domain");
private async Task LoadSettingsAsync() _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
{ _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); _requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true");
_search = SettingService.GetSetting(settings, settingSearch, ""); _requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true");
} _requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true");
_requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true");
private async Task UpdateSettingsAsync() _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
{ _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
}
_providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", "");
_providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", "");
_authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
_metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", "");
_authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", "");
_tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", "");
_userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", "");
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = Localizer["Show"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
_togglesecret = Localizer["Show"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20");
}
private List<UserRole> Search(string search)
{
var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)));
if (!string.IsNullOrEmpty(_search))
{
results = results.Where(item =>
(
item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)
)
);
}
return results.ToList();
}
private async Task OnSearch()
{
userroles = Search(_search);
await UpdateSettingsAsync();
}
private async Task DeleteUser(UserRole UserRole)
{
try
{
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = Search(_search);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private string settingSearch = "AU-search";
private async Task LoadSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, "");
}
private async Task UpdateSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
}
private async Task SaveSiteSettings()
{
try
{
var site = PageState.Site;
site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieType", _cookietype, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
if (!string.IsNullOrEmpty(_secret) && _secret.Length < 16) _secret = (_secret + "????????????????").Substring(0, 16);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync();
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
private void ProviderTypeChanged(ChangeEventArgs e)
{
_providertype = (string)e.Value;
if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
_scopes = "openid,profile,email";
}
else
{
_scopes = "";
}
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged();
}
private async Task CreateToken()
{
_token = await UserService.GetTokenAsync();
}
private void ToggleClientSecret()
{
if (_clientsecrettype == "password")
{
_clientsecrettype = "text";
_toggleclientsecret = Localizer["Hide"];
}
else
{
_clientsecrettype = "password";
_toggleclientsecret = Localizer["Show"];
}
}
private void ToggleSecret()
{
if (_secrettype == "password")
{
_secrettype = "text";
_togglesecret = Localizer["Hide"];
}
else
{
_secrettype = "password";
_togglesecret = Localizer["Show"];
}
}
} }

View File

@ -0,0 +1,140 @@
@namespace Oqtane.Modules.Admin.Visitors
@using System.Globalization
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IVisitorService VisitorService
@inject IUserService UserService
@inject IStringLocalizer<Detail> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_initialized)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ip" HelpText="The last recorded IP address for this visitor" ResourceKey="IP">IP Address: </Label>
<div class="col-sm-9">
<input id="ip" class="form-control" @bind="@_ip" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="language" HelpText="The last recorded language for this visitor" ResourceKey="Language">Language: </Label>
<div class="col-sm-9">
<input id="language" class="form-control" @bind="@_language" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="useragent" HelpText="The last recorded user agent for this visitor" ResourceKey="UserAgent">User Agent: </Label>
<div class="col-sm-9">
<input id="useragent" class="form-control" @bind="@_useragent" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The last recorded url for this visitor" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="referrer" HelpText="The last recorded referrer for this visitor" ResourceKey="Referrer">Referrer: </Label>
<div class="col-sm-9">
<input id="referrer" class="form-control" @bind="@_referrer" readonly />
</div>
</div>
@if (_user != string.Empty)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="The last recorded user associated with this visitor" ResourceKey="User">User: </Label>
<div class="col-sm-9">
<input id="user" class="form-control" @bind="@_user" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="visits" HelpText="The total number of visits by this visitor all time" ResourceKey="Visits">Visits: </Label>
<div class="col-sm-9">
<input id="visits" class="form-control" @bind="@_visits" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="visited" HelpText="The last recorded date/time when the visitor visited the site" ResourceKey="Visited">Visited: </Label>
<div class="col-sm-9">
<input id="visited" class="form-control" @bind="@_visited" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="created" HelpText="The first recorded date/time when this visitor visited the site" ResourceKey="Created">Created: </Label>
<div class="col-sm-9">
<input id="created" class="form-control" @bind="@_created" readonly />
</div>
</div>
</div>
}
<NavLink class="btn btn-secondary" href="@CloseUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private bool _initialized = false;
private int _visitorId;
private string _ip = string.Empty;
private string _language = string.Empty;
private string _useragent = string.Empty;
private string _url = string.Empty;
private string _referrer = string.Empty;
private string _user = string.Empty;
private string _visits = string.Empty;
private string _visited = string.Empty;
private string _created = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync()
{
try
{
_visitorId = Int32.Parse(PageState.QueryString["id"]);
var visitor = await VisitorService.GetVisitorAsync(_visitorId);
if (visitor != null)
{
_ip = visitor.IPAddress;
_language = visitor.Language;
_useragent = visitor.UserAgent;
_url = visitor.Url;
_referrer = visitor.Referrer;
_visits = visitor.Visits.ToString();
_visited = visitor.VisitedOn.ToString(CultureInfo.CurrentCulture);
_created = visitor.CreatedOn.ToString(CultureInfo.CurrentCulture);
if (visitor.UserId != null)
{
var user = await UserService.GetUserAsync(visitor.UserId.Value, PageState.Site.SiteId);
if (user != null)
{
_user = user.DisplayName;
}
}
_initialized = true;
}
else
{
AddModuleMessage(Localizer["Error.LoadVisitor"], MessageType.Error);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Visitor {VisitorId} {Error}", _visitorId, ex.Message);
AddModuleMessage(Localizer["Error.LoadVisitor"], MessageType.Error);
}
}
private string CloseUrl()
{
if (!PageState.QueryString.ContainsKey("type"))
{
return NavigateUrl();
}
else
{
return NavigateUrl(PageState.Page.Path, "type=" + PageState.QueryString["type"] + "&days=" + PageState.QueryString["days"] + "&page=" + PageState.QueryString["page"]);
}
}
}

View File

@ -0,0 +1,200 @@
@namespace Oqtane.Modules.Admin.Visitors
@inherits ModuleBase
@inject IVisitorService VisitorService
@inject ISiteService SiteService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_visitors == null)
{
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<TabStrip>
<TabPanel Name="Visitors" Heading="Visitors" ResourceKey="Visitors">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-6">
<select id="type" class="form-select custom-select" value="@_type" @onchange="(e => TypeChanged(e))">
<option value="visitors">@Localizer["AllVisitors"]</option>
<option value="users">@Localizer["UsersOnly"]</option>
</select>
</div>
<div class="col-sm-6">
<select id="days" class="form-select custom-select" value="@_days" @onchange="(e => DaysChanged(e))">
<option value="1">@Localizer["PastDay"]</option>
<option value="7">@Localizer["PastWeek"]</option>
<option value="30">@Localizer["PastMonth"]</option>
</select>
</div>
</div>
</div>
<br/>
<Pager Items="@_visitors" CurrentPage="@_page.ToString()" OnPageChange="OnPageChange">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["IP"]</th>
<th>@Localizer["User"]</th>
<th>@Localizer["Language"]</th>
<th>@Localizer["Visits"]</th>
<th>@Localizer["Visited"]</th>
<th>@Localizer["Created"]</th>
</Header>
<Row>
<td><ActionLink Action="Detail" Parameters="@($"id=" + context.VisitorId.ToString() + "&type=" + _type.ToString() + "&days=" + _days.ToString() + "&page=" + _page.ToString())" ResourceKey="Details" /></td>
<td>@context.IPAddress</td>
<td>
@if (context.UserId != null)
{
@context.User.DisplayName
}
</td>
<td>@context.Language</td>
<td>@context.Visits</td>
<td>@context.VisitedOn</td>
<td>@context.CreatedOn</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tracking" HelpText="Specify if visitor tracking is enabled" ResourceKey="Tracking">Tracking Enabled? </Label>
<div class="col-sm-9">
<select id="tracking" class="form-select" @bind="@_tracking" >
<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="filter" HelpText="Comma delimited list of terms which may exist in IP addresses, user agents, or languages identifying visitors which should not be tracked" ResourceKey="Filter">Filter: </Label>
<div class="col-sm-9">
<textarea id="filter" class="form-control" @bind="@_filter" rows="3"></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of visitor activity to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="correlation" HelpText="Indicate if new visitors to this site should be correlated based on their IP Address" ResourceKey="Correlation">Correlate Visitors? </Label>
<div class="col-sm-9">
<select id="correlation" class="form-select" @bind="@_correlation">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
}
@code {
private string _type = "visitors";
private int _days = 1;
private int _page = 1;
private List<Visitor> _visitors;
private string _tracking;
private string _filter = "";
private string _retention = "";
private string _correlation = "true";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
if (PageState.QueryString.ContainsKey("type"))
{
_type = PageState.QueryString["type"];
}
if (PageState.QueryString.ContainsKey("days") && int.TryParse(PageState.QueryString["days"], out int days))
{
_days = days;
}
if (PageState.QueryString.ContainsKey("page") && int.TryParse(PageState.QueryString["page"], out int page))
{
_page = page;
}
await GetVisitors();
_tracking = PageState.Site.VisitorTracking.ToString();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter);
_retention = SettingService.GetSetting(settings, "VisitorRetention", "30");
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
}
private async void TypeChanged(ChangeEventArgs e)
{
try
{
_type = e.Value.ToString();
await GetVisitors();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On TypeChanged");
}
}
private async void DaysChanged(ChangeEventArgs e)
{
try
{
_days = int.Parse(e.Value.ToString());
await GetVisitors();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On DateChanged");
}
}
private async Task GetVisitors()
{
_visitors = await VisitorService.GetVisitorsAsync(PageState.Site.SiteId, DateTime.UtcNow.AddDays(-_days));
if (_type == "users")
{
_visitors = _visitors.Where(item => item.UserId != null).ToList();
}
}
private async Task SaveSiteSettings()
{
try
{
var site = PageState.Site;
site.VisitorTracking = bool.Parse(_tracking);
await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention, true);
settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
private void OnPageChange(int page)
{
_page = page;
}
}

View File

@ -17,7 +17,7 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!string.IsNullOrEmpty(Action)) @if (!string.IsNullOrEmpty(Action))
{ {
<button type="button" class="@Class" @onclick="Confirm">@((MarkupString)_iconSpan) @Localize(Action)</button> <button type="button" class="@Class" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button>
} }
<button type="button" class="btn btn-secondary" @onclick="DisplayModal">@Localize("Cancel")</button> <button type="button" class="btn btn-secondary" @onclick="DisplayModal">@Localize("Cancel")</button>
</div> </div>
@ -30,16 +30,17 @@
{ {
if (Disabled) if (Disabled)
{ {
<button class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button> <button type="button" class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button>
} }
else else
{ {
<button class="@Class" @onclick="DisplayModal">@((MarkupString)_iconSpan) @Text</button> <button type="button" class="@Class" @onclick="DisplayModal">@((MarkupString)_iconSpan) @Text</button>
} }
} }
@code { @code {
private bool _visible = false; private bool _visible = false;
private string _permissions = string.Empty;
private bool _editmode = false; private bool _editmode = false;
private bool _authorized = false; private bool _authorized = false;
private string _iconSpan = string.Empty; private string _iconSpan = string.Empty;
@ -59,6 +60,9 @@
[Parameter] [Parameter]
public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel
[Parameter]
public string Permissions { get; set; } // optional - can be used to specify a permission string
[Parameter] [Parameter]
public string Class { get; set; } // optional public string Class { get; set; } // optional
@ -105,6 +109,7 @@
Header = Localize(nameof(Header), Header); Header = Localize(nameof(Header), Header);
Message = Localize(nameof(Message), Message); Message = Localize(nameof(Message), Message);
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions;
_authorized = IsAuthorized(); _authorized = IsAuthorized();
} }
@ -138,10 +143,10 @@
authorized = true; authorized = true;
break; break;
case SecurityAccessLevel.View: case SecurityAccessLevel.View:
authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.View, ModuleState.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.View, _permissions);
break; break;
case SecurityAccessLevel.Edit: case SecurityAccessLevel.Edit:
authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.Edit, ModuleState.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.Edit, _permissions);
break; break;
case SecurityAccessLevel.Admin: case SecurityAccessLevel.Admin:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin); authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin);

View File

@ -6,101 +6,118 @@
{ {
if (Disabled) if (Disabled)
{ {
<button class="@_classname" style="@_style" disabled>@((MarkupString)_iconSpan) @_text</button> <button type="button" class="@_classname" style="@_style" disabled>@((MarkupString)_iconSpan) @_text</button>
} }
else else
{ {
<NavLink class="@_classname" href="@_url" style="@_style">@((MarkupString)_iconSpan) @_text</NavLink> if (OnClick == null)
} {
<NavLink class="@_classname" href="@_url" style="@_style">@((MarkupString)_iconSpan) @_text</NavLink>
}
else
{
<button type="button" class="@_classname" style="@_style" onclick="@OnClick">@((MarkupString)_iconSpan) @_text</button>
}
}
} }
@code { @code {
private string _text = string.Empty; private string _text = string.Empty;
private string _url = string.Empty; private string _parameters = string.Empty;
private string _parameters = string.Empty; private string _url = string.Empty;
private string _classname = "btn btn-primary"; private string _permissions = string.Empty;
private string _style = string.Empty; private bool _editmode = false;
private bool _editmode = false; private bool _authorized = false;
private bool _authorized = false; private string _classname = "btn btn-primary";
private string _iconSpan = string.Empty; private string _style = string.Empty;
private string _iconSpan = string.Empty;
[Parameter] [Parameter]
public string Action { get; set; } // required public string Action { get; set; } // required
[Parameter] [Parameter]
public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel public string Text { get; set; } // optional - defaults to Action if not specified
[Parameter] [Parameter]
public string Text { get; set; } // optional - defaults to Action if not specified public string Parameters { get; set; } // optional - querystring parameters should be in the form of "id=x&name=y"
[Parameter] [Parameter]
public string Parameters { get; set; } // optional - querystring parameter should be in the form of "id=x&name=y" public int ModuleId { get; set; } = -1; // optional - allows the link to target a specific moduleid
[Parameter] [Parameter]
public string Class { get; set; } // optional - defaults to primary if not specified public Action OnClick { get; set; } = null; // optional - executes a method in the calling component
[Parameter] [Parameter]
public string Style { get; set; } // optional public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel
[Parameter] [Parameter]
public bool Disabled { get; set; } // optional public string Permissions { get; set; } // optional - can be used to specify a permission string
[Parameter] [Parameter]
public string EditMode { get; set; } // optional - specifies if an authorized user must be in edit mode to see the action - default is false. public bool Disabled { get; set; } // optional
[Parameter] [Parameter]
public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon public string EditMode { get; set; } // optional - specifies if an authorized user must be in edit mode to see the action - default is false.
[Parameter] [Parameter]
public bool IconOnly { get; set; } // optional - specifies only icon in link public string Class { get; set; } // optional - defaults to primary if not specified
protected override void OnParametersSet() [Parameter]
{ public string Style { get; set; } // optional
base.OnParametersSet();
_text = Action; [Parameter]
if (!string.IsNullOrEmpty(Text)) public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon
{
_text = Text;
}
if (IconOnly && !string.IsNullOrEmpty(IconName)) [Parameter]
{ public bool IconOnly { get; set; } // optional - specifies only icon in link
_text = string.Empty;
}
if (!string.IsNullOrEmpty(Parameters)) protected override void OnParametersSet()
{ {
_parameters = Parameters; base.OnParametersSet();
}
if (!string.IsNullOrEmpty(Class)) _text = Action;
{ if (!string.IsNullOrEmpty(Text))
_classname = Class; {
} _text = Text;
}
if (!string.IsNullOrEmpty(Style)) if (IconOnly && !string.IsNullOrEmpty(IconName))
{ {
_style = Style; _text = string.Empty;
} }
if (!string.IsNullOrEmpty(EditMode)) if (!string.IsNullOrEmpty(Parameters))
{ {
_editmode = bool.Parse(EditMode); _parameters = Parameters;
} }
if (!string.IsNullOrEmpty(IconName)) if (!string.IsNullOrEmpty(Class))
{ {
if (!IconName.Contains(" ")) _classname = Class;
{ }
IconName = "oi oi-" + IconName;
}
_iconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
} if (!string.IsNullOrEmpty(Style))
{
_style = Style;
}
_text = Localize(nameof(Text), _text); if (!string.IsNullOrEmpty(EditMode))
_url = EditUrl(Action, _parameters); {
_editmode = bool.Parse(EditMode);
}
if (!string.IsNullOrEmpty(IconName))
{
if (!IconName.Contains(" "))
{
IconName = "oi oi-" + IconName;
}
_iconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
}
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions;
_text = Localize(nameof(Text), _text);
_url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters);
_authorized = IsAuthorized(); _authorized = IsAuthorized();
} }
@ -136,10 +153,10 @@
authorized = true; authorized = true;
break; break;
case SecurityAccessLevel.View: case SecurityAccessLevel.View:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, ModuleState.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, _permissions);
break; break;
case SecurityAccessLevel.Edit: case SecurityAccessLevel.Edit:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, _permissions);
break; break;
case SecurityAccessLevel.Admin: case SecurityAccessLevel.Admin:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin); authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin);

View File

@ -52,7 +52,7 @@
<input type="file" id="@_fileinputid" name="file" accept="@_filter" /> <input type="file" id="@_fileinputid" name="file" accept="@_filter" />
} }
</div> </div>
<div class="col mt-2 text-center"> <div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button> <button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1) @if (ShowFiles && GetFileId() != -1)
{ {

View File

@ -25,8 +25,8 @@ else
[Parameter] [Parameter]
public string HelpText { get; set; } // optional - tooltip for this label public string HelpText { get; set; } // optional - tooltip for this label
private string _spanclass = "app-tooltip"; private string _spanclass;
private string _labelclass = "form-label"; private string _labelclass;
private string _helptext = string.Empty; private string _helptext = string.Empty;
protected override void OnParametersSet() protected override void OnParametersSet()
@ -36,11 +36,15 @@ else
if (!string.IsNullOrEmpty(HelpText)) if (!string.IsNullOrEmpty(HelpText))
{ {
_helptext = Localize(nameof(HelpText), HelpText); _helptext = Localize(nameof(HelpText), HelpText);
_spanclass += (!string.IsNullOrEmpty(Class)) ? " " + Class : ""; _labelclass = "form-label";
var spanclass = (!string.IsNullOrEmpty(Class)) ? " " + Class : "";
_spanclass = "app-tooltip" + spanclass;
} }
else else
{ {
_labelclass += (!string.IsNullOrEmpty(Class)) ? " " + Class : ""; var labelclass = (!string.IsNullOrEmpty(Class)) ? " " + Class : "";
_labelclass = "form-label" + labelclass;
} }
var text = Localize("Text", String.Empty); var text = Localize("Text", String.Empty);

View File

@ -13,6 +13,9 @@ namespace Oqtane.Modules.Controls
[Parameter] [Parameter]
public string ResourceKey { get; set; } public string ResourceKey { get; set; }
[Parameter]
public string ResourceType { get; set; }
protected bool IsLocalizable { get; private set; } protected bool IsLocalizable { get; private set; }
protected string Localize(string name) => _localizer?[name] ?? name; protected string Localize(string name) => _localizer?[name] ?? name;
@ -50,9 +53,14 @@ namespace Oqtane.Modules.Controls
{ {
IsLocalizable = false; IsLocalizable = false;
if (!String.IsNullOrEmpty(ResourceKey) && ModuleState?.ModuleType != null) if (string.IsNullOrEmpty(ResourceType))
{ {
var moduleType = Type.GetType(ModuleState.ModuleType); ResourceType = ModuleState?.ModuleType;
}
if (!String.IsNullOrEmpty(ResourceKey) && !string.IsNullOrEmpty(ResourceType))
{
var moduleType = Type.GetType(ResourceType);
if (moduleType != null) if (moduleType != null)
{ {
using (var scope = ServiceActivator.GetScope()) using (var scope = ServiceActivator.GetScope())

View File

@ -4,7 +4,7 @@
@if (ItemList != null) @if (ItemList != null)
{ {
@if (Toolbar == "Top" && _pages > 0 && Items.Count() > _maxItems) @if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? "" : " disabled")">
@ -54,61 +54,72 @@
} }
@if (Format == "Table" && Row != null) @if (Format == "Table" && Row != null)
{ {
<table class="@Class"> <div class="table-responsive">
<thead> <table class="@Class">
<tr>@Header</tr> <thead>
</thead> <tr class="@RowClass">@Header</tr>
<tbody> </thead>
@foreach (var item in ItemList) <tbody>
{ @foreach (var item in ItemList)
<tr>@Row(item)</tr> {
@if (Detail != null) <tr class="@RowClass">@Row(item)</tr>
{ @if (Detail != null)
<tr>@Detail(item)</tr> {
} <tr>@Detail(item)</tr>
} }
</tbody> }
</table> </tbody>
</table>
</div>
} }
@if (Format == "Grid" && Row != null) @if (Format == "Grid" && Row != null)
{ {
int count = 0; int count = 0;
if (ItemList != null) int rows = 0;
int cols = 0;
if (ItemList != null)
{ {
count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns; if (_columns == 0)
{
count = ItemList.Count();
rows = 1;
cols = count;
}
else
{
count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns;
rows = count / _columns;
cols = _columns;
}
} }
<div class="@Class"> <div class="@Class">
@if (Header != null) @for (int row = 0; row < rows; row++)
{ {
<div class="row"><div class="col">@Header</div></div> <div class="@RowClass">
} @for (int col = 0; col < cols; col++)
@for (int row = 0; row < (count / _columns); row++)
{
<div class="row">
@for (int col = 0; col < _columns; col++)
{ {
int index = (row * _columns) + col; int index = (row * _columns) + col;
if (index < ItemList.Count()) if (index < ItemList.Count())
{ {
<div class="col">@Row(ItemList.ElementAt(index))</div> <div class="@ColumnClass">@Row(ItemList.ElementAt(index))</div>
} }
else else
{ {
<div class="col">&nbsp;</div> <div>&nbsp;</div>
} }
} }
</div> </div>
} }
</div> </div>
} }
@if (Toolbar == "Bottom" && _pages > 0 && Items.Count() > _maxItems) @if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? "" : " disabled")"> <li class="page-item@((_page > _displayPages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
@ -135,7 +146,7 @@
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? "" : " disabled")"> <li class="page-item@((_endPage < _pages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
@ -145,7 +156,7 @@
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
<a class="page-link">Page @_page of @_pages</a> <a class="page-link" style="white-space: nowrap;">Page @_page of @_pages</a>
</li> </li>
</ul> </ul>
} }
@ -158,19 +169,19 @@
private int _displayPages = 5; private int _displayPages = 5;
private int _startPage = 0; private int _startPage = 0;
private int _endPage = 0; private int _endPage = 0;
private int _columns = 1; private int _columns = 0;
[Parameter] [Parameter]
public string Format { get; set; } // Table or Grid public string Format { get; set; } // Table or Grid
[Parameter] [Parameter]
public string Toolbar { get; set; } // Top or Bottom public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter] [Parameter]
public RenderFragment Header { get; set; } = null; public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
[Parameter] [Parameter]
public RenderFragment<TableItem> Row { get; set; } = null; public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter] [Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
@ -182,16 +193,25 @@
public string PageSize { get; set; } // number of items to display on a page public string PageSize { get; set; } // number of items to display on a page
[Parameter] [Parameter]
public string Columns { get; set; } // only applicable to Grid layouts public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter] [Parameter]
public string CurrentPage { get; set; } // optional property to set the initial page to display public string CurrentPage { get; set; } // sets the initial page to display
[Parameter] [Parameter]
public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter] [Parameter]
public string Class { get; set; } public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid
[Parameter]
public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid
[Parameter]
public string ColumnClass { get; set; } // class for column element - only applicable to Grid format
[Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes
private IEnumerable<TableItem> ItemList { get; set; } private IEnumerable<TableItem> ItemList { get; set; }
@ -215,11 +235,35 @@
} }
else else
{ {
Class = "container-fluid px-0"; Class = "container-fluid";
} }
} }
if (!string.IsNullOrEmpty(PageSize)) if (string.IsNullOrEmpty(RowClass))
{
if (Format == "Table")
{
RowClass = "";
}
else
{
RowClass = "row";
}
}
if (string.IsNullOrEmpty(ColumnClass))
{
if (Format == "Table")
{
ColumnClass = "";
}
else
{
ColumnClass = "col";
}
}
if (!string.IsNullOrEmpty(PageSize))
{ {
_maxItems = int.Parse(PageSize); _maxItems = int.Parse(PageSize);
} }
@ -266,6 +310,7 @@
{ {
_endPage = _pages; _endPage = _pages;
} }
OnPageChange?.Invoke(_page);
StateHasChanged(); StateHasChanged();
} }

View File

@ -100,160 +100,172 @@
} }
@code { @code {
private string _permissionnames = string.Empty; private string _permissionnames = string.Empty;
private List<Role> _roles; private List<Role> _roles;
private List<PermissionString> _permissions; private List<PermissionString> _permissions;
private List<User> _users = new List<User>(); private List<User> _users = new List<User>();
private string _username = string.Empty; private string _username = string.Empty;
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; } public string Permissions { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (string.IsNullOrEmpty(PermissionNames)) if (string.IsNullOrEmpty(PermissionNames))
{ {
_permissionnames = Shared.PermissionNames.View + "," + Shared.PermissionNames.Edit; _permissionnames = Shared.PermissionNames.View + "," + Shared.PermissionNames.Edit;
} }
else else
{ {
_permissionnames = PermissionNames; _permissionnames = PermissionNames;
} }
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId); _roles = await RoleService.GetRolesAsync(ModuleState.SiteId);
_roles.Insert(0, new Role { Name = RoleNames.Everyone }); _roles.Insert(0, new Role { Name = RoleNames.Everyone });
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles.Add(new Role { Name = RoleNames.Host });
}
_permissions = new List<PermissionString>(); _permissions = new List<PermissionString>();
foreach (string permissionname in _permissionnames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string permissionname in _permissionnames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
// initialize with admin role // initialize with admin role
_permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin }); _permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin });
} }
if (!string.IsNullOrEmpty(Permissions)) if (!string.IsNullOrEmpty(Permissions))
{ {
// populate permissions // populate permissions
foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions)) foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions))
{ {
if (_permissions.Find(item => item.PermissionName == permissionstring.PermissionName) != null) if (_permissions.Find(item => item.PermissionName == permissionstring.PermissionName) != null)
{ {
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionstring.PermissionName)].Permissions = permissionstring.Permissions; _permissions[_permissions.FindIndex(item => item.PermissionName == permissionstring.PermissionName)].Permissions = permissionstring.Permissions;
} }
if (permissionstring.Permissions.Contains("[")) if (permissionstring.Permissions.Contains("["))
{ {
foreach (string user in permissionstring.Permissions.Split(new char[] { '[' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string user in permissionstring.Permissions.Split(new char[] { '[' }, StringSplitOptions.RemoveEmptyEntries))
{ {
if (user.Contains("]")) if (user.Contains("]"))
{ {
var userid = int.Parse(user.Substring(0, user.IndexOf("]"))); var userid = int.Parse(user.Substring(0, user.IndexOf("]")));
if (_users.Where(item => item.UserId == userid).FirstOrDefault() == null) if (_users.Where(item => item.UserId == userid).FirstOrDefault() == null)
{ {
_users.Add(await UserService.GetUserAsync(userid, ModuleState.SiteId)); _users.Add(await UserService.GetUserAsync(userid, ModuleState.SiteId));
} }
} }
} }
} }
} }
} }
} }
private bool? GetPermissionValue(string permissions, string securityKey) private bool? GetPermissionValue(string permissions, string securityKey)
{ {
if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";")) if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";"))
{ {
return false; // deny permission return false; // deny permission
} }
else else
{ {
if ((";" + permissions + ";").Contains(";" + securityKey + ";")) if ((";" + permissions + ";").Contains(";" + securityKey + ";"))
{ {
return true; // grant permission return true; // grant permission
} }
else else
{ {
return null; // not specified return null; // not specified
} }
} }
} }
private bool GetPermissionDisabled(string roleName) private bool GetPermissionDisabled(string roleName)
=> roleName == RoleNames.Admin => (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) ? true : false;
? true
: false;
private async Task AddUser() private async Task AddUser()
{ {
if (_users.Where(item => item.Username == _username).FirstOrDefault() == null) if (_users.Where(item => item.Username == _username).FirstOrDefault() == null)
{ {
try try
{ {
var user = await UserService.GetUserAsync(_username, ModuleState.SiteId); var user = await UserService.GetUserAsync(_username, ModuleState.SiteId);
if (user != null) if (user != null)
{ {
_users.Add(user); _users.Add(user);
} }
} }
catch catch
{ {
_message = Localizer["Message.Username.DontExist"]; _message = Localizer["Message.Username.DontExist"];
} }
} }
_username = string.Empty; _username = string.Empty;
} }
private void PermissionChanged(bool? value, string permissionName, string securityId) private void PermissionChanged(bool? value, string permissionName, string securityId)
{ {
var selected = value; var selected = value;
var permission = _permissions.Find(item => item.PermissionName == permissionName); var permission = _permissions.Find(item => item.PermissionName == permissionName);
if (permission != null) if (permission != null)
{ {
var ids = permission.Permissions.Split(';').ToList(); var ids = permission.Permissions.Split(';').ToList();
ids.Remove(securityId); // remove grant permission ids.Remove(securityId); // remove grant permission
ids.Remove("!" + securityId); // remove deny permission ids.Remove("!" + securityId); // remove deny permission
switch (selected) switch (selected)
{ {
case true: case true:
ids.Add(securityId); // add grant permission ids.Add(securityId); // add grant permission
break; break;
case false: case false:
ids.Add("!" + securityId); // add deny permission ids.Add("!" + securityId); // add deny permission
break; break;
case null: case null:
break; // permission not specified break; // permission not specified
} }
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionName)].Permissions = string.Join(";", ids.ToArray()); _permissions[_permissions.FindIndex(item => item.PermissionName == permissionName)].Permissions = string.Join(";", ids.ToArray());
} }
} }
public string GetPermissions() public string GetPermissions()
{ {
ValidatePermissions(); ValidatePermissions();
return UserSecurity.SetPermissionStrings(_permissions); return UserSecurity.SetPermissionStrings(_permissions);
} }
private void ValidatePermissions() private void ValidatePermissions()
{ {
PermissionString permission; PermissionString permission;
for (int i = 0; i < _permissions.Count; i++) for (int i = 0; i < _permissions.Count; i++)
{ {
permission = _permissions[i]; permission = _permissions[i];
List<string> ids = permission.Permissions.Split(';').ToList(); List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Registered); // remove deny registered users ids.Remove("!" + RoleNames.Registered); // remove deny registered users
permission.Permissions = string.Join(";", ids.ToArray()); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
ids.Remove("!" + RoleNames.Admin); // remove deny administrators
ids.Remove("!" + RoleNames.Host); // remove deny host users
if (!ids.Contains(RoleNames.Host) && !ids.Contains(RoleNames.Admin))
{
// add administrators role if host user role is not assigned
ids.Add(RoleNames.Admin);
}
}
permission.Permissions = string.Join(";", ids.ToArray());
_permissions[i] = permission; _permissions[i] = permission;
} }
} }

View File

@ -71,11 +71,11 @@
</div> </div>
@if (ReadOnly) @if (ReadOnly)
{ {
<textarea class="form-control" placeholder="@Placeholder" @bind="@_content" rows="10" readonly></textarea> <textarea class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
} }
else else
{ {
<textarea class="form-control" placeholder="@Placeholder" @bind="@_content" rows="10"></textarea> <textarea class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
} }
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
@ -83,109 +83,121 @@
</div> </div>
@code { @code {
private ElementReference _editorElement; private ElementReference _editorElement;
private ElementReference _toolBar; private ElementReference _toolBar;
private bool _filemanagervisible = false; private bool _filemanagervisible = false;
private FileManager _fileManager; private FileManager _fileManager;
private string _content = string.Empty; private string _richhtml = string.Empty;
private string _original = string.Empty; private string _originalrichhtml = string.Empty;
private string _message = string.Empty; private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
[Parameter] [Parameter]
public string Content { get; set; } public string Content { get; set; }
[Parameter] [Parameter]
public bool ReadOnly { get; set; } = false; public bool ReadOnly { get; set; } = false;
[Parameter] [Parameter]
public string Placeholder { get; set; } = "Enter Your Content..."; public string Placeholder { get; set; } = "Enter Your Content...";
// parameters only applicable to rich text editor // parameters only applicable to rich text editor
[Parameter] [Parameter]
public RenderFragment ToolbarContent { get; set; } public RenderFragment ToolbarContent { get; set; }
[Parameter] [Parameter]
public string Theme { get; set; } = "snow"; public string Theme { get; set; } = "snow";
[Parameter] [Parameter]
public string DebugLevel { get; set; } = "info"; public string DebugLevel { get; set; } = "info";
[Parameter] [Parameter]
public bool AllowFileManagement { get; set; } = true; public bool AllowFileManagement { get; set; } = true;
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill1.3.6.min.js" }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js" }, new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js" },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js" } new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js" }
}; };
protected override void OnInitialized() protected override void OnParametersSet()
{ {
_content = Content; // raw HTML _richhtml = Content;
} _rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) await base.OnAfterRenderAsync(firstRender);
{
await base.OnAfterRenderAsync(firstRender);
var interop = new RichTextEditorInterop(JSRuntime); var interop = new RichTextEditorInterop(JSRuntime);
await interop.CreateEditor( if (firstRender)
_editorElement, {
_toolBar, await interop.CreateEditor(
ReadOnly, _editorElement,
Placeholder, _toolBar,
Theme, ReadOnly,
DebugLevel); Placeholder,
Theme,
DebugLevel);
await interop.LoadEditorContent(_editorElement, Content); await interop.LoadEditorContent(_editorElement, _richhtml);
_content = Content; // raw HTML // preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor)
_originalrichhtml = await interop.GetHtml(_editorElement);
}
else
{
await interop.LoadEditorContent(_editorElement, _richhtml);
}
}
// preserve a copy of the rich text content ( Quill sanitizes content so we need to retrieve it from the editor ) public void CloseFileManager()
_original = await interop.GetHtml(_editorElement); {
} _filemanagervisible = false;
} _message = string.Empty;
StateHasChanged();
}
public void CloseFileManager() public void RefreshRichText()
{ {
_filemanagervisible = false; _richhtml = _rawhtml;
_message = string.Empty; StateHasChanged();
StateHasChanged(); }
}
public async Task RefreshRichText() public async Task RefreshRawHtml()
{ {
var interop = new RichTextEditorInterop(JSRuntime); var interop = new RichTextEditorInterop(JSRuntime);
await interop.LoadEditorContent(_editorElement, _content); _rawhtml = await interop.GetHtml(_editorElement);
} StateHasChanged();
}
public async Task RefreshRawHtml() public async Task<string> GetHtml()
{ {
var interop = new RichTextEditorInterop(JSRuntime); // evaluate raw html content as first priority
_content = await interop.GetHtml(_editorElement); if (_rawhtml != _originalrawhtml)
StateHasChanged(); {
} return _rawhtml;
}
public async Task<string> GetHtml() else
{ {
// get rich text content // return rich text content if it has changed
var interop = new RichTextEditorInterop(JSRuntime); var interop = new RichTextEditorInterop(JSRuntime);
string content = await interop.GetHtml(_editorElement); var richhtml = await interop.GetHtml(_editorElement);
if (richhtml != _originalrichhtml)
if (_original != content) {
{ return richhtml;
// rich text content has changed - return it }
return content; else
} {
else // return original raw html content
{ return _originalrawhtml;
// return raw html content }
return _content; }
}
} }
public async Task InsertImage() public async Task InsertImage()
@ -211,23 +223,4 @@
} }
StateHasChanged(); StateHasChanged();
} }
// other rich text editor methods which can be used by developers
public async Task<string> GetText()
{
var interop = new RichTextEditorInterop(JSRuntime);
return await interop.GetText(_editorElement);
}
public async Task<string> GetContent()
{
var interop = new RichTextEditorInterop(JSRuntime);
return await interop.GetContent(_editorElement);
}
public async Task EnableEditor(bool mode)
{
var interop = new RichTextEditorInterop(JSRuntime);
await interop.EnableEditor(_editorElement, mode);
}
} }

View File

@ -40,6 +40,10 @@ else
{ {
Heading = Localize(nameof(Name), Name); Heading = Localize(nameof(Name), Name);
} }
else
{
Heading = Localize(nameof(Heading), Heading);
}
} }
public string DisplayHeading() public string DisplayHeading()

View File

@ -43,16 +43,12 @@
[Parameter] [Parameter]
public bool Refresh { get; set; } // optional - used in scenarios where TabPanels are added/removed dynamically within a parent form. ActiveTab may need to be reset as well when this property is used. public bool Refresh { get; set; } // optional - used in scenarios where TabPanels are added/removed dynamically within a parent form. ActiveTab may need to be reset as well when this property is used.
protected override void OnInitialized() protected override void OnParametersSet()
{ {
if (PageState.QueryString.ContainsKey("tab")) if (PageState.QueryString.ContainsKey("tab"))
{ {
ActiveTab = PageState.QueryString["tab"]; ActiveTab = PageState.QueryString["tab"];
} }
}
protected override void OnParametersSet()
{
if (_tabPanels == null || Refresh) if (_tabPanels == null || Refresh)
{ {
_tabPanels = new List<TabPanel>(); _tabPanels = new List<TabPanel>();

View File

@ -9,95 +9,186 @@
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_content != null) <TabStrip>
{ <TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit">
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" @ref="@RichTextEditorHtml"></RichTextEditor> @if (_content != null)
<button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button> {
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" @ref="@RichTextEditorHtml"></RichTextEditor>
@if (!string.IsNullOrEmpty(_content)) <br />
{ <button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button>
<br /> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br /> @if (!string.IsNullOrEmpty(_content))
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo> {
} <br />
} <br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
}
}
</TabPanel>
<TabPanel Name="Versions" Heading="Versions" ResourceKey="Versions">
<Pager Items="@_htmltexts">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["CreatedOn"]</th>
<th>@SharedLocalizer["CreatedBy"]</th>
</Header>
<Row>
<td><ActionLink Action="View" Security="SecurityAccessLevel.Edit" OnClick="@(async () => await View(context))" ResourceKey="View" /></td>
<td><ActionDialog Header="Restore Version" Message="@string.Format(Localizer["Confirm.Restore"], context.CreatedOn)" Action="Restore" Security="SecurityAccessLevel.Edit" Class="btn btn-success" OnClick="@(async () => await Restore(context))" ResourceKey="Restore" /></td>
<td><ActionDialog Header="Delete Version" Message="@string.Format(Localizer["Confirm.Delete"], context.CreatedOn)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" ResourceKey="Delete" /></td>
<td>@context.CreatedOn</td>
<td>@context.CreatedBy</td>
</Row>
</Pager>
@((MarkupString)_view)
</TabPanel>
</TabStrip>
@code { @code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Edit Html/Text"; public override string Title => "Edit Html/Text";
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill1.3.6.bubble.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill1.3.6.snow.css" } new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
}; };
private RichTextEditor RichTextEditorHtml; private RichTextEditor RichTextEditorHtml;
private bool _allowfilemanagement; private bool _allowfilemanagement;
private string _content = null; private string _content = null;
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 List<Models.HtmlText> _htmltexts;
private string _view = "";
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true")); _allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"));
await LoadContent();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.Load"], MessageType.Error);
}
}
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); private async Task LoadContent()
if (htmltext != null) {
{ var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
_content = htmltext.Content; if (htmltext != null)
_content = Utilities.FormatContent(_content, PageState.Alias, "render"); {
_createdby = htmltext.CreatedBy; _content = htmltext.Content;
_createdon = htmltext.CreatedOn; _content = Utilities.FormatContent(_content, PageState.Alias, "render");
_modifiedby = htmltext.ModifiedBy; _createdby = htmltext.CreatedBy;
_modifiedon = htmltext.ModifiedOn; _createdon = htmltext.CreatedOn;
} _modifiedby = htmltext.ModifiedBy;
else _modifiedon = htmltext.ModifiedOn;
{ }
_content = string.Empty; else
} {
} _content = string.Empty;
catch (Exception ex) }
{
await logger.LogError(ex, "An Error Occurred Loading Html/Text Content. " + ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private async Task SaveContent() _htmltexts = await HtmlTextService.GetHtmlTextsAsync(ModuleState.ModuleId);
{ _htmltexts = _htmltexts.OrderByDescending(item => item.CreatedOn).ToList();
string content = await RichTextEditorHtml.GetHtml();
content = Utilities.FormatContent(content, PageState.Alias, "save");
try _view = "";
{ }
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
htmltext.Content = content;
await HtmlTextService.UpdateHtmlTextAsync(htmltext);
}
else
{
htmltext = new HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
}
await logger.LogInformation("Html/Text Content Saved {HtmlText}", htmltext); private async Task SaveContent()
NavigationManager.NavigateTo(NavigateUrl()); {
} string content = await RichTextEditorHtml.GetHtml();
catch (Exception ex) content = Utilities.FormatContent(content, PageState.Alias, "save");
{
await logger.LogError(ex, "Error Saving Content {Error}", ex.Message); try
AddModuleMessage(Localizer["Error.Content.Save"], MessageType.Error); {
} var htmltext = new HtmlText();
} htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
await logger.LogInformation("Content Saved {HtmlText}", htmltext);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.Save"], MessageType.Error);
}
}
private async Task View(Models.HtmlText htmltext)
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
if (htmltext != null)
{
_view = htmltext.Content;
_view = Utilities.FormatContent(_view, PageState.Alias, "render");
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Viewing Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.View"], MessageType.Error);
}
}
private async Task Restore(Models.HtmlText htmltext)
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId);
if (htmltext != null)
{
var content = htmltext.Content;
htmltext = new HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
await logger.LogInformation("Content Restored {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Restoring Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.Restore"], MessageType.Error);
}
}
private async Task Delete(Models.HtmlText htmltext)
{
try
{
htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId);
if (htmltext != null)
{
await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId);
await logger.LogInformation("Content Deleted {HtmlText}", htmltext);
AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success);
await LoadContent();
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.Delete"], MessageType.Error);
}
}
} }

View File

@ -15,16 +15,16 @@
} }
@code { @code {
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }
}; };
private string content = ""; private string content = "";
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
try try
{ {
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null) if (htmltext != null)
@ -35,8 +35,8 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "An Error Occurred Loading Html/Text Content. " + ex.Message); await logger.LogError(ex, "Error Loading Content {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(Localizer["Error.Content.Load"], MessageType.Error);
} }
} }
} }

View File

@ -1,7 +1,9 @@
using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
namespace Oqtane.Modules.HtmlText namespace Oqtane.Modules.HtmlText
{ {
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule public class ModuleInfo : IModule
{ {
public ModuleDefinition ModuleDefinition => new ModuleDefinition public ModuleDefinition ModuleDefinition => new ModuleDefinition

View File

@ -1,34 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Modules.HtmlText.Services namespace Oqtane.Modules.HtmlText.Services
{ {
[PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")]
public class HtmlTextService : ServiceBase, IHtmlTextService, IService public class HtmlTextService : ServiceBase, IHtmlTextService, IService
{ {
public HtmlTextService(HttpClient http, SiteState siteState) : base(http, siteState) {} public HtmlTextService(HttpClient http, SiteState siteState) : base(http, siteState) {}
private string ApiUrl => CreateApiUrl("HtmlText"); private string ApiUrl => CreateApiUrl("HtmlText");
public async Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId)
{
return await GetJsonAsync<List<Models.HtmlText>>(CreateAuthorizationPolicyUrl($"{ApiUrl}?moduleid={moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int moduleId) public async Task<Models.HtmlText> GetHtmlTextAsync(int moduleId)
{ {
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId)); return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
} }
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
{
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task AddHtmlTextAsync(Models.HtmlText htmlText) public async Task AddHtmlTextAsync(Models.HtmlText htmlText)
{ {
await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText); await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText);
} }
public async Task UpdateHtmlTextAsync(Models.HtmlText htmlText) public async Task DeleteHtmlTextAsync(int htmlTextId, int moduleId)
{ {
await PutJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlText.HtmlTextId}", EntityNames.Module, htmlText.ModuleId), htmlText); await DeleteAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task DeleteHtmlTextAsync(int moduleId)
{
await DeleteAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
} }
} }
} }

View File

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

View File

@ -5,44 +5,45 @@
@inject IStringLocalizer<Settings> Localizer @inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<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="files" ResourceKey="Allow File Management" HelpText="Specify If Editors Can Upload and Select Files">Allow File Management: </Label> <Label Class="col-sm-3" For="files" ResourceKey="AllowFileManagement" ResourceType="@resourceType" HelpText="Specify If Editors Can Upload and Select Files">Allow File Management: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowfilemanagement"> <select id="files" class="form-select" @bind="@_allowfilemanagement">
<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>
</div>
@code { @code {
private string _allowfilemanagement; private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private string _allowfilemanagement;
protected override void OnInitialized() protected override void OnInitialized()
{
try
{ {
try _allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
{
_allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
}
catch (Exception ex)
{
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
}
} }
catch (Exception ex)
public async Task UpdateSettings()
{ {
try ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
{
var settings = ModuleState.Settings;
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
}
} }
} }
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -53,9 +53,9 @@ namespace Oqtane.Modules
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script)) if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{ {
var scripts = new List<object>(); var scripts = new List<object>();
foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script && item.Declaration != ResourceDeclaration.Global)) foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script))
{ {
scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "" }); scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
} }
if (scripts.Any()) if (scripts.Any())
{ {
@ -134,9 +134,19 @@ namespace Oqtane.Modules
return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment); return Utilities.ContentUrl(PageState.Alias, fileid, asAttachment);
} }
public string ImageUrl(int fileid, int width, int height)
{
return ImageUrl(fileid, width, height, "");
}
public string ImageUrl(int fileid, int width, int height, string mode) public string ImageUrl(int fileid, int width, int height, string mode)
{ {
return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode); return ImageUrl(fileid, width, height, mode, "", "", 0, false);
}
public string ImageUrl(int fileid, int width, int height, string mode, string position, string background, int rotate, bool recreate)
{
return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode, position, background, rotate, recreate);
} }
public virtual Dictionary<string, string> GetUrlParameters(string parametersTemplate = "") public virtual Dictionary<string, string> GetUrlParameters(string parametersTemplate = "")

View File

@ -5,7 +5,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion> <RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.0.0</Version> <Version>3.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -13,7 +13,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/v3.0.0</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0</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,11 +22,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup> </ItemGroup>
@ -34,11 +35,6 @@
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" /> <ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
<Folder Include="Resources\Themes\Controls\Theme\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<TrimmerRootAssembly Include="System.Runtime" /> <TrimmerRootAssembly Include="System.Runtime" />
<TrimmerRootAssembly Include="System.Linq.Parallel" /> <TrimmerRootAssembly Include="System.Linq.Parallel" />

View File

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Oqtane.Documentation;
using Oqtane.Modules; using Oqtane.Modules;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
@ -19,6 +20,7 @@ using Oqtane.UI;
namespace Oqtane.Client namespace Oqtane.Client
{ {
[PrivateApi("Mark Entry-Program as private, since it's not very useful in the public docs")]
public class Program public class Program
{ {
public static async Task Main(string[] args) public static async Task Main(string[] args)
@ -26,8 +28,9 @@ namespace Oqtane.Client
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
builder.Services.AddSingleton(httpClient); builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase
builder.Services.AddOptions(); builder.Services.AddOptions();
// Register localization services // Register localization services

View File

@ -130,12 +130,15 @@
<value>Install Now</value> <value>Install Now</value>
</data> </data>
<data name="Error.DbConfig.Load" xml:space="preserve"> <data name="Error.DbConfig.Load" xml:space="preserve">
<value>Error loading Database Configuration Control</value> <value>Error Loading Database Configuration Control</value>
</data> </data>
<data name="Message.Require.DbInfo" xml:space="preserve"> <data name="Message.Require.DbInfo" xml:space="preserve">
<value>Please Enter All Required Fields. Ensure Passwords Match And Are Greater Than 5 Characters In Length. Ensure Email Address Provided Is Valid.</value> <value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Message.Password.Invalid" xml:space="preserve">
<value>The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements.</value>
</data>
<data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value> <value>Please Register Me For Major Product Updates And Security Bulletins</value>
</data> </data>
<data name="Confirm.HelpText" xml:space="preserve"> <data name="Confirm.HelpText" xml:space="preserve">

View File

@ -159,4 +159,10 @@
<data name="Type" xml:space="preserve"> <data name="Type" xml:space="preserve">
<value>Type</value> <value>Type</value>
</data> </data>
<data name="DeleteFile.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="UploadFiles.Text" xml:space="preserve">
<value>Upload Files</value>
</data>
</root> </root>

View File

@ -186,4 +186,10 @@
<data name="NextExecution.Text" xml:space="preserve"> <data name="NextExecution.Text" xml:space="preserve">
<value>Next Execution: </value> <value>Next Execution: </value>
</data> </data>
<data name="Week(s)" xml:space="preserve">
<value>Week(s)</value>
</data>
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
</root> </root>

View File

@ -133,19 +133,16 @@
<value>Every</value> <value>Every</value>
</data> </data>
<data name="Minute" xml:space="preserve"> <data name="Minute" xml:space="preserve">
<value>Minute</value> <value>Minute(s)</value>
</data> </data>
<data name="Hour" xml:space="preserve"> <data name="Hour" xml:space="preserve">
<value>Hour</value> <value>Hour(s)</value>
</data> </data>
<data name="Day" xml:space="preserve"> <data name="Day" xml:space="preserve">
<value>Day</value> <value>Day(s)</value>
</data> </data>
<data name="Month" xml:space="preserve"> <data name="Month" xml:space="preserve">
<value>Month</value> <value>Month(s)</value>
</data>
<data name="s" xml:space="preserve">
<value>s</value>
</data> </data>
<data name="Error.Job.Delete" xml:space="preserve"> <data name="Error.Job.Delete" xml:space="preserve">
<value>Error Deleting Job</value> <value>Error Deleting Job</value>
@ -168,4 +165,31 @@
<data name="Stop" xml:space="preserve"> <data name="Stop" xml:space="preserve">
<value>Stop</value> <value>Stop</value>
</data> </data>
<data name="DeleteJob.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="EditJob.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="JobLog.Text" xml:space="preserve">
<value>Log</value>
</data>
<data name="Error.Job.Start" xml:space="preserve">
<value>An error occurred when starting the job</value>
</data>
<data name="Error.Job.Stop" xml:space="preserve">
<value>An error occurred when stopping the job</value>
</data>
<data name="Message.Job.Start" xml:space="preserve">
<value>The process responsible for executing this job has been started. The next execution will be based on the schedule criteria for the job.</value>
</data>
<data name="Message.Job.Stop" xml:space="preserve">
<value>The process responsible for executing this job has been stopped. In order to restart the process you will need to use the Start button or restart the application.</value>
</data>
<data name="Week" xml:space="preserve">
<value>Week(s)</value>
</data>
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
</root> </root>

View File

@ -153,4 +153,19 @@
<data name="Search.NoResults" xml:space="preserve"> <data name="Search.NoResults" xml:space="preserve">
<value>No Translations Match The Criteria Provided Or Package Service Is Disabled</value> <value>No Translations Match The Criteria Provided Or Package Service Is Disabled</value>
</data> </data>
<data name="Download.Heading" xml:space="preserve">
<value>Download</value>
</data>
<data name="LanguageUpload.HelpText" xml:space="preserve">
<value>Upload one or more translations. Once they are uploaded click Install to complete the installation.</value>
</data>
<data name="LanguageUpload.Text" xml:space="preserve">
<value>Upload Language</value>
</data>
<data name="Manage.Heading" xml:space="preserve">
<value>Manage</value>
</data>
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
</root> </root>

View File

@ -141,4 +141,7 @@
<data name="Default" xml:space="preserve"> <data name="Default" xml:space="preserve">
<value>Default</value> <value>Default</value>
</data> </data>
<data name="DeleteLanguage.Text" xml:space="preserve">
<value>Delete</value>
</data>
</root> </root>

View File

@ -117,9 +117,6 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="RememberMe" xml:space="preserve">
<value>Remember Me?</value>
</data>
<data name="ForgotPassword" xml:space="preserve"> <data name="ForgotPassword" xml:space="preserve">
<value>Forgot Password</value> <value>Forgot Password</value>
</data> </data>
@ -130,12 +127,78 @@
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value> <value>User Account Could Not Be Verified. 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 And User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email.</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 User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value>
</data> </data>
<data name="Message.Required.UserInfo" xml:space="preserve"> <data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide Your Username And Password</value> <value>Please Provide All Required Fields</value>
</data> </data>
<data name="Info.SignedIn" xml:space="preserve"> <data name="Info.SignedIn" xml:space="preserve">
<value>You Are Already Signed In</value> <value>You Are Already Signed In</value>
</data> </data>
<data name="Message.ForgotPassword" xml:space="preserve">
<value>Please Enter The Username Related To Your Account And Then Select The Forgot Password Option Again</value>
</data>
<data name="Message.ForgotUser" xml:space="preserve">
<value>Please Check The Email Address Associated To Your User Account For A Password Reset Notification</value>
</data>
<data name="Message.UserDoesNotExist" xml:space="preserve">
<value>User Does Not Exist</value>
</data>
<data name="Code.HelpText" xml:space="preserve">
<value>Please Enter The Secure Verification Code Which Was Sent To You By Email.</value>
</data>
<data name="Code.Placeholder" xml:space="preserve">
<value>Verification Code</value>
</data>
<data name="Code.Text" xml:space="preserve">
<value>Verification Code:</value>
</data>
<data name="Error.TwoFactor.Fail" xml:space="preserve">
<value>Verification Failed. Please Ensure You Entered The Code Exactly In The Form Provided In Your Email. If You Wish To Request A New Verification Code Please Select The Cancel Option And Sign In Again. </value>
</data>
<data name="Message.TwoFactor" xml:space="preserve">
<value>A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator.</value>
</data>
<data name="Password.HelpText" xml:space="preserve">
<value>Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time.</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="Password.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Remember.HelpText" xml:space="preserve">
<value>Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site</value>
</data>
<data name="Remember.Text" xml:space="preserve">
<value>Remember Me?</value>
</data>
<data name="Username.HelpText" xml:space="preserve">
<value>Please Enter The Username Related To Your Account</value>
</data>
<data name="Username.Placeholder" xml:space="preserve">
<value>Username</value>
</data>
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
<data name="Use" xml:space="preserve">
<value>Use</value>
</data>
<data name="Error.LoadLogin" xml:space="preserve">
<value>Error Loading Login</value>
</data>
<data name="Error.Login" xml:space="preserve">
<value>Error Performing Login</value>
</data>
<data name="Error.ResetPassword" xml:space="preserve">
<value>Error Resetting Password</value>
</data>
</root> </root>

View File

@ -189,4 +189,25 @@
<data name="Create" xml:space="preserve"> <data name="Create" xml:space="preserve">
<value>Create</value> <value>Create</value>
</data> </data>
<data name="LogDetails.Text" xml:space="preserve">
<value>Details</value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
<data name="Events.Heading" xml:space="preserve">
<value>Events</value>
</data>
<data name="Retention.HelpText" xml:space="preserve">
<value>Number of days of events to retain</value>
</data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
</root> </root>

View File

@ -138,4 +138,10 @@
<data name="Search.NoResults" xml:space="preserve"> <data name="Search.NoResults" xml:space="preserve">
<value>No Modules Match The Criteria Provided Or Package Service Is Disabled</value> <value>No Modules Match The Criteria Provided Or Package Service Is Disabled</value>
</data> </data>
<data name="Download.Heading" xml:space="preserve">
<value>Download</value>
</data>
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
</root> </root>

View File

@ -183,7 +183,16 @@
<data name="Runtimes.Text" xml:space="preserve"> <data name="Runtimes.Text" xml:space="preserve">
<value>Runtimes: </value> <value>Runtimes: </value>
</data> </data>
<data name="Definition.Name" xml:space="preserve"> <data name="Definition.Heading" xml:space="preserve">
<value>Definition</value> <value>Definition</value>
</data> </data>
<data name="Information.Heading" xml:space="preserve">
<value>Information</value>
</data>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="Information.Text" xml:space="preserve">
<value>Information</value>
</data>
</root> </root>

View File

@ -144,4 +144,10 @@
<data name="DeleteModule.Header" xml:space="preserve"> <data name="DeleteModule.Header" xml:space="preserve">
<value>Delete Module</value> <value>Delete Module</value>
</data> </data>
<data name="InUse" xml:space="preserve">
<value>In Use</value>
</data>
<data name="EditModule.Text" xml:space="preserve">
<value>Edit</value>
</data>
</root> </root>

View File

@ -231,4 +231,10 @@
<data name="Message.Page.Deleted" xml:space="preserve"> <data name="Message.Page.Deleted" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page in the Recycle Bin. Either recover the page or remove from the Recycle Bin and create it again.</value> <value>A page with path {0} already exists for the selected parent page in the Recycle Bin. Either recover the page or remove from the Recycle Bin and create it again.</value>
</data> </data>
<data name="Meta.HelpText" xml:space="preserve">
<value>Optionally enter meta tags (in exactly the form you want them to be included in the page output).</value>
</data>
<data name="Meta.Text" xml:space="preserve">
<value>Meta:</value>
</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
@ -210,7 +210,7 @@
<data name="Personalizable.Text" xml:space="preserve"> <data name="Personalizable.Text" xml:space="preserve">
<value>Personalizable? </value> <value>Personalizable? </value>
</data> </data>
<data name="Appearance.Name" xml:space="preserve"> <data name="Appearance.Heading" xml:space="preserve">
<value>Appearance</value> <value>Appearance</value>
</data> </data>
<data name="ThisLocation.Keep" xml:space="preserve"> <data name="ThisLocation.Keep" xml:space="preserve">
@ -231,4 +231,43 @@
<data name="Move.Text" xml:space="preserve"> <data name="Move.Text" xml:space="preserve">
<value>Move: </value> <value>Move: </value>
</data> </data>
<data name="ModuleDefinition" xml:space="preserve">
<value>Module</value>
</data>
<data name="ModuleTitle" xml:space="preserve">
<value>Title</value>
</data>
<data name="PageModules.Heading" xml:space="preserve">
<value>Modules</value>
</data>
<data name="ModuleSettings.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="DeleteModule.Header" xml:space="preserve">
<value>Delete Module</value>
</data>
<data name="DeleteModule.Message" xml:space="preserve">
<value>Are You Sure You Wish To Delete This Module?</value>
</data>
<data name="DeleteModule.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Permissions.Heading" xml:space="preserve">
<value>Permissions</value>
</data>
<data name="ThemeSettings.Heading" xml:space="preserve">
<value>Theme Settings</value>
</data>
<data name="Clickable.HelpText" xml:space="preserve">
<value>Select whether the link in the site navigation is enabled or disabled</value>
</data>
<data name="Clickable.Text" xml:space="preserve">
<value>Clickable?</value>
</data>
<data name="Meta.HelpText" xml:space="preserve">
<value>Optionally enter meta tags (in exactly the form you want them to be included in the page output).</value>
</data>
<data name="Meta.Text" xml:space="preserve">
<value>Meta:</value>
</data>
</root> </root>

View File

@ -132,4 +132,10 @@
<data name="Browse" xml:space="preserve"> <data name="Browse" xml:space="preserve">
<value>Browse</value> <value>Browse</value>
</data> </data>
<data name="DeletePage.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="EditPage.Text" xml:space="preserve">
<value>Edit</value>
</data>
</root> </root>

View File

@ -132,4 +132,10 @@
<data name="DeleteProfile.Header" xml:space="preserve"> <data name="DeleteProfile.Header" xml:space="preserve">
<value>Delete Profile</value> <value>Delete Profile</value>
</data> </data>
<data name="DeleteProfile.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="EditProfile.Text" xml:space="preserve">
<value>Edit</value>
</data>
</root> </root>

View File

@ -177,7 +177,10 @@
<data name="DeleteAllModules.Message" xml:space="preserve"> <data name="DeleteAllModules.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete All Modules?</value> <value>Are You Sure You Wish To Permanently Delete All Modules?</value>
</data> </data>
<data name="Pages.Name" xml:space="preserve"> <data name="Pages.Heading" xml:space="preserve">
<value>Pages</value> <value>Pages</value>
</data> </data>
<data name="Modules.Heading" xml:space="preserve">
<value>Modules</value>
</data>
</root> </root>

View File

@ -117,22 +117,37 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Password.Confirm" xml:space="preserve">
<value>Confirm Password:</value>
</data>
<data name="Message.Password.NoMatch" xml:space="preserve"> <data name="Message.Password.NoMatch" xml:space="preserve">
<value>Passwords Entered Do Not Match</value> <value>Passwords Entered Do Not Match</value>
</data> </data>
<data name="Message.Required.UserInfo" xml:space="preserve"> <data name="Message.Required.UserInfo" xml:space="preserve">
<value>You Must Provide A Username, Password, and Email Address</value> <value>You Must Provide The Password And Confirmation</value>
</data> </data>
<data name="Password.Reset" xml:space="preserve"> <data name="Password.Reset" xml:space="preserve">
<value>Reset Password</value> <value>Reset Password</value>
</data> </data>
<data name="Error.Password.ResetInfo" xml:space="preserve"> <data name="Error.Password.ResetInfo" xml:space="preserve">
<value>Error Resetting User Password. Please Ensure Password Meets Complexity Requirements.</value> <value>Error Resetting User Password. Please Ensure The Request To Reset Your Password Was Made Within The Past 24 Hours And The New Password Meets The Complexity Requirements.</value>
</data> </data>
<data name="Error.Password.Reset" xml:space="preserve"> <data name="Error.Password.Reset" xml:space="preserve">
<value>Error Resetting User Password</value> <value>Error Resetting User Password</value>
</data> </data>
<data name="Confirm.Text" xml:space="preserve">
<value>Confirm:</value>
</data>
<data name="Conform.HelpText" xml:space="preserve">
<value>Enter the password again. It must exactly match the password entered above.</value>
</data>
<data name="Password.HelpText" xml:space="preserve">
<value>The new password. It must satisfy complexity rules for the site.</value>
</data>
<data name="Password.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Username.HelpText" xml:space="preserve">
<value>Your username will be populated from the link you received in the password reset notification</value>
</data>
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
</root> </root>

View File

@ -129,4 +129,13 @@
<data name="DeleteRole.Header" xml:space="preserve"> <data name="DeleteRole.Header" xml:space="preserve">
<value>Delete Role</value> <value>Delete Role</value>
</data> </data>
<data name="DeleteRole.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Edit.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Users.Text" xml:space="preserve">
<value>Users</value>
</data>
</root> </root>

View File

@ -129,9 +129,6 @@
<data name="DefaultContainer.Text" xml:space="preserve"> <data name="DefaultContainer.Text" xml:space="preserve">
<value>Default Container: </value> <value>Default Container: </value>
</data> </data>
<data name="Appearance.Name" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="DefaultAdminContainer" xml:space="preserve"> <data name="DefaultAdminContainer" xml:space="preserve">
<value>Default Admin Container</value> <value>Default Admin Container</value>
</data> </data>
@ -145,7 +142,7 @@
<value>Site Settings Saved</value> <value>Site Settings Saved</value>
</data> </data>
<data name="Message.Aliases.Taken" xml:space="preserve"> <data name="Message.Aliases.Taken" xml:space="preserve">
<value>An Alias Specified Has Already Been Used For Another Site</value> <value>The Default Alias Has Not Been Specified Or An Alias Was Specified That Has Already Been Used For Another Site</value>
</data> </data>
<data name="Message.Required.SiteName" xml:space="preserve"> <data name="Message.Required.SiteName" xml:space="preserve">
<value>You Must Provide A Site Name, Alias, And Default Theme/Container</value> <value>You Must Provide A Site Name, Alias, And Default Theme/Container</value>
@ -171,9 +168,6 @@
<data name="Aliases.HelpText" xml:space="preserve"> <data name="Aliases.HelpText" xml:space="preserve">
<value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas.</value> <value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas.</value>
</data> </data>
<data name="AllowRegistration.HelpText" xml:space="preserve">
<value>Do you want the users to be able to register for an account on the site</value>
</data>
<data name="IsDeleted.HelpText" xml:space="preserve"> <data name="IsDeleted.HelpText" xml:space="preserve">
<value>Is this site deleted?</value> <value>Is this site deleted?</value>
</data> </data>
@ -225,11 +219,8 @@
<data name="Aliases.Text" xml:space="preserve"> <data name="Aliases.Text" xml:space="preserve">
<value>Aliases: </value> <value>Aliases: </value>
</data> </data>
<data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow User Registration? </value>
</data>
<data name="IsDeleted.Text" xml:space="preserve"> <data name="IsDeleted.Text" xml:space="preserve">
<value>Is Deleted? </value> <value>Deleted? </value>
</data> </data>
<data name="Logo.Text" xml:space="preserve"> <data name="Logo.Text" xml:space="preserve">
<value>Logo: </value> <value>Logo: </value>
@ -282,7 +273,7 @@
<data name="Theme.Select" xml:space="preserve"> <data name="Theme.Select" xml:space="preserve">
<value>Select Theme</value> <value>Select Theme</value>
</data> </data>
<data name="Hosting" xml:space="preserve"> <data name="Hosting.Heading" xml:space="preserve">
<value>Hosting Model</value> <value>Hosting Model</value>
</data> </data>
<data name="Prerender.HelpText" xml:space="preserve"> <data name="Prerender.HelpText" xml:space="preserve">
@ -300,4 +291,43 @@
<data name="Browse" xml:space="preserve"> <data name="Browse" xml:space="preserve">
<value>Browse</value> <value>Browse</value>
</data> </data>
<data name="TenantInformation.Heading" xml:space="preserve">
<value>Tenant Information</value>
</data>
<data name="PWASettings.Heading" xml:space="preserve">
<value>PWA Settings</value>
</data>
<data name="SMTPSettings.Heading" xml:space="preserve">
<value>SMTP Settings</value>
</data>
<data name="ConnectionString.Text" xml:space="preserve">
<value>Connection:</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>The connection information for the database</value>
</data>
<data name="Database.HelpText" xml:space="preserve">
<value>The database for the tenant</value>
</data>
<data name="DeleteSite.Text" xml:space="preserve">
<value>Delete Site</value>
</data>
<data name="DefaultAlias.HelpText" xml:space="preserve">
<value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value>
</data>
<data name="DefaultAlias.Text" xml:space="preserve">
<value>Default Alias: </value>
</data>
<data name="Aliases.Heading" xml:space="preserve">
<value>Aliases</value>
</data>
<data name="Hide" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Show" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -129,11 +129,11 @@
<data name="OSVersion.HelpText" xml:space="preserve"> <data name="OSVersion.HelpText" xml:space="preserve">
<value>Operating System Version</value> <value>Operating System Version</value>
</data> </data>
<data name="ServerPath.HelpText" xml:space="preserve"> <data name="ContentRootPath.HelpText" xml:space="preserve">
<value>Server Path</value> <value>Server Root Path</value>
</data> </data>
<data name="ServerTime.HelpText" xml:space="preserve"> <data name="ServerTime.HelpText" xml:space="preserve">
<value>Server Time</value> <value>Server Date/Time (in UTC)</value>
</data> </data>
<data name="FrameworkVersion.Text" xml:space="preserve"> <data name="FrameworkVersion.Text" xml:space="preserve">
<value>Framework Version: </value> <value>Framework Version: </value>
@ -144,11 +144,11 @@
<data name="OSVersion.Text" xml:space="preserve"> <data name="OSVersion.Text" xml:space="preserve">
<value>OS Version: </value> <value>OS Version: </value>
</data> </data>
<data name="ServerPath.Text" xml:space="preserve"> <data name="ContentRootPath.Text" xml:space="preserve">
<value>Server Path: </value> <value>Root Path: </value>
</data> </data>
<data name="ServerTime.Text" xml:space="preserve"> <data name="ServerTime.Text" xml:space="preserve">
<value>Server Time: </value> <value>Server Date/Time: </value>
</data> </data>
<data name="RestartApplication.Header" xml:space="preserve"> <data name="RestartApplication.Header" xml:space="preserve">
<value>Restart Application</value> <value>Restart Application</value>
@ -204,10 +204,10 @@
<data name="Critical" xml:space="preserve"> <data name="Critical" xml:space="preserve">
<value>Critical</value> <value>Critical</value>
</data> </data>
<data name="Info" xml:space="preserve"> <data name="Info.Heading" xml:space="preserve">
<value>Info</value> <value>Info</value>
</data> </data>
<data name="Options" xml:space="preserve"> <data name="Options.Heading" xml:space="preserve">
<value>Options</value> <value>Options</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Register" xml:space="preserve">
@ -228,4 +228,46 @@
<data name="Swagger.Text" xml:space="preserve"> <data name="Swagger.Text" xml:space="preserve">
<value>Swagger Enabled?</value> <value>Swagger Enabled?</value>
</data> </data>
<data name="RestartApplication.Text" xml:space="preserve">
<value>Restart Application</value>
</data>
<data name="None" xml:space="preserve">
<value>None</value>
</data>
<data name="NotificationLevel.HelpText" xml:space="preserve">
<value>The Minimum Logging Level For Which Notifications Should Be Sent To Host Users.</value>
</data>
<data name="NotificationLevel.Text" xml:space="preserve">
<value>Notification Level:</value>
</data>
<data name="IPAddress.HelpText" xml:space="preserve">
<value>Server IP Address</value>
</data>
<data name="IPAddress.Text" xml:space="preserve">
<value>IP Address:</value>
</data>
<data name="MachineName.HelpText" xml:space="preserve">
<value>Server Machine Name</value>
</data>
<data name="MachineName.Text" xml:space="preserve">
<value>Machine Name:</value>
</data>
<data name="TickCount.HelpText" xml:space="preserve">
<value>Amount Of Time The Service Has Been Available And Operational</value>
</data>
<data name="TickCount.Text" xml:space="preserve">
<value>Service Uptime:</value>
</data>
<data name="WebRootPath.HelpText" xml:space="preserve">
<value>Server Web Root Path</value>
</data>
<data name="WebRootPath.Text" xml:space="preserve">
<value>Web Path:</value>
</data>
<data name="WorkingSet.HelpText" xml:space="preserve">
<value>Memory Allocation Of Service (in MB)</value>
</data>
<data name="WorkingSet.Text" xml:space="preserve">
<value>Memory Allocation:</value>
</data>
</root> </root>

View File

@ -138,4 +138,10 @@
<data name="DeleteTheme.Header" xml:space="preserve"> <data name="DeleteTheme.Header" xml:space="preserve">
<value>Delete Theme</value> <value>Delete Theme</value>
</data> </data>
<data name="CreateTheme.Text" xml:space="preserve">
<value>Create Theme</value>
</data>
<data name="ViewTheme.Text" xml:space="preserve">
<value>View</value>
</data>
</root> </root>

View File

@ -135,4 +135,10 @@
<data name="Success.Framework.Download" xml:space="preserve"> <data name="Success.Framework.Download" xml:space="preserve">
<value>Framework Downloaded Successfully... Please Select Upgrade To Complete the Process</value> <value>Framework Downloaded Successfully... Please Select Upgrade To Complete the Process</value>
</data> </data>
<data name="Download.Heading" xml:space="preserve">
<value>Download</value>
</data>
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
</root> </root>

View File

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

View File

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

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Confirm.DeleteUrlMapping" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="AddUrlMapping.Text" xml:space="preserve">
<value>Add Url Mapping</value>
</data>
<data name="DeleteUrlMapping.Header" xml:space="preserve">
<value>Delete Url Mapping</value>
</data>
<data name="Requested" xml:space="preserve">
<value>Requested</value>
</data>
<data name="Requests" xml:space="preserve">
<value>Requests</value>
</data>
<data name="Mapped" xml:space="preserve">
<value>Mapped Urls</value>
</data>
<data name="Broken" xml:space="preserve">
<value>Broken Urls</value>
</data>
<data name="CaptureBrokenUrls.HelpText" xml:space="preserve">
<value>Specify if broken Urls should be captured automatically and saved in Url Mappings</value>
</data>
<data name="CaptureBrokenUrls.Text" xml:space="preserve">
<value>Capture Broken Urls?</value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
<data name="Urls.Heading" xml:space="preserve">
<value>Urls</value>
</data>
<data name="Url" xml:space="preserve">
<value>Url</value>
</data>
</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
@ -169,10 +169,10 @@
<value>Identity</value> <value>Identity</value>
</data> </data>
<data name="Confirm.HelpText" xml:space="preserve"> <data name="Confirm.HelpText" xml:space="preserve">
<value>If you are changing your password you must enter it again to confirm it matches</value> <value>If you are changing your password you must enter it again to confirm it matches the value entered above</value>
</data> </data>
<data name="Confirm.Text" xml:space="preserve"> <data name="Confirm.Text" xml:space="preserve">
<value>Confirm Password:</value> <value>Confirmation:</value>
</data> </data>
<data name="DisplayName.HelpText" xml:space="preserve"> <data name="DisplayName.HelpText" xml:space="preserve">
<value>Your full name</value> <value>Your full name</value>
@ -204,4 +204,10 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="TwoFactor.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value>
</data>
</root> </root>

View File

@ -177,4 +177,10 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="Identity.Heading" xml:space="preserve">
<value>Identity</value>
</data>
<data name="Profile.Heading" xml:space="preserve">
<value>Profile</value>
</data>
</root> </root>

View File

@ -126,4 +126,244 @@
<data name="DeleteUser.Header" xml:space="preserve"> <data name="DeleteUser.Header" xml:space="preserve">
<value>Delete User</value> <value>Delete User</value>
</data> </data>
<data name="AllowRegistration.HelpText" xml:space="preserve">
<value>Do you want anonymous visitors to be able to register for an account on the site</value>
</data>
<data name="AllowRegistration.Text" xml:space="preserve">
<value>Allow Registration? </value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
<data name="Users.Heading" xml:space="preserve">
<value>Users</value>
</data>
<data name="DeleteUser.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="EditUser.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Roles.Text" xml:space="preserve">
<value>Roles</value>
</data>
<data name="LockoutDuration.HelpText" xml:space="preserve">
<value>The number of minutes a user should be locked out</value>
</data>
<data name="LockoutDuration.Text" xml:space="preserve">
<value>Lockout Duration:</value>
</data>
<data name="MaximumFailures.HelpText" xml:space="preserve">
<value>The maximum number of sign in attempts before a user is locked out</value>
</data>
<data name="MaximumFailures.Text" xml:space="preserve">
<value>Maximum Failures:</value>
</data>
<data name="RequireDigit.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain a digit</value>
</data>
<data name="RequireDigit.Text" xml:space="preserve">
<value>Require Digit?</value>
</data>
<data name="RequiredLength.HelpText" xml:space="preserve">
<value>The minimum length for a password</value>
</data>
<data name="RequiredLength.Text" xml:space="preserve">
<value>Minimum Length:</value>
</data>
<data name="RequireLower.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain a lower case character</value>
</data>
<data name="RequireLower.Text" xml:space="preserve">
<value>Require Lowercase?</value>
</data>
<data name="RequirePunctuation.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain a non-alphanumeric character (ie. punctuation)</value>
</data>
<data name="RequirePunctuation.Text" xml:space="preserve">
<value>Require Punctuation?</value>
</data>
<data name="RequireUpper.HelpText" xml:space="preserve">
<value>Indicate if passwords must contain an upper case character</value>
</data>
<data name="RequireUpper.Text" xml:space="preserve">
<value>Require Uppercase?</value>
</data>
<data name="Success.UpdateConfig.Restart" xml:space="preserve">
<value>Configuration Updated. Please Select Restart Application For These Changes To Be Activated.</value>
</data>
<data name="UniqueCharacters.HelpText" xml:space="preserve">
<value>The minimum number of unique characters which a password must contain</value>
</data>
<data name="UniqueCharacters.Text" xml:space="preserve">
<value>Unique Characters:</value>
</data>
<data name="AllowSiteLogin.HelpText" xml:space="preserve">
<value>Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site.</value>
</data>
<data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Login?</value>
</data>
<data name="Authority.HelpText" xml:space="preserve">
<value>The Authority Url or Issuer Url associated with the OpenID Connect provider</value>
</data>
<data name="Authority.Text" xml:space="preserve">
<value>Authority:</value>
</data>
<data name="AuthorizationUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Authorization Code</value>
</data>
<data name="AuthorizationUrl.Text" xml:space="preserve">
<value>Authorization Url:</value>
</data>
<data name="ClientID.HelpText" xml:space="preserve">
<value>The Client ID from the provider</value>
</data>
<data name="ClientID.Text" xml:space="preserve">
<value>Client ID:</value>
</data>
<data name="ClientSecret.HelpText" xml:space="preserve">
<value>The Client Secret from the provider</value>
</data>
<data name="ClientSecret.Text" xml:space="preserve">
<value>Client Secret:</value>
</data>
<data name="CreateUsers.HelpText" xml:space="preserve">
<value>Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login.</value>
</data>
<data name="CreateUsers.Text" xml:space="preserve">
<value>Create New Users?</value>
</data>
<data name="DomainFilter.HelpText" xml:space="preserve">
<value>Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses.</value>
</data>
<data name="DomainFilter.Text" xml:space="preserve">
<value>Domain Filter:</value>
</data>
<data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The type name for the email address claim provided by the provider</value>
</data>
<data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim Type:</value>
</data>
<data name="ExternalLoginSettings.Heading" xml:space="preserve">
<value>External Login Settings</value>
</data>
<data name="LockoutSettings.Heading" xml:space="preserve">
<value>Lockout Settings</value>
</data>
<data name="MetadataUrl.HelpText" xml:space="preserve">
<value>The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration)</value>
</data>
<data name="MetadataUrl.Text" xml:space="preserve">
<value>Metadata Url:</value>
</data>
<data name="PasswordSettings.Heading" xml:space="preserve">
<value>Password Settings</value>
</data>
<data name="PKCE.HelpText" xml:space="preserve">
<value>Indicate if the provider supports Proof Key for Code Exchange (PKCE)</value>
</data>
<data name="PKCE.Text" xml:space="preserve">
<value>Use PKCE?</value>
</data>
<data name="ProviderName.HelpText" xml:space="preserve">
<value>The external login provider name which will be displayed on the login page</value>
</data>
<data name="ProviderName.Text" xml:space="preserve">
<value>Provider Name:</value>
</data>
<data name="ProviderType.HelpText" xml:space="preserve">
<value>Select the external login provider type</value>
</data>
<data name="ProviderType.Text" xml:space="preserve">
<value>Provider Type:</value>
</data>
<data name="RedirectUrl.HelpText" xml:space="preserve">
<value>The Redirect Url (or Callback Url) which usually needs to be registered with the provider</value>
</data>
<data name="RedirectUrl.Text" xml:space="preserve">
<value>Redirect Url:</value>
</data>
<data name="Scopes.HelpText" xml:space="preserve">
<value>A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default.</value>
</data>
<data name="Scopes.Text" xml:space="preserve">
<value>Scopes:</value>
</data>
<data name="TokenUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Auth Token</value>
</data>
<data name="TokenUrl.Text" xml:space="preserve">
<value>Token Url:</value>
</data>
<data name="UserInfoUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address.</value>
</data>
<data name="UserInfoUrl.Text" xml:space="preserve">
<value>User Info Url:</value>
</data>
<data name="Audience.HelpText" xml:space="preserve">
<value>Optionally provide the audience for the token</value>
</data>
<data name="Audience.Text" xml:space="preserve">
<value>Audience:</value>
</data>
<data name="UserSettings.Heading" xml:space="preserve">
<value>User Settings</value>
</data>
<data name="CookieType.HelpText" xml:space="preserve">
<value>Cookies are usually managed per domain. However you can also choose to have distinct cookies for each site (this option is only applicable to micro-sites).</value>
</data>
<data name="CookieType.Text" xml:space="preserve">
<value>Login Cookie Type:</value>
</data>
<data name="CreateToken" xml:space="preserve">
<value>Create Token</value>
</data>
<data name="Issuer.HelpText" xml:space="preserve">
<value>Optionally provide the issuer of the token</value>
</data>
<data name="Issuer.Text" xml:space="preserve">
<value>Issuer:</value>
</data>
<data name="Lifetime.HelpText" xml:space="preserve">
<value>The number of minutes for which a token should be valid</value>
</data>
<data name="Lifetime.Text" xml:space="preserve">
<value>Lifetime:</value>
</data>
<data name="Secret.HelpText" xml:space="preserve">
<value>If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated.</value>
</data>
<data name="Secret.Text" xml:space="preserve">
<value>Secret:</value>
</data>
<data name="Token.HelpText" xml:space="preserve">
<value>Select the Create Token button to generate a long-lived access token (valid for 1 year). Be sure to store this token in a safe location as you will not be able to access it in the future.</value>
</data>
<data name="Token.Text" xml:space="preserve">
<value>Token:</value>
</data>
<data name="TokenSettings.Heading" xml:space="preserve">
<value>Token Settings</value>
</data>
<data name="TwoFactor.HelpText" xml:space="preserve">
<value>Do you want to allow users to use two factor authentication? Note that the Notification Job in Scheduled Jobs needs to be enabled and your SMTP options need to be configured in Site Settings for this option to work properly.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Allow Two Factor?</value>
</data>
<data name="Hide" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Show" xml:space="preserve">
<value>Show</value>
</data>
</root> </root>

View File

@ -177,4 +177,7 @@
<data name="Expiry" xml:space="preserve"> <data name="Expiry" xml:space="preserve">
<value>Expiry</value> <value>Expiry</value>
</data> </data>
<data name="DeleteUserRole.Text" xml:space="preserve">
<value>Delete</value>
</data>
</root> </root>

View File

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

View File

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Url" xml:space="preserve">
<value>Url</value>
</data>
<data name="Visited" xml:space="preserve">
<value>Visited</value>
</data>
<data name="Visits" xml:space="preserve">
<value>Visits</value>
</data>
<data name="AllVisitors" xml:space="preserve">
<value>All Visitors</value>
</data>
<data name="PastDay" xml:space="preserve">
<value>Past Day</value>
</data>
<data name="PastMonth" xml:space="preserve">
<value>Past Month</value>
</data>
<data name="PastWeek" xml:space="preserve">
<value>Past Week</value>
</data>
<data name="UsersOnly" xml:space="preserve">
<value>Users Only</value>
</data>
<data name="Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
<data name="Settings.Heading" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
<data name="Visitors.Heading" xml:space="preserve">
<value>Visitors</value>
</data>
<data name="Tracking.HelpText" xml:space="preserve">
<value>Specify if visitor tracking is enabled</value>
</data>
<data name="Tracking.Text" xml:space="preserve">
<value>Tracking Enabled?</value>
</data>
<data name="IP" xml:space="preserve">
<value>IP</value>
</data>
<data name="User" xml:space="preserve">
<value>User</value>
</data>
<data name="Created" xml:space="preserve">
<value>Created</value>
</data>
<data name="Details.Text" xml:space="preserve">
<value>Details</value>
</data>
<data name="Filter.HelpText" xml:space="preserve">
<value>Comma delimited list of terms which may exist in IP addresses, user agents, or languages identifying visitors which should not be tracked</value>
</data>
<data name="Filter.Text" xml:space="preserve">
<value>Filter:</value>
</data>
<data name="Retention.HelpText" xml:space="preserve">
<value>Number of days of visitor activity to retain</value>
</data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="Correlation.HelpText" xml:space="preserve">
<value>Indicate if new visitors to this site should be correlated based on their IP Address</value>
</data>
<data name="Correlation.Text" xml:space="preserve">
<value>Correlate Visitors?</value>
</data>
</root>

View File

@ -117,7 +117,52 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Confirm.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete The {0} Version?</value>
</data>
<data name="Confirm.Restore" xml:space="preserve">
<value>Are You Sure You Wish To Restore The {0} Version?</value>
</data>
<data name="CreatedBy" xml:space="preserve">
<value>Created By</value>
</data>
<data name="CreatedOn" xml:space="preserve">
<value>Created On</value>
</data>
<data name="Delete.Header" xml:space="preserve">
<value>Delete Version</value>
</data>
<data name="Delete.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="Error.Content.Delete" xml:space="preserve">
<value>Error Deleting Version</value>
</data>
<data name="Error.Content.Load" xml:space="preserve">
<value>An Error Occurred Loading Content</value>
</data>
<data name="Error.Content.Restore" xml:space="preserve">
<value>Error Restoring Version</value>
</data>
<data name="Error.Content.Save" xml:space="preserve"> <data name="Error.Content.Save" xml:space="preserve">
<value>Error Saving Content</value> <value>An Error Occurred Saving Content</value>
</data>
<data name="Error.Content.View" xml:space="preserve">
<value>Error Viewing Version</value>
</data>
<data name="Message.Content.Deleted" xml:space="preserve">
<value>Version Deleted</value>
</data>
<data name="Message.Content.Restored" xml:space="preserve">
<value>Version Restored</value>
</data>
<data name="Restore.Header" xml:space="preserve">
<value>Restore Version</value>
</data>
<data name="Restore.Text" xml:space="preserve">
<value>Restore</value>
</data>
<data name="View.Text" xml:space="preserve">
<value>View</value>
</data> </data>
</root> </root>

View File

@ -1,65 +1,65 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, 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="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="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value> <value>[base64 mime encoded serialized .NET Framework object]</value>
</data> </data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <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> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
@ -120,4 +120,7 @@
<data name="Edit.Action" xml:space="preserve"> <data name="Edit.Action" xml:space="preserve">
<value>Edit</value> <value>Edit</value>
</data> </data>
<data name="Error.Content.Load" xml:space="preserve">
<value>An Error Occurred Loading Content</value>
</data>
</root> </root>

View File

@ -259,7 +259,7 @@
<value>Upgrade</value> <value>Upgrade</value>
</data> </data>
<data name="Username" xml:space="preserve"> <data name="Username" xml:space="preserve">
<value>Username:</value> <value>Username</value>
</data> </data>
<data name="Version" xml:space="preserve"> <data name="Version" xml:space="preserve">
<value>Version</value> <value>Version</value>
@ -318,4 +318,7 @@
<data name="BlazorWebAssembly" xml:space="preserve"> <data name="BlazorWebAssembly" xml:space="preserve">
<value>Blazor WebAssembly</value> <value>Blazor WebAssembly</value>
</data> </data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
</root> </root>

View File

@ -177,4 +177,13 @@
<data name="System.Update" xml:space="preserve"> <data name="System.Update" xml:space="preserve">
<value>Check For System Updates</value> <value>Check For System Updates</value>
</data> </data>
<data name="Visibility" xml:space="preserve">
<value>Visibility:</value>
</data>
<data name="VisibilityEdit" xml:space="preserve">
<value>Page Editors Only</value>
</data>
<data name="VisibilityView" xml:space="preserve">
<value>Same As Page</value>
</data>
</root> </root>

View File

@ -1,65 +1,65 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, 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="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="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value> <value>[base64 mime encoded serialized .NET Framework object]</value>
</data> </data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <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> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <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:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>

View File

@ -121,7 +121,7 @@
<value>Specify if a Footer pane should always be displayed in a fixed location at the bottom of the page.</value> <value>Specify if a Footer pane should always be displayed in a fixed location at the bottom of the page.</value>
</data> </data>
<data name="Footer.Text" xml:space="preserve"> <data name="Footer.Text" xml:space="preserve">
<value>Display Footer?</value> <value>Display Fixed Footer?</value>
</data> </data>
<data name="Login.HelpText" xml:space="preserve"> <data name="Login.HelpText" xml:space="preserve">
<value>Specify if a Login option should be displayed, Note that this option does not prevent the login page from being accessible via a direct url.</value> <value>Specify if a Login option should be displayed, Note that this option does not prevent the login page from being accessible via a direct url.</value>

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
@ -123,4 +123,7 @@
<data name="Error.Module.InvalidType" xml:space="preserve"> <data name="Error.Module.InvalidType" xml:space="preserve">
<value>Module Type Is Invalid For {0}</value> <value>Module Type Is Invalid For {0}</value>
</data> </data>
<data name="Error.Module.Exception" xml:space="preserve">
<value>An Unexpected Error Has Occurred</value>
</data>
</root> </root>

View File

@ -21,7 +21,9 @@ namespace Oqtane.Services
_siteState = siteState; _siteState = siteState;
} }
private string ApiUrl => CreateApiUrl("Installation", null, ControllerRoutes.ApiRoute); // tenant agnostic private string ApiUrl => (_siteState.Alias == null)
? CreateApiUrl("Installation", null, ControllerRoutes.ApiRoute) // tenant agnostic needed for initial installation
: CreateApiUrl("Installation", _siteState.Alias);
public async Task<Installation> IsInstalled() public async Task<Installation> IsInstalled()
{ {
@ -48,14 +50,5 @@ namespace Oqtane.Services
{ {
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true); await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
} }
public void SetAntiForgeryTokenHeader(string antiforgerytokenvalue)
{
if (!string.IsNullOrEmpty(antiforgerytokenvalue))
{
AddRequestHeader(Constants.AntiForgeryTokenHeaderName, antiforgerytokenvalue);
}
}
} }
} }

View File

@ -42,10 +42,5 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task RegisterAsync(string email); Task RegisterAsync(string email);
/// <summary>
/// Sets the antiforgerytoken header so that it is included on all HttpClient calls for the lifetime of the app
/// </summary>
/// <returns></returns>
void SetAntiForgeryTokenHeader(string antiforgerytokenvalue);
} }
} }

View File

@ -1,4 +1,5 @@
using Oqtane.Models; using Oqtane.Models;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -37,6 +38,12 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateSiteSettingsAsync(Dictionary<string, string> siteSettings, int siteId); Task UpdateSiteSettingsAsync(Dictionary<string, string> siteSettings, int siteId);
/// <summary>
/// Clears site option cache
/// </summary>
/// <returns></returns>
Task ClearSiteSettingsCacheAsync();
/// <summary> /// <summary>
/// Returns a key-value dictionary of all page settings for the given page /// Returns a key-value dictionary of all page settings for the given page
/// </summary> /// </summary>
@ -70,7 +77,7 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Returns a key-value dictionary of all module settings for the given module /// Returns a key-value dictionary of all module settings for the given module
/// </summary> /// </summary>
/// <param name="pageId"></param> /// <param name="moduleId"></param>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetModuleSettingsAsync(int moduleId); Task<Dictionary<string, string>> GetModuleSettingsAsync(int moduleId);
@ -82,6 +89,21 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateModuleSettingsAsync(Dictionary<string, string> moduleSettings, int moduleId); Task UpdateModuleSettingsAsync(Dictionary<string, string> moduleSettings, int moduleId);
/// <summary>
/// Returns a key-value dictionary of all module settings for the given module
/// </summary>
/// <param name="moduleDefinitionId"></param>
/// <returns></returns>
Task<Dictionary<string, string>> GetModuleDefinitionSettingsAsync(int moduleDefinitionId);
/// <summary>
/// Updates a module setting
/// </summary>
/// <param name="moduleDefinitionSettings"></param>
/// <param name="moduleDefinitionId"></param>
/// <returns></returns>
Task UpdateModuleDefinitionSettingsAsync(Dictionary<string, string> moduleDefinitionSettings, int moduleDefinitionId);
/// <summary> /// <summary>
/// Returns a key-value dictionary of all user settings for the given user /// Returns a key-value dictionary of all user settings for the given user
/// </summary> /// </summary>
@ -113,6 +135,19 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateFolderSettingsAsync(Dictionary<string, string> folderSettings, int folderId); Task UpdateFolderSettingsAsync(Dictionary<string, string> folderSettings, int folderId);
/// <summary>
/// Returns a key-value dictionary of all tenant settings
/// </summary>
/// <returns></returns>
Task<Dictionary<string, string>> GetHostSettingsAsync();
/// <summary>
/// Updates a host setting
/// </summary>
/// <param name="hostSettings"></param>
/// <returns></returns>
Task UpdateHostSettingsAsync(Dictionary<string, string> hostSettings);
/// <summary> /// <summary>
/// Returns a key-value dictionary of all settings for the given entityName /// Returns a key-value dictionary of all settings for the given entityName
/// </summary> /// </summary>
@ -120,7 +155,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId); Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId);
/// <summary> /// <summary>
/// Updates settings for a given entityName and Id /// Updates settings for a given entityName and Id
/// </summary> /// </summary>
@ -135,8 +169,7 @@ namespace Oqtane.Services
/// </summary> /// </summary>
/// <param name="settingId"></param> /// <param name="settingId"></param>
/// <returns></returns> /// <returns></returns>
Task<Setting> GetSettingAsync(int settingId); Task<Setting> GetSettingAsync(string entityName, int settingId);
/// <summary> /// <summary>
/// Creates a new setting /// Creates a new setting
@ -157,7 +190,7 @@ namespace Oqtane.Services
/// </summary> /// </summary>
/// <param name="settingId"></param> /// <param name="settingId"></param>
/// <returns></returns> /// <returns></returns>
Task DeleteSettingAsync(int settingId); Task DeleteSettingAsync(string entityName, int settingId);
/// <summary> /// <summary>
/// Gets the value of the given settingName (key) from the given key-value dictionary /// Gets the value of the given settingName (key) from the given key-value dictionary
@ -177,8 +210,16 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue); Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue);
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPublic); 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> settings1, Dictionary<string, string> settings2);
}
[Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)]
Task<Setting> GetSettingAsync(int settingId);
[Obsolete("DeleteSettingAsync(int settingId) is deprecated. Use DeleteSettingAsync(string entityName, int settingId) instead.", false)]
Task DeleteSettingAsync(int settingId);
}
} }

View File

@ -9,16 +9,34 @@ namespace Oqtane.Services
public interface ISystemService public interface ISystemService
{ {
/// <summary> /// <summary>
/// returns a key-value directory with the current system information (os-version, clr-version, etc.) /// returns a key-value directory with the current system configuration information
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetSystemInfoAsync(); Task<Dictionary<string, object>> GetSystemInfoAsync();
/// <summary>
/// returns a key-value directory with the current system information - "environment" or "configuration"
/// </summary>
/// <returns></returns>
Task<Dictionary<string, object>> GetSystemInfoAsync(string type);
/// <summary>
/// returns a config value
/// </summary>
/// <returns></returns>
Task<object> GetSystemInfoAsync(string settingKey, object defaultValue);
/// <summary> /// <summary>
/// Updates system information /// Updates system information
/// </summary> /// </summary>
/// <param name="settings"></param> /// <param name="settings"></param>
/// <returns></returns> /// <returns></returns>
Task UpdateSystemInfoAsync(Dictionary<string, string> settings); Task UpdateSystemInfoAsync(Dictionary<string, object> settings);
/// <summary>
/// updates a config value
/// </summary>
/// <returns></returns>
Task UpdateSystemInfoAsync(string settingKey, object settingValue);
} }
} }

View File

@ -0,0 +1,56 @@
using Oqtane.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="UrlMapping"/>s on a <see cref="Site"/>
/// </summary>
public interface IUrlMappingService
{
/// <summary>
/// Get all <see cref="UrlMapping"/>s of this <see cref="Site"/>.
///
/// </summary>
/// <param name="siteId">ID-reference of a <see cref="Site"/></param>
/// <returns></returns>
Task<List<UrlMapping>> GetUrlMappingsAsync(int siteId, bool isMapped);
/// <summary>
/// Get one specific <see cref="UrlMapping"/>
/// </summary>
/// <param name="urlMappingId">ID-reference of a <see cref="UrlMapping"/></param>
/// <returns></returns>
Task<UrlMapping> GetUrlMappingAsync(int urlMappingId);
/// <summary>
/// Get one specific <see cref="UrlMapping"/>
/// </summary>
/// <param name="siteId">ID-reference of a <see cref="Site"/></param>
/// <param name="url">A url</param>
/// <returns></returns>
Task<UrlMapping> GetUrlMappingAsync(int siteId, string url);
/// <summary>
/// Add / save a new <see cref="UrlMapping"/> to the database.
/// </summary>
/// <param name="urlMapping"></param>
/// <returns></returns>
Task<UrlMapping> AddUrlMappingAsync(UrlMapping urlMapping);
/// <summary>
/// Update a <see cref="UrlMapping"/> in the database.
/// </summary>
/// <param name="urlMapping"></param>
/// <returns></returns>
Task<UrlMapping> UpdateUrlMappingAsync(UrlMapping urlMapping);
/// <summary>
/// Delete a <see cref="UrlMapping"/> in the database.
/// </summary>
/// <param name="urlMappingId">ID-reference of a <see cref="UrlMapping"/></param>
/// <returns></returns>
Task DeleteUrlMappingAsync(int urlMappingId);
}
}

View File

@ -88,5 +88,26 @@ namespace Oqtane.Services
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<User> ResetPasswordAsync(User user, string token); Task<User> ResetPasswordAsync(User user, string token);
/// <summary>
/// Verify the two factor verification code <see cref="User"/>
/// </summary>
/// <param name="user"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<User> VerifyTwoFactorAsync(User user, string token);
/// <summary>
/// Validate a users password against the password policy
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
Task<bool> ValidatePasswordAsync(string password);
/// <summary>
/// Get token for current user
/// </summary>
/// <returns></returns>
Task<string> GetTokenAsync();
} }
} }

View File

@ -0,0 +1,29 @@
using Oqtane.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to manage <see cref="Visitor"/>s on a <see cref="Site"/>
/// </summary>
public interface IVisitorService
{
/// <summary>
/// Get all <see cref="Visitor"/>s of this <see cref="Site"/>.
///
/// </summary>
/// <param name="siteId">ID-reference of a <see cref="Site"/></param>
/// <returns></returns>
Task<List<Visitor>> GetVisitorsAsync(int siteId, DateTime fromDate);
/// <summary>
/// Get a specific <see cref="Visitor"/> of this <see cref="Site"/>.
///
/// </summary>
/// <param name="visitorId">ID-reference of a <see cref="Visitor"/></param>
/// <returns></returns>
Task<Visitor> GetVisitorAsync(int visitorId);
}
}

View File

@ -0,0 +1,147 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Oqtane.Shared;
namespace Oqtane.Services
{
public class RemoteServiceBase
{
private readonly SiteState _siteState;
private readonly IHttpClientFactory _httpClientFactory;
protected RemoteServiceBase(IHttpClientFactory httpClientFactory, SiteState siteState)
{
_siteState = siteState;
_httpClientFactory = httpClientFactory;
}
private HttpClient GetHttpClient()
{
var httpClient = _httpClientFactory.CreateClient("Remote");
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && _siteState != null && !string.IsNullOrEmpty(_siteState.AuthorizationToken))
{
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + _siteState.AuthorizationToken);
}
return httpClient;
}
protected async Task GetAsync(string uri)
{
var response = await GetHttpClient().GetAsync(uri);
CheckResponse(response);
}
protected async Task<string> GetStringAsync(string uri)
{
try
{
return await GetHttpClient().GetStringAsync(uri);
}
catch (Exception e)
{
Console.WriteLine(e);
}
return default;
}
protected async Task<byte[]> GetByteArrayAsync(string uri)
{
try
{
return await GetHttpClient().GetByteArrayAsync(uri);
}
catch (Exception e)
{
Console.WriteLine(e);
}
return default;
}
protected async Task<T> GetJsonAsync<T>(string uri)
{
var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
{
return await response.Content.ReadFromJsonAsync<T>();
}
return default;
}
protected async Task PutAsync(string uri)
{
var response = await GetHttpClient().PutAsync(uri, null);
CheckResponse(response);
}
protected async Task<T> PutJsonAsync<T>(string uri, T value)
{
return await PutJsonAsync<T, T>(uri, value);
}
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
{
var response = await GetHttpClient().PutAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadFromJsonAsync<TResult>();
return result;
}
return default;
}
protected async Task PostAsync(string uri)
{
var response = await GetHttpClient().PostAsync(uri, null);
CheckResponse(response);
}
protected async Task<T> PostJsonAsync<T>(string uri, T value)
{
return await PostJsonAsync<T, T>(uri, value);
}
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
{
var response = await GetHttpClient().PostAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content))
{
var result = await response.Content.ReadFromJsonAsync<TResult>();
return result;
}
return default;
}
protected async Task DeleteAsync(string uri)
{
var response = await GetHttpClient().DeleteAsync(uri);
CheckResponse(response);
}
private bool CheckResponse(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode) return true;
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
{
Console.WriteLine($"Request: {response.RequestMessage.RequestUri}");
Console.WriteLine($"Response status: {response.StatusCode} {response.ReasonPhrase}");
}
return false;
}
private static bool ValidateJsonContent(HttpContent content)
{
var mediaType = content?.Headers.ContentType?.MediaType;
return mediaType != null && mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -5,24 +5,31 @@ using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Services namespace Oqtane.Services
{ {
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class ServiceBase public class ServiceBase
{ {
private readonly HttpClient _http; private readonly HttpClient _httpClient;
private readonly SiteState _siteState; private readonly SiteState _siteState;
protected ServiceBase(HttpClient client, SiteState siteState) protected ServiceBase(HttpClient httpClient, SiteState siteState)
{ {
_http = client; _httpClient = httpClient;
_siteState = siteState; _siteState = siteState;
} }
private HttpClient GetHttpClient()
{
if (!_httpClient.DefaultRequestHeaders.Contains(Constants.AntiForgeryTokenHeaderName) && _siteState != null && !string.IsNullOrEmpty(_siteState.AntiForgeryToken))
{
_httpClient.DefaultRequestHeaders.Add(Constants.AntiForgeryTokenHeaderName, _siteState.AntiForgeryToken);
}
return _httpClient;
}
// should be used with new constructor // should be used with new constructor
public string CreateApiUrl(string serviceName) public string CreateApiUrl(string serviceName)
{ {
@ -95,24 +102,9 @@ namespace Oqtane.Services
} }
} }
// note that HttpClient is registered as a Scoped(shared) service and therefore you should not use request headers whose value can vary over the lifetime of the service
protected void AddRequestHeader(string name, string value)
{
RemoveRequestHeader(name);
_http.DefaultRequestHeaders.Add(name, value);
}
protected void RemoveRequestHeader(string name)
{
if (_http.DefaultRequestHeaders.Contains(name))
{
_http.DefaultRequestHeaders.Remove(name);
}
}
protected async Task GetAsync(string uri) protected async Task GetAsync(string uri)
{ {
var response = await _http.GetAsync(uri); var response = await GetHttpClient().GetAsync(uri);
CheckResponse(response); CheckResponse(response);
} }
@ -120,7 +112,7 @@ namespace Oqtane.Services
{ {
try try
{ {
return await _http.GetStringAsync(uri); return await GetHttpClient().GetStringAsync(uri);
} }
catch (Exception e) catch (Exception e)
{ {
@ -134,7 +126,7 @@ namespace Oqtane.Services
{ {
try try
{ {
return await _http.GetByteArrayAsync(uri); return await GetHttpClient().GetByteArrayAsync(uri);
} }
catch (Exception e) catch (Exception e)
{ {
@ -146,7 +138,7 @@ namespace Oqtane.Services
protected async Task<T> GetJsonAsync<T>(string uri) protected async Task<T> GetJsonAsync<T>(string uri)
{ {
var response = await _http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); var response = await GetHttpClient().GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
if (CheckResponse(response) && ValidateJsonContent(response.Content)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
return await response.Content.ReadFromJsonAsync<T>(); return await response.Content.ReadFromJsonAsync<T>();
@ -157,7 +149,7 @@ namespace Oqtane.Services
protected async Task PutAsync(string uri) protected async Task PutAsync(string uri)
{ {
var response = await _http.PutAsync(uri, null); var response = await GetHttpClient().PutAsync(uri, null);
CheckResponse(response); CheckResponse(response);
} }
@ -168,7 +160,7 @@ namespace Oqtane.Services
protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value) protected async Task<TResult> PutJsonAsync<TValue, TResult>(string uri, TValue value)
{ {
var response = await _http.PutAsJsonAsync(uri, value); var response = await GetHttpClient().PutAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
var result = await response.Content.ReadFromJsonAsync<TResult>(); var result = await response.Content.ReadFromJsonAsync<TResult>();
@ -179,7 +171,7 @@ namespace Oqtane.Services
protected async Task PostAsync(string uri) protected async Task PostAsync(string uri)
{ {
var response = await _http.PostAsync(uri, null); var response = await GetHttpClient().PostAsync(uri, null);
CheckResponse(response); CheckResponse(response);
} }
@ -190,7 +182,7 @@ namespace Oqtane.Services
protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value) protected async Task<TResult> PostJsonAsync<TValue, TResult>(string uri, TValue value)
{ {
var response = await _http.PostAsJsonAsync(uri, value); var response = await GetHttpClient().PostAsJsonAsync(uri, value);
if (CheckResponse(response) && ValidateJsonContent(response.Content)) if (CheckResponse(response) && ValidateJsonContent(response.Content))
{ {
var result = await response.Content.ReadFromJsonAsync<TResult>(); var result = await response.Content.ReadFromJsonAsync<TResult>();
@ -202,7 +194,7 @@ namespace Oqtane.Services
protected async Task DeleteAsync(string uri) protected async Task DeleteAsync(string uri)
{ {
var response = await _http.DeleteAsync(uri); var response = await GetHttpClient().DeleteAsync(uri);
CheckResponse(response); CheckResponse(response);
} }
@ -228,7 +220,7 @@ namespace Oqtane.Services
// This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead. // This constructor is obsolete. Use ServiceBase(HttpClient client, SiteState siteState) : base(http, siteState) {} instead.
protected ServiceBase(HttpClient client) protected ServiceBase(HttpClient client)
{ {
_http = client; _httpClient = client;
} }
[Obsolete("This method is obsolete. Use CreateApiUrl(string serviceName, Alias alias) in conjunction with ControllerRoutes.ApiRoute in Controllers instead.", false)] [Obsolete("This method is obsolete. Use CreateApiUrl(string serviceName, Alias alias) in conjunction with ControllerRoutes.ApiRoute in Controllers instead.", false)]
@ -240,7 +232,7 @@ namespace Oqtane.Services
[Obsolete("This property of ServiceBase is deprecated. Cross tenant service calls are not supported.", false)] [Obsolete("This property of ServiceBase is deprecated. Cross tenant service calls are not supported.", false)]
public Alias Alias { get; set; } public Alias Alias { get; set; }
[Obsolete("This method is obsolete. Use CreateApiUrl(string entityName, int entityId) instead.", false)] [Obsolete("This method is obsolete. Use CreateAuthorizationPolicyUrl(string url, string entityName, int entityId) where entityName = EntityNames.Module instead.", false)]
public string CreateAuthorizationPolicyUrl(string url, int entityId) public string CreateAuthorizationPolicyUrl(string url, int entityId)
{ {
return url + ((url.Contains("?")) ? "&" : "?") + "entityid=" + entityId.ToString(); return url + ((url.Contains("?")) ? "&" : "?") + "entityid=" + entityId.ToString();

View File

@ -21,6 +21,7 @@ namespace Oqtane.Services
} }
private string Apiurl => CreateApiUrl("Setting", _siteState.Alias); private string Apiurl => CreateApiUrl("Setting", _siteState.Alias);
public async Task<Dictionary<string, string>> GetTenantSettingsAsync() public async Task<Dictionary<string, string>> GetTenantSettingsAsync()
{ {
return await GetSettingsAsync(EntityNames.Tenant, -1); return await GetSettingsAsync(EntityNames.Tenant, -1);
@ -41,6 +42,11 @@ namespace Oqtane.Services
await UpdateSettingsAsync(siteSettings, EntityNames.Site, siteId); await UpdateSettingsAsync(siteSettings, EntityNames.Site, siteId);
} }
public async Task ClearSiteSettingsCacheAsync()
{
await DeleteAsync($"{Apiurl}/clear");
}
public async Task<Dictionary<string, string>> GetPageSettingsAsync(int pageId) public async Task<Dictionary<string, string>> GetPageSettingsAsync(int pageId)
{ {
return await GetSettingsAsync(EntityNames.Page, pageId); return await GetSettingsAsync(EntityNames.Page, pageId);
@ -71,6 +77,16 @@ namespace Oqtane.Services
await UpdateSettingsAsync(moduleSettings, EntityNames.Module, moduleId); await UpdateSettingsAsync(moduleSettings, EntityNames.Module, moduleId);
} }
public async Task<Dictionary<string, string>> GetModuleDefinitionSettingsAsync(int moduleDefinitionId)
{
return await GetSettingsAsync(EntityNames.ModuleDefinition, moduleDefinitionId);
}
public async Task UpdateModuleDefinitionSettingsAsync(Dictionary<string, string> moduleDefinitionSettings, int moduleDefinitionId)
{
await UpdateSettingsAsync(moduleDefinitionSettings, EntityNames.ModuleDefinition, moduleDefinitionId);
}
public async Task<Dictionary<string, string>> GetUserSettingsAsync(int userId) public async Task<Dictionary<string, string>> GetUserSettingsAsync(int userId)
{ {
return await GetSettingsAsync(EntityNames.User, userId); return await GetSettingsAsync(EntityNames.User, userId);
@ -91,6 +107,16 @@ namespace Oqtane.Services
await UpdateSettingsAsync(folderSettings, EntityNames.Folder, folderId); await UpdateSettingsAsync(folderSettings, EntityNames.Folder, folderId);
} }
public async Task<Dictionary<string, string>> GetHostSettingsAsync()
{
return await GetSettingsAsync(EntityNames.Host, -1);
}
public async Task UpdateHostSettingsAsync(Dictionary<string, string> hostSettings)
{
await UpdateSettingsAsync(hostSettings, EntityNames.Host, -1);
}
public async Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId) public async Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId)
{ {
var dictionary = new Dictionary<string, string>(); var dictionary = new Dictionary<string, string>();
@ -110,14 +136,21 @@ namespace Oqtane.Services
foreach (KeyValuePair<string, string> kvp in settings) foreach (KeyValuePair<string, string> kvp in settings)
{ {
string value = kvp.Value; string value = kvp.Value;
bool ispublic = false; bool modified = false;
bool isprivate = false;
// manage settings modified with SetSetting method
if (value.StartsWith("[Private]"))
{
modified = true;
isprivate = true;
value = value.Substring(9);
}
if (value.StartsWith("[Public]")) if (value.StartsWith("[Public]"))
{ {
if (entityName == EntityNames.Site) modified = true;
{ isprivate = false;
ispublic = true; value = value.Substring(8);
}
value = value.Substring(8); // remove [Public]
} }
Setting setting = settingsList.FirstOrDefault(item => item.SettingName.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase)); Setting setting = settingsList.FirstOrDefault(item => item.SettingName.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase));
@ -128,25 +161,24 @@ namespace Oqtane.Services
setting.EntityId = entityId; setting.EntityId = entityId;
setting.SettingName = kvp.Key; setting.SettingName = kvp.Key;
setting.SettingValue = value; setting.SettingValue = value;
setting.IsPublic = ispublic; setting.IsPrivate = isprivate;
setting = await AddSettingAsync(setting); setting = await AddSettingAsync(setting);
} }
else else
{ {
if (setting.SettingValue != kvp.Value) if (setting.SettingValue != value || (modified && setting.IsPrivate != isprivate))
{ {
setting.SettingValue = value; setting.SettingValue = value;
setting.IsPublic = ispublic; setting.IsPrivate = isprivate;
setting = await UpdateSettingAsync(setting); setting = await UpdateSettingAsync(setting);
} }
} }
} }
} }
public async Task<Setting> GetSettingAsync(string entityName, int settingId)
public async Task<Setting> GetSettingAsync(int settingId)
{ {
return await GetJsonAsync<Setting>($"{Apiurl}/{settingId}"); return await GetJsonAsync<Setting>($"{Apiurl}/{settingId}/{entityName}");
} }
public async Task<Setting> AddSettingAsync(Setting setting) public async Task<Setting> AddSettingAsync(Setting setting)
@ -159,9 +191,9 @@ namespace Oqtane.Services
return await PutJsonAsync<Setting>($"{Apiurl}/{setting.SettingId}", setting); return await PutJsonAsync<Setting>($"{Apiurl}/{setting.SettingId}", setting);
} }
public async Task DeleteSettingAsync(int settingId) public async Task DeleteSettingAsync(string entityName, int settingId)
{ {
await DeleteAsync($"{Apiurl}/{settingId}"); await DeleteAsync($"{Apiurl}/{settingId}/{entityName}");
} }
@ -180,13 +212,13 @@ namespace Oqtane.Services
return SetSetting(settings, settingName, settingValue, false); return SetSetting(settings, settingName, settingValue, false);
} }
public Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPublic) public Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPrivate)
{ {
if (settings == null) if (settings == null)
{ {
settings = new Dictionary<string, string>(); settings = new Dictionary<string, string>();
} }
settingValue = (isPublic) ? "[Public]" + settingValue : settingValue; settingValue = (isPrivate) ? "[Private]" + settingValue : "[Public]" + settingValue;
if (settings.ContainsKey(settingName)) if (settings.ContainsKey(settingName))
{ {
settings[settingName] = settingValue; settings[settingName] = settingValue;
@ -220,5 +252,19 @@ namespace Oqtane.Services
} }
return settings1; return settings1;
} }
[Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)]
public async Task<Setting> GetSettingAsync(int settingId)
{
return await GetJsonAsync<Setting>($"{Apiurl}/{settingId}/tenant");
}
[Obsolete("DeleteSettingAsync(int settingId) is deprecated. Use DeleteSettingAsync(string entityName, int settingId) instead.", false)]
public async Task DeleteSettingAsync(int settingId)
{
await DeleteAsync($"{Apiurl}/{settingId}/tenant");
}
} }
} }

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