Compare commits

...

1067 Commits

Author SHA1 Message Date
a16bc5db28 Merge pull request #2200 from oqtane/master
Merge pull request #2199 from oqtane/dev
2022-05-14 09:49:24 -04:00
51657338f5 Merge pull request #2199 from oqtane/dev
3.1.2 release
2022-05-14 09:48:59 -04:00
0fe3ea25af Merge pull request #2198 from sbwalker/dev
remove columns from main user management view  and migrate them to edit view
2022-05-13 17:00:32 -04:00
806daaf7c9 remove columns from main user management view and migrate them to edit view 2022-05-13 17:00:10 -04:00
21ff4a83b5 Merge pull request #2197 from sbwalker/dev
resolve login issue related to 'LoginOptions:TwoFactor' and order list of files alphabetically
2022-05-13 12:03:57 -04:00
ecc9aa40d7 resolve login issue related to 'LoginOptions:TwoFactor' and order list of files alphabetically 2022-05-13 12:03:34 -04:00
c34ca2a59b Merge pull request #2196 from sbwalker/dev
prepare for 3.1.2
2022-05-12 20:55:26 -04:00
dde7094fe3 prepare for 3.1.2 2022-05-12 20:55:11 -04:00
105afdfefc Merge pull request #2195 from sbwalker/dev
fix #2192 - Adding a new site fails
2022-05-12 20:42:20 -04:00
4c254a8686 fix #2192 - Adding a new site fails 2022-05-12 20:42:05 -04:00
33ca203e57 Merge pull request #2194 from sbwalker/dev
updated resource file
2022-05-12 13:56:01 -04:00
2ff4133cd4 updated resource file 2022-05-12 13:55:47 -04:00
49ad85713e Merge pull request #2193 from sbwalker/dev
add support for external login parameters and improve diagnostic messages related to claims
2022-05-12 13:52:06 -04:00
1978bf151f add support for external login parameters and improve diagnostic messages related to claims 2022-05-12 13:51:46 -04:00
506378de82 Merge pull request #2191 from sbwalker/dev
fix #2185 - alias auto registration including trailing slash
2022-05-10 08:03:55 -04:00
53ead7a03f fix #2185 - alias auto registration including trailing slash 2022-05-10 08:03:38 -04:00
ab979fd63c Merge pull request #2189 from sbwalker/dev
fix #2180 - Error in Module Creator if the template is not set
2022-05-09 11:35:17 -04:00
1e84a2238b fix #2180 - Error in Module Creator if the template is not set 2022-05-09 11:35:01 -04:00
5618adf86c Merge pull request #2187 from sbwalker/dev
fix #2182 - modifications to address MySQL compatibility issues
2022-05-08 22:14:54 -04:00
345b0bc95f fix #2182 - modifications to address MySQL compatibility issues 2022-05-08 22:14:26 -04:00
b1d6c35e99 Merge pull request #2183 from leigh-pointer/UserEmail
Args not in sink
2022-05-06 11:43:59 -04:00
d767f1a101 Args not in sink
The Display name and email address  not is the correct order!
2022-05-06 12:43:40 +02:00
c15f2b9a12 Merge pull request #2178 from leigh-pointer/UserEmail
Added the User Email field to the List
2022-05-05 17:14:08 -04:00
a21a53662b Update for real-estate
Removed Name
Removed Seconds from DateTime fields
Added Name to the Email link
2022-05-05 16:35:43 +02:00
ebb5340019 Merge pull request #2177 from leigh-pointer/NotIficationDateFormat
Updated the CreatedOn date format
2022-05-05 10:13:24 -04:00
6108bd214e Merge pull request #2179 from sbwalker/dev
fix #2176 - update LastIPAddress correctly during login
2022-05-05 09:57:26 -04:00
eed27e101a fix #2176 - update LastIPAddress correctly during login 2022-05-05 09:57:09 -04:00
2767680bed Added the User Email field to the List
Added the formatted email address of the user to the list view.
2022-05-05 13:25:35 +02:00
4080e30b6f Updated the CreatedOn date format
Updated the format to a more readable format of dd-MMM-yyyy
2022-05-05 13:07:09 +02:00
e89257be62 Merge pull request #2174 from sbwalker/dev
fix #2172 - File Upload issue caused by JS Interop not passing AntiForgery token in POST method
2022-05-04 17:15:10 -04:00
d3c40a7e8b fix #2172 - File Upload issue caused by JS Interop not passing AntiForgery token in POST methid 2022-05-04 17:14:45 -04:00
60657d5d25 Update README.md 2022-05-03 08:13:34 -04:00
71d5ae0e68 Merge pull request #2169 from oqtane/master
Merge pull request #2168 from oqtane/dev
2022-05-03 08:08:51 -04:00
46b8d202c7 Merge pull request #2168 from oqtane/dev
3.1.1 release
2022-05-03 08:08:34 -04:00
bc193a6470 Merge pull request #2167 from sbwalker/dev
remove custom module assets not part of framework
2022-05-03 07:54:30 -04:00
577528fa0a remove custom module assets not part of framework 2022-05-03 07:54:14 -04:00
35e78ea633 Merge pull request #2166 from sbwalker/dev
3.1.1 database providers, default module creator version to local install version
2022-05-02 17:08:56 -04:00
d5d4f85003 3.1.1 database providers, default module creator version to local install version 2022-05-02 17:08:29 -04:00
c6a49a6f5d Merge pull request #2164 from sbwalker/dev
enhance UserRole service with filtering and moved workload to server for better performance, improve error message details during installation
2022-04-29 21:39:36 -04:00
a3ff9373a2 enhance UserRole service with filtering and moved workload to server for better performance, improve error message details during installation 2022-04-29 21:39:11 -04:00
e6d2e74b17 Merge pull request #2162 from sbwalker/dev
refactor module upgrade logic, implement for themes and translations
2022-04-27 19:29:49 -04:00
e8464206e7 refactor module upgrade logic, implement for themes and translations 2022-04-27 19:29:29 -04:00
b6c4934123 Merge pull request #2161 from 2sic-forks/dev
Fix #2160 - upgrade module option
2022-04-27 15:03:46 -04:00
8ee83f738b fix# https://github.com/oqtane/oqtane.framework/issues/2160 2022-04-27 17:44:54 +02:00
c359300375 Merge pull request #2158 from sbwalker/dev
fix path on app-stylesheets
2022-04-26 16:34:08 -04:00
eb3361fa07 fix path on app-stylesheets 2022-04-26 16:33:50 -04:00
1b53da6749 Merge pull request #2155 from sbwalker/dev
external login improvements
2022-04-25 20:05:03 -04:00
c701895e29 external login improvements 2022-04-25 20:04:43 -04:00
e81222821e Merge pull request #2153 from sbwalker/dev
prepare for 3.1.1 release
2022-04-24 20:20:02 -04:00
cbca8c9e93 prepare for 3.1.1 release 2022-04-24 20:19:44 -04:00
92c4edacf2 Merge pull request #2152 from sbwalker/dev
completed antiforgery implementation, improved external login claim mapping, principal construction, and user experience
2022-04-22 17:54:49 -04:00
e4c648ee92 completed antiforgery implementation, improved external login claim mapping, principal construction, and user experience 2022-04-22 17:54:20 -04:00
69f6586aa9 Merge pull request #2149 from sbwalker/dev
Fix #2144 - install issue, Fix #2146 - move file issue, require verification of external login account linkage
2022-04-20 16:01:21 -04:00
391713b84d Fix #2144 - install issue, Fix #2146 - move file issue, require verification of external login account linkage 2022-04-20 16:00:58 -04:00
8c6a25e4b4 Merge pull request #2140 from sbwalker/dev
remove web.release.config as it causes installation issues in pure .net core environments (see #1957)
2022-04-15 09:23:14 -04:00
250701aff0 remove web.release.config as it causes installation issues in pure .net core environments (see #1957) 2022-04-15 09:22:51 -04:00
2dfd1bd5a2 Merge pull request #2139 from sbwalker/dev
removed method-level [ValidateAntiForgeryToken] attribute as it is now handled by global AutoValidateAntiforgeryTokenFilter, adjusted gitignore to improve filtering of Module and Theme folders in wwwroot and exclude all files in Oqtane.Server/Data
2022-04-15 08:02:04 -04:00
1c7380d4cf removed method-level [ValidateAntiForgeryToken] attribute as it is now handled by global AutoValidateAntiforgeryTokenFilter, adjusted gitignore to improve filtering of Module and Theme folders in wwwroot and exclude all files in Oqtane.Server/Data 2022-04-15 08:01:32 -04:00
68d9ac88b3 Merge pull request #2138 from sbwalker/dev
create separate API methods for tokens (short-lived) and personal access tokens (long-lived), include global antiforgery filter to mitigate XSRF when using cookie auth (ignored when using Jwt)
2022-04-14 19:42:24 -04:00
f6b3874668 create separate API methods for tokens (short-lived) and personal access tokens (long-lived), include global antiforgery filter to mitigate XSRF when using cookie auth (ignored when using Jwt) 2022-04-14 19:41:43 -04:00
c616878a64 Merge pull request #2134 from leigh-pointer/TogglePassword
User Areas to use the Toggle Password method
2022-04-14 08:58:59 -04:00
ad485a68ce Merge pull request #2136 from leigh-pointer/UsersCreatedOn
Added CreatedOn class "align-middle" to RowClass
2022-04-14 08:58:49 -04:00
2ebba3b8e7 Added CreatedOn class "align-middle" to RowClass 2022-04-14 14:52:34 +02:00
4117e6e1c5 Merge pull request #2135 from sbwalker/dev
Fix #2128 - site settings validation issue when logged in as Administrator (not Host)
2022-04-14 08:31:29 -04:00
423ee04879 Fix #2128 - site settings validation issue when logged in as Administrator (not Host) 2022-04-14 08:30:55 -04:00
1625e3ba6c User Areas to use the Toggle Password method
Updated the Components where the Password is required to allow toggle show / hide
2022-04-14 13:47:27 +02:00
5a71ab3c20 Merge pull request #2130 from leigh-pointer/LastLoggedIn
Update to User Management
2022-04-13 19:31:10 -04:00
b5833bf556 Merge pull request #2129 from leigh-pointer/NotificationDelAll
Allow the deletion of all Notifications
2022-04-13 19:28:37 -04:00
6c31765965 Merge pull request #2132 from sbwalker/dev
fix #2125 - cannot login using WebAssembly, remove granular 404 logging as it is already managed by url mapping, make IModule ReleaseVersions optional when using EF Core migrations
2022-04-13 19:27:35 -04:00
6dc1d42d90 fix #2125 - cannot login using WebAssembly, remove granular 404 logging as it is already managed by url mapping, make IModule ReleaseVersions optional when using EF Core migrations 2022-04-13 19:27:12 -04:00
e273a954e6 Update to User Management
Updated user management to display more of the User ; Last Login Last IP and Is Authenticated.
2022-04-13 15:24:28 +02:00
a602a942c4 Allow the deletion of all Notifications
Added button to delete all the notifications for the selected filter.
2022-04-13 14:33:30 +02:00
dd32b33621 Merge pull request #2124 from sbwalker/dev
minor improvements to security features, use ActivatorUtilities.CreateInstance with SiteMigration to enable simpler DI
2022-04-12 07:47:10 -04:00
355d0405f4 minor improvements to security features, use ActivatorUtilities.CreateInstance with SiteMigration to enable simpler DI 2022-04-12 07:46:43 -04:00
c1e1595d58 Update README.md 2022-04-11 08:30:24 -04:00
ef476ac9b3 Update README.md 2022-04-11 07:56:10 -04:00
a7aaa7b18b Merge pull request #2116 from sbwalker/dev
Fix #2111 - Adding user to Host role removes all other users roles
2022-04-05 17:11:29 -04:00
3abfbab5d1 Fix #2111 - Adding user to Host role removes all other users roles 2022-04-05 17:11:13 -04:00
b158474e21 Merge pull request #2113 from oqtane/master
Merge pull request #2112 from oqtane/dev
2022-04-05 08:51:40 -04:00
cfbcc41543 Merge pull request #2112 from oqtane/dev
3.1.0 release
2022-04-05 08:51:10 -04:00
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
b7bbfe2a46 Merge pull request #1789 from sbwalker/dev
updated database provider references
2021-11-11 09:12:49 -05:00
69783b0709 updated database provider references 2021-11-11 09:21:01 -05:00
614041d55e update Blazor theme with bootstrap bundle js 2021-11-11 07:50:24 -05:00
856f61c2ee Merge pull request #1787 from sbwalker/dev
update Blazor theme with bootstrap bundle js
2021-11-11 07:42:10 -05:00
b0b196a522 Merge pull request #1786 from leigh-pointer/ActionMenu
Fix for Action Menus not displaying. #1785
2021-11-11 07:38:25 -05:00
62f04d239f Fix for Action Menus not displaying. #1785
Dropdowns are built on a third party library, Popper, which provides dynamic positioning and viewport detection.  / bootstrap.bundle.js which contains Popper. Popper isn’t used to position dropdowns in navbars though as dynamic positioning isn’t required.

updated the Bootstrap to reference the ../5.1.3/js/bootstrap.bundle.min.js
2021-11-11 10:27:09 +01:00
4210d10fca Merge pull request #1782 from leigh-pointer/Bootstrap
Upgrade to 5.1.3 Bootstrap and Bootswatch Cyborg
2021-11-10 17:25:27 -05:00
d02842f0ea Merge branch 'dev' into Bootstrap 2021-11-10 17:25:16 -05:00
2543b7db79 Upgrade to 5.1.3 Bootstrap and Bootswatch Cyborg
Fixed issue with OffCanvas not rendering properly.
2021-11-10 22:34:19 +01:00
41f430429b Merge pull request #1781 from sbwalker/dev
fix UX in module/theme creators
2021-11-10 15:48:36 -05:00
4ed4f8d942 fix UX in module/theme creators 2021-11-10 15:56:51 -05:00
cc5396801b update Test project dependencies 2021-11-10 13:12:38 -05:00
a72dc36d67 Merge pull request #1780 from sbwalker/dev
update Test project dependencies
2021-11-10 13:04:23 -05:00
50989e4e1e Merge pull request #1778 from sbwalker/dev
use Cloudflare CDN for static resources
2021-11-10 08:52:38 -05:00
41487440e3 use Cloudflare CDN for static resources 2021-11-10 09:00:48 -05:00
af72750354 Merge pull request #1776 from leigh-pointer/ReplaceRoot
[RootFolder] was missing from Release.cmd
2021-11-10 08:44:15 -05:00
a58dc49acc [RootFolder] was missing from Release.cmd
One of the replace tokens was not added to the second command line.
2021-11-10 14:26:26 +01:00
6dbb493d10 updating module and theme templates 2021-11-08 15:12:41 -05:00
b8c37ff5d7 Merge pull request #1772 from sbwalker/dev
updating module and theme templates
2021-11-08 15:04:29 -05:00
04319195c6 update to official .NET 6 release 2021-11-08 14:55:24 -05:00
aadd78b7ac Merge pull request #1771 from sbwalker/dev
update to official .NET 6 release
2021-11-08 14:47:12 -05:00
e23da1f5fb Update README.md 2021-11-06 07:53:54 -04:00
50eeaf8497 add support for TrustServerCertificate connection string option for SQL Server 2021-11-05 16:01:00 -04:00
039202559f Merge pull request #1764 from sbwalker/dev
add support for TrustServerCertificate connection string option for SQL Server
2021-11-05 15:53:00 -04:00
5419032e8d upgrade module and theme templates to .NET6 2021-11-05 12:53:13 -04:00
017a92c4bc Merge pull request #1763 from sbwalker/dev
upgrade module and theme templates to .NET6
2021-11-05 12:45:10 -04:00
a16040a595 remove unnecessary cascading parameter to improve efficiency 2021-11-05 09:03:12 -04:00
3f6936a999 Merge pull request #1762 from sbwalker/dev
remove unnecessary cascading parameter to improve efficiency
2021-11-05 08:55:03 -04:00
d3f3359f66 fix #1745 - error on WebAssembly when logging out 2021-11-04 08:06:28 -04:00
3f110aaabd Merge pull request #1761 from sbwalker/dev
fix #1745 - error on WebAssembly when logging out
2021-11-04 07:58:22 -04:00
4e884d57ca Merge pull request #1760 from leigh-pointer/Navigation
Navigation
2021-11-04 07:34:10 -04:00
efbe0562f9 Navigation was not completed 2021-11-04 06:09:19 +01:00
096dfea1a6 Merge remote-tracking branch 'oqtane/dev' into dev 2021-11-04 05:53:42 +01:00
bd5a827593 fix #1746 - SQL Server installation needs to allow configuration of encryption setting on .NET 6 2021-11-03 16:37:37 -04:00
404bcaddd4 Merge pull request #1759 from sbwalker/dev
fix #1746 - SQL Server installation needs to allow configuration of encryption setting on .NET 6
2021-11-03 16:29:45 -04:00
d2d52a7eb3 update SqlClient to latest preview version 2021-11-03 14:42:24 -04:00
1761c47713 Merge pull request #1758 from sbwalker/dev
update SqlClient to latest preview version
2021-11-03 14:34:28 -04:00
82e97aa4fa Merge pull request #1750 from leigh-pointer/BlazorTheme
Fix for #1736 Blazor theme not rendering correctly
2021-11-03 12:32:45 -04:00
e598178869 Merge pull request #1752 from leigh-pointer/PView
Fix for #1749 navigate to sub sub pages
2021-11-03 12:32:34 -04:00
b6f89195ab Merge pull request #1754 from leigh-pointer/1753
Update for #1753 Date format for the Audit
2021-11-03 12:31:43 -04:00
7aa92c039a Merge remote-tracking branch 'oqtane/dev' into dev 2021-11-02 20:06:37 +01:00
fff36949b7 Fix for #1749 navigate to sub sub pages
Also added missing "Browse" localization from site/index,resx
2021-11-02 19:59:59 +01:00
c524f17978 Merge pull request #1757 from sbwalker/dev
Fix #1751 - error when creating site with new tenant
2021-11-02 14:41:21 -04:00
e0a0497dd2 Fix #1751 - error when creating site with new tenant 2021-11-02 14:49:06 -04:00
fce9220dcb Update for #1753 Date format for the Audit
Added Parameter DateTimeFormat with default value of  "MMM dd yyyy HH:mm:ss"
2021-11-02 07:01:24 +01:00
a8ddb64b49 Fix for #1749 navigate to sub sub pages
Added Open button that will navigate to sub pages
2021-11-02 05:57:05 +01:00
6d8df2661c modification for responsive theme
small modification to ensure theme is responsive
2021-10-31 07:08:19 +01:00
1659de3a2b Fix for #1736 Server Css
Update to the Server file Theme.css
2021-10-28 20:35:48 +02:00
9752c72998 Fix for #1736 Blazor theme not rendering correctly
Fix to the Default theme and container
2021-10-28 19:43:51 +02:00
db2e3a518d Update README.md 2021-10-27 10:04:00 -04:00
94b20662a4 Update README.md 2021-10-26 08:37:09 -04:00
7bfc0998fd fix #1713 - link to home path displays login page 2021-10-26 08:30:50 -04:00
6707f0efdf Update README.md 2021-10-26 08:27:18 -04:00
c7fe5a538f Merge pull request #1735 from sbwalker/dev
fix #1713 - link to home path displays login page
2021-10-26 08:23:12 -04:00
9b20006938 removed hidden form fields which are unnecessary as a result of recent changes 2021-10-22 10:33:09 -04:00
0b258bd384 Merge pull request #1730 from sbwalker/dev
removed hidden form fields which are unnecessary as a result of recent changes
2021-10-22 10:25:41 -04:00
2302c17273 Update README.md 2021-10-19 16:21:13 -04:00
b619699637 Update README.md 2021-10-19 16:19:43 -04:00
34434e03fe Merge pull request #1725 from sbwalker/dev
upgrade to .NET 6 and increment version to 3.0.0
2021-10-19 15:27:19 -04:00
29bd31f609 upgrade to .NET 6 and increment version to 3.0.0 2021-10-19 15:33:03 -04:00
cf69f9e4c4 Add proper help text to aliases field in default resource file for Site Settings. Set default value for new ShowLogin parameter in Login theme component. 2021-10-17 13:27:12 -04:00
028c9bf0a8 Merge pull request #1720 from sbwalker/dev
Add proper help text to aliases field in default resource file for Site Settings. Set default value for new ShowLogin parameter in Login theme component.
2021-10-17 13:20:24 -04:00
3e9a4f2c1a Fixed validation issue in Role Managment - Users. Modified FileManager component to allow Folder parameter to contain a folder path which is translated to a FolderId internally and refactored Packages folder logic. 2021-10-06 17:20:44 -04:00
960543d14d Merge pull request #1709 from sbwalker/dev
Fixed validation issue in Role Managment - Users. Modified FileManager component to allow Folder parameter to contain a folder path which is translated to a FolderId internally and refactored Packages folder logic.
2021-10-06 17:14:45 -04:00
299674f53b Update README.md 2021-10-06 09:55:47 -04:00
306b78b526 Added ability for Runtime and RenderMode to be set per Site - enabling the framework to support multiple hosting models concurrently in the same installation. Fixed WebAssembly Prerendering issue (this also resolved the issue where the component taghelper was not passing parameters correctly to the app when running on WebAssembly). Fix #1702 - remove web,config from upgrade package. 2021-10-05 14:32:05 -04:00
f369382a54 Merge pull request #1708 from sbwalker/dev
Added ability for Runtime and RenderMode to be set per Site - enabling the framework to support multiple hosting models concurrently in the same installation. Fixed WebAssembly Prerendering issue (this also resolved the issue where the component taghelper was not passing parameters correctly to the app when running on WebAssembly). Fix #1702 - remove web,config from upgrade package.
2021-10-05 14:25:12 -04:00
ac67d88e74 fix logic which sometimes results in System.InvalidOperationException: The value of IsFixed cannot be changed dynamically 2021-10-01 15:58:17 -04:00
1f4f70009c Merge pull request #1704 from sbwalker/dev
fix logic which sometimes results in  System.InvalidOperationException: The value of IsFixed cannot be changed dynamically
2021-10-01 15:51:26 -04:00
838d918451 Merge pull request #1701 from leigh-pointer/1690-1
1690 User Management Tab needs clicking to render UI when language is not default.
2021-10-01 11:23:01 -04:00
8e6c73d2bc Merge pull request #1703 from sbwalker/dev
Allow root page paths (rather than specifying a magic "home" string). More UX improvements to FileManager and Pager.
2021-10-01 11:22:02 -04:00
aeb599867c Allow root page paths (rather than specifying a magic "home" string). More UX improvements to FileManager and Pager. 2021-10-01 11:28:48 -04:00
2fe93d4e64 Fix for #1690 Tab needs clicking to render UI
User Management Tab needs clicking to render UI when language is not default.  Modification to the TabPanel fixes the issue without  forcing the Heading property to be populated.
2021-09-29 18:05:59 +02:00
3e789e0642 UX improvements to FileManager and Pager components 2021-09-29 10:46:23 -04:00
a762f206a1 Merge pull request #1699 from sbwalker/dev
UX improvements to FileManager and Pager components
2021-09-29 10:39:43 -04:00
c0b13a1f09 2.3.1 database provider packages 2021-09-27 14:22:23 -04:00
070ddff1b4 Merge pull request #1697 from sbwalker/dev
2.3.1 database provider packages
2021-09-27 14:15:37 -04:00
9d0770e360 Merge pull request #1696 from oqtane/master
Merge pull request #1695 from oqtane/dev
2021-09-27 14:04:23 -04:00
088cb2a30e Merge pull request #1695 from oqtane/dev
2.3.1 release
2021-09-27 14:03:55 -04:00
c2be84a367 increment version to 2.3.1 2021-09-27 11:43:57 -04:00
4f61dd7bb3 Merge pull request #1694 from sbwalker/dev
increment version to 2.3.1
2021-09-27 11:37:34 -04:00
30fb6fd8e2 Merge pull request #1693 from sbwalker/dev
fix #1691 - AntiForgeryToken header not being set during startup
2021-09-27 08:37:27 -04:00
4bfb5d9f34 fix #1691 - AntiForgeryToken header not being set during startup 2021-09-27 08:44:16 -04:00
023f29491d Update README.md 2021-09-25 09:08:20 -04:00
5166fe2b41 Update README.md 2021-09-25 09:04:36 -04:00
71aa41d55e Update README.md 2021-09-25 09:03:44 -04:00
f6fd50f449 Update README.md 2021-09-25 09:02:06 -04:00
81e2f7c288 Update README.md 2021-09-25 09:01:40 -04:00
895d5e50de Merge pull request #1689 from oqtane/master
Merge pull request #1688 from oqtane/dev
2021-09-24 17:12:22 -04:00
6cd1b1ceaa Merge pull request #1688 from oqtane/dev
2.3.0 release
2021-09-24 17:11:32 -04:00
82ae9409f1 Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2021-09-24 17:07:04 -04:00
764b879a77 changes to build/publish params for WebAssembly 2021-09-24 17:06:45 -04:00
d3e71b6a7e Merge pull request #1687 from sbwalker/dev
changes to build/publish params for WebAssembly
2021-09-24 17:00:39 -04:00
80d23d1c95 Add paging to SQL Manager results 2021-09-23 18:02:15 -04:00
531e89346e Merge pull request #1685 from sbwalker/dev
Add paging to SQL Manager results
2021-09-23 17:55:44 -04:00
f220cb52bb Merge pull request #1682 from gjwalk/dev
Sites Validation
2021-09-23 17:12:36 -04:00
58ff42f813 Merge pull request #1683 from nicpitsch/pr_user-management-profile
Profile properties as dropdown in User Management
2021-09-23 17:10:33 -04:00
6b82b03bf1 Merge pull request #1684 from sbwalker/dev
Use ComponentTagHelper parameters on Blazor Server for passing state to allow pre-rendering to function properly ( ComponentTagHelper parameters do not work on Blazor WebAssembly - likely a .NET 5 bug )
2021-09-23 17:10:24 -04:00
005843ef2d Use ComponentTagHelper parameters on Blazor Server for passing state to allow pre-rendering to function properly ( ComponentTagHelper parameters do not work on Blazor WebAssembly - likely a .NET 5 bug ) 2021-09-23 17:16:51 -04:00
10917644ab Profile properties as dropdown in User Management (same as User Profile). 2021-09-23 10:05:24 +02:00
9fa3ade832 Sites Validation 2021-09-22 18:22:30 -04:00
e10135271d Update README.md 2021-09-22 16:37:57 -04:00
c1b482e0c0 check in latest database provider packages 2021-09-22 14:47:14 -04:00
a3d7760a09 Merge pull request #1681 from sbwalker/dev
check in latest database provider packages
2021-09-22 14:40:54 -04:00
57db7c1efc update version to 2.3.0 in preparation for release 2021-09-22 11:55:01 -04:00
586c9b6db6 Merge pull request #1680 from sbwalker/dev
update version to 2.3.0 in preparation for release
2021-09-22 11:48:23 -04:00
58a86b67ee fix #1672 - releases need to be published with IL trimming disabled or else dynamic methods will be stripped. Unfortunately compression needs to be disabled as well as if it is not, a "None of the “sha256” hashes in the integrity attribute match the content of the subresource." error is thrown. This seems to be a bug - which I will pursue with Microsoft. 2021-09-22 10:24:13 -04:00
43ebf50b61 Merge pull request #1679 from sbwalker/dev
fix #1672 - releases need to be published with IL trimming disabled or else dynamic methods will be stripped. Unfortunately compression needs to be disabled as well as if it is not, a "None of the “sha256” hashes in the integrity attribute match the content of the subresource." error is thrown. This seems to be a bug - which I will pursue with Microsoft.
2021-09-22 10:17:48 -04:00
5071cf4752 modify method for determining Runtime in SiteRouter as ComponentTagHelper "param-" appears to only work on Blazor Server - not on WebAssembly 2021-09-21 12:48:15 -04:00
00e2e79fc5 Merge pull request #1676 from sbwalker/dev
modify method for determining Runtime in SiteRouter as ComponentTagHelper "param-" appears to only work on Blazor Server - not on WebAssembly
2021-09-21 12:41:31 -04:00
8d37444755 improve validation for public site settings 2021-09-21 07:45:43 -04:00
20d81bee00 Merge pull request #1675 from sbwalker/dev
improve validation for public site settings
2021-09-21 07:39:35 -04:00
ca387d7b26 fix Oqtane theme settings for page scope 2021-09-20 17:23:56 -04:00
a0580f6861 Merge pull request #1674 from sbwalker/dev
fix Oqtane theme settings for page scope
2021-09-20 17:17:09 -04:00
f739db1e42 Enhance Settings API for public Site Settings. Added Settings to Site model by default. Added new parameters to Login and UserProfile components. Enhanced Oqtane Theme settings to use new component parameters. Enhanced image download and resizing logic. 2021-09-20 17:15:52 -04:00
d5bfe7cfbd Merge pull request #1673 from sbwalker/dev
Enhance Settings API for public Site Settings. Added Settings to Site model by default. Added new parameters to Login and UserProfile components. Enhanced Oqtane Theme settings to use new component parameters. Enhanced image download and resizing logic.
2021-09-20 17:09:52 -04:00
db85e088bf fix #1659 installation issue on PostgreSQL by ntroducing a new RewriteValue method which can be overridden in a database provider to provide custom behavior. Updated PostgreSQL provide to utilize new method. Also added an Oqtane.Server project reference to the module and theme external templates to streamline the development experience (credit @leighpointer). 2021-09-17 13:56:19 -04:00
d053901b32 Merge pull request #1670 from 2sic-forks/dev
Documentation only
2021-09-17 13:50:02 -04:00
2957c7d6a9 Merge pull request #1671 from sbwalker/dev
fix #1659 installation issue on PostgreSQL by ntroducing a new RewriteValue method which can be overridden in a database provider to provide custom behavior. Updated PostgreSQL provide to utilize new method. Also added an Oqtane.Server project reference to the module and theme external templates to streamline the development experience (credit @leighpointer).
2021-09-17 13:49:52 -04:00
a69d52a389 undo accidental / internal commit 2021-09-17 15:40:05 +02:00
7b5dbbd7ed undo change by 2sic 2021-09-17 15:36:23 +02:00
6d0fb6ca80 undo change by 2sxc 2021-09-17 15:35:43 +02:00
b4f7344ae4 removed unnecessary message from top of module, theme, language installation pages 2021-09-17 09:28:27 -04:00
a5cecb7354 Merge pull request #1669 from sbwalker/dev
removed unnecessary message from top of module, theme, language installation pages
2021-09-17 09:21:57 -04:00
406a15c5bd constrain file logger size 2021-09-17 09:17:42 -04:00
ed0408de95 Merge pull request #1668 from sbwalker/dev
constrain file logger size
2021-09-17 09:11:15 -04:00
b5bba1fd11 improved method for determining Runtime in SiteRouter 2021-09-17 09:06:27 -04:00
9065bad2bb Merge pull request #1667 from sbwalker/dev
improved method for determining Runtime in SiteRouter
2021-09-17 09:00:06 -04:00
289034fd4f fix #1640 - prevent UX "flicker" which was caused by pre-rendering 2021-09-17 08:48:01 -04:00
f052f6696f Merge pull request #1666 from sbwalker/dev
fix #1640 - prevent UX "flicker" which was caused by pre-rendering
2021-09-17 08:41:44 -04:00
267ca178ed added basic xml comments to all Oqtane.Services interfaces 2021-09-17 10:13:26 +02:00
c01c16c7bc Merge pull request #1660 from gjwalk/dev
site validation
2021-09-16 18:00:50 -04:00
2ac069082d Merge pull request #1663 from sbwalker/dev
file manager component improvements
2021-09-16 17:58:20 -04:00
f00ea09b6e file manager component improvements 2021-09-16 18:04:50 -04:00
b9259ce6ca added optional event callback delegates to FileManager component to allow calling components to be notified on upload, change, or delete 2021-09-16 07:59:36 -04:00
0490638657 Merge pull request #1662 from sbwalker/dev
added optional event callback delegates to FileManager component to allow calling components to be notified on upload, change, or delete
2021-09-16 07:53:31 -04:00
467b6ba9da site validation 2021-09-15 19:10:14 -04:00
423a42d25b fixed invalid XML comment 2021-09-15 17:07:40 +02:00
b496dc488c Merge branch 'oqtane-dev' into dev 2021-09-15 15:23:28 +02:00
9750723035 Merge branch 'dev' of https://github.com/oqtane/oqtane.framework into oqtane-dev
# Conflicts:
#	Oqtane.Client/App.razor
2021-09-15 15:23:16 +02:00
a4f147b547 Merge pull request #1655 from gjwalk/dev
roles validation
2021-09-15 07:58:21 -04:00
0827fedb74 Merge pull request #1658 from sbwalker/dev
Added support for File descriptions, Folder capacity and image sizes. Added image resizing capability using ImageSharp - implemented in user profile. Added parameter to disable image preview in FileManager component. Overhauled Pager component and added Columns parameter for Grid mode. Populated PageState.User.IsAuthenticated in SiteRouter. Added support for zero price commercial extentions.
2021-09-15 07:57:06 -04:00
898b908c1b Added support for File descriptions, Folder capacity and image sizes. Added image resizing capability using ImageSharp - implemented in user profile. Added parameter to disable image preview in FileManager component. Overhauled Pager component and added Columns parameter for Grid mode. Populated PageState.User.IsAuthenticated in SiteRouter. Added support for zero price commercial extentions. 2021-09-15 08:02:55 -04:00
f21b70a51e roles validation 2021-09-11 18:18:23 -04:00
ba7524b754 Merge pull request #1651 from leigh-pointer/RecycleCheck
Validate if Page in Recycle Bin During Creation
2021-09-10 13:06:59 -04:00
eb068a8d53 Merge pull request #1652 from sbwalker/dev
fix #1647 - module reordering on page issue
2021-09-10 13:05:35 -04:00
14fbc3a5b4 fix #1647 - module reordering on page issue 2021-09-10 13:12:00 -04:00
d2fa8902f9 Auto stash before rebase of "origin/RecycleCheck"
correction
2021-09-10 18:59:23 +02:00
53e5728ad2 fix #1640 to resolve issue with server prerendering, upgrade Installer to Bootstrap5, add more defensive logic and logging to DatabaseManager, fix file logger issue, update Pager to use Bootstrap5 pagination, add expiry date support for commercial extensions 2021-09-10 08:24:05 -04:00
dd7de055f6 Merge pull request #1650 from sbwalker/dev
fix #1640 to resolve issue with server prerendering,  upgrade Installer to Bootstrap5, add more defensive logic and logging to DatabaseManager, fix file logger issue, update Pager to use Bootstrap5 pagination, add expiry date support for commercial extensions
2021-09-10 08:17:52 -04:00
3cd7249750 Page create - Recycle Bin Check
After Delete Page, Cant create page of same name #1645 issue. Added check and message if the page is in the recycle bin.
2021-09-08 08:08:24 +02:00
07165ce68d add support for trial periods 2021-09-03 15:24:51 -04:00
2c0fe71f14 Merge pull request #1642 from sbwalker/dev
add support for trial periods
2021-09-03 15:18:58 -04:00
c74a53b301 update fix https://github.com/oqtane/oqtane.framework/issues/1640 2021-09-02 20:50:16 +02:00
3663f723e7 fix https://github.com/oqtane/oqtane.framework/issues/1640 2021-09-02 20:32:04 +02:00
233da1508b Replacing dependency on System.Drawing with SixLabors.ImageSharp based on cross platform guidance from Microsoft 9b4520703c/accepted/2021/system-drawing-win-only/system-drawing-win-only.md 2021-09-02 11:58:31 -04:00
ad34e9aeb8 Merge pull request #1639 from sbwalker/dev
Replacing dependency on System.Drawing with SixLabors.ImageSharp based on cross platform guidance from Microsoft 9b4520703c/accepted/2021/system-drawing-win-only/system-drawing-win-only.md
2021-09-02 11:54:40 -04:00
d29dc9d036 make containing class overridable in Control Panel ( header and body are already overridable ) 2021-09-01 09:13:09 -04:00
d71030fdef Merge pull request #1638 from sbwalker/dev
make containing class overridable in Control Panel ( header and body are already overridable )
2021-09-01 09:07:02 -04:00
bb5ca475d3 fix #1628 - make DBContext Transient, modify Control Panel to use standard Bootstrap 5 offcanvas classes, add auto trimming to file logger, fix issue in File Repository related to populating Url on Add/Update. 2021-09-01 09:01:11 -04:00
01c7a8fcdb Merge pull request #1637 from sbwalker/dev
fix #1628 - make DBContext Transient, modify Control Panel to use standard Bootstrap 5 offcanvas classes, add auto trimming to file logger, fix issue in File Repository related to populating Url on Add/Update.
2021-09-01 08:55:06 -04:00
f6c46878c6 add new overloads to client-side logging methods to include LogFunction enum parameter so that it can be specified explicitly rather than only being inferred from the page action 2021-08-30 08:46:53 -04:00
acda6bba74 Merge pull request #1634 from gjwalk/dev
reset validation
2021-08-30 08:42:23 -04:00
c8ee066608 Merge pull request #1635 from sbwalker/dev
add new overloads to client-side logging methods to include LogFunction enum parameter so that it can be specified explicitly rather than only being inferred from the page action
2021-08-30 08:40:55 -04:00
ca9fffaa71 reset validation 2021-08-29 21:03:35 -04:00
e00b7c9be9 add some missing localization keys 2021-08-27 17:29:45 -04:00
ee1a2b1810 Merge pull request #1631 from sbwalker/dev
add some missing localization keys
2021-08-27 17:23:37 -04:00
14266a99b3 Merge pull request #1614 from gjwalk/dev
register validation
2021-08-27 12:57:09 -04:00
7b105107cc Merge pull request #1630 from sbwalker/dev
fix #1617 convert line break to comma when saving aliases, improve license acceptance user experience
2021-08-27 08:11:54 -04:00
bb75242a4f fix #1617 convert line break to comma when saving aliases, improve license acceptance user experience 2021-08-27 08:17:48 -04:00
39ccc30680 fix Type label in Add Folder UI, make Profile description required, fix misc Bootstrap 5 cosmetic issues, fix #1618 Alias case sensitivity in router, fix File add and update methods so they return Url, fix UrlCombine helper method to use proper slash, enhance package installation to support commercial options 2021-08-26 18:20:58 -04:00
41651075e6 Merge pull request #1627 from horacioj/typo-Message.Required.Smtp
Fix typo for RESX message Message.Required.Smtp
2021-08-26 18:15:08 -04:00
e3af463e65 Merge pull request #1629 from sbwalker/dev
fix Type label in Add Folder UI, make Profile description required, fix misc Bootstrap 5 cosmetic issues, fix #1618 Alias case sensitivity in router, fix File add and update methods so they return Url, fix UrlCombine helper method to use proper slash, enhance package installation to support commercial options
2021-08-26 18:14:59 -04:00
5cbb8b1fa3 Fix typo for RESX message Message.Required.Smtp 2021-08-24 17:05:08 -03:00
2a7b74a0e1 Merge pull request #1622 from oqtane/revert-1603-dev
Revert "EntityBuilder UpdateColumn to dynamic type of value"
2021-08-20 16:32:18 -04:00
21fc493322 Revert "EntityBuilder UpdateColumn to dynamic type of value" 2021-08-20 16:32:04 -04:00
ea85eae4ce register validation 2021-08-18 10:55:34 -04:00
35ee97c145 Update LICENSE 2021-08-17 10:02:00 -04:00
097318cf9e make profile category optional 2021-08-17 08:13:17 -04:00
31d4f3d1b3 Merge pull request #1613 from sbwalker/dev
make profile category optional
2021-08-17 08:07:29 -04:00
83b3235a6b Merge pull request #1611 from gjwalk/dev
profiles validation
2021-08-17 08:02:57 -04:00
e47fc64c33 profiles validation 2021-08-16 14:37:11 -04:00
151a49f1ec Merge pull request #1610 from sbwalker/dev
fix #1607 - issue with setting Site Root when adding/editing a page
2021-08-16 11:33:05 -04:00
b78644f7e2 fix #1607 - issue with setting Site Root when adding/editing a page 2021-08-16 11:39:00 -04:00
d744e36dde Merge pull request #1603 from tomsync/dev
EntityBuilder UpdateColumn to dynamic type of value
2021-08-16 09:42:04 -04:00
90f4bd5120 Merge pull request #1605 from gjwalk/dev
pages validation
2021-08-16 09:41:09 -04:00
b436fcf749 Merge pull request #1609 from sbwalker/dev
support for commercial modules, themes, translations
2021-08-16 09:40:58 -04:00
ffcc229c78 support for commercial modules, themes, translations 2021-08-16 09:46:02 -04:00
ffe724b32d add support for free/paid in module, theme, translation installation 2021-08-13 15:56:22 -04:00
154d32cd31 Merge pull request #1606 from sbwalker/dev
add support for free/paid in module, theme, translation installation
2021-08-13 15:50:39 -04:00
7f056277ae pages validation 2021-08-12 14:48:23 -04:00
b19cbf54e0 add error handling to module export 2021-08-12 14:47:51 -04:00
aef0e363d8 Merge pull request #1604 from sbwalker/dev
add error handling to module export
2021-08-12 14:42:35 -04:00
6324034259 Merge pull request #1600 from gjwalk/dev
modules validation
2021-08-12 14:21:20 -04:00
5a1bada10c EntityBuilder UpdateColumn to dynamic type of value 2021-08-12 16:59:15 +07:00
ef90305bd7 modules validation 2021-08-09 12:38:12 -04:00
fe06a29ad2 Merge pull request #1594 from gjwalk/dev
moduleDefinitions validation
2021-08-09 11:24:00 -04:00
f6ae9822f3 Merge pull request #1599 from sbwalker/dev
fix #1593 - remove duplicate form tag from edit.razor
2021-08-09 11:21:56 -04:00
eb3e4916a8 fix #1593 - remove duplicate form tag from edit.razor 2021-08-09 11:27:39 -04:00
5a5535ea98 format license terms 2021-08-06 15:28:48 -04:00
4f8d0b1e7a Merge pull request #1596 from sbwalker/dev
format license terms
2021-08-06 15:23:22 -04:00
b1d64eac88 moduleDefinitions validation 2021-08-06 13:32:40 -04:00
04673a4804 Merge pull request #1591 from gjwalk/dev
moduleCreator validation
2021-08-06 12:55:17 -04:00
4d2f9d038a Merge pull request #1592 from sbwalker/dev
allow host username to be specified during installation, allow user to be added to host role, refresh user list after delete, improve date/time entry in scheduled jobs, require license acceptance during module and theme install
2021-08-06 12:54:39 -04:00
e4201c1a4d allow host username to be specified during installation, allow user to be added to host role, refresh user list after delete, improve date/time entry in scheduled jobs, require license acceptance during module and theme install 2021-08-06 12:59:56 -04:00
030c001371 moduleCreator validation 2021-08-06 11:44:52 -04:00
bd351f770b Merge pull request #1588 from gjwalk/dev
langauges validation
2021-08-06 08:56:47 -04:00
63093cca3b langauges validation 2021-08-05 12:24:11 -04:00
5c42e8e5bc Merge pull request #1587 from gjwalk/dev
jobs validation
2021-08-05 08:38:18 -04:00
153934b311 Merge pull request #1582 from leigh-pointer/1579
Fixes #1579 Exception when browsing to /login when you are already logged in
2021-08-05 08:38:05 -04:00
4b1ead1a36 jobs validation 2021-08-04 19:15:52 -04:00
ddafd21706 Fixes #1579 Exception when browsing to /login when you are already logged in
Added PageState.User check
2021-08-04 19:33:54 +02:00
6059a944bf Merge pull request #1577 from gjwalk/dev
files validation
2021-08-03 08:45:48 -04:00
1cc26c3902 files validation 2021-07-31 16:59:03 -04:00
00ca3d856b reset admin 2021-07-31 15:09:14 -04:00
9af8ab92c9 themes - users validation changes 2021-07-29 16:54:32 -04:00
46fcfcc321 reset - systemInfo validation changes 2021-07-29 16:52:27 -04:00
cf40462531 moduleDefintions - profiles validation changes 2021-07-29 16:46:58 -04:00
2dbf9671d9 dashboard - moduleCretaor changes 2021-07-29 16:38:36 -04:00
e42a687c9b fixing spacing 2021-07-29 15:28:15 -04:00
72c154b048 fix appsettings 2021-07-27 21:17:13 -04:00
b0921513c1 fix to theme 2021-07-27 21:12:18 -04:00
33a76c61ca updated modules for input requirements 2021-07-27 16:24:01 -04:00
c0254d0b92 Merge pull request #1567 from ADefWebserver/dev
Removes writing database connection string to appsettings.json in AzureDeploy.json
2021-07-22 09:21:19 -04:00
99d05fe4f9 Update README.md 2021-07-21 06:56:10 -07:00
f8ba6a0c5f Update README.md
Temporarily pointing to my Dev version of azuredeploy (do not merge)
2021-07-21 06:18:34 -07:00
3a58ed63e9 Update azuredeploy.json
https://github.com/oqtane/oqtane.framework/issues/1547
2021-07-21 06:14:42 -07:00
8aac801af2 Merge pull request #1566 from sbwalker/dev
update install wizard to Bootstrap 5
2021-07-19 16:44:43 -04:00
1cc16a7ed4 update install wizard to Bootstrap 5 2021-07-19 16:49:31 -04:00
7fe04d9651 Merge pull request #1564 from nicpitsch/dev
Copy external Theme to Oqtane.Server.
2021-07-19 16:44:34 -04:00
01a9dc4d2d Merge pull request #1562 from leigh-pointer/OffCanvas
user experience updates
2021-07-19 16:44:25 -04:00
1456462ca8 Copy external Theme to Oqtane.Server. 2021-07-19 20:53:45 +02:00
77415dc0e0 user experience updates 2021-07-18 17:30:32 +02:00
696b1970fc Update README.md 2021-07-18 11:15:03 -04:00
0e6baae366 Merge pull request #1554 from leigh-pointer/OffCanvas
ConltolPanel to use Bootstrap Offcanvas component
2021-07-18 10:42:09 -04:00
437e9ee43b Modifcations for ControlPanel BS5
current styles in app.css and theme.css for app-controlpanel removed as no longer needed.

Removed the _display var as that is no longer beign used.
2021-07-15 16:28:12 +02:00
6546f47bb2 Merge pull request #1556 from leigh-pointer/UsernameReadOnly 2021-07-15 09:47:51 -04:00
88c866057f Fix for #1555 Username readonly on Register form
Removed the readonly attribute from the username field.
2021-07-15 15:41:20 +02:00
3521dd41cd ConltolPanel to use Bootstrap Offcanvas component 2021-07-14 10:18:39 +02:00
dff3e6aaca Merge pull request #1550 from leigh-pointer/ModificationsBS5 2021-07-12 10:24:42 -04:00
4b04e6c8dc Merge pull request #1551 from leigh-pointer/ExtModDiv 2021-07-12 10:23:57 -04:00
37672ea632 Modified External Module Template
replace tables in markup with responsive approach.
2021-07-11 14:27:18 +02:00
31f35ad902 Modifications for Bootstrap 5
replace tables in markup with responsive approach
2021-07-11 14:16:33 +02:00
0b7b34f336 Merge pull request #1549 from leigh-pointer/Bootstrap5
modifications for Bootstrap 5
2021-07-10 19:35:41 -04:00
fca290f8f5 Modifications for Bootstrap 5
Admin section now finished.  All Tables now replaced with div
2021-07-10 13:37:05 +02:00
9da3b77f7d modifications for Bootstrap 5
Updated Admin areas Users and Roles
2021-07-10 09:00:34 +02:00
7b796f4a5f Merge pull request #1548 from sbwalker/dev
modifications for Bootstrap 5
2021-07-09 11:38:42 -04:00
0ce81169a6 modifications for Bootstrap 5 2021-07-09 11:43:37 -04:00
d1c2abf7e3 Merge pull request #1545 from hishamco/fix
Fix loading satellite assemblies
2021-07-08 10:34:02 -04:00
010872ef35 Merge pull request #1546 from sbwalker/dev
bootstrap5 fix to theme creator template, modifications to updater app
2021-07-08 10:31:19 -04:00
f536033087 framework updater UX improvements 2021-07-08 10:36:08 -04:00
9083888686 bootstrap5 fix to theme creator template, modifications to updater app 2021-07-07 16:15:38 -04:00
cf2d8531a3 Fix loading satellite assemblies 2021-07-07 20:38:53 +03:00
48b9e2f97b Merge pull request #1538 from oqtane/master
Merge pull request #1537 from oqtane/dev
2021-07-06 15:31:55 -04:00
57ac20519a Merge pull request #1537 from oqtane/dev
2.2.0 release
2021-07-06 15:31:18 -04:00
792086140b fix horizontal menu highlighting issue 2021-07-06 14:21:54 -04:00
ddeb05dfe2 Merge pull request #1535 from sbwalker/dev
fix horizontal menu highlighting issue
2021-07-06 14:17:12 -04:00
276431b62d update theme creator template to Bootstrap5 2021-07-06 11:00:45 -04:00
a89e7c06d4 Merge pull request #1534 from sbwalker/dev
update theme creator template to Bootstrap5
2021-07-06 10:56:02 -04:00
a79535d7d1 prepare for 2.2.0 release 2021-07-05 15:22:24 -04:00
649a134214 Merge pull request #1533 from sbwalker/dev
prepare for 2.2.0 release
2021-07-05 15:17:43 -04:00
910630cd26 Merge pull request #1531 from leigh-pointer/ModuleTemplates
Fix for #1530 Error in Module templates
2021-07-05 14:56:18 -04:00
a1baf70524 fix #1530 for Error in Module templates
The Get public Models.[Module] Get(int id) method in the [Module]Controller has an incorrect IF statement which in turn returns nothing and logs an error
2021-07-05 08:25:16 +02:00
7348540c4d Update README.md 2021-07-02 20:07:41 -04:00
534c6794f2 Merge pull request #1529 from sbwalker/dev
upgrade to Boostrap 5
2021-07-02 19:59:10 -04:00
cb7d9a0371 upgrade to Boostrap 5 2021-07-02 20:03:51 -04:00
525cbb87b0 allow disabling of swagger and package service 2021-07-01 09:11:29 -04:00
7f420e8a8f Merge pull request #1528 from sbwalker/dev
allow disabling of swagger and package service
2021-07-01 09:06:44 -04:00
eea417ff44 added logging for startup issues 2021-07-01 07:37:03 -04:00
070e00b735 Merge pull request #1527 from sbwalker/dev
added logging for startup issues
2021-07-01 07:32:34 -04:00
5268c326a1 suppress Internal EF Core API usage warning messages on build 2021-06-28 10:27:29 -04:00
49b2c323d0 Merge pull request #1523 from sbwalker/dev
suppress Internal EF Core API usage warning messages on build
2021-06-28 10:22:49 -04:00
bf6edceb36 Fix issue where module definition version was not being loaded correctly on startup. Also user customizable module definition properties were being overwritten on upgrade. 2021-06-28 10:21:48 -04:00
91eeeaf064 Merge pull request #1522 from sbwalker/dev
Fix issue where module definition version was not being loaded correctly on startup. Also user customizable module definition properties were being overwritten on upgrade.
2021-06-28 10:17:40 -04:00
17c0aec1fb show friendly message when no packages match criteria 2021-06-27 20:20:01 -04:00
e4c98ba24f Merge pull request #1519 from sbwalker/dev
show friendly message when no packages match criteria
2021-06-27 20:15:23 -04:00
8ef83b2de8 improve register for updates 2021-06-27 18:33:41 -04:00
376482e66e Merge pull request #1518 from sbwalker/dev
improve register for updates
2021-06-27 18:29:05 -04:00
2433958a0f fixed issue in SharedResource file 2021-06-27 08:59:04 -04:00
50bddd072b Merge pull request #1517 from sbwalker/dev
fixed issue in SharedResource file
2021-06-27 08:54:35 -04:00
b77d313715 Merge pull request #1515 from gjwalk/dev
Shared resources added
2021-06-27 08:50:46 -04:00
b59706ab8b Merge pull request #1516 from sbwalker/dev
add ability to register for updates
2021-06-27 08:47:25 -04:00
9e004f5b3c add ability to register for updates 2021-06-27 08:48:18 -04:00
f989f63546 Merge branch 'dev' into dev 2021-06-25 17:56:21 -04:00
ef6f5f2769 update resources 2021-06-25 17:03:29 -04:00
ba9ca22aaa update to resources 2021-06-25 16:34:30 -04:00
db1808d3e9 additional system info 2021-06-25 15:06:52 -04:00
8ad61a23c8 Merge pull request #1514 from sbwalker/dev
additional system info
2021-06-25 15:02:38 -04:00
e1e4d82684 resx file corrections 2021-06-25 08:22:36 -04:00
5d84eeea1d Merge pull request #1513 from sbwalker/dev
resx file corrections
2021-06-25 08:18:05 -04:00
509f054961 package UI updates 2021-06-25 08:10:15 -04:00
3a85eed397 Merge pull request #1512 from sbwalker/dev
package UI updates
2021-06-25 08:05:51 -04:00
52bcdb12c5 package management modifications 2021-06-24 18:02:01 -04:00
30a667debf Merge pull request #1511 from sbwalker/dev
package management modifications
2021-06-24 17:57:23 -04:00
161ab56701 improve handling of response status and logging in package controller 2021-06-24 10:42:43 -04:00
55bf2f1b6a Merge pull request #1510 from sbwalker/dev
improve handling of response status and logging in package controller
2021-06-24 10:38:31 -04:00
fb2a8e987e default resx fixes 2021-06-24 08:02:39 -04:00
0608287b5f Merge pull request #1509 from sbwalker/dev
default resx fixes
2021-06-24 07:58:08 -04:00
7fa30ff23c Merge pull request #1501 from Gotiap/dev
Allowed pages for external modules.
2021-06-24 07:37:49 -04:00
b0f76ff4e8 Merge pull request #1508 from sbwalker/dev
Page IsClickable column must be nullable in order to support upgrades, add more defensive logic
2021-06-24 07:37:27 -04:00
8e7b553ca8 Page IsClickable column must be nullable in order to support upgrades, add more defensive logic 2021-06-24 07:41:34 -04:00
89cc389918 Undo changes for login 2021-06-24 11:30:09 +05:30
bfafffd8cb add search to package manager components 2021-06-23 13:00:44 -04:00
4ea92652dd Merge pull request #1499 from ijaz-saeed/rich-text-editor
OnInitialized is not the right method to do this, Content is not set yet
2021-06-23 12:58:25 -04:00
6fbb325c42 Merge pull request #1506 from sbwalker/dev
add search to package manager components
2021-06-23 12:56:16 -04:00
c2f7546488 add database diagram 2021-06-22 14:30:20 -04:00
90778f5e1e Update README.md 2021-06-22 14:28:06 -04:00
c7d5f95949 Update README.md 2021-06-22 14:27:17 -04:00
67dff508ef Merge pull request #1504 from sbwalker/dev
add database diagram
2021-06-22 14:25:43 -04:00
c4e6a4af49 fix remaining default resx differences, enhance module message with ability to dismiss, fix issue in ConfigManager.RemoveSetting, introduce package registry service 2021-06-22 14:14:46 -04:00
c236e80e95 Merge pull request #1498 from ijaz-saeed/dev
adding SaveChangesAsync overloads to support Async save
2021-06-22 14:10:33 -04:00
1906a0ee8a Merge pull request #1502 from sbwalker/dev
fix remaining default resx differences, enhance module message with ability to dismiss, fix issue in ConfigManager.RemoveSetting, introduce package registry service
2021-06-22 14:10:23 -04:00
d348e9715f Allowed pages for external module.
Login internal module while edit, resolved error to be edit.
2021-06-22 10:51:05 +05:30
91a1bfcab2 adding SaveChangesAsync overloads to support Async save 2021-06-20 17:52:41 +05:00
73f2fc4f13 OnInitialized is not the right method to do this, Content is not available yet 2021-06-20 17:51:25 +05:00
247e00ecd4 implement shared resources 2021-06-18 16:56:54 -04:00
3274ab6258 Merge pull request #1497 from sbwalker/dev
implement shared resources
2021-06-18 16:52:27 -04:00
6bff09d0ca fix Localizer class specification 2021-06-18 16:46:15 -04:00
19f3095483 Merge pull request #1496 from sbwalker/dev
fix Localizer class specification
2021-06-18 16:41:47 -04:00
2e947625cd fix issue with HtmlText module rendering 2021-06-18 16:36:18 -04:00
8198f36530 Merge pull request #1495 from sbwalker/dev
fix issue with HtmlText module rendering
2021-06-18 16:32:09 -04:00
8bc0402801 fix syntax error 2021-06-18 16:30:49 -04:00
87aadcff5b Merge pull request #1494 from sbwalker/dev
fix syntax error
2021-06-18 16:27:00 -04:00
dc44fcd0d9 Merge pull request #1493 from gjwalk/dev
create default rex files with static keys
2021-06-18 16:21:37 -04:00
f7363504c2 Merge branch 'dev' into dev 2021-06-18 15:35:20 -04:00
ae0edcfd2d create default rex files with static keys 2021-06-18 14:45:38 -04:00
b180c260e5 Merge pull request #1492 from sbwalker/dev
improved error handling, improved consistency of console error messages, added ability to add a Decimal column in Migrations
2021-06-18 12:57:21 -04:00
3bc5744007 improved error handling, improved consistency of console error messages, added ability to add a Decimal column in Migrations 2021-06-18 13:01:42 -04:00
32c49f74d3 fix module template issues 2021-06-17 12:12:19 -04:00
8b70d1ccdc Merge pull request #1491 from sbwalker/dev
fix module template issues
2021-06-17 12:08:03 -04:00
f330c4fcb6 add extension method for Localization which allows specification of key and value 2021-06-16 22:16:48 -04:00
a953ca752e Merge pull request #1490 from sbwalker/dev
add extension method for Localization which allows specification of key and value
2021-06-16 22:12:48 -04:00
d32b622f7e allow order to be defined in page templates 2021-06-16 17:24:45 -04:00
8354d66c84 Merge pull request #1489 from sbwalker/dev
allow order to be defined in page templates
2021-06-16 17:20:53 -04:00
7ee9923b52 Merge pull request #1483 from leigh-pointer/SResControlPanel
Introduce SharedResource Localisation to ControlPanel
2021-06-16 16:27:52 -04:00
31c412a886 Merge pull request #1488 from sbwalker/dev
improvements to refresh logic, module template enhancements
2021-06-16 16:26:53 -04:00
72ff6fa0e7 improvements to refresh logic, module template enhancements 2021-06-16 16:31:02 -04:00
c6e12a614a Merge pull request #1485 from hishamco/database-projects
Add missing Oqtane.Server into Databases solution
2021-06-16 10:09:26 -04:00
cec24e7446 improve multi-tenancy navigation 2021-06-16 08:30:41 -04:00
05d7c7fc74 Merge pull request #1484 from hishamco/localization-cookie
Fix parsing localization cookie when the value is not present
2021-06-16 08:26:50 -04:00
0d10645d57 Merge pull request #1486 from sbwalker/dev
improve multi-tenancy navigation
2021-06-16 08:26:36 -04:00
1bfe4fbd4f Add missing Oqtane.Server to Databases solutions 2021-06-16 11:55:36 +03:00
c62a4ae8ae Fix parsing localization cookie when the value is not present 2021-06-16 11:02:12 +03:00
4fcfb2ab4e Introduce SharedResource Localisation
Updated the ControlPanel to use Shared resources for;
 Add, Edit, Delete, Cancel

Corrected the SharedResources.cs namespace.
2021-06-16 09:46:33 +02:00
0f208f3c30 Merge pull request #1 from oqtane/dev
merge
2021-06-16 09:19:09 +02:00
65a14da5a9 improve validation and exception handling in API Controllers 2021-06-15 19:11:00 -04:00
53f3245f1d Merge pull request #1482 from sbwalker/dev
improve validation and exception handling in API Controllers
2021-06-15 19:06:52 -04:00
ffbabcfb28 Update README.md 2021-06-15 09:18:17 -04:00
0a2293119e added back missing ITenantManager registration removed in #1245 2021-06-15 08:32:39 -04:00
abd9e2798d Merge pull request #1480 from sbwalker/dev
added back missing ITenantManager registration removed in #1245
2021-06-15 08:28:44 -04:00
f6cc11bd3b add logic removed in #1245 back to HttpClient creation 2021-06-15 08:23:26 -04:00
3e3bf937fd Merge pull request #1479 from sbwalker/dev
add logic removed in #1245 back to HttpClient creation
2021-06-15 08:19:20 -04:00
0fd16fbb59 added Oqtane.Client to _Imports so it can find the new SharedResources class 2021-06-15 07:59:05 -04:00
b2098bb2db Merge pull request #1478 from sbwalker/dev
added Oqtane.Client to _Imports so it can find the new SharedResources class
2021-06-15 07:54:54 -04:00
87c99e2ba7 Merge branch 'oqtane:dev' into dev 2021-06-15 07:46:51 -04:00
4a11524db3 Merge pull request #1245 from hishamco/clean-startup
Clean Startup Class
2021-06-15 07:45:12 -04:00
a9efc2792e Merge pull request #96 from oqtane/dev
sync
2021-06-15 07:42:22 -04:00
9558894a4f moved SharedResources class to proper location 2021-06-15 07:43:12 -04:00
abfb451290 Merge branch 'dev' into dev 2021-06-15 07:42:02 -04:00
e279bca8ad Merge pull request #1476 from leigh-pointer/SharedResource
SharedResources to reside at the project root.
2021-06-15 07:40:27 -04:00
f2b3d6bc5f SharedResources to reside at the project root.
Namespace was changed from Oqtane to Oqtane.Client
2021-06-15 07:09:22 +02:00
28694fc11f added defensive logic to app.razor, relocated shared resource definition in preparation for utilizing shared resources 2021-06-14 17:29:23 -04:00
e99d62d5c7 Merge pull request #1475 from sbwalker/dev
added defensive logic to app.razor, relocated shared resource definition in preparation for utilizing shared resources
2021-06-14 17:25:23 -04:00
216dd39474 Merge branch 'oqtane:dev' into dev 2021-06-13 15:24:05 -04:00
643895b62b add icon for reuse 2021-06-12 09:21:24 -04:00
bd0ee1370c Merge pull request #1471 from sbwalker/dev
add icon for reuse
2021-06-12 09:17:10 -04:00
7c181b65cd Fix merge conflict 2021-06-12 00:18:57 +03:00
bdf36fc49c bug fixes 2021-06-11 17:07:54 -04:00
965b935128 Merge pull request #1470 from sbwalker/dev
bug fixes
2021-06-11 17:03:57 -04:00
126024991c Merge remote-tracking branch 'upstream/dev' into clean-startup
# Conflicts:
#	Oqtane.Client/Program.cs
#	Oqtane.Server/Startup.cs
2021-06-11 23:54:38 +03:00
8f944e29ac set the DefaultDBType as the default database option in the Installer and Add Site UI 2021-06-11 08:43:46 -04:00
7b26ecfec8 Merge pull request #1467 from sbwalker/dev
set the DefaultDBType as the default database option in the Installer and Add Site UI
2021-06-11 08:40:17 -04:00
aa5aca3a8e back out auth policy header support as Blazor HttpClient is registered as Scoped and can not support variable headers 2021-06-11 07:54:02 -04:00
c46ef0fbc4 Merge pull request #1466 from sbwalker/dev
back out auth policy header support as Blazor HttpClient is registered as Scoped and can not support variable headers
2021-06-11 07:50:00 -04:00
d82fc8be90 added IsClickable Page property #1092, improve validation in Role Management, display database information in SQL Management, improve HttpClient header support 2021-06-10 20:10:46 -04:00
a51e7c2b44 Merge pull request #1465 from sbwalker/dev
added IsClickable Page property #1092, improve validation in Role Management, display database information in SQL Management, improve HttpClient header support
2021-06-10 20:06:41 -04:00
52d9c14d78 Update README.md 2021-06-10 13:06:05 -04:00
4bee097e66 fix Site Settings issue 2021-06-10 10:55:59 -04:00
6899a7cbb2 Merge pull request #1464 from sbwalker/dev
fix Site Settings issue
2021-06-10 10:51:49 -04:00
edc0e2a8fe Merge pull request #1462 from albahadly/dev
fix issue: WebAssembly with IDM app
2021-06-10 10:46:52 -04:00
f5f374d2ae #1460 fix issue: WebAssembly with IDM app 2021-06-11 01:06:20 +12:00
ece90c39cb Merge pull request #1461 from sbwalker/dev
refactoring, enhancements, and some fixes
2021-06-10 08:13:35 -04:00
bc720555c4 refactoring, enhancements, and some fixes 2021-06-10 08:16:02 -04:00
25dc6b9b08 when set Runtime as WebAssembly, IDM app recognize oqtane.zip and download this file and this make an issue and oqtane not work correctly and for this I set diffrent extention that IDM cant recognize it 2021-06-09 12:45:31 +12:00
36bd8e4b7f Update README.md 2021-06-07 15:58:04 -04:00
82c05a841f Improve validation and error handling in Controller methods 2021-06-07 15:29:08 -04:00
dd3385c447 Update README.md 2021-06-07 15:25:57 -04:00
93031c9aaa Merge pull request #1457 from sbwalker/dev
Improve validation and error handling in Controller methods
2021-06-07 15:25:04 -04:00
54cd360bb5 allow host to change runtime and rendermode settings in System Info 2021-06-06 11:04:37 -04:00
aa30caa1a7 Merge pull request #1451 from sbwalker/dev
allow host to change runtime and rendermode settings in System Info
2021-06-06 11:00:58 -04:00
900ea8cfbc allow host to view tenant information in Site Settings 2021-06-06 10:36:13 -04:00
cf99f04451 Merge pull request #1450 from sbwalker/dev
allow host to view tenant information in Site Settings
2021-06-06 10:32:21 -04:00
357ef09dd1 new controller auth parameter should take precedence over legacy 2021-06-06 10:03:54 -04:00
42d653969d Merge pull request #1449 from sbwalker/dev
new controller auth parameter should take precedence over legacy
2021-06-06 10:00:09 -04:00
a2b808fde2 use new service auth pattern in module template 2021-06-04 15:01:25 -04:00
33405b9457 Merge pull request #1448 from sbwalker/dev
use new service auth pattern in module template
2021-06-04 14:57:23 -04:00
ffb5ca44ec Merge pull request #1447 from oqtane/master
Merge pull request #1446 from oqtane/dev
2021-06-04 13:05:35 -04:00
3f8a0d4933 Merge pull request #1446 from oqtane/dev
2.1.0 release
2021-06-04 12:56:13 -04:00
970845d640 Merge pull request #1445 from sbwalker/dev
create Public folder during installation
2021-06-04 12:26:42 -04:00
fab06a5279 create Public folder during installation 2021-06-04 12:30:46 -04:00
ff59a12443 UX clarification 2021-06-04 12:18:04 -04:00
b7af8d0d91 Merge pull request #1444 from sbwalker/dev
UX clarification
2021-06-04 12:14:38 -04:00
3401d33a35 Update README.md 2021-06-04 09:32:58 -04:00
69c948ad79 misc site settings UX fixes 2021-06-04 08:42:33 -04:00
07b684d49a Merge pull request #1443 from sbwalker/dev
misc site settings UX fixes
2021-06-04 08:38:51 -04:00
ab3af5f294 use full package name for manifest 2021-06-04 07:30:33 -04:00
aefdcdcd8a Merge pull request #1442 from sbwalker/dev
use full package name for manifest
2021-06-04 07:26:32 -04:00
7b32a16fd8 handle versionless package names in installer 2021-06-03 15:03:11 -04:00
7c7dbfe638 Merge pull request #1440 from sbwalker/dev
handle versionless package names in installer
2021-06-03 14:59:13 -04:00
e527f6e3d1 updater improvements 2021-06-03 11:05:40 -04:00
5f068081f3 Merge pull request #1439 from sbwalker/dev
updater improvements
2021-06-03 11:01:57 -04:00
02de8e0bf7 fix path 2021-06-03 09:01:52 -04:00
f412657987 Merge pull request #1438 from sbwalker/dev
fix path
2021-06-03 08:57:46 -04:00
ec462e2372 Merge pull request #1437 from sbwalker/dev
create all artifacts in Oqtane.Packages folder
2021-06-03 08:56:01 -04:00
0deffff370 create all artifacts in Oqtane.Packages folder 2021-06-03 08:58:45 -04:00
57885f71a7 Revert "create all artifacts in Oqtane.Package folder"
This reverts commit a05a1d6321.
2021-06-03 08:52:51 -04:00
a05a1d6321 create all artifacts in Oqtane.Package folder 2021-06-03 08:52:25 -04:00
060f764da7 separated updater from main solution 2021-06-03 08:37:56 -04:00
ae2caacc81 Merge pull request #1436 from sbwalker/dev
separated updater from main solution
2021-06-03 08:34:02 -04:00
dd9f2e6675 improve System Update user experience 2021-06-02 19:22:20 -04:00
b5a81d8328 Merge pull request #1435 from sbwalker/dev
improve System Update user experience
2021-06-02 19:19:22 -04:00
d1204c7dea Merge pull request #1434 from sbwalker/dev
use secure Packages location for upgrade process
2021-06-02 17:10:44 -04:00
3db12a225b use secure Packages location for upgrade process 2021-06-02 16:53:55 -04:00
912b775553 preserve backward compatibility of CreateAuthorizationPolicyUrl method 2021-06-02 12:20:31 -04:00
25952b53f2 Merge pull request #1433 from sbwalker/dev
preserve backward compatibility of CreateAuthorizationPolicyUrl method
2021-06-02 12:16:31 -04:00
9bccc402a0 use versionless package names for database providers to support future framework upgrades 2021-06-02 08:09:46 -04:00
b44cc06324 Merge pull request #1432 from sbwalker/dev
use versionless package names for database providers to support future framework upgrades
2021-06-02 08:05:48 -04:00
1b4934d623 Merge pull request #1431 from sbwalker/dev
fix issue in theme creator
2021-06-01 15:45:09 -04:00
8dfe8eba27 fix issue in theme creator 2021-06-01 15:49:06 -04:00
56a1c1a57d Update README.md 2021-06-01 14:51:13 -04:00
3c48657e73 version all DLLs and packages consistently and fix all deprecated iconurl warnings 2021-06-01 12:30:21 -04:00
c7442f12e8 Merge pull request #1430 from sbwalker/dev
version all DLLs and packages consistently and fix all deprecated iconurl warnings
2021-06-01 12:26:23 -04:00
c91b8e72c0 fix EF Core version value in upgrade scripts 2021-06-01 10:31:16 -04:00
dc4e3213b1 Merge pull request #1429 from sbwalker/dev
fix EF Core version value in upgrade scripts
2021-06-01 10:27:30 -04:00
5a46d6b842 Merge pull request #1428 from cnurse/dev 2021-05-31 21:50:11 -04:00
c6f4723ee3 Fix for Issue #1420 2021-05-31 15:55:09 -07:00
b76e8498d7 move logic for inserting migrations history from MigrateableModuleBase to MigrationUtils 2021-05-31 16:17:06 -04:00
0347aae140 Merge pull request #1427 from sbwalker/dev
move logic for inserting migrations history from MigrateableModuleBase to MigrationUtils
2021-05-31 16:13:12 -04:00
4576f056d5 Merge pull request #1424 from 2sic-forks/dev
More docs for #1382
2021-05-31 11:56:18 -04:00
879829b9bb Merge pull request #1425 from sbwalker/dev
added metadata support for Module and Theme templates
2021-05-31 11:55:54 -04:00
ddd657bfa7 added metadata support for Module and Theme templates 2021-05-31 11:59:19 -04:00
d52cbf6817 More docs for https://github.com/oqtane/oqtane.framework/issues/1382 - should not affect any code 2021-05-31 15:45:07 +02:00
276817c89d made RenderMode configurable 2021-05-30 15:37:23 -04:00
d8b811e09f Update appsettings.release.json 2021-05-30 15:35:19 -04:00
b21ffc2d63 Update appsettings.json 2021-05-30 15:35:04 -04:00
c402113df3 Merge pull request #1423 from sbwalker/dev
made RenderMode configurable
2021-05-30 15:34:04 -04:00
afcc5e2170 handle HtmlText module transition from SQL scripts to Migrations in module rather than in core framework 2021-05-30 13:16:26 -04:00
5a2ad4f827 Merge pull request #1422 from sbwalker/dev
handle HtmlText module transition from SQL scripts to Migrations in module rather than in core framework
2021-05-30 13:12:27 -04:00
967f0fe626 leave a copy of database provider packages in distribution folder 2021-05-29 17:38:58 -04:00
f8aeef06b2 Merge pull request #1421 from sbwalker/dev
leave a copy of database provider packages in distribution folder
2021-05-29 17:35:03 -04:00
7ed93b5ce6 package installer fix to handle .bak files 2021-05-29 15:13:27 -04:00
89cd0c760a Merge pull request #1418 from sbwalker/dev
package installer fix to handle .bak files
2021-05-29 15:09:29 -04:00
d73e1d21c7 prepare for 2.1.0 release 2021-05-29 13:00:36 -04:00
21ad5f4c1a Merge pull request #1417 from sbwalker/dev
prepare for 2.1.0 release
2021-05-29 12:56:55 -04:00
e84908485f add support for custom "internal" module and theme templates. fix package installer issue related to absolute paths 2021-05-29 11:48:29 -04:00
b05fbae401 Merge pull request #1416 from sbwalker/dev
add support for custom "internal" module and theme templates. fix package installer issue related to absolute paths
2021-05-29 11:44:30 -04:00
54a639d1d5 Module Creator external template changes for 2.1 - supporting mutliple databases, EF Core migrations, and other multi-tenancy improvements 2021-05-28 17:01:25 -04:00
ba61c3318b Merge pull request #1413 from sbwalker/dev
Module Creator external template changes for 2.1 - supporting mutliple databases, EF Core migrations, and other multi-tenancy improvements
2021-05-28 16:57:39 -04:00
8529a42075 fixed upgrade logic 2021-05-28 16:01:11 -04:00
b62a9fa1ff Merge pull request #1412 from sbwalker/dev
fixed upgrade logic
2021-05-28 15:57:16 -04:00
e559a11261 Delete Oqtane.Server/Content/Tenants/1/Sites/1 directory 2021-05-28 08:43:42 -04:00
e878db2f21 Merge pull request #1411 from sbwalker/dev
add new packages location to gitignore
2021-05-28 08:40:12 -04:00
e693f6b52b add new packages location to gitignore 2021-05-28 08:44:14 -04:00
e0c2763c9f refactoring in preparation for release 2021-05-28 07:53:49 -04:00
723ae68609 Merge pull request #1410 from sbwalker/dev
refactoring in preparation for release
2021-05-28 07:49:56 -04:00
9dbfbd78a4 Update README.md 2021-05-27 21:47:04 -04:00
9cd29b62ed Update README.md 2021-05-27 21:46:27 -04:00
4108c07862 moved Packages folder to secure location 2021-05-27 21:39:43 -04:00
01716099d6 Merge pull request #1408 from sbwalker/dev
moved Packages folder to secure location
2021-05-27 21:35:57 -04:00
a144a5c432 improved legacy support for module authorization policy 2021-05-27 21:01:25 -04:00
dbccfe4bf7 Merge pull request #1407 from sbwalker/dev
improved legacy support for module authorization policy
2021-05-27 20:57:46 -04:00
9aff82e504 Update appsettings.release.json 2021-05-27 16:17:09 -04:00
9ff4d9c143 Update appsettings.json 2021-05-27 16:16:48 -04:00
6c8703356f Merge pull request #1406 from sbwalker/dev
automate the 2.1.0 upgrade
2021-05-27 16:15:02 -04:00
fbce6c7248 automate the 2.1.0 upgrade 2021-05-27 16:18:45 -04:00
908d572cc9 Update appsettings.release.json 2021-05-26 17:43:02 -04:00
f0384732ac Update appsettings.json 2021-05-26 17:42:43 -04:00
c089283645 Merge pull request #1405 from sbwalker/dev
move database projects into their own solution to keep the streamline main solution
2021-05-26 17:42:18 -04:00
50ac9236af move database projects into their own solution to keep the streamline main solution 2021-05-26 17:46:08 -04:00
d520d50b75 Update appsettings.release.json
synchronized with appsettings.json
2021-05-26 12:00:49 -04:00
99838679bd Update appsettings.json
reorganized to move most relevant settings to the top of the file
2021-05-26 12:00:15 -04:00
4b49358968 Merge pull request #1404 from sbwalker/dev
add support for public content folders
2021-05-26 11:57:46 -04:00
c07e766e57 add support for public content folders 2021-05-26 12:01:35 -04:00
1d171d2e56 Merge pull request #1400 from 2sic-forks/dev
More documentation - almost all Models done #1382
2021-05-26 07:31:16 -04:00
6527d383db Merge pull request #1401 from cnurse/database-options
Move Available Databases to appsettings and use IOptions
2021-05-26 07:30:34 -04:00
d280a4aa01 Move Available Databases to appsettings and use IOptions 2021-05-25 15:03:27 -07:00
bcff9caf5c More documentation - almost all Models done
https://github.com/oqtane/oqtane.framework/issues/1382
Should not contain any code changes, just docs
2021-05-26 00:01:22 +02:00
bb92011641 Merge pull request #1398 from cnurse/dev
Merge AppVersions Information into __EFMigrationsHistory table
2021-05-25 17:46:19 -04:00
9c0cef870c Merge AppVersions information into EFMigrationsHistory table 2021-05-25 12:17:44 -07:00
077b866e8c Move Migrations into Master and Tenant folders so its clear what type of Migration is being applied 2021-05-25 12:16:10 -07:00
7ec3376308 Merge pull request #1397 from cnurse/dev
Fix Upgrade issue with new componentized Database projects
2021-05-25 07:39:24 -04:00
7753988f64 Remove log files from git 2021-05-24 16:02:45 -07:00
a62a1be1f3 Merge branch 'dev' of https://github.com/cnurse/oqtane.framework into dev
# Conflicts:
#	Oqtane.Server/Infrastructure/DatabaseManager.cs
2021-05-24 15:47:02 -07:00
f2ae1e3bff Merge commit '42265cdda4ea03541522bf5cf5d104059927393f' into dev
# Conflicts:
#	Oqtane.Server/Infrastructure/DatabaseManager.cs
2021-05-24 15:40:35 -07:00
a0838cbc84 3rd attempt to resolve conflicts 2021-05-24 15:14:58 -07:00
17f5f39a54 2nd attempt to Fix conflict 2021-05-24 15:12:41 -07:00
4853a64cf9 Fix conflict 2021-05-24 15:08:44 -07:00
69376c5baf Missing files from previous commit 2021-05-24 14:46:54 -07:00
42265cdda4 Fix Upgrade issue with new componentized Database projects 2021-05-24 14:46:12 -07:00
6283fc9abb Merge pull request #1396 from sbwalker/dev
remove Microsoft.EntityFrameworkCore.Relational from Oqtane.Shared as it is no longer needed
2021-05-24 16:39:33 -04:00
723002968a remove Microsoft.EntityFrameworkCore.Relational from Oqtane.Shared as it is no longer needed 2021-05-24 16:43:17 -04:00
1f7207bd5a Merge pull request #1395 from sbwalker/dev
fix #1272 - add support for ref folder in package installation
2021-05-24 15:51:02 -04:00
72b06b16cf fix #1272 - add support for ref folder in package installation 2021-05-24 15:50:38 -04:00
8c70913a72 Merge pull request #1394 from cnurse/dev
Rename IOqtaneDatabase interface (and related base class)
2021-05-24 15:19:33 -04:00
4a609b444e Rename IOqtaneDatabase interface (and related base class) 2021-05-24 11:57:47 -07:00
791e786db8 Update appsettings.release.json 2021-05-24 09:03:57 -04:00
e5229626ab Update appsettings.json 2021-05-24 09:03:04 -04:00
34ed19deda Merge pull request #1393 from sbwalker/dev
added PackageName property to IModule and ITheme interfaces to allow creators to specify the Nuget package name associated to a specific module/theme. This is necessary for packages which contain multiple extensions.
2021-05-24 08:55:57 -04:00
5c21ab37ee added PackageName property to IModule and ITheme interfaces to allow creators to specify the Nuget package name associated to a specific module/theme. This is necessary for packages which contain multiple extensions. 2021-05-24 08:59:50 -04:00
41ed069072 fix #1389 - exception in PrincipalValidator 2021-05-24 08:17:46 -04:00
80c82a9ec3 Merge pull request #1392 from sbwalker/dev
fix #1389 - exception in PrincipalValidator
2021-05-24 08:14:56 -04:00
d69ceecb41 Merge pull request #1388 from sbwalker/dev
implemented Label component in Installer for consistency and removed redundant logic
2021-05-23 11:13:42 -04:00
63378e1654 implemented Label component in Installer for consistency and removed redundant logic 2021-05-23 11:17:23 -04:00
3f48c1f8fe fix #1367 - provides support for multiple entities in auth policy and makes parameter names more intuitive - backward compatible with entityid 2021-05-23 10:29:05 -04:00
35aaf476d0 Merge pull request #1387 from sbwalker/dev
fix #1367 - provides support for multiple entities in auth policy and makes parameter names more intuitive - backward compatible with entityid
2021-05-23 10:25:17 -04:00
fc3ba14d0f Merge pull request #1385 from 2sic-forks/dev 2021-05-22 08:25:06 -04:00
76879163d4 Merge pull request #1386 from cnurse/dev 2021-05-22 08:21:26 -04:00
342dae4aa7 Fix bug with MySQL Installation caused by new Migration 2021-05-21 16:38:48 -07:00
f533279dec Merge branch 'oqtane:dev' into dev 2021-05-21 18:30:03 +02:00
1bccf449fc Add docs to 2 core interfaces IAliasService and IFileService 2021-05-21 18:29:25 +02:00
de6acf0029 Tell all concrete implementations to be private / not show in the docs 2021-05-21 18:29:06 +02:00
074b998bbc Document most models 2021-05-21 18:28:21 +02:00
e4b12aa87f Document most core interfaces 2021-05-21 18:28:14 +02:00
6f123c0fff Merge pull request #1383 from cnurse/dev
Convert Database projects so they build installable Packages
2021-05-20 17:04:06 -04:00
47c04dc150 Convert Database projects so they build installable Packages rather than deploy to bin and modify installation to deploy Databases on demand as needed. 2021-05-20 12:39:09 -07:00
4474d49c6a Merge pull request #1379 from sbwalker/dev
fix #1359 - Image bug in src attribute - image is not displayed - caused by multi-tenancy refactoring
2021-05-19 13:28:48 -04:00
0b6efdbc57 fix #1359 - Image bug in src attribute - image is not displayed - caused by multi-tenancy refactoring 2021-05-19 13:32:34 -04:00
6f981e0928 revert unnecessary code change in router 2021-05-19 09:15:05 -04:00
bc53c9e443 Merge pull request #1374 from sbwalker/dev
revert unnecessary code change in router
2021-05-19 09:13:06 -04:00
9256c88fc4 update template to remove ASP.NET Core 3 reference 2021-05-19 09:08:59 -04:00
9d877bc072 Merge pull request #1373 from sbwalker/dev
update template to remove ASP.NET Core 3 reference
2021-05-19 09:05:16 -04:00
470c7bb2a9 Merge pull request #1355 from hishamco/folder-columns
Change Folder Name & Path length
2021-05-19 08:50:56 -04:00
bb195e4796 Merge pull request #1357 from hishamco/database-type
Database type name should use type AssemblyQualifiedName instead of magic string
2021-05-19 08:44:12 -04:00
4d59460fbb Merge pull request #1358 from hishamco/unit-tests
Fix broken unit tests
2021-05-19 08:42:59 -04:00
ddc4254053 Merge pull request #1372 from sbwalker/dev
auth improvements related to multi-tenancy
2021-05-19 08:42:50 -04:00
09537ab0e4 auth improvements related to multi-tenancy 2021-05-19 08:46:02 -04:00
07e8215919 Fix broken unit tests 2021-05-16 22:54:34 +03:00
2324ae1ccb Database type name should use type AssemblyQualifiedName instead of maigic string 2021-05-16 22:50:39 +03:00
b3d367329e Fix migration version name 2021-05-16 21:47:34 +03:00
795e71278b Change Folder Name & Path length 2021-05-16 20:28:41 +03:00
943adec3a0 fixes for running on WebAssembly 2021-05-13 07:58:57 -04:00
6e21eb7327 Merge pull request #1347 from sbwalker/dev
fixes for running on WebAssembly
2021-05-13 07:55:31 -04:00
fe3b42feed Merge pull request #1345 from cnurse/dev
Remove DbConfig and new constructors on DbContextBase and refactor Migrations
2021-05-12 20:20:02 -04:00
c036a9d11f Remove DbConfig and new constructors on DbContextBase and refactor Migrations to use explcit generation of IOqtaneDatabase instance 2021-05-12 15:17:40 -07:00
c958f90ee2 Merge pull request #1342 from cnurse/dev
Switch DBType to use the fully-qualified type name rather than a simple name
2021-05-12 14:41:47 -04:00
92dd8354ba Switch DBType to use the fully-qualified type name rather than a simple name 2021-05-12 10:22:17 -07:00
c926fa0d27 Merge pull request #1340 from cnurse/dev 2021-05-11 18:39:40 -04:00
4ffdcf1e52 Remove dependency of Oqtane.Server on SqlClient 2021-05-11 13:56:49 -07:00
efe803f147 Merge branch 'oqtane:dev' into dev 2021-05-11 13:29:23 -07:00
0bd6fea476 Merge pull request #1339 from sbwalker/dev
added DatabaseService to get list of database types from server
2021-05-11 15:53:19 -04:00
bae6120e3b added DatabaseService to get list of database types from server 2021-05-11 15:56:41 -04:00
7c0721797c Update README.md 2021-05-11 09:07:19 -04:00
d88bd87aea add support for Blazor pre-rendering 2021-05-11 09:03:05 -04:00
dab94defa0 Merge pull request #1338 from sbwalker/dev
add support for Blazor pre-rendering
2021-05-11 08:59:39 -04:00
6eaa3d259e Merge pull request #1337 from sbwalker/dev
optimizing tenant resolution and routing
2021-05-11 08:37:44 -04:00
d88b6987a1 Update README.md 2021-05-11 08:35:27 -04:00
b003e3734d Update README.md 2021-05-11 08:33:49 -04:00
a5de639d15 optimizing tenant resolution and routing 2021-05-10 17:45:39 -04:00
75c336454b Merge pull request #1323 from hishamco/tiny-fixes
Few Enhancements in Language Management
2021-05-07 12:17:20 -04:00
15b0bed257 fix #1319 - module title is required 2021-05-07 12:14:54 -04:00
7b15c0a542 Merge pull request #1325 from sbwalker/dev
fix #1319 - module title is required
2021-05-07 12:11:33 -04:00
9f3b6197fe Fix the UX for add a new language 2021-05-07 14:32:41 +03:00
4c4553a1d1 Ability to delete a default language if there's one language alongside with English 2021-05-07 14:10:15 +03:00
c5f65e4767 Fix IsDefault for English 2021-05-07 13:55:45 +03:00
d86beecf5c Add default language by default 2021-05-07 13:43:21 +03:00
be38d2cd70 add upgrade support for language packages 2021-05-06 08:20:09 -04:00
5a511e63ad Merge pull request #1320 from sbwalker/dev
add upgrade support for language packages
2021-05-06 08:16:52 -04:00
38aebf5aff Allow installation of Language packages through Language Management 2021-05-05 19:03:06 -04:00
6625fa7c23 Merge pull request #1318 from sbwalker/dev
Allow installation of Language packages through Language Management
2021-05-05 18:59:46 -04:00
3d5a7fdc21 Merge branch 'dev' of https://github.com/cnurse/oqtane.framework into dev 2021-05-05 10:13:05 -07:00
e836e27a5a fix #1305 - tabpanel loses focus 2021-05-03 08:19:25 -04:00
71e004aca6 Merge pull request #1310 from sbwalker/dev
fix #1305 - tabpanel loses focus
2021-05-03 08:17:59 -04:00
42c4b351f3 Merge pull request #95 from oqtane/dev
sync
2021-05-02 09:21:15 -04:00
3f28282ab6 Merge pull request #1261 from hishamco/sql-server
Move SqlServer databases into a separate project
2021-05-02 09:20:43 -04:00
d83eac5af3 Merge remote-tracking branch 'upstream/dev' into sql-server 2021-05-01 21:36:08 +03:00
4dc364b623 Merge pull request #1304 from leigh-pointer/NuGetPackageIcon
NuGet Icon Warning
2021-04-30 10:55:31 -04:00
d1abbde20c Merge pull request #1303 from leigh-pointer/AddModulePermissions
Permissions for edit applied from the page permissions.
2021-04-30 10:55:22 -04:00
2be48e910e Set module view permissions to page edit permissions 2021-04-30 14:15:56 +02:00
a310ac4ed6 Merge remote-tracking branch 'origin/AddModulePermissions' into AddModulePermissions 2021-04-30 13:53:07 +02:00
bc2e3a7333 Permissions for edit are applied from the page permissions. 2021-04-30 13:52:50 +02:00
9180ceaeb7 Merge pull request #94 from oqtane/dev
sync
2021-04-30 07:36:10 -04:00
b710ac8705 Merge pull request #1301 from leigh-pointer/AddModulePermissions
Permissions for edit are applied from the page permissions.
2021-04-30 07:35:37 -04:00
2244fcc814 Merge pull request #1259 from hishamco/more-localization
Localize Page Template Module Titles
2021-04-30 07:35:21 -04:00
c02e83547b Merge pull request #1302 from leigh-pointer/AddPageWarning
Check now displays warning instead of an Error
2021-04-30 07:32:12 -04:00
14801e7f0d NuGet Icon Warning
The IconURL is depreciate in favor of an embedded icon file in the package
2021-04-30 10:50:12 +02:00
c36cd77ab4 Check now displays warning instead of an Error 2021-04-30 09:49:51 +02:00
9e814fad64 Permissions for edit are applied from the page permissions. 2021-04-30 09:36:50 +02:00
18aa8d48fe Use SharedResources 2021-04-29 02:44:41 +03:00
d7966f2acf Revert changes in SiteRepository 2021-04-29 02:42:01 +03:00
9d3b460a6c Merge branch 'dev' of https://github.com/oqtane/oqtane.framework into dev 2021-04-28 15:14:36 -07:00
7296f94360 Merge remote-tracking branch 'upstream/dev' into dev 2021-04-28 15:08:00 -07:00
9037f49680 Merge pull request #1294 from cnurse/dev
Adding new DatabaseConfig components in the Client project for supported Databases
2021-04-28 15:30:22 -04:00
c759d22b3d Merge pull request #1295 from leigh-pointer/dev
Make container fluid to fill the pane
2021-04-28 15:25:53 -04:00
6ae5777e1c Revert "Fix NuGet Warning NU5048"
This reverts commit cad5ce51fc.
2021-04-28 20:31:52 +02:00
cad5ce51fc Fix NuGet Warning NU5048
Icon URL is deprecated in favor of embedding the icon inside the NuGet package.
2021-04-28 09:17:01 +02:00
a79ba591fe Make container fluid to fill the pane 2021-04-28 07:01:39 +02:00
1efd623a99 Adding new DatabaseConfig components in the Client project for supported Databases to avoid deploying server dlls to client 2021-04-27 15:35:10 -07:00
c32003320d Merge pull request #1284 from iJungleboy/dev
Make version static-readonly
2021-04-27 07:56:28 -04:00
307f97c60c Merge pull request #1281 from hishamco/localization-manager
LocalizationManager enhancements
2021-04-27 07:51:01 -04:00
722c234cb5 Merge pull request #1285 from leigh-pointer/dev.fork
Fix Error with Footer on Oqtane Theme #1282
2021-04-27 07:50:36 -04:00
189dcf5b90 Fix for Error with Footer on Oqtane Theme #1282 2021-04-26 15:38:33 +02:00
a531a235a0 Make version static-readonly
https://github.com/oqtane/oqtane.framework/issues/1283
2021-04-26 13:51:37 +02:00
d7569aa41a Merge pull request #3 from oqtane/dev
Merge with Fork
2021-04-26 11:31:42 +02:00
55b69f0afc Order the cultures alphabatically 2021-04-26 01:55:55 +03:00
76c2a2f2f9 Add English to supported cultures by default 2021-04-26 01:55:27 +03:00
d42c7a5ea5 user and role management improvements 2021-04-24 13:47:20 -04:00
60f386838e Merge pull request #1280 from sbwalker/dev
user and role management improvements
2021-04-24 13:44:22 -04:00
5a519510a9 improve comments and logging 2021-04-23 16:29:18 -04:00
a3449f5beb Merge pull request #1275 from sbwalker/dev
improve comments and logging
2021-04-23 16:26:22 -04:00
ffafe5d14c Merge pull request #93 from oqtane/dev
sync
2021-04-23 16:10:06 -04:00
9d083726be user and role management improvements 2021-04-23 16:11:35 -04:00
310c782326 Merge pull request #1274 from sbwalker/dev
user and role management improvements
2021-04-23 16:09:19 -04:00
65aac34f8c Merge pull request #1267 from sbwalker/dev
modify nuget package installer to support satellite assemblies in subfolders
2021-04-23 09:34:17 -04:00
856b11b574 Merge pull request #1265 from hishamco/language-switcher
Add English to the language switcher
2021-04-23 09:34:01 -04:00
1088e9adda Merge pull request #1264 from hishamco/language-management-exculde
Exclude English from the languages list
2021-04-23 09:33:22 -04:00
7470513892 Merge pull request #2 from oqtane/dev
Updates
2021-04-22 05:49:01 +02:00
2780e4d029 modify nuget package installer to support satellite assemblies in subfolders 2021-04-21 19:31:02 -04:00
c52f6c92f1 Add English to the language switcher 2021-04-21 17:47:50 +03:00
3cafb68f6b Localize module titles in UI 2021-04-21 17:31:22 +03:00
6e5496e969 Exclude English from the languages list 2021-04-21 16:41:24 +03:00
f9f0999315 Merge pull request #1260 from hishamco/supported-cultures
Remove supported cultures entry from appsettings.json
2021-04-20 17:14:21 -04:00
e7c5ca2c1a Merge pull request #1254 from chlupac/alias
Default Alias
2021-04-20 17:07:20 -04:00
d2ffb93fa4 Merge pull request #92 from oqtane/dev
sync
2021-04-20 14:13:41 -04:00
c4680c1972 Merge pull request #1262 from cnurse/dev
Fix bug with installs due to missing Migrations
2021-04-20 14:13:09 -04:00
8f5beaf3fe Fix bug with installs due to missing Migrations 2021-04-20 10:58:26 -07:00
97fb6ede7e Reuse AddOqtaneScopedServices() 2021-04-20 19:10:06 +03:00
f7d8888232 Refactor Program.cs 2021-04-20 19:01:56 +03:00
e7f5fe9827 Merge branch 'database' into clean-startup
# Conflicts:
#	Oqtane.Server/Startup.cs
2021-04-20 17:42:39 +03:00
35225b2bb9 Move SqlServer databases into a separate project 2021-04-20 17:29:24 +03:00
0b32dcf9b3 Make English default culture instead of current installed culture 2021-04-20 16:27:18 +03:00
e33c9e417d Remove SupportedCultures entry from appsettings.json 2021-04-20 16:26:17 +03:00
9d9a5a0275 Localize modules title 2021-04-20 16:20:24 +03:00
8069d838d5 Merge pull request #4 from oqtane/master
sync
2021-04-20 08:50:44 -04:00
a5494be8f1 Merge pull request #1 from oqtane/dev
Update to 2.0.2
2021-04-20 06:44:31 +02:00
bc6be52ae7 Merge pull request #91 from oqtane/dev
sync
2021-04-19 21:13:47 -04:00
40f8436947 Merge pull request #1239 from cnurse/dev
Implement Database Migrations and add Multi-Database Support
2021-04-19 21:11:11 -04:00
13f7db7b19 Resolve conflict 2021-04-19 11:57:45 -07:00
6b204cc988 Second attempt to resolve conflcit in Server project 2021-04-19 11:56:51 -07:00
a11d30b1e4 Attempt to resolve conflict in Server project file 2021-04-19 11:53:45 -07:00
2402cab3f3 Attempt to resolve conflict in DatabaseManager 2021-04-19 11:50:25 -07:00
20b5a10882 Resolve name of variable in AddSite 2021-04-19 11:15:53 -07:00
4598d0d6ba Resolve conflict in AddSite 2021-04-19 11:13:10 -07:00
7516be41d9 Update README.md 2021-04-19 14:10:13 -04:00
74c403cef1 Merge remote-tracking branch 'upstream/dev' into dev 2021-04-19 11:08:35 -07:00
468f186511 Default Alias
When alias is not found in alias table, Oqtane fails with exception. This solution allows define default alias (*) for default site with unknown alias.
2021-04-19 20:03:18 +02:00
5455fadd65 Merge pull request #90 from oqtane/dev
sync
2021-04-19 09:33:34 -04:00
58ac86db8d Merge pull request #1253 from oqtane/master
Merge pull request #1252 from oqtane/dev
2021-04-19 09:28:24 -04:00
7417e8dd27 Merge branch 'dev' of https://github.com/oqtane/oqtane.framework into dev 2021-04-18 02:29:39 +03:00
a018e853a8 Register configuration in startup 2021-04-18 02:27:31 +03:00
73b13d7a54 Add Oqtane extension methods for clean startup 2021-04-18 02:25:40 +03:00
096f8249c3 Resolve conflict in Constants 2021-04-14 16:05:59 -07:00
e11a65c6ca Rollback removal of script files for Html Module to resolve conflict 2021-04-14 16:03:00 -07:00
23bab67bd3 Resolve new conflict 2021-04-14 15:59:31 -07:00
713b19e0e9 Remove Html Module Script files 2021-04-14 15:57:07 -07:00
f8b607911d Resolve conflict in project file 2021-04-14 15:53:07 -07:00
0222bbdeae Resolve conflict with main Oqtane repo 2021-04-14 15:48:54 -07:00
a0329b1b6c Fix bug in new Migration to support PostgreSQL 2021-04-08 14:58:02 -07:00
8c45b7e42f Added support for migrating existing Oqtane installations from DbUp to Migrations. Also added a Migration for version 2.0.2, and set current version to 2.1.0 2021-04-08 12:20:21 -07:00
d12b18350f Fixed bug in AddSite due to missing assignment of database type 2021-04-02 11:49:38 -07:00
985987d8f4 Updated initial appsettings.json 2021-04-02 11:28:34 -07:00
e6530ee127 Added support for MySQL and ProgreSQL and AddSite/Tenant 2021-04-02 10:55:00 -07:00
8da55e1926 Merge pull request #3 from oqtane/dev
sync
2021-04-01 19:31:21 -04:00
2fb63e8117 Added suuport to inject an IOqtaneDatabase in EntityBuilders to allow each Database to control certain Migration behaviors. Also updated Installer to dynamically build Database Configuration section 2021-03-27 11:16:16 -07:00
3a032f401a Added IDatabase interface and refactored to use it to handle database type - updated Installer to dynamically add databases to selector 2021-03-24 11:45:44 -07:00
cbcfc88492 Add support for Sqlite - Installation path tested but AddSite not supported yet 2021-03-23 11:06:18 -07:00
8f1c760e87 Updated the Installation of Oqtane to use Migrations 2021-03-21 14:52:45 -07:00
83e5502111 Creation of EF Core Migrations - these execute using EF Tools, but are not integrated to run programmatically 2021-03-19 17:01:49 -07:00
12b44f6fa5 Merge pull request #2 from oqtane/dev
sync
2021-02-23 18:50:55 -05:00
8cd355135b Merge pull request #1 from oqtane/dev
sync
2021-02-15 13:49:36 -05:00
690 changed files with 48014 additions and 25138 deletions

15
.gitignore vendored
View File

@ -7,12 +7,23 @@ msbuild.binlog
.vscode/
*.binlog
*.nupkg
*.zip
*.idea
Oqtane.Server/appsettings.json
Oqtane.Server/Data/*.mdf
Oqtane.Server/Data/*.ldf
Oqtane.Server/Data
/Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml
Oqtane.Server/Content
Oqtane.Server/Packages
Oqtane.Server/wwwroot/Content
Oqtane.Server/wwwroot/Packages/*.log
Oqtane.Server/wwwroot/Modules
!Oqtane.Server/wwwroot/Modules/Oqtane.Modules.*
!Oqtane.Server/wwwroot/Modules/Templates
Oqtane.Server/wwwroot/Themes
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
!Oqtane.Server/wwwroot/Themes/Templates

View File

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

View File

@ -1,4 +1,6 @@
@inject IInstallationService InstallationService
@inject IJSRuntime JSRuntime
@inject SiteState SiteState
@if (_initialized)
{
@ -10,33 +12,71 @@
{
@if (string.IsNullOrEmpty(_installation.Message))
{
<CascadingAuthenticationState>
<CascadingValue Value="@PageState">
<SiteRouter OnStateChange="@ChangeState" />
</CascadingValue>
</CascadingAuthenticationState>
<div style="@_display">
<CascadingAuthenticationState>
<CascadingValue Value="@PageState">
<SiteRouter Runtime="@Runtime" RenderMode="@RenderMode" VisitorId="@VisitorId" OnStateChange="@ChangeState" />
</CascadingValue>
</CascadingAuthenticationState>
</div>
}
else
{
<div class="app-alert">
@_installation.Message
</div>
</div>
}
}
}
@code {
private Installation _installation;
private bool _initialized;
[Parameter]
public string AntiForgeryToken { get; set; }
private PageState PageState { get; set; }
[Parameter]
public string Runtime { get; set; }
[Parameter]
public string RenderMode { get; set; }
[Parameter]
public int VisitorId { get; set; }
[Parameter]
public string RemoteIPAddress { get; set; }
[Parameter]
public string AuthorizationToken { get; set; }
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;
protected override async Task OnParametersSetAsync()
{
_installation = await InstallationService.IsInstalled();
if (_installation.Alias != null)
{
SiteState.Alias = _installation.Alias;
}
_initialized = true;
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_display = "";
StateHasChanged();
}
}
private void ChangeState(PageState pageState)
{
PageState = pageState;

View File

@ -1,3 +1,5 @@
using Microsoft.Extensions.Localization;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Localization;
[assembly: RootNamespace("Oqtane")]
[assembly: InternalsVisibleTo("Oqtane.Server")]

View File

@ -0,0 +1,22 @@
namespace Microsoft.Extensions.Localization
{
public static class OqtaneLocalizationExtensions
{
/// <summary>
/// Gets the string resource for the specified key and returns the value if the resource does not exist
/// </summary>
/// <param name="localizer"></param>
/// <param name="key">the static key used to identify the string resource</param>
/// <param name="value">the default value if the resource for the static key does not exist</param>
/// <returns></returns>
public static string GetString(this IStringLocalizer localizer, string key, string value)
{
string localizedValue = localizer[key];
if (localizedValue == key && !string.IsNullOrEmpty(value)) // not localized
{
localizedValue = value;
}
return localizedValue;
}
}
}

View File

@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components.Authorization;
using Oqtane.Providers;
using Oqtane.Services;
using Oqtane.Shared;
namespace Microsoft.Extensions.DependencyInjection
{
public static class OqtaneServiceCollectionExtensions
{
public static IServiceCollection AddOqtaneAuthorization(this IServiceCollection services)
{
services.AddAuthorizationCore();
services.AddScoped<IdentityAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<IdentityAuthenticationStateProvider>());
return services;
}
internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services)
{
services.AddScoped<SiteState>();
services.AddScoped<IInstallationService, InstallationService>();
services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>();
services.AddScoped<IThemeService, ThemeService>();
services.AddScoped<IAliasService, AliasService>();
services.AddScoped<ITenantService, TenantService>();
services.AddScoped<ISiteService, SiteService>();
services.AddScoped<IPageService, PageService>();
services.AddScoped<IModuleService, ModuleService>();
services.AddScoped<IPageModuleService, PageModuleService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IProfileService, ProfileService>();
services.AddScoped<IRoleService, RoleService>();
services.AddScoped<IUserRoleService, UserRoleService>();
services.AddScoped<ISettingService, SettingService>();
services.AddScoped<IPackageService, PackageService>();
services.AddScoped<ILogService, LogService>();
services.AddScoped<IJobService, JobService>();
services.AddScoped<IJobLogService, JobLogService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IFolderService, FolderService>();
services.AddScoped<IFileService, FileService>();
services.AddScoped<ISiteTemplateService, SiteTemplateService>();
services.AddScoped<ISqlService, SqlService>();
services.AddScoped<ISystemService, SystemService>();
services.AddScoped<ILocalizationService, LocalizationService>();
services.AddScoped<ILanguageService, LanguageService>();
services.AddScoped<IDatabaseService, DatabaseService>();
services.AddScoped<IUrlMappingService, UrlMappingService>();
services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>();
return services;
}
}
}

View File

@ -0,0 +1,32 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server" ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
@code {
private string _server = "(LocalDb)\\MSSQLLocalDB";
private string _database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database))
{
connectionString = $"Data Source={_server};AttachDbFilename=|DataDirectory|\\{_database}.mdf;Initial Catalog={_database};Integrated Security=SSPI;Encrypt=false;";
}
return connectionString;
}
}

View File

@ -0,0 +1,58 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server" ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port used to connect to the server" ResourceKey="Port">Port:</Label>
<div class="col-sm-9">
<input id="port" type="text" class="form-control" @bind="@_port" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uid" HelpText="Enter the username to use for the database" ResourceKey="Uid">User Id:</Label>
<div class="col-sm-9">
<input id="uid" type="text" class="form-control" @bind="@_uid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div>
</div>
@code {
private string _server = "127.0.0.1";
private string _port = "3306";
private string _database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
private string _uid = String.Empty;
private string _pwd = String.Empty;
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database) && !String.IsNullOrEmpty(_uid) && !String.IsNullOrEmpty(_pwd))
{
connectionString = $"Server={_server};Database={_database};Uid={_uid};Pwd={_pwd};";
}
if (!String.IsNullOrEmpty(_port))
{
connectionString += $"Port={_port};";
}
return connectionString;
}
}

View File

@ -0,0 +1,83 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
@inject IStringLocalizer<PostgreSQLConfig> Localizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server" ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port used to connect to the server" ResourceKey="Port">Port:</Label>
<div class="col-sm-9">
<input id="port" type="text" class="form-control" @bind="@_port" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="security" HelpText="Select your security method" ResourceKey="Security">Security:</Label>
<div class="col-sm-9">
<select id="security" class="form-select custom-select" @bind="@_security">
<option value="integrated" selected>@Localizer["Integrated"]</option>
<option value="custom">@Localizer["Custom"]</option>
</select>
</div>
</div>
@if (_security == "custom")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uid" HelpText="Enter the username to use for the database" ResourceKey="Uid">User Id:</Label>
<div class="col-sm-9">
<input id="uid" type="text" class="form-control" @bind="@_uid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div>
</div>
}
@code {
private string _server = "127.0.0.1";
private string _port = "5432";
private string _database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
private string _security = "integrated";
private string _uid = String.Empty;
private string _pwd = String.Empty;
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database) && !String.IsNullOrEmpty(_port))
{
connectionString = $"Server={_server};Port={_port};Database={_database};";
}
if (_security == "integrated")
{
connectionString += "Integrated Security=true;";
}
else
{
if (!String.IsNullOrEmpty(_uid) && !String.IsNullOrEmpty(_pwd))
{
connectionString += $"User ID={_uid};Password={_pwd};";
}
else
{
connectionString = String.Empty;
}
}
return connectionString;
}
}

View File

@ -0,0 +1,95 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
@inject IStringLocalizer<SqlServerConfig> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server" ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="Enter the name of the database" ResourceKey="Database">Database:</Label>
<div class="col-sm-9">
<input id="database" type="text" class="form-control" @bind="@_database" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="security" HelpText="Select your security method" ResourceKey="Security">Security:</Label>
<div class="col-sm-9">
<select id="security" class="form-select custom-select" @bind="@_security">
<option value="integrated" selected>@Localizer["Integrated"]</option>
<option value="custom">@Localizer["Custom"]</option>
</select>
</div>
</div>
@if (_security == "custom")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uid" HelpText="Enter the username to use for the database" ResourceKey="Uid">User Id:</Label>
<div class="col-sm-9">
<input id="uid" type="text" class="form-control" @bind="@_uid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pwd" HelpText="Enter the password to use for the database" ResourceKey="Pwd">Password:</Label>
<div class="col-sm-9">
<input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="encryption" HelpText="Specify if you are using an encrypted database connection. It is highly recommended to use encryption in a production environment." ResourceKey="Encryption">Encryption:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_encryption">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
@if (_encryption == "true")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption" ResourceKey="TrustServerCertificate">Certificate:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate">
<option value="true">@Localizer["Self Signed"]</option>
<option value="false">@Localizer["Verifiable"]</option>
</select>
</div>
</div>
}
@code {
private string _server = String.Empty;
private string _database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
private string _security = "integrated";
private string _uid = String.Empty;
private string _pwd = String.Empty;
private string _encryption = "false";
private string _trustservercertificate = "false";
public string GetConnectionString()
{
var connectionString = String.Empty;
if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database))
{
connectionString = $"Data Source={_server};Initial Catalog={_database};";
}
if (_security == "integrated")
{
connectionString += "Integrated Security=SSPI;";
}
else
{
connectionString += $"User ID={_uid};Password={_pwd};";
}
connectionString += $"Encrypt={_encryption};";
connectionString += $"TrustServerCertificate={_trustservercertificate};";
return connectionString;
}
}

View File

@ -0,0 +1,25 @@
@namespace Oqtane.Installer.Controls
@implements Oqtane.Interfaces.IDatabaseConfigControl
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the file name to use for the database" ResourceKey="Server">File Name:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
</div>
@code {
private string _server = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm") + ".db";
public string GetConnectionString()
{
var connectionstring = String.Empty;
if (!String.IsNullOrEmpty(_server))
{
connectionstring = $"Data Source={_server};";
}
return connectionstring;
}
}

View File

@ -0,0 +1,244 @@
@namespace Oqtane.Installer
@using Oqtane.Interfaces
@inject NavigationManager NavigationManager
@inject IInstallationService InstallationService
@inject ISiteService SiteService
@inject IUserService UserService
@inject IDatabaseService DatabaseService
@inject IJSRuntime JSRuntime
@inject IStringLocalizer<Installer> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row">
<div class="mx-auto text-center">
<img src="oqtane-black.png" />
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version</div>
</div>
</div>
<hr class="app-rule" />
<div class="row justify-content-center">
<div class="col text-center">
<h2>@Localizer["DatabaseConfig"]</h2><br />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="Select the type of database you wish to use" ResourceKey="DatabaseType">Database:</Label>
<div class="col-sm-9">
@if (_databases != null)
{
<select id="databasetype" class="form-select custom-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))">
@foreach (var database in _databases)
{
if (database.IsDefault)
{
<option value="@database.Name" selected>@Localizer[@database.Name]</option>
}
else
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
}
</select>
}
</div>
</div>
@{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent;
}
}
</div>
</div>
<div class="col text-center">
<h2>@Localizer["ApplicationAdmin"]</h2><br />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Provide a username for the primary user accountt" ResourceKey="Username">Username:</Label>
<div class="col-sm-9">
<input id="username" type="text" class="form-control" @bind="@_hostUsername" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Provide the email address for the host user account" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
<input type="text" class="form-control" @bind="@_hostEmail" />
</div>
</div>
</div>
</div>
</div>
<hr class="app-rule" />
<div class="row">
<div class="mx-auto text-center">
<button type="button" class="btn btn-success" @onclick="Install">@Localizer["InstallNow"]</button><br /><br />
<ModuleMessage Message="@_message" Type="MessageType.Error"></ModuleMessage>
</div>
<div class="app-progress-indicator" style="@_loadingDisplay"></div>
</div>
<div class="row">
<div class="mx-auto text-center">
<input type="checkbox" @bind="@_register" /> @Localizer["Register"]
</div>
</div>
</div>
@code {
private List<Database> _databases;
private string _databaseName;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private string _hostUsername = string.Empty;
private string _hostPassword = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty;
private bool _register = true;
private string _message = string.Empty;
private string _loadingDisplay = "display: none;";
protected override async Task OnInitializedAsync()
{
_togglepassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault))
{
_databaseName = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databaseName = "LocalDB";
}
LoadDatabaseConfigComponent();
}
private void DatabaseChanged(ChangeEventArgs eventArgs)
{
try
{
_databaseName = (string)eventArgs.Value;
LoadDatabaseConfigComponent();
}
catch
{
_message = Localizer["Error.DbConfig.Load"];
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (database != null)
{
_databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder =>
{
builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent();
};
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var interop = new Interop(JSRuntime);
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.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head");
}
}
private async Task Install()
{
var connectionString = String.Empty;
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
{
if (await UserService.ValidatePasswordAsync(_hostPassword))
{
_loadingDisplay = "";
StateHasChanged();
Uri uri = new Uri(NavigationManager.Uri);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var config = new InstallConfig
{
DatabaseType = database.DBType,
ConnectionString = connectionString,
Aliases = uri.Authority,
HostUsername = _hostUsername,
HostPassword = _hostPassword,
HostEmail = _hostEmail,
HostName = _hostUsername,
TenantName = TenantNames.Master,
IsNewTenant = true,
SiteName = Constants.DefaultSite,
Register = _register
};
var installation = await InstallationService.Install(config);
if (installation.Success)
{
NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true);
}
else
{
_message = installation.Message;
_loadingDisplay = "display: none;";
}
}
else
{
_message = Localizer["Message.Password.Invalid"];
}
}
else
{
_message = Localizer["Message.Require.DbInfo"];
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -2,6 +2,7 @@
@inherits ModuleBase
@inject IPageService PageService
@inject IUserService UserService
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row">
@foreach (var p in _pages)
@ -11,7 +12,7 @@
string url = NavigateUrl(p.Path);
<div class="col-md-2 mx-auto text-center">
<NavLink class="nav-link" href="@url" Match="NavLinkMatch.All">
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>@p.Name
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>@SharedLocalizer[p.Name]
</NavLink>
</div>
}
@ -20,7 +21,7 @@
@code {
private List<Page> _pages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override void OnInitialized()

View File

@ -11,7 +11,7 @@
Module module = await ModuleService.GetModuleAsync(ModuleState.ModuleId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
string message = Localizer["A Problem Was Encountered Loading Module {0}", module.ModuleDefinitionName];
string message = string.Format(Localizer["Error.Module.Load"], module.ModuleDefinitionName);
AddModuleMessage(message, MessageType.Error);
}

View File

@ -5,58 +5,64 @@
@inject IFileService FileService
@inject IFolderService FolderService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Upload" Heading="Upload Files" ResourceKey="UploadFiles">
<table class="table table-borderless">
<tr>
<td>
<Label For="upload" HelpText="Upload the file you want" ResourceKey="Upload">Upload: </Label>
</td>
<td>
<FileManager UploadMultiple="true" ShowFiles="false" FolderId="@_folderId" />
</td>
</tr>
</table>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="upload" HelpText="Upload the file you want" ResourceKey="Upload">Upload: </Label>
<div class="col-sm-9">
<FileManager UploadMultiple="true" ShowFiles="false" FolderId="@_folderId" ShowSuccess="true" />
</div>
</div>
</div>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="Download" Heading="Download Files" ResourceKey="DownloadFiles">
@if (_folders != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="url" HelpText="Enter the url of the file you wish to download" ResourceKey="Url">Url: </Label>
</td>
<td>
<input id="url" class="form-control" @bind="@url" />
</td>
</tr>
<tr>
<td>
<Label For="folder" HelpText="Select the folder to save the file in" ResourceKey="Folder">Folder: </Label>
</td>
<td>
<select id="folder" class="form-control" @bind="@_folderId">
<option value="-1">&lt;@Localizer["Select Folder"]&gt;</option>
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="Download">@Localizer["Download"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<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="Enter the url of the file you wish to download" ResourceKey="Url">Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="folder" HelpText="Select the folder to save the file in" ResourceKey="Folder">Folder: </Label>
<div class="col-sm-9">
<select id="folder" class="form-select" @bind="@_folderId" required>
<option value="-1">&lt;@Localizer["Folder.Select"]&gt;</option>
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the name of the file being downloaded" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="Download">@SharedLocalizer["Download"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
}
</TabPanel>
</TabStrip>
@code {
private string url = string.Empty;
private ElementReference form;
private bool validated = false;
private string _url = string.Empty;
private List<Folder> _folders;
private int _folderId = -1;
private string _name = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -72,37 +78,48 @@
private async Task Download()
{
if (url == string.Empty || _folderId == -1)
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
AddModuleMessage(Localizer["You Must Enter A Url And Select A Folder"], MessageType.Warning);
return;
}
if (_url == string.Empty || _folderId == -1)
{
AddModuleMessage(Localizer["Message.Required.UrlFolder"], MessageType.Warning);
return;
}
var filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
if (string.IsNullOrEmpty(_name))
{
_name = _url.Substring(_url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
if (!Constants.UploadableFiles.Split(',')
.Contains(Path.GetExtension(filename).ToLower().Replace(".", "")))
{
AddModuleMessage(Localizer["File Could Not Be Downloaded From Url Due To Its File Extension"], MessageType.Warning);
return;
}
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(_name).ToLower().Replace(".", "")))
{
AddModuleMessage(Localizer["Message.Download.InvalidExtension"], MessageType.Warning);
return;
}
if (!filename.IsPathOrFileValid())
{
AddModuleMessage(Localizer["You Must Enter A Url With A Valid File Name"], MessageType.Warning);
return;
}
if (!_name.IsPathOrFileValid())
{
AddModuleMessage(Localizer["Message.Required.UrlName"], MessageType.Warning);
return;
}
try
{
await FileService.UploadFileAsync(url, _folderId);
await logger.LogInformation("File Downloaded Successfully From Url {Url}", url);
AddModuleMessage(Localizer["File Downloaded Successfully From Url"], MessageType.Success);
try
{
await FileService.UploadFileAsync(_url, _folderId, _name);
await logger.LogInformation("File Downloaded Successfully From Url {Url}", _url);
AddModuleMessage(Localizer["Success.Download.File"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading File From Url {Url} {Error}", _url, ex.Message);
AddModuleMessage(Localizer["Error.Download.InvalidUrl"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Downloading File From Url {Url} {Error}", url, ex.Message);
AddModuleMessage(Localizer["Error Downloading File From Url. Please Verify That The Url Is Valid."], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}
}

View File

@ -4,52 +4,58 @@
@inject IFolderService FolderService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Details> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_folders != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label for="name" HelpText="The name of the file" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="parent" HelpText="The folder where the file is located" ResourceKey="Folder">Folder: </Label>
</td>
<td>
<select id="parent" class="form-control" @bind="@_folderId">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label for="size" HelpText="The size of the file (in bytes)" ResourceKey="Size">Size: </Label>
</td>
<td>
<input id="size" class="form-control" @bind="@_size" readonly />
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveFile">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdBy" CreatedOn="@_createdOn" ModifiedBy="@_modifiedBy" ModifiedOn="@_modifiedOn"></AuditInfo>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="The name of the file" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parent" HelpText="The folder where the file is located" ResourceKey="Folder">Folder: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_folderId" required>
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="A description of the file. This can be used as a caption for image files." ResourceKey="Description">Description: </Label>
<div class="col-sm-9">
<input id="description" class="form-control" @bind="@_description" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="size" HelpText="The size of the file (in bytes)" ResourceKey="Size">Size: </Label>
<div class="col-sm-9">
<input id="size" class="form-control" @bind="@_size" readonly />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="SaveFile">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdBy" CreatedOn="@_createdOn" ModifiedBy="@_modifiedBy" ModifiedOn="@_modifiedOn"></AuditInfo>
</form>
}
@code {
private ElementReference form;
private bool validated = false;
private int _fileId = -1;
private string _name;
private List<Folder> _folders;
private int _folderId = -1;
private string _description = string.Empty;
private int _size;
private string _createdBy;
private DateTime _createdOn;
@ -71,6 +77,7 @@
{
_name = file.Name;
_folderId = file.FolderId;
_description = file.Description;
_size = file.Size;
_createdBy = file.CreatedBy;
_createdOn = file.CreatedOn;
@ -81,32 +88,42 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading File {FileId} {Error}", _fileId, ex.Message);
AddModuleMessage(Localizer["Error Loading File"], MessageType.Error);
AddModuleMessage(Localizer["Error.File.Load"], MessageType.Error);
}
}
private async Task SaveFile()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_name.IsPathOrFileValid())
try
{
File file = await FileService.GetFileAsync(_fileId);
file.Name = _name;
file.FolderId = _folderId;
file = await FileService.UpdateFileAsync(file);
await logger.LogInformation("File Saved {File}", file);
NavigationManager.NavigateTo(NavigateUrl());
if (_name.IsPathOrFileValid())
{
File file = await FileService.GetFileAsync(_fileId);
file.Name = _name;
file.FolderId = _folderId;
file.Description = _description;
file = await FileService.UpdateFileAsync(file);
await logger.LogInformation("File Saved {File}", file);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.File.InvalidName"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["File Name Not Valid"], MessageType.Warning);
await logger.LogError(ex, "Error Saving File {FileId} {Error}", _fileId, ex.Message);
AddModuleMessage(Localizer["Error.File.Save"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving File {FileId} {Error}", _fileId, ex.Message);
AddModuleMessage(Localizer["Error Saving File"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}
}

View File

@ -4,49 +4,80 @@
@inject IFileService FileService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_folders != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
</td>
<td>
<select id="parent" class="form-control" @bind="@_parentId">
<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="parent" HelpText="Select the parent folder" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @bind="@_parentId" required>
@if (PageState.QueryString.ContainsKey("id"))
{
<option value="-1">&lt;@Localizer["NoParent"]&gt;</option>
}
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="Select the folder type. Private folders are only accessible by authorized users. Public folders can be accessed by all users" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
@if (PageState.QueryString.ContainsKey("id"))
{
<option value="-1">&lt;@Localizer["No Parent"]&gt;</option>
<input id="type" class="form-control" readonly @bind="@_type" />
}
@foreach (Folder folder in _folders)
else
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
<select id="type" class="form-select" @bind="@_type" required>
<option value="@FolderTypes.Private">@Localizer[FolderTypes.Private]</option>
<option value="@FolderTypes.Public">@Localizer[FolderTypes.Public]</option>
</select>
}
</select>
</td>
</tr>
<tr>
<td>
<Label for="name" HelpText="Enter the folder name" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td colspan="2" align="center">
<Label For="permissions" HelpText="Select the permissions you want for the folder" ResourceKey="Permissions">Permissions: </Label>
<PermissionGrid EntityName="@EntityNames.Folder" PermissionNames="@(PermissionNames.Browse + "," + PermissionNames.View + "," + PermissionNames.Edit)" Permissions="@_permissions" @ref="_permissionGrid" />
</td>
</tr>
</table>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,x200,200x)" ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label>
<div class="col-sm-9">
<input id="capacity" class="form-control" @bind="@_capacity" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="col-sm-12">
<Label Class="col-sm-3" For="permissions" HelpText="Select the permissions you want for the folder" ResourceKey="Permissions">Permissions: </Label>
<PermissionGrid EntityName="@EntityNames.Folder" PermissionNames="@(PermissionNames.Browse + "," + PermissionNames.View + "," + PermissionNames.Edit)" Permissions="@_permissions" @ref="_permissionGrid" />
</div>
</div>
</div>
</form>
@if (!_isSystem)
{
<button type="button" class="btn btn-success" @onclick="SaveFolder">@Localizer["Save"]</button>
<button type="button" class="btn btn-success" @onclick="SaveFolder">@SharedLocalizer["Save"]</button>
@((MarkupString)"&nbsp;")
}
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (!_isSystem && PageState.QueryString.ContainsKey("id"))
{
@((MarkupString)"&nbsp;")
<ActionDialog Header="Delete Folder" Message="Are You Sure You Wish To Delete This Folder?" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFolder())" ResourceKey="DeleteFolder" />
}
<br />
@ -58,10 +89,15 @@
}
@code {
private ElementReference form;
private bool validated = false;
private List<Folder> _folders;
private int _folderId = -1;
private string _name;
private int _parentId = -1;
private string _name;
private string _type = FolderTypes.Private;
private string _imagesizes = string.Empty;
private string _capacity = "0";
private bool _isSystem;
private string _permissions = string.Empty;
private string _createdBy;
@ -91,6 +127,9 @@
{
_parentId = folder.ParentId ?? -1;
_name = folder.Name;
_type = folder.Type;
_imagesizes = folder.ImageSizes;
_capacity = folder.Capacity.ToString();
_isSystem = folder.IsSystem;
_permissions = folder.Permissions;
_createdBy = folder.CreatedBy;
@ -102,81 +141,92 @@
else
{
_parentId = _folders[0].FolderId;
_permissions = string.Empty;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Folder {FolderId} {Error}", _folderId, ex.Message);
AddModuleMessage(Localizer["Error Loading Folder"], MessageType.Error);
AddModuleMessage(Localizer["Error.Folder.Load"], MessageType.Error);
}
}
private async Task SaveFolder()
{
if (_name == string.Empty || _parentId == -1)
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
AddModuleMessage(Localizer["Folders Must Have A Parent And A Name"], MessageType.Warning);
return;
}
if (!_name.IsPathOrFileValid())
{
AddModuleMessage(Localizer["Folder Name Not Valid."], MessageType.Warning);
return;
}
try
{
Folder folder;
if (_folderId != -1)
if (_name == string.Empty || _parentId == -1)
{
folder = await FolderService.GetFolderAsync(_folderId);
}
else
{
folder = new Folder();
AddModuleMessage(Localizer["Message.Required.FolderParent"], MessageType.Warning);
return;
}
folder.SiteId = PageState.Site.SiteId;
if (_parentId == -1)
if (!_name.IsPathOrFileValid())
{
folder.ParentId = null;
}
else
{
folder.ParentId = _parentId;
AddModuleMessage(Localizer["Message.Folder.InvalidName"], MessageType.Warning);
return;
}
folder.Name = _name;
folder.IsSystem = _isSystem;
folder.Permissions = _permissionGrid.GetPermissions();
try
{
Folder folder;
if (_folderId != -1)
{
folder = await FolderService.GetFolderAsync(_folderId);
}
else
{
folder = new Folder();
}
if (_folderId != -1)
{
folder = await FolderService.UpdateFolderAsync(folder);
}
else
{
folder = await FolderService.AddFolderAsync(folder);
}
folder.SiteId = PageState.Site.SiteId;
if (folder != null)
{
await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId);
await logger.LogInformation("Folder Saved {Folder}", folder);
NavigationManager.NavigateTo(NavigateUrl());
if (_parentId == -1)
{
folder.ParentId = null;
}
else
{
folder.ParentId = _parentId;
}
folder.Name = _name;
folder.Type = _type;
folder.ImageSizes = _imagesizes;
folder.Capacity = int.Parse(_capacity);
folder.IsSystem = _isSystem;
folder.Permissions = _permissionGrid.GetPermissions();
if (_folderId != -1)
{
folder = await FolderService.UpdateFolderAsync(folder);
}
else
{
folder = await FolderService.AddFolderAsync(folder);
}
if (folder != null)
{
await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId);
await logger.LogInformation("Folder Saved {Folder}", folder);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Error.Folder.Save"], MessageType.Error);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["An Error Was Encountered Saving The Folder"], MessageType.Error);
await logger.LogError(ex, "Error Saving Folder {FolderId} {Error}", _folderId, ex.Message);
AddModuleMessage(Localizer["Error.Folder.Save"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving Folder {FolderId} {Error}", _folderId, ex.Message);
AddModuleMessage(Localizer["Error Saving Folder"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
@ -204,18 +254,18 @@
}
else
{
AddModuleMessage(Localizer["Folder Has Files And Cannot Be Deleted"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Folder.Files.InvalidDelete"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Folder Has Subfolders And Cannot Be Deleted"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Folder.Subfolders.InvalidDelete"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Folder {Folder} {Error}", _folderId, ex.Message);
AddModuleMessage(Localizer["Error Deleting Folder"], MessageType.Error);
AddModuleMessage(Localizer["Error.Folder.Delete"], MessageType.Error);
}
}
}
}

View File

@ -4,50 +4,51 @@
@inject IFolderService FolderService
@inject IFileService FileService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_files != null)
{
<table class="table table-borderless">
<tr>
<td>
<label class="control-label">@Localizer["Folder:"] </label>
</td>
<td>
<select class="form-control" @onchange="(e => FolderChanged(e))">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-2">
<label class="control-label">@Localizer["Folder"] </label>
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</td>
<td>
</div>
<div class="col-sm-4">
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />&nbsp;
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</td>
</tr>
</table>
</div>
</div>
</div>
<Pager Items="@_files">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@Localizer["Modified"]</th>
<th>@Localizer["Type"]</th>
<th>@Localizer["Size"]</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Modified"]</th>
<th>@Localizer["Type"]</th>
<th>@Localizer["Size"]</th>
</Header>
<Row>
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td><ActionDialog Header="Delete File" Message="@Localizer["Are You Sure You Wish To Delete {0}?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@(ContentUrl(context.FileId))" target="_new">@context.Name</a></td>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@context.ModifiedOn</td>
<td>@context.Extension.ToUpper() @Localizer["File"]</td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
</Row>
</Pager>
@if (_files.Count == 0)
{
<div class="text-center">@Localizer["No Files Exist In Selected Folder"]</div>
<div class="text-center">@Localizer["NoFiles"]</div>
}
}
@ -63,7 +64,7 @@
try
{
_folders = await FolderService.GetFoldersAsync(PageState.Site.SiteId);
if (_folderId == -1 && _folders.Count > 0)
{
_folderId = _folders[0].FolderId;
@ -73,7 +74,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Files {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Files"], MessageType.Error);
AddModuleMessage(Localizer["Error.File.Load"], MessageType.Error);
}
}
@ -93,7 +94,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Files {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Files"], MessageType.Error);
AddModuleMessage(Localizer["Error.File.Load"], MessageType.Error);
}
}
@ -103,14 +104,14 @@
{
await FileService.DeleteFileAsync(file.FileId);
await logger.LogInformation("File Deleted {File}", file.Name);
AddModuleMessage(Localizer["File {0} Deleted", file.Name], MessageType.Success);
AddModuleMessage(string.Format(Localizer["Success.File.Delete"], file.Name), MessageType.Success);
await GetFiles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", file.Name, ex.Message);
AddModuleMessage(Localizer["Error Deleting File {0}", file.Name], MessageType.Error);
AddModuleMessage(string.Format(Localizer["Error.File.Delete"], file.Name), MessageType.Error);
}
}
}

View File

@ -3,164 +3,211 @@
@inject NavigationManager NavigationManager
@inject IJobService JobService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
</td>
<td>
<input id="type" class="form-control" @bind="@_jobType" readonly />
</td>
</tr>
<tr>
<td>
<Label For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
</td>
<td>
<select id="enabled" class="form-control" @bind="@_isEnabled">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
</td>
<td>
<input id="runs-every" class="form-control" @bind="@_interval" />
<select id="runs-every" class="form-control" @bind="@_frequency">
<option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="starting" HelpText="What time do you want the job to start" ResourceKey="Starting">Starting: </Label>
</td>
<td>
<input id="starting" class="form-control" @bind="@_startDate" />
</td>
</tr>
<tr>
<td>
<Label For="ending" HelpText="When do you want the job to end" ResourceKey="Ending">Ending: </Label>
</td>
<td>
<input id="ending" class="form-control" @bind="@_endDate" />
</td>
</tr>
<tr>
<td>
<Label For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
</td>
<td>
<input id="retention" class="form-control" @bind="@_retentionHistory" />
</td>
</tr>
<tr>
<td>
<Label For="next" HelpText="Next execution for this job." ResourceKey="NextExecution">Next Execution: </Label>
</td>
<td>
<input id="next" class="form-control" @bind="@_nextExecution" />
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveJob">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<div class="col-sm-9">
<select id="enabled" class="form-select" @bind="@_isEnabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<div class="col-sm-9">
<input id="runs-every" class="form-control" @bind="@_interval" maxlength="4" required />
<select id="runs-every" class="form-select" @bind="@_frequency" required>
<option value="m">@Localizer["Minute(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retentionHistory" maxlength="4" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div>
<div class="col">
<input id="starting" type="text" class="form-control" placeholder="hh:mm" @bind="@_startTime" />
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div>
<div class="col">
<input id="ending" type="text" class="form-control" placeholder="hh:mm" @bind="@_endTime" />
</div>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label>
<div class="col-sm-9">
<div class="row">
<div class="col">
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="text" class="form-control" placeholder="hh:mm" @bind="@_nextTime" />
</div>
</div>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
@code {
private int _jobId;
private string _name = string.Empty;
private string _jobType = string.Empty;
private string _isEnabled = "True";
private string _interval = string.Empty;
private string _frequency = string.Empty;
private string _startDate = string.Empty;
private string _endDate = string.Empty;
private string _retentionHistory = string.Empty;
private string _nextExecution = string.Empty;
private ElementReference form;
private bool validated = false;
private int _jobId;
private string _name = string.Empty;
private string _jobType = string.Empty;
private string _isEnabled = "True";
private string _interval = string.Empty;
private string _frequency = string.Empty;
private DateTime? _startDate = null;
private string _startTime = string.Empty;
private DateTime? _endDate = null;
private string _endTime = string.Empty;
private string _retentionHistory = string.Empty;
private DateTime? _nextDate = null;
private string _nextTime = string.Empty;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = (job.StartDate != null) ? job.StartDate.ToString() : string.Empty;
_endDate = (job.EndDate != null) ? job.EndDate.ToString() : string.Empty;
_retentionHistory = job.RetentionHistory.ToString();
_nextExecution = job.NextExecution.ToString();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error Loading Job"], MessageType.Error);
}
}
protected override async Task OnInitializedAsync()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = job.StartDate;
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
_startTime = job.StartDate.Value.ToString("HH:mm");
}
_endDate = job.EndDate;
if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0)
{
_endTime = job.EndDate.Value.ToString("HH:mm");
}
_retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution;
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{
_nextTime = job.NextExecution.Value.ToString("HH:mm");
}
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
private async Task SaveJob()
{
if (_name != string.Empty && !string.IsNullOrEmpty(_jobType) && _frequency != string.Empty && _interval != string.Empty && _retentionHistory != string.Empty)
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
job.Interval = int.Parse(_interval);
if (_startDate == string.Empty)
private async Task SaveJob()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate;
if (job.StartDate != null)
{
job.StartDate = null;
job.StartDate = job.StartDate.Value.Date;
if (!string.IsNullOrEmpty(_startTime))
{
job.StartDate = DateTime.Parse(job.StartDate.Value.ToShortDateString() + " " + _startTime);
}
}
else
job.EndDate = _endDate;
if (job.EndDate != null)
{
job.StartDate = DateTime.Parse(_startDate);
job.EndDate = job.EndDate.Value.Date;
if (!string.IsNullOrEmpty(_endTime))
{
job.EndDate = DateTime.Parse(job.EndDate.Value.ToShortDateString() + " " + _endTime);
}
}
if (_endDate == string.Empty)
{
job.EndDate = null;
}
else
{
job.EndDate = DateTime.Parse(_endDate);
}
if (_nextExecution == string.Empty)
{
job.NextExecution = null;
}
else
{
job.NextExecution = DateTime.Parse(_nextExecution);
}
job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = _nextDate;
if (job.NextExecution != null)
{
job.NextExecution = job.NextExecution.Value.Date;
if (!string.IsNullOrEmpty(_nextTime))
{
job.NextExecution = DateTime.Parse(job.NextExecution.Value.ToShortDateString() + " " + _nextTime);
}
}
try
{
@ -171,13 +218,12 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error Updating Job"], MessageType.Error);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide The Job Name, Type, Frequency, and Retention"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
}

View File

@ -2,10 +2,11 @@
@inherits ModuleBase
@inject IJobService JobService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_jobs == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
@ -19,10 +20,10 @@ else
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@Localizer["Status"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@SharedLocalizer["Status"]</th>
<th>@Localizer["Frequency"]</th>
<th>@Localizer["Next Execution"]</th>
<th>@Localizer["NextExecution"]</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
<Row>
@ -35,75 +36,75 @@ else
<td>@context.NextExecution</td>
<td>
@if (context.IsStarted)
{
{
<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>
}
}
</td>
</Row>
</Pager>
}
@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()
{
_jobs = await JobService.GetJobsAsync();
}
protected override async Task OnParametersSetAsync()
{
_jobs = await JobService.GetJobsAsync();
}
private string DisplayStatus(bool isEnabled, bool isExecuting)
{
var status = string.Empty;
if (!isEnabled)
{
status = Localizer["Disabled"];
}
else
{
if (isExecuting)
{
status = Localizer["Executing"];
}
else
{
status = Localizer["Idle"];
}
}
private string DisplayStatus(bool isEnabled, bool isExecuting)
{
var status = string.Empty;
if (!isEnabled)
{
status = Localizer["Disabled"];
}
else
{
if (isExecuting)
{
status = Localizer["Executing"];
}
else
{
status = Localizer["Idle"];
}
}
return status;
}
return status;
}
private string DisplayFrequency(int interval, string frequency)
{
var result = $"{Localizer["Every"]} {interval.ToString()} ";
switch (frequency)
{
case "m":
result += Localizer["Minute"];
break;
case "H":
result += Localizer["Hour"];
break;
case "d":
result += Localizer["Day"];
break;
case "M":
result += Localizer["Month"];
break;
}
if (interval > 1)
{
result += Localizer["s"];
}
private string DisplayFrequency(int interval, string frequency)
{
var result = "";
switch (frequency)
{
case "m":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Minute"];
break;
case "H":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Hour"];
break;
case "d":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Day"];
break;
case "w":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Week"];
break;
case "M":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Month"];
break;
case "O":
result = Localizer["Once"];
break;
}
return result;
}
@ -113,23 +114,48 @@ else
{
await JobService.DeleteJobAsync(job.JobId);
await logger.LogInformation("Job Deleted {Job}", job);
_jobs = await JobService.GetJobsAsync();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error Deleting Job"], MessageType.Error);
AddModuleMessage(Localizer["Error.Job.Delete"], MessageType.Error);
}
}
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)
{
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()

View File

@ -2,46 +2,47 @@
@inherits ModuleBase
@inject IJobLogService JobLogService
@inject IStringLocalizer<Log> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_jobLogs == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<Pager Items="@_jobLogs">
<Header>
<th>@Localizer["Name"]</th>
<th>@Localizer["Status"]</th>
<th>@Localizer["Started"]</th>
<th>@Localizer["Finished"]</th>
</Header>
<Row>
<td>@context.Job.Name</td>
<td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td>
<td>@context.StartDate</td>
<td>@context.FinishDate</td>
</Row>
<Detail>
<td colspan="4">@((MarkupString)context.Notes)</td>
</Detail>
</Pager>
<Pager Items="@_jobLogs">
<Header>
<th>@SharedLocalizer["Name"]</th>
<th>@SharedLocalizer["Status"]</th>
<th>@Localizer["Started"]</th>
<th>@Localizer["Finished"]</th>
</Header>
<Row>
<td>@context.Job.Name</td>
<td>@DisplayStatus(context.Job.IsExecuting, context.Succeeded)</td>
<td>@context.StartDate</td>
<td>@context.FinishDate</td>
</Row>
<Detail>
<td colspan="4">@((MarkupString)context.Notes)</td>
</Detail>
</Pager>
}
@code {
private List<JobLog> _jobLogs;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
{
_jobLogs = await JobLogService.GetJobLogsAsync();
if (PageState.QueryString.ContainsKey("id"))
{
_jobLogs = _jobLogs.Where(item => item.JobId == Int32.Parse(PageState.QueryString["id"])).ToList();
}
_jobLogs = _jobLogs.OrderByDescending(item => item.JobLogId).ToList();
}
@ -63,7 +64,7 @@ else
status = Localizer["Failed"];
}
}
return status;
}
}

View File

@ -5,91 +5,340 @@
@inject NavigationManager NavigationManager
@inject ILocalizationService LocalizationService
@inject ILanguageService LanguageService
@inject IPackageService PackageService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_supportedCultures == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
@if (_supportedCultures?.Count() > 1)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="Name Of The Langauage" ResourceKey="Name">Name:</Label>
</td>
<td>
<select id="_code" class="form-control" @bind="@_code">
@foreach (var culture in _supportedCultures)
{
<option value="@culture.Name">@culture.DisplayName</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="default" HelpText="Indicates Whether Or Not This Language Is The Default For The Site" ResourceKey="IsDefault">Default?</Label>
</td>
<td>
<select id="default" class="form-control" @bind="@_isDefault">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveLanguage">@Localizer["Save"]</button>
}
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<TabStrip>
<TabPanel Name="Manage" ResourceKey="Manage">
@if (_availableCultures.Count() == 0)
{
<ModuleMessage Type="MessageType.Info" Message="@_message"></ModuleMessage>
}
else
{
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Name Of The Language" ResourceKey="Name">Name:</Label>
<div class="col-sm-9">
<select id="_code" class="form-select" @bind="@_code" required>
@foreach (var culture in _availableCultures)
{
<option value="@culture.Name">@culture.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="default" HelpText="Indicates Whether Or Not This Language Is The Default For The Site" ResourceKey="IsDefault">Default?</Label>
<div class="col-sm-9">
<select id="default" class="form-select" @bind="@_isDefault" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button>
</form>
}
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="Download" ResourceKey="Download" Security="SecurityAccessLevel.Host">
<div class="row justify-content-center mb-3">
<div class="col-sm-6">
<div class="input-group">
<select id="price" class="form-select custom-select" @onchange="(e => PriceChanged(e))">
<option value="free">@SharedLocalizer["Free"]</option>
<option value="paid">@SharedLocalizer["Paid"]</option>
</select>
<input id="search" class="form-control" placeholder="@SharedLocalizer["Search.Hint"]" @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
</div>
</div>
</div>
@if (_packages != null)
{
@if (_packages.Count > 0)
{
<Pager Items="@_packages">
<Row>
<td>
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_new">@context.Name</a></h3>&nbsp;&nbsp;by:&nbsp;&nbsp;<strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-primary" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@context.Price.Value.ToString("$#,##0.00")</a>
}
else
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
</Row>
</Pager>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
}
<button type="button" class="btn btn-success" @onclick="InstallLanguages">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload" Security="SecurityAccessLevel.Host">
<div class="container">
<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="LanguageUpload">Language: </Label>
<div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="InstallLanguages">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
</TabStrip>
}
@if (_productname != "")
{
<div class="app-actiondialog">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@SharedLocalizer["Review License Terms"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="HideModal"></button>
</div>
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3>
@if (!string.IsNullOrEmpty(_license))
{
@((MarkupString)_license)
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
}
@code {
private ElementReference form;
private bool validated = false;
private string _code = string.Empty;
private string _isDefault = "False";
private string _message;
private IEnumerable<Culture> _supportedCultures;
private IEnumerable<Culture> _availableCultures;
private List<Package> _packages;
private string _price = "free";
private string _search = "";
private string _productname = "";
private string _license = "";
private string _packageid = "";
private string _version = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private IEnumerable<Culture> _supportedCultures;
protected override async Task OnParametersSetAsync()
{
var languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
var languagesCodes = languages.Select(l => l.Code).ToList();
_supportedCultures = await LocalizationService.GetCulturesAsync();
if (_supportedCultures.Count() <= 1)
_availableCultures = _supportedCultures
.Where(c => !c.Name.Equals(Constants.DefaultCulture) && !languagesCodes.Contains(c.Name));
await LoadTranslations();
if (_supportedCultures.Count() == 1)
{
AddModuleMessage(Localizer["The Only Supported Culture That Has Been Defined Is English"], MessageType.Warning);
_message = Localizer["OnlyEnglish"];
}
else if (_availableCultures.Count() == 0)
{
_message = Localizer["AllLanguages"];
}
}
private async Task LoadTranslations()
{
_packages = await PackageService.GetPackagesAsync("translation", _search, _price, "");
}
private async void PriceChanged(ChangeEventArgs e)
{
try
{
_price = (string)e.Value;
_search = "";
await LoadTranslations();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On PriceChanged");
}
}
private async Task Search()
{
try
{
await LoadTranslations();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Search");
}
}
private async Task Reset()
{
try
{
_search = "";
await LoadTranslations();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Reset");
}
}
private async Task SaveLanguage()
{
var language = new Language
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
SiteId = PageState.Page.SiteId,
Name = CultureInfo.GetCultureInfo(_code).DisplayName,
Code = _code,
IsDefault = (_isDefault == null ? false : Boolean.Parse(_isDefault))
};
var language = new Language
{
SiteId = PageState.Page.SiteId,
Name = CultureInfo.GetCultureInfo(_code).DisplayName,
Code = _code,
IsDefault = (_isDefault == null ? false : Boolean.Parse(_isDefault))
};
try
{
language = await LanguageService.AddLanguageAsync(language);
if (language.IsDefault)
{
await SetCultureAsync(language.Code);
}
await logger.LogInformation("Language Added {Language}", language);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Language {Language} {Error}", language, ex.Message);
AddModuleMessage(Localizer["Error.Language.Add"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void HideModal()
{
_productname = "";
_license = "";
StateHasChanged();
}
private async Task GetPackage(string packageid, string version)
{
try
{
language = await LanguageService.AddLanguageAsync(language);
if (language.IsDefault)
var package = await PackageService.GetPackageAsync(packageid, version);
if (package != null)
{
await SetCultureAsync(language.Code);
_productname = package.Name;
if (!string.IsNullOrEmpty(package.License))
{
_license = package.License.Replace("\n", "<br />");
}
_packageid = package.PackageId;
_version = package.Version;
}
await logger.LogInformation("Language Added {Language}", language);
NavigationManager.NavigateTo(NavigateUrl());
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Language {Language} {Error}", language, ex.Message);
AddModuleMessage(Localizer["Error Adding Language"], MessageType.Error);
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await logger.LogInformation("Language Package {Name} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(Localizer["Success.Language.Download"], MessageType.Success);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Translation {Name} {Version}", _packageid, _version);
AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error);
}
}
private async Task InstallLanguages()
{
try
{
await PackageService.InstallPackagesAsync();
AddModuleMessage(string.Format(Localizer["Success.Language.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Translations");
}
}
@ -101,7 +350,7 @@ else
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360);
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}
}

View File

@ -1,11 +1,14 @@
@namespace Oqtane.Modules.Admin.Languages
@inherits ModuleBase
@inject ILanguageService LanguageService
@inject ILocalizationService LocalizationService
@inject IPackageService PackageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_languages == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
@ -14,27 +17,46 @@ else
<Pager Items="@_languages">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th>
<th>@Localizer["Default?"]</th>
<th>@Localizer["Default"]</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
<Row>
<td><ActionDialog Header="Delete Langauge" Message="@Localizer["Are You Sure You Wish To Delete The {0} Language?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteLanguage(context))" Disabled="@(context.IsDefault)" ResourceKey="DeleteLanguage" /></td>
<td><ActionDialog Header="Delete Language" Message="@string.Format(Localizer["Confirm.Language.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteLanguage(context))" Disabled="@((context.IsDefault && _languages.Count > 2) || context.Code == Constants.DefaultCulture)" ResourceKey="DeleteLanguage" /></td>
<td>@context.Name</td>
<td>@context.Code</td>
<td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td>
<td>
@if (UpgradeAvailable(context.Code))
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadLanguage(context.Code))>@SharedLocalizer["Upgrade"]</button>
}
</td>
</Row>
</Pager>
}
@code {
private List<Language> _languages;
private List<Package> _packages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
var cultures = await LocalizationService.GetCulturesAsync();
var culture = cultures.First(c => c.Name.Equals(Constants.DefaultCulture));
// Adds English as default language
_languages.Insert(0, new Language { Name = culture.DisplayName, Code = culture.Name, IsDefault = !_languages.Any(l => l.IsDefault) });
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_packages = await PackageService.GetPackagesAsync("translation");
}
}
private async Task DeleteLanguage(Language language)
@ -50,7 +72,41 @@ else
{
await logger.LogError(ex, "Error Deleting Language {Language} {Error}", language, ex.Message);
AddModuleMessage(Localizer["Error Deleting Language"], MessageType.Error);
AddModuleMessage(Localizer["Error.Language.Delete"], MessageType.Error);
}
}
private bool UpgradeAvailable(string code)
{
var upgradeavailable = false;
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == (Constants.PackageId + ".Client." + code)).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0);
}
}
return upgradeavailable;
}
private async Task DownloadLanguage(string code)
{
try
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await PackageService.DownloadPackageAsync(Constants.PackageId + ".Client." + code, Constants.Version, Constants.PackagesFolder);
await logger.LogInformation("Translation Downloaded {Code} {Version}", code, Constants.Version);
await PackageService.InstallPackagesAsync();
AddModuleMessage(string.Format(Localizer["Success.Language.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Translation {Code} {Version} {Error}", code, Constants.Version, ex.Message);
AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error);
}
}
}

View File

@ -3,188 +3,313 @@
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IServiceProvider ServiceProvider
@inject SiteState SiteState
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_message != string.Empty)
{
<ModuleMessage Message="@_message" Type="@_type" />
}
<AuthorizeView>
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<div>@Localizer["Info.SignedIn"]</div>
</Authorized>
<NotAuthorized>
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
<div class="form-group">
<label for="Username" class="control-label">@Localizer["Username:"] </label>
<input type="text" @ref="username" name="Username" class="form-control username" placeholder="Username" @bind="@_username" id="Username" required />
</div>
<div class="form-group">
<label for="Password" class="control-label">@Localizer["Password:"] </label>
<input type="password" name="Password" class="form-control password" placeholder="Password" @bind="@_password" id="Password" required />
</div>
<div class="form-group">
<div class="form-check form-check-inline">
<label class="form-check-label" for="Remember">@Localizer["Remember Me?"]</label>&nbsp;
<input type="checkbox" class="form-check-input" name="Remember" @bind="@_remember" id="Remember" />
</div>
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@Localizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@Localizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["Forgot Password"]</button>
</div>
</form>
</NotAuthorized>
@if (!twofactor)
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /><br />
}
@if (_allowsitelogin)
{
<div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div>
<div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
<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>
@code {
private string _returnUrl = string.Empty;
private string _message = string.Empty;
private MessageType _type = MessageType.Info;
private string _username = string.Empty;
private string _password = string.Empty;
private bool _remember = false;
private bool validated = false;
private bool _allowsitelogin = true;
private bool _allowexternallogin = false;
private ElementReference login;
private bool validated = false;
private bool twofactor = false;
private string _username = string.Empty;
private ElementReference username;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private bool _remember = false;
private string _code = string.Empty;
private ElementReference login;
private ElementReference username;
private string _returnUrl = string.Empty;
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" }
};
protected override async Task OnInitializedAsync()
{
if (PageState.QueryString.ContainsKey("returnurl"))
{
_returnUrl = PageState.QueryString["returnurl"];
}
protected override async Task OnInitializedAsync()
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name"))
{
_username = PageState.QueryString["name"];
}
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{
_allowsitelogin = bool.Parse(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]);
}
if (PageState.QueryString.ContainsKey("token"))
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"]))
{
_allowexternallogin = true;
}
if (user != null)
{
_message = Localizer["User Account Verified Successfully. You Can Now Login With Your Username And Password Below."];
}
else
{
_message = Localizer["User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions."];
_type = MessageType.Warning;
}
}
}
if (PageState.QueryString.ContainsKey("returnurl"))
{
_returnUrl = PageState.QueryString["returnurl"];
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await username.FocusAsync();
}
}
if (PageState.QueryString.ContainsKey("name"))
{
_username = PageState.QueryString["name"];
}
private async Task Login()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(login))
{
if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
{
// server-side Blazor
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
user.Password = _password;
user = await UserService.LoginUserAsync(user, false, false);
if (PageState.QueryString.ContainsKey("token") && !string.IsNullOrEmpty(_username))
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _username;
if (user.IsAuthenticated)
{
await logger.LogInformation("Login Successful For Username {Username}", _username);
// complete the login on the server so that the cookies are set correctly on SignalR
string antiforgerytoken = await interop.GetElementByName("__RequestVerificationToken");
var fields = new { __RequestVerificationToken = antiforgerytoken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
await interop.SubmitForm($"/{PageState.Alias.AliasId}/pages/login/", fields);
}
else
{
await logger.LogInformation("Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["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."], MessageType.Error);
}
}
else
{
// 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("Login Successful For Username {Username}", _username);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, "reload"));
}
else
{
await logger.LogInformation("Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["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."], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Please Provide Your Username And Password"], MessageType.Warning);
}
}
if (PageState.QueryString.ContainsKey("key"))
{
user = await UserService.LinkUserAsync(user, PageState.QueryString["token"], PageState.Site.Settings["ExternalLogin:ProviderType"], PageState.QueryString["key"], PageState.Site.Settings["ExternalLogin:ProviderName"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "External Login Linkage Successful For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Linked"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "External Login Linkage Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotLinked"], MessageType.Warning);
}
_username = "";
}
else
{
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Message.Account.NotVerified"], MessageType.Warning);
}
}
}
else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Login {Error}", ex.Message);
AddModuleMessage(Localizer["Error.LoadLogin"], MessageType.Error);
}
}
private void Cancel()
{
NavigationManager.NavigateTo(_returnUrl);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && PageState.User == null)
{
await username.FocusAsync();
}
}
private async Task Forgot()
{
if (_username != string.Empty)
{
var user = await UserService.GetUserAsync(_username, PageState.Site.SiteId);
if (user != null)
{
await UserService.ForgotPasswordAsync(user);
_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";
}
private async Task Login()
{
try
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(login))
{
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
StateHasChanged();
}
if (!twofactor)
{
user = await UserService.LoginUserAsync(user);
}
else
{
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
else
{
if ((PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && PageState.Site.Settings["LoginOptions:TwoFactor"] == "required") || 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 = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["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

@ -6,137 +6,114 @@
@inject IPageService PageService
@inject IPageModuleService PageModuleService
@inject IUserService UserService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<Detail> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="dateTime" HelpText="The date and time of this log" ResourceKey="DateTime">Date/Time: </Label>
</td>
<td>
<input id="dateTime" class="form-control" @bind="@_logDate" readonly />
</td>
</tr>
<tr>
<td>
<Label For="level" HelpText="The level of this log" ResourceKey="Level">Level: </Label>
</td>
<td>
<input id="level" class="form-control" @bind="@_level" readonly />
</td>
</tr>
<tr>
<td>
<Label For="feature" HelpText="The feature that was affected" ResourceKey="Feature">Feature: </Label>
</td>
<td>
<input id="feature" class="form-control" @bind="@_feature" readonly />
</td>
</tr>
<tr>
<td>
<Label For="function" HelpText="The function that was performed" ResourceKey="Function">Function: </Label>
</td>
<td>
<input id="function" class="form-control" @bind="@_function" readonly />
</td>
</tr>
<tr>
<td>
<Label For="category" HelpText="The categories that were affected" ResourceKey="Category">Category: </Label>
</td>
<td>
<input id="category" class="form-control" @bind="@_category" readonly />
</td>
</tr>
@if (_pageName != string.Empty)
{
<tr>
<td>
<Label For="page" HelpText="The page that was affected" ResourceKey="Page">Page: </Label>
</td>
<td>
<input id="page" class="form-control" @bind="@_pageName" readonly />
</td>
</tr>
}
@if (_moduleTitle != string.Empty)
{
<tr>
<td>
<Label For="module" HelpText="The module that was affected" ResourceKey="Module">Module: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_moduleTitle" readonly />
</td>
</tr>
}
@if (_username != string.Empty)
{
<tr>
<td>
<Label For="user" HelpText="The user that caused this log" ResourceKey="User">User: </Label>
</td>
<td>
<input id="user" class="form-control" @bind="@_username" readonly />
</td>
</tr>
}
<tr>
<td>
<Label For="url" HelpText="The url the log comes from" ResourceKey="Url">Url: </Label>
</td>
<td>
<input id="url" class="form-control" @bind="@_url" readonly />
</td>
</tr>
<tr>
<td>
<Label For="template" HelpText="What the log is about" ResourceKey="Template">Template: </Label>
</td>
<td>
<input id="template" class="form-control" @bind="@_template" readonly />
</td>
</tr>
<tr>
<td>
<Label For="message" HelpText="The message that the system generated" class="control-label" ResourceKey="Message">Message: </Label>
</td>
<td>
<textarea id="message" class="form-control" @bind="@_message" rows="5" readonly></textarea>
</td>
</tr>
@if (!string.IsNullOrEmpty(_exception))
{
<tr>
<td>
<Label For="exception" HelpText="The exceptions generated by the system" ResourceKey="Exception">Exception: </Label>
</td>
<td>
<textarea id="exception" class="form-control" @bind="@_exception" rows="5" readonly></textarea>
</td>
</tr>
}
<tr>
<td>
<Label For="properties" HelpText="The properties that were affected" ResourceKey="Properties">Properties: </Label>
</td>
<td>
<textarea id="properties" class="form-control" @bind="@_properties" rows="5" readonly></textarea>
</td>
</tr>
<tr>
<td>
<Label For="server" HelpText="The server that was affected" ResourceKey="Server">Server: </Label>
</td>
<td>
<input id="server" class="form-control" @bind="@_server" readonly />
</td>
</tr>
</table>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
@if (_initialized)
{
<div class="container">
<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 class="col-sm-9">
<input id="dateTime" class="form-control" @bind="@_logDate" readonly />
</div>
</div>
<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 class="col-sm-9">
<input id="level" class="form-control" @bind="@_level" readonly />
</div>
</div>
<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 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>
</div>
</div>
}
<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>
<div class="col-sm-9">
<textarea id="properties" class="form-control" @bind="@_properties" rows="5" readonly></textarea>
</div>
</div>
<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>
<div class="col-sm-9">
<input id="server" class="form-control" @bind="@_server" readonly />
</div>
</div>
</div>
}
<NavLink class="btn btn-secondary" href="@CloseUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private bool _initialized = false;
private int _logId;
private string _logDate = string.Empty;
private string _level = string.Empty;
@ -178,7 +155,7 @@
}
}
if (log.PageId != null && log.ModuleId != null)
if (log.PageId != null && log.ModuleId != null && log.ModuleId != -1)
{
var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value);
if (pagemodule != null)
@ -202,12 +179,25 @@
_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 Loading Log"], MessageType.Error);
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,81 +1,102 @@
@namespace Oqtane.Modules.Admin.Logs
@inherits ModuleBase
@inject ILogService LogService
@inject IStringLocalizer<Index> Localizer
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_logs == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<table class="table table-borderless">
<tr>
<td>
<Label For="level" HelpText="Select the log level for event log items" ResourceKey="Level">Level: </Label><br /><br />
<select id="level" class="form-control" @onchange="(e => LevelChanged(e))">
<option value="-">&lt;@Localizer["All Levels"]&gt;</option>
<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>
</select>
</td>
<td>
<Label For="function" HelpText="Select the function for event log items" ResourceKey="Function">Function: </Label><br /><br />
<select id="function" class="form-control" @onchange="(e => FunctionChanged(e))">
<option value="-">&lt;@Localizer["All Functions"]&gt;</option>
<option value="Create">@Localizer["Create"]</option>
<option value="Read">@Localizer["Read"]</option>
<option value="Update">@Localizer["Update"]</option>
<option value="Delete">@Localizer["Delete"]</option>
<option value="Security">@Localizer["Security"]</option>
<option value="Other">@Localizer["Other"]</option>
</select>
</td>
<td>
<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 />
<select id="rows" class="form-control" @onchange="(e => RowsChanged(e))">
<option value="10">10</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</td>
</tr>
</table>
<TabStrip>
<TabPanel Name="Events" Heading="Events" ResourceKey="Events">
<div class="container g-0">
<div class="row mb-1 align-items-center">
<div class="col-sm-4">
<Label For="level" HelpText="Select the log level for event log items" ResourceKey="Level">Level: </Label><br /><br />
<select id="level" class="form-select" value="@_level" @onchange="(e => LevelChanged(e))">
<option value="-">&lt;@Localizer["AllLevels"]&gt;</option>
<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>
</select>
</div>
<div class="col-sm-4">
<Label For="function" HelpText="Select the function for event log items" ResourceKey="Function">Function: </Label><br /><br />
<select id="function" class="form-select" value="@_function" @onchange="(e => FunctionChanged(e))">
<option value="-">&lt;@Localizer["AllFunctions"]&gt;</option>
<option value="Create">@Localizer["Create"]</option>
<option value="Read">@Localizer["Read"]</option>
<option value="Update">@SharedLocalizer["Update"]</option>
<option value="Delete">@SharedLocalizer["Delete"]</option>
<option value="Security">@Localizer["Security"]</option>
<option value="Other">@Localizer["Other"]</option>
</select>
</div>
<div class="col-sm-4">
<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 />
<select id="rows" class="form-select" value="@_rows" @onchange="(e => RowsChanged(e))">
<option value="10">10</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
</div>
<br />
@if (_logs.Any())
{
<Pager Items="@_logs">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Date"]</th>
<th>@Localizer["Level"]</th>
<th>@Localizer["Feature"]</th>
<th>@Localizer["Function"]</th>
</Header>
<Row>
<td class="@GetClass(context.Function)"><ActionLink Action="Detail" Parameters="@($"id=" + context.LogId.ToString())" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td>
<td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td>
<td class="@GetClass(context.Function)">@context.Function</td>
</Row>
</Pager>
}
else
{
<p><em>@Localizer["No Logs Match The Criteria Specified"]</em></p>
}
@if (_logs.Any())
{
<Pager Items="@_logs" CurrentPage="@_page.ToString()" OnPageChange="OnPageChange">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Date"]</th>
<th>@Localizer["Level"]</th>
<th>@Localizer["Feature"]</th>
<th>@Localizer["Function"]</th>
</Header>
<Row>
<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.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td>
<td class="@GetClass(context.Function)">@context.Function</td>
</Row>
</Pager>
}
else
{
<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 {
private string _level = "-";
private string _function = "-";
private string _rows = "10";
private int _page = 1;
private List<Log> _logs;
private string _retention = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -83,12 +104,32 @@ else
{
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();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = SettingService.GetSetting(settings, "LogRetention", "30");
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Logs {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Logs"], MessageType.Error);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
}
}
@ -103,7 +144,7 @@ else
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Logs {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Logs"], MessageType.Error);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
}
}
@ -118,7 +159,7 @@ else
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Logs {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Logs"], MessageType.Error);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
}
}
@ -134,7 +175,7 @@ else
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Logs {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Logs"], MessageType.Error);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
}
}
@ -169,4 +210,27 @@ else
}
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

@ -1,153 +1,155 @@
@namespace Oqtane.Modules.Admin.ModuleCreator
@inherits ModuleBase
@using System.Text.RegularExpressions
@inject NavigationManager NavigationManager
@inject IModuleDefinitionService ModuleDefinitionService
@inject IModuleService ModuleService
@inject ISystemService SystemService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@using System.Text.RegularExpressions
@using System.IO;
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (string.IsNullOrEmpty(_moduledefinitionname) && _systeminfo != null && _templates != null)
@if (string.IsNullOrEmpty(_moduledefinitionname) && _templates != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="owner" HelpText="Enter the name of the organization who is developing this module. It should not contain spaces or punctuation." ResourceKey="OwnerName">Owner Name: </Label>
</td>
<td>
<input id="owner" class="form-control" @bind="@_owner" />
</td>
</tr>
<tr>
<td>
<Label For="module" HelpText="Enter a name for this module. It should not contain spaces or punctuation." ResourceKey="ModuleName">Module Name: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_module" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="Enter a short description for the module" ResourceKey="Description">Description: </Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="3"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="template" HelpText="Select a module template. Templates are located in the wwwroot/Modules/Templates folder on the server." ResourceKey="Template">Template: </Label>
</td>
<td>
<select id="template" class="form-control" @onchange="(e => TemplateChanged(e))">
<option value="-">&lt;@Localizer["Select Template"]&gt;</option>
@foreach (string template in _templates)
{
<option value="@template">@template</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
</td>
<td>
<select id="reference" class="form-control" @bind="@_reference">
@foreach (string version in Constants.ReleaseVersions.Split(','))
{
if (Version.Parse(version).CompareTo(Version.Parse("2.0.0")) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@Localizer["Local Version"]</option>
</select>
</td>
</tr>
@if (!string.IsNullOrEmpty(_location))
{
<tr>
<td>
<Label For="location" HelpText="Location where the module will be created" ResourceKey="Location">Location: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_location" readonly />
</td>
</tr>
}
</table>
<button type="button" class="btn btn-success" @onclick="CreateModule">@Localizer["Create Module"]</button>
<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="owner" HelpText="Enter the name of the organization who is developing this module. It should not contain spaces or punctuation." ResourceKey="OwnerName">Owner Name: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="Enter a name for this module. It should not contain spaces or punctuation." ResourceKey="ModuleName">Module Name: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_module" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="Enter a short description for the module" ResourceKey="Description">Description: </Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="3" ></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="template" HelpText="Select a module template. Templates are located in the wwwroot/Modules/Templates folder on the server." ResourceKey="Template">Template: </Label>
<div class="col-sm-9">
<select id="template" class="form-select" @onchange="(e => TemplateChanged(e))" required>
<option value="-">&lt;@Localizer["Template.Select"]&gt;</option>
@foreach (Template template in _templates)
{
<option value="@template.Name">@template.Title</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<select id="reference" class="form-select" @bind="@_reference" required>
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the module will be created" ResourceKey="Location">Location: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
</div>
</div>
}
</div>
</form>
<button type="button" class="btn btn-success" @onclick="CreateModule">@Localizer["Module.Create"]</button>
}
else
{
<button type="button" class="btn btn-success" @onclick="ActivateModule">@Localizer["Activate Module"]</button>
<button type="button" class="btn btn-success" @onclick="ActivateModule">@Localizer["Module.Activate"]</button>
}
@code {
private string _moduledefinitionname = string.Empty;
private string _owner = string.Empty;
private string _module = string.Empty;
private string _description = string.Empty;
private string _template = "-";
private string _reference = Constants.Version;
private string _location = string.Empty;
private ElementReference form;
private bool validated = false;
private string _moduledefinitionname = string.Empty;
private string _owner = string.Empty;
private string _module = string.Empty;
private string _description = string.Empty;
private List<Template> _templates;
private string _template = "-";
private string[] _versions;
private string _reference = Constants.Version;
private string _minversion = "2.0.0";
private string _location = string.Empty;
private Dictionary<string, string> _systeminfo;
private List<string> _templates;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override void OnInitialized()
{
_moduledefinitionname = SettingService.GetSetting(ModuleState.Settings, "ModuleDefinitionName", "");
if (string.IsNullOrEmpty(_moduledefinitionname))
{
AddModuleMessage(Localizer["Info.Module.Creator"], MessageType.Info);
}
else
{
AddModuleMessage(Localizer["Info.Module.Activate"], MessageType.Info);
}
}
protected override async Task OnParametersSetAsync()
{
try
{
_moduledefinitionname = SettingService.GetSetting(ModuleState.Settings, "ModuleDefinitionName", "");
_systeminfo = await SystemService.GetSystemInfoAsync();
_templates = await ModuleDefinitionService.GetModuleDefinitionTemplatesAsync();
protected override async Task OnParametersSetAsync()
{
try
{
_templates = await ModuleDefinitionService.GetModuleDefinitionTemplatesAsync();
_versions = Constants.ReleaseVersions.Split(',').Where(item => Version.Parse(item).CompareTo(Version.Parse("2.0.0")) >= 0).ToArray();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Module Creator");
}
}
if (string.IsNullOrEmpty(_moduledefinitionname))
{
AddModuleMessage(Localizer["Please Note That The Module Creator Is Only Intended To Be Used In A Development Environment"], MessageType.Info);
private async Task CreateModule()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
{
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
GetLocation();
AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Require.ValidName"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["Once You Have Compiled The Module And Restarted The Application You Can Activate The Module Below"], MessageType.Info);
await logger.LogError(ex, "Error Creating Module");
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Loading Module Creator");
}
}
private async Task CreateModule()
{
try
{
if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
{
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = ModuleState.Settings;
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
GetLocation();
AddModuleMessage(Localizer["The Source Code For Your Module Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href=\"{0}\">Restart</a> Your Application To Apply These Changes.", NavigateUrl("admin/system")], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["You Must Provide A Valid Owner Name And Module Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Creating Module");
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
@ -172,23 +174,29 @@ else
private bool IsValid(string name)
{
// must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
}
private void TemplateChanged(ChangeEventArgs e)
{
_template = (string)e.Value;
_minversion = "2.0.0";
if (_template != "-")
{
var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version;
}
GetLocation();
}
private void GetLocation()
{
_location = string.Empty;
if (_template != "-" && _systeminfo != null && _systeminfo.ContainsKey("serverpath"))
if (_owner != "" && _module != "" && _template != "-")
{
string[] path = _systeminfo["serverpath"].Split(Path.DirectorySeparatorChar);
_location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 2) +
Path.DirectorySeparatorChar + _owner + "." + _module;
var template = _templates.FirstOrDefault(item => item.Name == _template);
_location = template.Location + _owner + "." + _module;
}
StateHasChanged();
}

View File

@ -1,7 +1,9 @@
using Oqtane.Models;
using Oqtane.Documentation;
using Oqtane.Models;
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 ModuleDefinition ModuleDefinition => new ModuleDefinition

View File

@ -5,50 +5,122 @@
@inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_packages != null)
{
<TabStrip>
@if (_packages.Count > 0)
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
<div class="row justify-content-center mb-3">
<div class="col-sm-6">
<div class="input-group">
<select id="price" class="form-select custom-select" @onchange="(e => PriceChanged(e))">
<option value="free">@SharedLocalizer["Free"]</option>
<option value="paid">@SharedLocalizer["Paid"]</option>
</select>
<input id="search" class="form-control" placeholder="@SharedLocalizer["Search.Hint"]" @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
</div>
</div>
</div>
@if (_packages != null)
{
<TabPanel Name="Download" ResourceKey="Download">
<ModuleMessage Type="MessageType.Info" Message="Download one or more modules from the list below. Once you are ready click Install to complete the installation."></ModuleMessage>
if (_packages.Count > 0)
{
<Pager Items="@_packages">
<Header>
<th>@Localizer["Name"]</th>
<th>@Localizer["Version"]</th>
<th style="width: 1px"></th>
</Header>
<Row>
<td>@context.Name</td>
<td>@context.Version</td>
<td>
<button type="button" class="btn btn-primary" @onclick=@(async () => await DownloadModule(context.PackageId, context.Version))>@Localizer["Download"]</button>
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_new">@context.Name</a></h3>&nbsp;&nbsp;by:&nbsp;&nbsp;<strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-primary" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@context.Price.Value.ToString("$#,##0.00")</a>
}
else
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
</Row>
</Pager>
</TabPanel>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
}
}
<TabPanel Name="Upload" ResourceKey="Upload">
<table class="table table-borderless">
<tr>
<td>
<Label HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Module: </Label>
</td>
<td>
<FileManager Filter="nupkg" ShowFiles="false" Folder="Modules" UploadMultiple="true" />
</td>
</tr>
</table>
</TabPanel>
</TabStrip>
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Module: </Label>
<div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" />
</div>
</div>
</div>
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-success" @onclick="InstallModules">@Localizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
@if (_productname != "")
{
<div class="app-actiondialog">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@SharedLocalizer["Review License Terms"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="HideModal"></button>
</div>
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3>
@if (!string.IsNullOrEmpty(_license))
{
@((MarkupString)_license)
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
}
<button type="button" class="btn btn-success" @onclick="InstallModules">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private List<Package> _packages;
private string _price = "free";
private string _search = "";
private string _productname = "";
private string _license = "";
private string _packageid = "";
private string _version = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -56,21 +128,118 @@
{
try
{
var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module");
await LoadModuleDefinitions();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Package.Load"], MessageType.Error);
}
}
private async Task LoadModuleDefinitions()
{
var moduledefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module", _search, _price, "");
if (_packages != null)
{
foreach (Package package in _packages.ToArray())
{
if (moduledefinitions.Exists(item => Utilities.GetTypeName(item.ModuleDefinitionName) == package.PackageId))
if (moduledefinitions.Exists(item => item.PackageName == package.PackageId))
{
_packages.Remove(package);
}
}
}
}
private async void PriceChanged(ChangeEventArgs e)
{
try
{
_price = (string)e.Value;
_search = "";
await LoadModuleDefinitions();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Packages"], MessageType.Error);
await logger.LogError(ex, "Error On PriceChanged");
}
}
private async Task Search()
{
try
{
await LoadModuleDefinitions();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Search");
}
}
private async Task Reset()
{
try
{
_search = "";
await LoadModuleDefinitions();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Reset");
}
}
private void HideModal()
{
_productname = "";
_license = "";
StateHasChanged();
}
private async Task GetPackage(string packageid, string version)
{
try
{
var package = await PackageService.GetPackageAsync(packageid, version);
if (package != null)
{
_productname = package.Name;
if (!string.IsNullOrEmpty(package.License))
{
_license = package.License.Replace("\n", "<br />");
}
_packageid = package.PackageId;
_version = package.Version;
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success);
_productname = "";
_license = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _version);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
@ -79,27 +248,11 @@
try
{
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(Localizer["Module Installed Successfully. You Must <a href=\"{0}\">Restart</a> Your Application To Apply These Changes.", NavigateUrl("admin/system")], MessageType.Success);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Module");
}
}
private async Task DownloadModule(string packageid, string version)
{
try
{
await PackageService.DownloadPackageAsync(packageid, version, "Modules");
await logger.LogInformation("Module {ModuleDefinitionName} {Version} Downloaded Successfully", packageid, version);
AddModuleMessage(Localizer["Modules Downloaded Successfully. Click Install To Complete Installation."], MessageType.Success);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version}", packageid, version);
AddModuleMessage(Localizer["Error Downloading Module"], MessageType.Error);
}
}
}

View File

@ -1,108 +1,103 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase
@using System.Text.RegularExpressions
@inject NavigationManager NavigationManager
@inject IModuleDefinitionService ModuleDefinitionService
@inject IModuleService ModuleService
@inject ISystemService SystemService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@using System.Text.RegularExpressions
@using System.IO;
@inject IStringLocalizer<Create> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_systeminfo != null && _templates != null)
@if (_templates != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="owner" HelpText="Enter the name of the organization who is developing this module. It should not contain spaces or punctuation." ResourceKey="OwnerName">Owner Name: </Label>
</td>
<td>
<input id="owner" class="form-control" @bind="@_owner" />
</td>
</tr>
<tr>
<td>
<Label For="module" HelpText="Enter a name for this module. It should not contain spaces or punctuation." ResourceKey="ModuleName">Module Name: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_module" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="Enter a short description for the module" ResourceKey="Description">Description: </Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="3"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="template" HelpText="Select a module template. Templates are located in the wwwroot/Modules/Templates folder on the server." ResourceKey="Template">Template: </Label>
</td>
<td>
<select id="template" class="form-control" @onchange="(e => TemplateChanged(e))">
<option value="-">&lt;@Localizer["Select Template"]&gt;</option>
@foreach (string template in _templates)
{
<option value="@template">@template</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
</td>
<td>
<select id="reference" class="form-control" @bind="@_reference">
@foreach (string version in Constants.ReleaseVersions.Split(','))
{
if (Version.Parse(version).CompareTo(Version.Parse("2.0.0")) >= 0)
<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="owner" HelpText="Enter the name of the organization who is developing this module. It should not contain spaces or punctuation." ResourceKey="OwnerName">Owner Name: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="Enter a name for this module. It should not contain spaces or punctuation." ResourceKey="ModuleName">Module Name: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_module" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="Enter a short description for the module" ResourceKey="Description">Description: </Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="3" maxlength="2000" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="template" HelpText="Select a module template. Templates are located in the wwwroot/Modules/Templates folder on the server." ResourceKey="Template">Template: </Label>
<div class="col-sm-9">
<select id="template" class="form-select" @onchange="(e => TemplateChanged(e))" required>
<option value="-">&lt;@Localizer["Template.Select"]&gt;</option>
@foreach (Template template in _templates)
{
<option value="@(version)">@(version)</option>
<option value="@template.Name">@template.Title</option>
}
}
<option value="local">@Localizer["Local Version"]</option>
</select>
</td>
</tr>
@if (!string.IsNullOrEmpty(_location))
{
<tr>
<td>
<Label For="location" HelpText="Location where the module will be created" ResourceKey="Location">Location: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_location" readonly />
</td>
</tr>
}
</table>
<button type="button" class="btn btn-success" @onclick="CreateModule">@Localizer["Create Module"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<select id="reference" class="form-select" @bind="@_reference" required>
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the module will be created" ResourceKey="Location">Location: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
</div>
</div>
}
</div>
<button type="button" class="btn btn-success" @onclick="CreateModule">@Localizer["CreateModule"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
}
@code {
private ElementReference form;
private bool validated = false;
private string _owner = string.Empty;
private string _module = string.Empty;
private string _description = string.Empty;
private List<Template> _templates;
private string _template = "-";
private string _reference = Constants.Version;
private string[] _versions;
private string _reference = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;
private Dictionary<string, string> _systeminfo;
private List<string> _templates;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override void OnInitialized()
{
AddModuleMessage(Localizer["Info.Module.Development"], MessageType.Info);
}
protected override async Task OnParametersSetAsync()
{
try
{
_systeminfo = await SystemService.GetSystemInfoAsync();
_templates = await ModuleDefinitionService.GetModuleDefinitionTemplatesAsync();
AddModuleMessage(Localizer["Please Note That The Module Creator Is Only Intended To Be Used In A Development Environment"], MessageType.Info);
_versions = Constants.ReleaseVersions.Split(',').Where(item => Version.Parse(item).CompareTo(Version.Parse("2.0.0")) >= 0).ToArray();
}
catch (Exception ex)
{
@ -112,46 +107,61 @@
private async Task CreateModule()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
try
{
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
GetLocation();
AddModuleMessage(Localizer["The Source Code For Your Module Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href=\"{0}\">Restart</a> Your Application To Activate The Module.", NavigateUrl("admin/system")], MessageType.Success);
if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
{
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
GetLocation();
AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Require.ValidName"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["You Must Provide A Valid Owner Name And Module Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template"], MessageType.Warning);
await logger.LogError(ex, "Error Creating Module");
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Creating Module");
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private bool IsValid(string name)
{
// must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
}
private void TemplateChanged(ChangeEventArgs e)
{
_template = (string)e.Value;
_minversion = "2.0.0";
if (_template != "-")
{
var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version;
}
GetLocation();
}
private void GetLocation()
{
_location = string.Empty;
if (_template != "-" && _systeminfo != null && _systeminfo.ContainsKey("serverpath"))
if (_owner != "" && _module != "" && _template != "-")
{
string[] path = _systeminfo["serverpath"].Split(Path.DirectorySeparatorChar);
_location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 2) +
Path.DirectorySeparatorChar + _owner + "." + _module;
var template = _templates.FirstOrDefault(item => item.Name == _template);
_location = template.Location + _owner + "." + _module;
}
StateHasChanged();
}

View File

@ -3,113 +3,96 @@
@inject IModuleDefinitionService ModuleDefinitionService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Definition" ResourceKey="Definition">
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="The description of the module" ResourceKey="Description">Description: </Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="2"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="categories" HelpText="Comma delimited list of module categories" ResourceKey="Categories">Categories: </Label>
</td>
<td>
<input id="categories" class="form-control" @bind="@_categories" />
</td>
</tr>
</table>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="The name of the module" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="The description of the module" ResourceKey="Description">Description: </Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="2" maxlength="2000" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="categories" HelpText="Comma delimited list of module categories" ResourceKey="Categories">Categories: </Label>
<div class="col-sm-9">
<input id="categories" class="form-control" @bind="@_categories" maxlength="200" required />
</div>
</div>
</div>
</form>
<Section Name="Information" ResourceKey="Information">
<table class="table table-borderless">
<tr>
<td>
<Label For="moduledefinitionname" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
</td>
<td>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="moduledefinitionname" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="moduledefinitionname" class="form-control" @bind="@_moduledefinitionname" disabled />
</td>
</tr>
<tr>
<td>
<Label For="version" HelpText="The version of the module" ResourceKey="Version">Version: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the module" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</td>
</tr>
<tr>
<td>
<Label For="owner" HelpText="The owner or creator of the module" ResourceKey="Owner">Owner: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the module" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</td>
</tr>
<tr>
<td>
<Label For="url" HelpText="The reference url of the module" ResourceKey="ReferenceUrl">Reference Url: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the module" ResourceKey="ReferenceUrl">Reference Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</td>
</tr>
<tr>
<td>
<Label For="contact" HelpText="The contact for the module" ResourceKey="Contact">Contact: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the module" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</td>
</tr>
<tr>
<td>
<Label For="license" HelpText="The module license terms" ResourceKey="License">License: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The module license terms" ResourceKey="License">License: </Label>
<div class="col-sm-9">
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
</td>
</tr>
<tr>
<td>
<Label For="runtimes" HelpText="The Blazor runtimes which this module supports" ResourceKey="Runtimes">Runtimes: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtimes" HelpText="The Blazor runtimes which this module supports" ResourceKey="Runtimes">Runtimes: </Label>
<div class="col-sm-9">
<input id="runtimes" class="form-control" @bind="@_runtimes" disabled />
</td>
</tr>
</table>
</div>
</div>
</div>
</Section>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
<table class="table table-borderless">
<tr>
<td>
<PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" Permissions="@_permissions" @ref="_permissionGrid" />
</td>
</tr>
</table>
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" Permissions="@_permissions" @ref="_permissionGrid" />
</div>
</div>
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
@code {
private ElementReference form;
private bool validated = false;
private int _moduleDefinitionId;
private string _name;
private string _version;
@ -161,36 +144,45 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error Loading Module"], MessageType.Error);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
private async Task SaveModuleDefinition()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduledefinition.Name != _name)
try
{
moduledefinition.Name = _name;
var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduledefinition.Name != _name)
{
moduledefinition.Name = _name;
}
if (moduledefinition.Description != _description)
{
moduledefinition.Description = _description;
}
if (moduledefinition.Categories != _categories)
{
moduledefinition.Categories = _categories;
}
moduledefinition.Permissions = _permissionGrid.GetPermissions();
await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition);
await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition);
NavigationManager.NavigateTo(NavigateUrl());
}
if (moduledefinition.Description != _description)
catch (Exception ex)
{
moduledefinition.Description = _description;
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
if (moduledefinition.Categories != _categories)
{
moduledefinition.Categories = _categories;
}
moduledefinition.Permissions = _permissionGrid.GetPermissions();
await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition);
await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error Saving Module"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -4,23 +4,26 @@
@inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_moduleDefinitions == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" />
<Pager Items="@_moduleDefinitions">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@Localizer["Version"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@SharedLocalizer["Version"]</th>
<th>@Localizer["InUse"]</th>
<th>@SharedLocalizer["Expires"]</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
<Row>
@ -28,72 +31,107 @@ else
<td>
@if (context.AssemblyName != "Oqtane.Client")
{
<ActionDialog Header="Delete Module" Message="@Localizer["Are You Sure You Wish To Delete The {0} Module?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" />
<ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete", context.Name])" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" />
}
</td>
<td>@context.Name</td>
<td>@context.Version</td>
<td>
@if (UpgradeAvailable(context.ModuleDefinitionName, context.Version))
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.ModuleDefinitionName, context.Version))>@Localizer["Upgrade"]</button>
}
@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>
@((MarkupString)PurchaseLink(context.PackageName))
</td>
<td>
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
}
</td>
</Row>
</Pager>
}
@code {
private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages;
private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
protected override async Task OnParametersSetAsync()
{
try
{
_moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module");
}
catch (Exception ex)
{
if (_moduleDefinitions == null)
{
await logger.LogError(ex, "Error Loading Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
}
private string PurchaseLink(string packagename)
{
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
if (package.ExpiryDate != null && package.ExpiryDate.Value.Date != DateTime.MaxValue.Date)
{
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span>";
if (!string.IsNullOrEmpty(package.PaymentUrl))
{
link += "&nbsp;&nbsp;<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + SharedLocalizer["Extend"] + "</a>";
}
}
}
}
return link;
}
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
}
return version;
}
private async Task DownloadModule(string packagename, string version)
{
try
{
_moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module");
}
catch (Exception ex)
{
if (_moduleDefinitions == null)
{
await logger.LogError(ex, "Error Loading Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Modules"], MessageType.Error);
}
}
}
private bool UpgradeAvailable(string moduledefinitionname, string version)
{
var upgradeavailable = false;
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == Utilities.GetTypeName(moduledefinitionname)).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
}
}
return upgradeavailable;
}
private async Task DownloadModule(string moduledefinitionname, string version)
{
try
{
await PackageService.DownloadPackageAsync(moduledefinitionname, version, "Modules");
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", moduledefinitionname, version);
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(Localizer["Module Installed Successfully. You Must <a href=\"{0}\">Restart</a> Your Application To Apply These Changes.", NavigateUrl("admin/system")], MessageType.Success);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", moduledefinitionname, version, ex.Message);
AddModuleMessage(Localizer["Error Downloading Module"], MessageType.Error);
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
@ -102,13 +140,13 @@ else
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Module Deleted Successfully"], MessageType.Success);
StateHasChanged();
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error Deleting Module"], MessageType.Error);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
}

View File

@ -3,32 +3,37 @@
@inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IStringLocalizer<Export> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="The Exported Module Content" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" readonly></textarea>
</div>
</div>
</div>
<table class="table table-borderless">
<tbody>
<tr>
<td>
<Label For="content" HelpText="Enter the module content" ResourceKey="Content">Content: </Label>
</td>
<td>
<textarea id="content" class="form-control" @bind="@_content" rows="5"></textarea>
</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _content = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override string Title => "Export Content";
private async Task ExportModule()
{
_content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId);
try
{
_content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId);
AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error);
}
}
}

View File

@ -3,54 +3,65 @@
@inject NavigationManager NavigationManager
@inject IModuleService ModuleService
@inject IStringLocalizer<Import> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tbody>
<tr>
<td>
<Label For="content" HelpText="Enter the module content" ResourceKey="Content">Content: </Label>
</td>
<td>
<textarea id="content" class="form-control" @bind="@_content" rows="5"></textarea>
</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="content" HelpText="Enter The Module Content To Import" ResourceKey="Content">Content: </Label>
<div class="col-sm-9">
<textarea id="content" class="form-control" @bind="@_content" rows="5" required></textarea>
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
@code {
private string _content = string.Empty;
private ElementReference form;
private bool validated = false;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override string Title => "Import Content";
private async Task ImportModule()
{
if (_content != string.Empty)
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
if (_content != string.Empty)
{
bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, _content);
if (success)
try
{
AddModuleMessage(Localizer["Content Imported Successfully"], MessageType.Success);
bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, _content);
if (success)
{
AddModuleMessage(Localizer["Success.Content.Import"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Content.ImportProblem"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["A Problem Was Encountered Importing Content. Please Ensure The Content Is Formatted Correctly For The Module."], MessageType.Warning);
await logger.LogError(ex, "Error Importing Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Import"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Importing Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message);
AddModuleMessage(Localizer["Error Importing Module"], MessageType.Error);
AddModuleMessage(Localizer["Message.Required.ImportContent"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Enter Some Content To Import"], MessageType.Warning);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -6,95 +6,95 @@
@inject IModuleService ModuleService
@inject IPageModuleService PageModuleService
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
@if (_containers != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="title" HelpText="Enter the title of the module" ResourceKey="Title">Title: </Label>
</td>
<td>
<input id="title" type="text" name="Title" class="form-control" @bind="@_title" />
</td>
</tr>
<tr>
<td>
<Label For="container" HelpText="Select the module's container" ResourceKey="Container">Container: </Label>
</td>
<td>
<select id="container" class="form-control" @bind="@_containerType">
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="allpages" HelpText="Indicate if this module should be displayed on all pages" ResourceKey="DisplayOnAllPages">Display On All Pages? </Label>
</td>
<td>
<select id="allpages" class="form-control" @bind="@_allPages">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label>
</td>
<td>
<select id="page" class="form-control" @bind="@_pageId">
@foreach (Page p in PageState.Pages)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<TabStrip>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
@if (_containers != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Enter the title of the module" ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" type="text" name="Title" class="form-control" @bind="@_title" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="container" HelpText="Select the module's container" ResourceKey="Container">Container: </Label>
<div class="col-sm-9">
<select id="container" class="form-select" @bind="@_containerType" required>
@foreach (var container in _containers)
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
<option value="@container.TypeName">@container.Name</option>
}
}
</select>
</td>
</tr>
</table>
}
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
@if (_permissions != null)
{
<table class="table table-borderless">
<tr>
<td>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allpages" HelpText="Indicate if this module should be displayed on all pages" ResourceKey="DisplayOnAllPages">Display On All Pages? </Label>
<div class="col-sm-9">
<select id="allpages" class="form-select" @bind="@_allPages" 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="page" HelpText="The page that the module is located on" ResourceKey="Page">Page: </Label>
<div class="col-sm-9">
<select id="page" class="form-select" @bind="@_pageId" required>
@foreach (Page p in PageState.Pages)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
}
}
</select>
</div>
</div>
</div>
}
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
@if (_permissions != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Module" PermissionNames="@_permissionNames" Permissions="@_permissions" @ref="_permissionGrid" />
</td>
</tr>
</table>
</div>
</div>
}
</TabPanel>
@if (_moduleSettingsType != null)
{
<TabPanel Name="ModuleSettings" Heading="@_moduleSettingsTitle" ResourceKey="ModuleSettings">
@ModuleSettingsComponent
</TabPanel>
}
</TabPanel>
@if (_moduleSettingsType != null)
{
<TabPanel Name="ModuleSettings" Heading="@_moduleSettingsTitle" ResourceKey="ModuleSettings">
@ModuleSettingsComponent
</TabPanel>
}
@if (_containerSettingsType != null)
{
<TabPanel Name="ContainerSettings" Heading="Container Settings" ResourceKey="ContainerSettings">
@ContainerSettingsComponent
</TabPanel>
}
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SaveModule">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
@if (_containerSettingsType != null)
{
<TabPanel Name="ContainerSettings" Heading="Container Settings" ResourceKey="ContainerSettings">
@ContainerSettingsComponent
</TabPanel>
}
</TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModule">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Module Settings";
private ElementReference form;
private bool validated = false;
private List<Theme> _themes;
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _title;
@ -111,6 +111,10 @@
private Type _containerSettingsType;
private object _containerSettings;
private RenderFragment ContainerSettingsComponent { get; set; }
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
protected override async Task OnInitializedAsync()
{
@ -122,6 +126,10 @@
_permissions = ModuleState.Permissions;
_permissionNames = ModuleState.ModuleDefinition.PermissionNames;
_pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
{
@ -167,46 +175,62 @@
private async Task SaveModule()
{
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title;
pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty;
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType)
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
pagemodule.ContainerType = string.Empty;
}
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Site.DefaultContainerType)
{
pagemodule.ContainerType = string.Empty;
}
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState;
module.AllPages = bool.Parse(_allPages);
module.Permissions = _permissionGrid.GetPermissions();
await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
if (!string.IsNullOrEmpty(_title))
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title;
pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty;
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType)
{
pagemodule.ContainerType = string.Empty;
}
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Site.DefaultContainerType)
{
pagemodule.ContainerType = string.Empty;
}
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
var module = ModuleState;
module.AllPages = bool.Parse(_allPages);
module.Permissions = _permissionGrid.GetPermissions();
await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null)
{
if (_moduleSettings is ISettingsControl moduleSettingsControl)
{
// module settings updated using explicit interface
await moduleSettingsControl.UpdateSettings();
}
else
{
// legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component )
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
}
}
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
{
await containerSettingsControl.UpdateSettings();
}
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
// legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component )
_moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null);
AddModuleMessage(Localizer["Message.Required.Title"], MessageType.Warning);
}
}
if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl)
else
{
await containerSettingsControl.UpdateSettings();
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
NavigationManager.NavigateTo(NavigateUrl());
}
}

View File

@ -4,390 +4,409 @@
@inject IPageService PageService
@inject IThemeService ThemeService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Settings" ResourceKey="Settings">
@if (_themeList != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="Name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="Name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="Parent" HelpText="Select the parent for the page in the site hierarchy" ResourceKey="Parent">Parent: </Label>
</td>
<td>
<select id="Parent" class="form-control" @onchange="(e => ParentChanged(e))">
<option value="-1">&lt;@Localizer["Site Root"]&gt;</option>
@foreach (Page page in _pageList)
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<TabStrip Refresh="@_refresh">
<TabPanel Name="Settings" ResourceKey="Settings">
@if (_themeList != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parent" HelpText="Select the parent for the page in the site hierarchy" ResourceKey="Parent">Parent: </Label>
<div class="col-sm-9">
<select id="parent" class="form-select" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pageList)
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="insert" HelpText="Select the location where you would like the page to be inserted in relation to other pages" ResourceKey="Insert">Insert: </Label>
<div class="col-sm-9">
<select id="insert" class="form-select" @bind="@_insert" required>
<option value="<<">@Localizer["AtBeginning"]</option>
@if (_children != null && _children.Count > 0)
{
<option value="<">@Localizer["Before"]</option>
<option value=">">@Localizer["After"]</option>
}
<option value=">>">@Localizer["AtEnd"]</option>
</select>
@if (_children != null && _children.Count > 0 && (_insert == "<" || _insert == ">"))
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
<select class="form-select" @bind="@_childid">
<option value="-1">&lt;@Localizer["Page.Select"]&gt;</option>
@foreach (Page page in _children)
{
<option value="@(page.PageId)">@(page.Name)</option>
}
</select>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="Insert" HelpText="Select the location where you would like the page to be inserted in relation to other pages" ResourceKey="Insert">Insert: </Label>
</td>
<td>
<select id="Insert" class="form-control" @bind="@_insert">
<option value="<<">@Localizer["At Beginning"]</option>
@if (_children != null && _children.Count > 0)
{
<option value="<">@Localizer["Before"]</option>
<option value=">">@Localizer["After"]</option>
}
<option value=">>">@Localizer["At End"]</option>
</select>
@if (_children != null && _children.Count > 0 && (_insert == "<" || _insert == ">"))
{
<select class="form-control" @bind="@_childid">
<option value="-1">&lt;@Localizer["Select Page"]&gt;</option>
@foreach (Page page in _children)
{
<option value="@(page.PageId)">@(page.Name)</option>
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
}
</td>
</tr>
<tr>
<td>
<Label For="Navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
</td>
<td>
<select id="Navigation" class="form-control" @bind="@_isnavigation">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="Path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used." ResourceKey="UrlPath">Url Path: </Label>
</td>
<td>
<input id="Path" class="form-control" @bind="@_path" />
</td>
</tr>
<tr>
<td>
<Label For="Url" HelpText="Optionally enter a url which this page should redirect to when a user navigates to it" ResourceKey="Redirect">Redirect: </Label>
</td>
<td>
<input id="Url" class="form-control" @bind="@_url" />
</td>
</tr>
</table>
<Section Name="Appearance" ResourceKey="Appearance">
<table class="table table-borderless">
<tr>
<td>
<Label For="Title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
</td>
<td>
<input id="Title" class="form-control" @bind="@_title" />
</td>
</tr>
<tr>
<td>
<Label For="Theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
</td>
<td>
<select id="Theme" class="form-control" value="@_themetype" @onchange="(e => ThemeChanged(e))">
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="defaultContainer" HelpText="Select the default container for the page" ResourceKey="DefaultContainer">Default Container: </Label>
</td>
<td>
<select id="defaultContainer" class="form-control" @bind="@_containertype">
<option value="-">&lt;@Localizer["Select Container"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="Icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
</td>
<td>
<input id="Icon" class="form-control" @bind="@_icon" />
</td>
</tr>
<tr>
<td>
<Label For="Personalizable" HelpText="Select whether you would like users to be able to personalize this page with their own content" ResourceKey="Personalizable">Personalizable? </Label>
</td>
<td>
<select id="Personalizable" class="form-control" @bind="@_ispersonalizable">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
</table>
</Section>
}
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
<table class="table table-borderless">
<tr>
<td>
<PermissionGrid EntityName="@EntityNames.Page" Permissions="@_permissions" @ref="_permissionGrid" />
</td>
</tr>
</table>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@ThemeSettingsComponent
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="Optionally enter a url which this page should redirect to when a user navigates to it" ResourceKey="Redirect">Redirect: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" />
</div>
</div>
</div>
<Section Name="Appearance" ResourceKey="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" />
</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">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@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="container" HelpText="Select the default container for the page" ResourceKey="DefaultContainer">Default Container: </Label>
<div class="col-sm-9">
<select id="container" 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="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-9">
<input id="icon" class="form-control" @bind="@_icon" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="personalizable" HelpText="Select whether you would like users to be able to personalize this page with their own content" ResourceKey="Personalizable">Personalizable? </Label>
<div class="col-sm-9">
<select id="personalizable" class="form-select" @bind="@_ispersonalizable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</Section>
}
</TabPanel>
}
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@Localizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@Localizer["Cancel"]</button>
<TabPanel Name="Permissions" ResourceKey="Permissions">
<div class="container">
<div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" Permissions="@_permissions" @ref="_permissionGrid" />
</div>
</div>
</TabPanel>
@if (_themeSettingsType != null)
{
<TabPanel Name="ThemeSettings" Heading="Theme Settings" ResourceKey="ThemeSettings">
@ThemeSettingsComponent
</TabPanel>
}
</TabStrip>
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pageList;
private string _name;
private string _title;
private string _path = string.Empty;
private string _parentid;
private string _insert = ">>";
private List<Page> _children;
private int _childid = -1;
private string _isnavigation = "True";
private string _url;
private string _ispersonalizable = "False";
private string _themetype = string.Empty;
private string _containertype = string.Empty;
private string _icon = string.Empty;
private string _permissions = string.Empty;
private PermissionGrid _permissionGrid;
private Type _themeSettingsType;
private object _themeSettings;
private RenderFragment ThemeSettingsComponent { get; set; }
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pageList;
private string _name;
private string _title;
private string _meta;
private string _path = string.Empty;
private string _parentid = "-1";
private string _insert = ">>";
private List<Page> _children;
private int _childid = -1;
private string _isnavigation = "True";
private string _isclickable = "True";
private string _url;
private string _ispersonalizable = "False";
private string _themetype = string.Empty;
private string _containertype = string.Empty;
private string _icon = string.Empty;
private string _permissions = string.Empty;
private PermissionGrid _permissionGrid;
private Type _themeSettingsType;
private object _themeSettings;
private RenderFragment ThemeSettingsComponent { get; set; }
private bool _refresh = false;
private ElementReference form;
private bool validated = false;
protected override async Task OnInitializedAsync()
{
try
{
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
_themetype = PageState.Site.DefaultThemeType;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = PageState.Site.DefaultContainerType;
_pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_permissions = string.Empty;
ThemeSettings();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Initializing Page {Error}", ex.Message);
AddModuleMessage(Localizer["Error Initializing Page"], MessageType.Error);
}
}
protected override async Task OnInitializedAsync()
{
try
{
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
_themetype = PageState.Site.DefaultThemeType;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = PageState.Site.DefaultContainerType;
_pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_permissions = string.Empty;
ThemeSettings();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Initializing Page {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Page.Initialize"], MessageType.Error);
}
}
private async void ParentChanged(ChangeEventArgs e)
{
try
{
_parentid = (string)e.Value;
_children = new List<Page>();
if (_parentid == "-1")
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
private async void ParentChanged(ChangeEventArgs e)
{
try
{
_parentid = (string)e.Value;
_children = new List<Page>();
if (_parentid == "-1")
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{
_children.Add(p);
}
}
}
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);
}
}
}
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)
{
try
{
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = "-";
ThemeSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Pane.Load"], MessageType.Error);
}
}
private void ThemeSettings()
{
_themeSettingsType = null;
var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype)));
if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
_themeSettingsType = Type.GetType(theme.ThemeSettingsType);
if (_themeSettingsType != null)
{
ThemeSettingsComponent = builder =>
{
builder.OpenComponent(0, _themeSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
_refresh = true;
}
}
private async Task SavePage()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
Page page = null;
try
{
if (!string.IsNullOrEmpty(_themetype) && _containertype != "-")
{
page = new Page();
page.SiteId = PageState.Page.SiteId;
page.Name = _name;
page.Title = _title;
if (string.IsNullOrEmpty(_path))
{
_path = _name;
}
if (_path.Contains("/"))
{
_path = _path.Substring(_path.LastIndexOf("/") + 1);
}
if (_parentid == "-1")
{
page.ParentId = null;
page.Path = Utilities.GetFriendlyUrl(_path);
}
else
{
page.ParentId = Int32.Parse(_parentid);
var parent = PageState.Pages.Where(item => item.PageId == page.ParentId).FirstOrDefault();
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
if(PagePathIsDeleted(page.Path, page.SiteId, _pageList))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Deleted"], _path), MessageType.Warning);
return;
}
if (!PagePathIsUnique(page.Path, page.SiteId, _pageList))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
return;
}
Page child;
switch (_insert)
{
case "<<":
page.Order = 0;
break;
case "<":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order - 1;
break;
case ">":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order + 1;
break;
case ">>":
page.Order = int.MaxValue;
break;
}
page.IsNavigation = (_isnavigation == null ? true : Boolean.Parse(_isnavigation));
page.IsClickable = (_isclickable == null ? true : Boolean.Parse(_isclickable));
page.Url = _url;
page.ThemeType = (_themetype != "-") ? _themetype : string.Empty;
if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType)
{
page.ThemeType = string.Empty;
}
page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty;
if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType)
{
page.DefaultContainerType = string.Empty;
}
page.Icon = (_icon == null ? string.Empty : _icon);
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable));
page.UserId = null;
page.Meta = _meta;
page = await PageService.AddPageAsync(page);
await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId);
await logger.LogInformation("Page Added {Page}", page);
if (PageState.QueryString.ContainsKey("cp"))
{
_children.Add(p);
}
}
}
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);
}
}
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error Loading Child Pages For Parent"], MessageType.Error);
}
}
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = "-";
ThemeSettings();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error Loading Pane Layouts For Theme"], MessageType.Error);
}
}
private void ThemeSettings()
{
_themeSettingsType = null;
var theme = _themeList.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype)));
if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
_themeSettingsType = Type.GetType(theme.ThemeSettingsType);
if (_themeSettingsType != null)
{
ThemeSettingsComponent = builder =>
{
builder.OpenComponent(0, _themeSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); });
builder.CloseComponent();
};
}
}
}
private async Task SavePage()
{
Page page = null;
try
{
if (_name != string.Empty && !string.IsNullOrEmpty(_themetype) && _containertype != "-")
{
page = new Page();
page.SiteId = PageState.Page.SiteId;
page.Name = _name;
page.Title = _title;
if (_path == "")
{
_path = _name;
}
if (_path.Contains("/"))
{
_path = _path.Substring(_path.LastIndexOf("/") + 1);
}
if (string.IsNullOrEmpty(_parentid))
{
page.ParentId = null;
page.Path = Utilities.GetFriendlyUrl(_path);
}
else
{
page.ParentId = Int32.Parse(_parentid);
var parent = PageState.Pages.Where(item => item.PageId == page.ParentId).FirstOrDefault();
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
NavigationManager.NavigateTo(NavigateUrl(PageState.Pages.First(item => item.PageId == int.Parse(PageState.QueryString["cp"])).Path));
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
NavigationManager.NavigateTo(NavigateUrl(page.Path));
}
}
if (!PagePathIsUnique(page.Path, page.SiteId, _pageList))
{
AddModuleMessage(Localizer["A page with path {0} already exists for the selected parent page. The page path needs to be unique for the selected parent.", _path], MessageType.Warning);
return;
}
Page child;
switch (_insert)
{
case "<<":
page.Order = 0;
break;
case "<":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order - 1;
break;
case ">":
child = PageState.Pages.Where(item => item.PageId == _childid).FirstOrDefault();
page.Order = child.Order + 1;
break;
case ">>":
page.Order = int.MaxValue;
break;
}
page.IsNavigation = (_isnavigation == null ? true : Boolean.Parse(_isnavigation));
page.Url = _url;
page.ThemeType = (_themetype != "-") ? _themetype : string.Empty;
if (!string.IsNullOrEmpty(page.ThemeType) && page.ThemeType == PageState.Site.DefaultThemeType)
{
page.ThemeType = string.Empty;
}
page.DefaultContainerType = (_containertype != "-") ? _containertype : string.Empty;
if (!string.IsNullOrEmpty(page.DefaultContainerType) && page.DefaultContainerType == PageState.Site.DefaultContainerType)
{
page.DefaultContainerType = string.Empty;
}
page.Icon = (_icon == null ? string.Empty : _icon);
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable));
page.UserId = null;
page = await PageService.AddPageAsync(page);
await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId);
await logger.LogInformation("Page Added {Page}", page);
if (PageState.QueryString.ContainsKey("cp"))
{
NavigationManager.NavigateTo(NavigateUrl(PageState.Pages.First(item => item.PageId == int.Parse(PageState.QueryString["cp"])).Path));
}
else
{
NavigationManager.NavigateTo(NavigateUrl(page.Path));
AddModuleMessage(Localizer["Message.Required.PageInfo"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide Page Name, Theme, and Container"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Page {Page} {Error}", page, ex.Message);
AddModuleMessage(Localizer["Error.Page.Save"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving Page {Page} {Error}", page, ex.Message);
AddModuleMessage(Localizer["Error Saving Page"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
@ -407,4 +426,9 @@
{
return !existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath);
}
private static bool PagePathIsDeleted(string pagePath, int siteId, List<Page> existingPages)
{
return existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath && page.IsDeleted == true);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IPageService PageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.Pages != null)
{
@ -10,13 +11,15 @@
<Pager Items="@PageState.Pages.Where(item => !item.IsDeleted)">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.PageId.ToString())" ResourceKey="EditPage" /></td>
<td><ActionDialog Header="Delete Page" Message="@Localizer["Are You Sure You Wish To Delete The {0} Page?", 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><button type="button" class="btn btn-secondary" @onclick="@(async () => NavigationManager.NavigateTo(Browse(context)))">@Localizer["Browse"]</button></td>
<td>@(new string('-', context.Level * 2))@(context.Name)</td>
</Row>
</Pager>
@ -30,7 +33,7 @@
try
{
page.IsDeleted = true;
await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Deleted {Page}", page);
NavigationManager.NavigateTo(NavigateUrl("admin/pages"));
@ -38,7 +41,11 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Page {Page} {Error}", page, ex.Message);
AddModuleMessage(Localizer["Error Deleting Page"], MessageType.Error);
AddModuleMessage(Localizer["Error.Page.Delete"], MessageType.Error);
}
}
protected string Browse(Page page)
{
return string.IsNullOrEmpty(page.Url) ? NavigateUrl(page.Path) : page.Url;
}
}

View File

@ -3,100 +3,92 @@
@inject NavigationManager NavigationManager
@inject IProfileService ProfileService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="The name of this profile item" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="title" HelpText="The title of the profile item to display to the user" ResourceKey="Title">Title: </Label>
</td>
<td>
<input id="title" class="form-control" @bind="@_title" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="The help text displayed to the user for this profile item" ResourceKey="Description">Description: </Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="5"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="category" HelpText="The category of this profile item (for grouping)" ResourceKey="Category">Category: </Label>
</td>
<td>
<input id="category" class="form-control" @bind="@_category" />
</td>
</tr>
<tr>
<td>
<Label For="order" HelpText="The index order of where this profile item should be displayed" ResourceKey="Order">Order: </Label>
</td>
<td>
<input id="order" class="form-control" @bind="@_vieworder" />
</td>
</tr>
<tr>
<td>
<Label For="length" HelpText="The max number of characters this profile item should accept (enter zero for unlimited)" ResourceKey="Length">Length: </Label>
</td>
<td>
<input id="length" class="form-control" @bind="@_maxlength" />
</td>
</tr>
<tr>
<td>
<Label For="defaultVal" HelpText="The default value for this profile item" ResourceKey="DefaultValue">Default Value: </Label>
</td>
<td>
<input id="defaultVal" class="form-control" @bind="@_defaultvalue" />
</td>
</tr>
<tr>
<td>
<Label For="options" HelpText="A comma delimited list of options the user can select from" ResourceKey="Options">Options: </Label>
</td>
<td>
<input id="options" class="form-control" @bind="@_options" />
</td>
</tr>
<tr>
<td>
<Label For="required" HelpText="Should a user be required to provide a value for this profile item?" ResourceKey="Required">Required? </Label>
</td>
<td>
<select id="required" class="form-control" @bind="@_isrequired">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="private" HelpText="Should this profile item be visible to all users?" ResourceKey="Private">Private? </Label>
</td>
<td>
<select id="private" class="form-control" @bind="@_isprivate">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveProfile">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="The name of this profile item" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="The title of the profile item to display to the user" ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" maxlength="50" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="The help text displayed to the user for this profile item" ResourceKey="Description">Description: </Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="5" maxlength="256" required ></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="category" HelpText="The category of this profile item (for grouping)" ResourceKey="Category">Category: </Label>
<div class="col-sm-9">
<input id="category" class="form-control" @bind="@_category" maxlength="50" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="order" HelpText="The index order of where this profile item should be displayed" ResourceKey="Order">Order: </Label>
<div class="col-sm-9">
<input id="order" class="form-control" @bind="@_vieworder" maxlength="4" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="length" HelpText="The max number of characters this profile item should accept (enter zero for unlimited)" ResourceKey="Length">Length: </Label>
<div class="col-sm-9">
<input id="length" class="form-control" @bind="@_maxlength" maxlength="4" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultVal" HelpText="The default value for this profile item" ResourceKey="DefaultValue">Default Value: </Label>
<div class="col-sm-9">
<input id="defaultVal" class="form-control" @bind="@_defaultvalue" maxlength="2000"/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="options" HelpText="A comma delimited list of options the user can select from" ResourceKey="Options">Options: </Label>
<div class="col-sm-9">
<input id="options" class="form-control" @bind="@_options" maxlength="2000" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="required" HelpText="Should a user be required to provide a value for this profile item?" ResourceKey="Required">Required? </Label>
<div class="col-sm-9">
<select id="required" class="form-select" @bind="@_isrequired" 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="private" HelpText="Should this profile item be visible to all users?" ResourceKey="Private">Private? </Label>
<div class="col-sm-9">
<select id="private" class="form-select" @bind="@_isprivate" required>
<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="SaveProfile">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (PageState.QueryString.ContainsKey("id"))
{
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
}
</form>
@code {
private int _profileid = -1;
private ElementReference form;
private bool validated = false;
private string _name = string.Empty;
private string _title = string.Empty;
private string _description = string.Empty;
@ -107,6 +99,10 @@
private string _options = string.Empty;
private string _isrequired = "False";
private string _isprivate = "False";
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -132,57 +128,70 @@
_options = profile.Options;
_isrequired = profile.IsRequired.ToString();
_isprivate = profile.IsPrivate.ToString();
createdby = profile.CreatedBy;
createdon = profile.CreatedOn;
modifiedby = profile.ModifiedBy;
modifiedon = profile.ModifiedOn;
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Profile {ProfileId} {Error}", _profileid, ex.Message);
AddModuleMessage(Localizer["Error Loading Profile"], MessageType.Error);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
private async Task SaveProfile()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
Profile profile;
if (_profileid != -1)
try
{
profile = await ProfileService.GetProfileAsync(_profileid);
}
else
{
profile = new Profile();
}
Profile profile;
if (_profileid != -1)
{
profile = await ProfileService.GetProfileAsync(_profileid);
}
else
{
profile = new Profile();
}
profile.SiteId = PageState.Site.SiteId;
profile.Name = _name;
profile.Title = _title;
profile.Description = _description;
profile.Category = _category;
profile.ViewOrder = int.Parse(_vieworder);
profile.MaxLength = int.Parse(_maxlength);
profile.DefaultValue = _defaultvalue;
profile.Options = _options;
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));
profile.IsPrivate = (_isprivate == null ? false : Boolean.Parse(_isprivate));
if (_profileid != -1)
{
profile = await ProfileService.UpdateProfileAsync(profile);
}
else
{
profile = await ProfileService.AddProfileAsync(profile);
}
profile.SiteId = PageState.Site.SiteId;
profile.Name = _name;
profile.Title = _title;
profile.Description = _description;
profile.Category = _category;
profile.ViewOrder = int.Parse(_vieworder);
profile.MaxLength = int.Parse(_maxlength);
profile.DefaultValue = _defaultvalue;
profile.Options = _options;
profile.IsRequired = (_isrequired == null ? false : Boolean.Parse(_isrequired));
profile.IsPrivate = (_isprivate == null ? false : Boolean.Parse(_isprivate));
if (_profileid != -1)
{
profile = await ProfileService.UpdateProfileAsync(profile);
}
else
{
profile = await ProfileService.AddProfileAsync(profile);
}
await logger.LogInformation("Profile Saved {Profile}", profile);
NavigationManager.NavigateTo(NavigateUrl());
await logger.LogInformation("Profile Saved {Profile}", profile);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Profile {ProfleId} {Error}", _profileid, ex.Message);
AddModuleMessage(Localizer["Error.Profile.Save"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving Profile {ProfleId} {Error}", _profileid, ex.Message);
AddModuleMessage(Localizer["Error Saving Profile"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -2,10 +2,11 @@
@inherits ModuleBase
@inject IProfileService ProfileService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_profiles == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
@ -15,11 +16,11 @@ else
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@SharedLocalizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" ResourceKey="EditProfile" /></td>
<td><ActionDialog Header="Delete Profile" Message="@Localizer["Are You Sure You Wish To Delete {0}?", context.Name]" Action="Delete" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td>
<td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td>
<td>@context.Name</td>
</Row>
</Pager>
@ -42,7 +43,7 @@ else
await ProfileService.DeleteProfileAsync(profileId);
await logger.LogInformation("Profile Deleted {ProfileId}", profileId);
AddModuleMessage(Localizer["Profile Deleted"], MessageType.Success);
AddModuleMessage(Localizer["Success.Profile.Delete"], MessageType.Success);
await GetProfilesAsync();
@ -51,7 +52,7 @@ else
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Profile {ProfileId} {Error}", profileId, ex.Message);
AddModuleMessage(Localizer["Error Deleting Profile"], MessageType.Error);
AddModuleMessage(Localizer["Error.Profile.Delete"], MessageType.Error);
}
}

View File

@ -5,13 +5,14 @@
@inject IModuleService ModuleService
@inject IPageService PageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages">
@if (_pages == null)
{
<br />
<p>@Localizer["No Deleted Pages"]</p>
<p>@Localizer["NoPage.Deleted"]</p>
}
else
{
@ -19,13 +20,13 @@
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@Localizer["Deleted By"]</th>
<th>@Localizer["Deleted On"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button @onclick="@(() => RestorePage(context))" class="btn btn-info" title="Restore">Restore</button></td>
<td><ActionDialog Header="Delete Page" Message="@Localizer["Are You Sure You Wish To Permanently Delete The {0} Page?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></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>@context.Name</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
@ -33,9 +34,7 @@
</Pager>
@if (_pages.Any())
{
<div style="text-align:right;">
<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>
<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" />
}
}
</TabPanel>
@ -43,7 +42,7 @@
@if (_modules == null)
{
<br />
<p>@Localizer["No Deleted Modules"]</p>
<p>@Localizer["NoModule.Deleted"]</p>
}
else
{
@ -53,12 +52,12 @@
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Page"]</th>
<th>@Localizer["Module"]</th>
<th>@Localizer["Deleted By"]</th>
<th>@Localizer["Deleted On"]</th>
<th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button @onclick="@(() => RestoreModule(context))" class="btn btn-info" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@Localizer["Are You Sure You Wish To Permanently Delete The {0} Module?", context.Title]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></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>@PageState.Pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td>
<td>@context.DeletedBy</td>
@ -67,9 +66,7 @@
</Pager>
@if (_modules.Any())
{
<div style="text-align:right;">
<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>
<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" />
}
}
@ -91,7 +88,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Deleted Pages Or Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Deleted Pages Or Modules"], MessageType.Error);
AddModuleMessage(Localizer["Error.DeletedModulePage.Load"], MessageType.Error);
}
}
@ -118,7 +115,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Restoring Deleted Page {Page} {Error}", page, ex.Message);
AddModuleMessage(Localizer["Error Restoring Deleted Page"], MessageType.Error);
AddModuleMessage(Localizer["Error.Page.Restore"], MessageType.Error);
}
}
@ -175,7 +172,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Restoring Deleted Module {Module} {Error}", module, ex.Message);
AddModuleMessage(Localizer["Error Restoring Deleted Module"], MessageType.Error);
AddModuleMessage(Localizer["Error.Module.Restore"], MessageType.Error);
}
}
@ -199,7 +196,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting Module {Module} {Error}", module, ex.Message);
AddModuleMessage(Localizer["Error Permanently Deleting Module"], MessageType.Error);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
@ -226,7 +223,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error Permanently Deleting Modules"], MessageType.Error);
AddModuleMessage(Localizer["Error.Modules.Delete"], MessageType.Error);
}
}
}

View File

@ -3,105 +3,143 @@
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.Site.AllowRegistration)
{
<AuthorizeView>
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<ModuleMessage Message="@Localizer["You Are Already Registered"]" Type="MessageType.Info" />
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
</Authorized>
<NotAuthorized>
<ModuleMessage Message="@Localizer["Please Note That Registration Requires A Valid Email Address In Order To Verify Your Identity"]" Type="MessageType.Info" />
<div class="container">
<div class="form-group">
<label for="Username" class="control-label">@Localizer["Username:"] </label>
<input type="text" class="form-control" placeholder="Username" @bind="@_username" id="Username" />
</div>
<div class="form-group">
<label for="Password" class="control-label">@Localizer["Password:"] </label>
<input type="password" class="form-control" placeholder="Password" @bind="@_password" id="Password" />
</div>
<div class="form-group">
<label for="Confirm" class="control-label">@Localizer["Confirm Password:"] </label>
<input type="password" class="form-control" placeholder="Password" @bind="@_confirm" id="Confirm" />
</div>
<div class="form-group">
<label for="Email" class="control-label">@Localizer["Email:"] </label>
<input type="text" class="form-control" placeholder="Email" @bind="@_email" id="Email" />
</div>
<div class="form-group">
<label for="DisplayName" class="control-label">@Localizer["Full Name:"] </label>
<input type="text" class="form-control" placeholder="Full Name" @bind="@_displayName" id="DisplayName" />
<ModuleMessage Message="@Localizer["Info.Registration.InvalidEmail"]" Type="MessageType.Info" />
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@Localizer["Cancel"]</button>
</div>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>
</NotAuthorized>
</AuthorizeView>
}
else
{
<ModuleMessage Message="@Localizer["Registration is Disabled For This Site"]" Type="MessageType.Info" />
<ModuleMessage Message="@Localizer["Info.Registration.Disabled"]" Type="MessageType.Info" />
}
@code {
private string _username = string.Empty;
private string _password = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayName = string.Empty;
private string _username = string.Empty;
private ElementReference form;
private bool validated = false;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnParametersSet()
{
_togglepassword = SharedLocalizer["ShowPassword"];
}
private async Task Register()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
bool _isEmailValid = Utilities.IsValidEmail(_email);
if (_username != "" && _password != "" && _confirm != "" && _isEmailValid)
try
{
if (_password == _confirm)
{
var user = new User
{
SiteId = PageState.Site.SiteId,
Username = _username,
DisplayName = (_displayName == string.Empty ? _username : _displayName),
Email = _email,
Password = _password
};
user = await UserService.AddUserAsync(user);
bool _isEmailValid = Utilities.IsValidEmail(_email);
if (user != null)
if (_isEmailValid)
{
if (_password == _confirm)
{
await logger.LogInformation("User Created {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["User Account Created. Please Check Your Email For Verification Instructions."], MessageType.Info);
var user = new User
{
SiteId = PageState.Site.SiteId,
Username = _username,
Password = _password,
Email = _email,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
PhotoFileId = null
};
user = await UserService.AddUserAsync(user);
if (user != null)
{
await logger.LogInformation("User Created {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info);
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Error.User.AddInfo"], MessageType.Error);
}
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Error Adding User. Please Ensure Password Meets Complexity Requirements And Username Is Not Already In Use."], MessageType.Error);
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Passwords Entered Do Not Match"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["You Must Provide A Username, Password, and Email Address"], MessageType.Warning);
await logger.LogError(ex, "Error Adding User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error.User.Add"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Adding User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error Adding User"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
@ -109,4 +147,18 @@ else
{
NavigationManager.NavigateTo(NavigateUrl(string.Empty));
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -3,84 +3,116 @@
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="form-group">
<label for="Username" class="control-label">@Localizer["Username:"] </label>
<input type="text" class="form-control" placeholder="Username" @bind="@_username" readonly id="Username"/>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username will be populated from the link you received in the password reset notification" ResourceKey="Username">Username: </Label>
<div class="col-sm-9">
<input id="username" type="text" class="form-control" @bind="@_username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The new password. It must satisfy complexity rules for the site." ResourceKey="Password">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter the password again. It must exactly match the password entered above." ResourceKey="Confirm">Confirm: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="Password" class="control-label">@Localizer["Password:"] </label>
<input type="password" class="form-control" placeholder="Password" @bind="@_password" id="Password"/>
</div>
<div class="form-group">
<label for="Confirm" class="control-label">@Localizer["Confirm Password:"] </label>
<input type="password" class="form-control" placeholder="Password" @bind="@_confirm" id="Confirm"/>
</div>
<button type="button" class="btn btn-primary" @onclick="Reset">@Localizer["Reset Password"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@Localizer["Cancel"]</button>
</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>
@code {
private string _username = string.Empty;
private string _password = string.Empty;
private string _confirm = string.Empty;
private ElementReference form;
private bool validated = false;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override void OnInitialized()
{
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
protected override async Task OnInitializedAsync()
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{
_username = PageState.QueryString["name"];
}
else
{
NavigationManager.NavigateTo(NavigateUrl(string.Empty));
{
await logger.LogError(LogFunction.Security, "Invalid Attempt To Access User Password Reset");
NavigationManager.NavigateTo(NavigateUrl("")); // home page
}
}
private async Task Reset()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_username != string.Empty && _password != string.Empty && _confirm != string.Empty)
try
{
if (_password == _confirm)
if (_username != string.Empty && _password != string.Empty && _confirm != string.Empty)
{
var user = new User
if (_password == _confirm)
{
SiteId = PageState.Site.SiteId,
Username = _username,
Password = _password
};
user = await UserService.ResetPasswordAsync(user, PageState.QueryString["token"]);
var user = new User
{
SiteId = PageState.Site.SiteId,
Username = _username,
Password = _password
};
user = await UserService.ResetPasswordAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation("User Password Reset {Username}", _username);
NavigationManager.NavigateTo(NavigateUrl("login"));
if (user != null)
{
await logger.LogInformation("User Password Reset {Username}", _username);
NavigationManager.NavigateTo(NavigateUrl("login"));
}
else
{
await logger.LogError("Error Resetting User Password {Username}", _username);
AddModuleMessage(Localizer["Error.Password.ResetInfo"], MessageType.Error);
}
}
else
{
await logger.LogError("Error Resetting User Password {Username}", _username);
AddModuleMessage(Localizer["Error Resetting User Password. Please Ensure Password Meets Complexity Requirements."], MessageType.Error);
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Passwords Entered Do Not Match"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["You Must Provide A Username, Password, and Email Address"], MessageType.Warning);
await logger.LogError(ex, "Error Resetting User Password {Username} {Error}", _username, ex.Message);
AddModuleMessage(Localizer["Error.Password.Reset"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Resetting User Password {Username} {Error}", _username, ex.Message);
AddModuleMessage(Localizer["Error Resetting User Password"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
@ -88,4 +120,18 @@
{
NavigationManager.NavigateTo(NavigateUrl(string.Empty));
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -3,40 +3,41 @@
@inject NavigationManager NavigationManager
@inject IRoleService RoleService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="Name Of The Role" ResourceKey="Name">Name:</Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="A Short Description Of The Role Which Describes Its Purpose" ResourceKey="Description">Description:</Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="5"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="isautoassigned" HelpText="Indicates Whether Or Not New Users Are Automatically Assigned To This Role" ResourceKey="AutoAssigned">Auto Assigned?</Label>
</td>
<td>
<select id="isautoassigned" class="form-control" @bind="@_isautoassigned">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveRole">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Name Of The Role" ResourceKey="Name">Name:</Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="A Short Description Of The Role Which Describes Its Purpose" ResourceKey="Description">Description:</Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="5" maxlength="256" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isautoassigned" HelpText="Indicates Whether Or Not New Users Are Automatically Assigned To This Role" ResourceKey="AutoAssigned">Auto Assigned?</Label>
<div class="col-sm-9">
<select id="isautoassigned" class="form-select" @bind="@_isautoassigned" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveRole">@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 _name = string.Empty;
private string _description = string.Empty;
private string _isautoassigned = "False";
@ -45,24 +46,33 @@
private async Task SaveRole()
{
var role = new Role();
role.SiteId = PageState.Page.SiteId;
role.Name = _name;
role.Description = _description;
role.IsAutoAssigned = (_isautoassigned == null ? false : Boolean.Parse(_isautoassigned));
role.IsSystem = false;
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
role = await RoleService.AddRoleAsync(role);
await logger.LogInformation("Role Added {Role}", role);
var role = new Role();
role.SiteId = PageState.Page.SiteId;
role.Name = _name;
role.Description = _description;
role.IsAutoAssigned = (_isautoassigned == null ? false : Boolean.Parse(_isautoassigned));
role.IsSystem = false;
NavigationManager.NavigateTo(NavigateUrl());
try
{
role = await RoleService.AddRoleAsync(role);
await logger.LogInformation("Role Added {Role}", role);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Role {Role} {Error}", role, ex.Message);
AddModuleMessage(Localizer["Error.AddRole"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Adding Role {Role} {Error}", role, ex.Message);
AddModuleMessage(Localizer["Error Adding Role"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}

View File

@ -3,44 +3,51 @@
@inject NavigationManager NavigationManager
@inject IRoleService RoleService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="Name Of The Role" ResourceKey="Name">Name:</Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="description" HelpText="A Short Description Of The Role Which Describes Its Purpose" ResourceKey="Description">Description:</Label>
</td>
<td>
<textarea id="description" class="form-control" @bind="@_description" rows="5"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="isautoassigned" HelpText="Indicates Whether Or Not New Users Are Automatically Assigned To This Role" ResourceKey="AutoAssigned">Auto Assigned?</Label>
</td>
<td>
<select id="isautoassigned" class="form-control" @bind="@_isautoassigned">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveRole">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Name Of The Role" ResourceKey="Name">Name:</Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="A Short Description Of The Role Which Describes Its Purpose" ResourceKey="Description">Description:</Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="5" maxlength="256" required></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isautoassigned" HelpText="Indicates Whether Or Not New Users Are Automatically Assigned To This Role" ResourceKey="AutoAssigned">Auto Assigned?</Label>
<div class="col-sm-9">
<select id="isautoassigned" class="form-select" @bind="@_isautoassigned" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveRole">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br /><br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
</div>
</form>
@code {
private ElementReference form;
private bool validated = false;
private int _roleid;
private string _name = string.Empty;
private string _description = string.Empty;
private string _isautoassigned = "False";
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -55,33 +62,46 @@
_name = role.Name;
_description = role.Description;
_isautoassigned = role.IsAutoAssigned.ToString();
_createdby = role.CreatedBy;
_createdon = role.CreatedOn;
_modifiedby = role.ModifiedBy;
_modifiedon = role.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Role {RoleId} {Error}", _roleid, ex.Message);
AddModuleMessage(Localizer["Error Loading Role"], MessageType.Error);
AddModuleMessage(Localizer["Error.LoadRole"], MessageType.Error);
}
}
private async Task SaveRole()
{
var role = await RoleService.GetRoleAsync(_roleid);
role.Name = _name;
role.Description = _description;
role.IsAutoAssigned = (_isautoassigned != null && Boolean.Parse(_isautoassigned));
role.IsSystem = false;
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var role = await RoleService.GetRoleAsync(_roleid);
role.Name = _name;
role.Description = _description;
role.IsAutoAssigned = (_isautoassigned != null && Boolean.Parse(_isautoassigned));
role.IsSystem = false;
try
{
role = await RoleService.UpdateRoleAsync(role);
await logger.LogInformation("Role Saved {Role}", role);
NavigationManager.NavigateTo(NavigateUrl());
try
{
role = await RoleService.UpdateRoleAsync(role);
await logger.LogInformation("Role Saved {Role}", role);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Role {Role} {Error}", role, ex.Message);
AddModuleMessage(Localizer["Error.SaveRole"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving Role {Role} {Error}", role, ex.Message);
AddModuleMessage(Localizer["Error Saving Role"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -2,10 +2,11 @@
@inherits ModuleBase
@inject IRoleService RoleService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_roles == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
@ -16,11 +17,11 @@ else
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@SharedLocalizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Role" Message="@Localizer["Are You Sure You Wish To Delete The {0} Role?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" ResourceKey="Users" /></td>
<td>@context.Name</td>
</Row>
@ -34,7 +35,7 @@ else
protected override async Task OnParametersSetAsync()
{
_roles = await RoleService.GetRolesAsync(PageState.Site.SiteId);
await GetRoles();
}
private async Task DeleteRole(Role role)
@ -43,12 +44,26 @@ else
{
await RoleService.DeleteRoleAsync(role.RoleId);
await logger.LogInformation("Role Deleted {Role}", role);
await GetRoles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Role {Role} {Error}", role, ex.Message);
AddModuleMessage(Localizer["Error Deleting Role"], MessageType.Error);
AddModuleMessage(Localizer["Error.DeleteRole"], MessageType.Error);
}
}
private async Task GetRoles()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true);
_roles = _roles.Where(item => item.Name != RoleNames.Everyone).ToList();
}
else
{
_roles = await RoleService.GetRolesAsync(PageState.Site.SiteId);
}
}
}

View File

@ -3,74 +3,81 @@
@inject IRoleService RoleService
@inject IUserRoleService UserRoleService
@inject IStringLocalizer<Users> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (userroles == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<table class="table table-borderless">
<tr>
<td>
<Label For="role" HelpText="The role you are assigning users to" ResourceKey="Role">Role: </Label>
</td>
<td>
<input id="role" class="form-control" @bind="@name" disabled />
</td>
</tr>
<tr>
<td>
<Label For="user" HelpText="Select a user" ResourceKey="User">User: </Label>
</td>
<td>
<select id="user" class="form-control" @bind="@userid">
<option value="-1">&lt;@Localizer["Select User"]&gt;</option>
@foreach (UserRole userrole in users)
{
<option value="@(userrole.UserId)">@userrole.User.DisplayName</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="effectiveDate" HelpText="The date that this role assignment is active" ResourceKey="EffectiveDate">Effective Date: </Label>
</td>
<td>
<input type="date" id="effectiveDate" class="form-control" @bind="@effectivedate" />
</td>
</tr>
<tr>
<td>
<Label For="expiryDate" HelpText="The date that this role assignment expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
</td>
<td>
<input type="date" id="expiryDate" class="form-control" @bind="@expirydate" />
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveUserRole">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<hr class="app-rule" />
<p align="center">
<Pager Items="@userroles">
<Header>
<th>@Localizer["Users"]</th>
<th>&nbsp;</th>
</Header>
<Row>
<td>@context.User.DisplayName</td>
<td>
<button type="button" class="btn btn-danger" @onclick=@(async () => await DeleteUserRole(context.UserRoleId))>@Localizer["Delete"]</button>
</td>
</Row>
</Pager>
</p>
<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="role" HelpText="The role you are assigning users to" ResourceKey="Role">Role: </Label>
<div class="col-sm-9">
<input id="role" class="form-control" @bind="@name" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="Select a user" ResourceKey="User">User: </Label>
<div class="col-sm-9">
<select id="user" class="form-select" @bind="@userid" required>
<option value="-1">&lt;@Localizer["User.Select"]&gt;</option>
@foreach (UserRole userrole in users)
{
<option value="@(userrole.UserId)">@userrole.User.DisplayName</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this role assignment is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this role assignment expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@expirydate" />
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveUserRole">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<hr class="app-rule" />
<div class="row mb-1 align-items-center">
<p align="center">
<Pager Items="@userroles">
<Header>
<th>@Localizer["Users"]</th>
<th>@SharedLocalizer["Username"]</th>
<th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th>
<th>&nbsp;</th>
</Header>
<Row>
<td>@context.User.DisplayName</td>
<td>@context.User.Username</td>
<td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td>
<td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || PageState.User.Username == UserNames.Host)" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
</p>
</div>
</div>
</form>
}
@code {
private ElementReference form;
private bool validated = false;
private int roleid;
private string name = string.Empty;
private List<UserRole> users;
@ -88,17 +95,13 @@ else
roleid = Int32.Parse(PageState.QueryString["id"]);
Role role = await RoleService.GetRoleAsync(roleid);
name = role.Name;
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
users = users
.Where(u => u.Role.Name == RoleNames.Registered)
.OrderBy(u => u.User.DisplayName)
.ToList();
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
await GetUserRoles();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Users"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
@ -106,69 +109,89 @@ else
{
try
{
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = userroles.Where(item => item.RoleId == roleid).ToList();
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, name);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Roles {RoleId} {Error}", roleid, ex.Message);
AddModuleMessage(Localizer["Error Loading User Roles"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.LoadRole"], MessageType.Error);
}
}
private async Task SaveUserRole()
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (userid != -1)
try
{
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
if (userid != -1)
{
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
{
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
{
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.AddUserRoleAsync(userrole);
}
await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
else
{
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
userrole.EffectiveDate = effectivedate;
userrole.ExpiryDate = expirydate;
await UserRoleService.AddUserRoleAsync(userrole);
AddModuleMessage(Localizer["Message.Required.UserSelect"], MessageType.Warning);
}
await GetUserRoles();
await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["User Assigned To Role"], MessageType.Success);
}
else
catch (Exception ex)
{
AddModuleMessage(Localizer["You Must Select A User"], MessageType.Warning);
await logger.LogError(ex, "Error Saving User Roles {RoleId} {Error}", roleid, ex.Message);
AddModuleMessage(Localizer["Error.User.SaveRole"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Saving User Roles {RoleId} {Error}", roleid, ex.Message);
AddModuleMessage(Localizer["Error Saving User Roles"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task DeleteUserRole(int UserRoleId)
{
try
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await GetUserRoles();
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
AddModuleMessage(Localizer["User Removed From Role"], MessageType.Success);
try
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
}
}
catch (Exception ex)
else
{
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error Removing User From Role"], MessageType.Error);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
@namespace Oqtane.Modules.Admin.Sites
@using Oqtane.Interfaces
@using System.Text.RegularExpressions
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ITenantService TenantService
@ -8,397 +10,391 @@
@inject ISiteTemplateService SiteTemplateService
@inject IUserService UserService
@inject IInstallationService InstallationService
@inject IDatabaseService DatabaseService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_tenants == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="Enter the name of the site" ResourceKey="Name">Site Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="alias" HelpText="Enter the alias for the server" ResourceKey="Aliases">Aliases: </Label>
</td>
<td>
<textarea id="alias" class="form-control" @bind="@_urls" rows="3"></textarea>
</td>
</tr>
<tr>
<td>
<Label For="defaultTheme" HelpText="Select the default theme for the website" ResourceKey="DefaultTheme">Default Theme: </Label>
</td>
<td>
<select id="defaultTheme" class="form-control" @onchange="(e => ThemeChanged(e))">
<option value="-">&lt;@Localizer["Select Theme"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
</td>
<td>
<select id="defaultContainer" class="form-control" @bind="@_containertype">
<option value="-">&lt;@Localizer["Select Container"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="adminContainer" HelpText="Select the admin container for the site" ResourceKey="AdminContainer">Admin Container: </Label>
</td>
<td>
<select id="adminContainer" class="form-control" @bind="@_admincontainertype">
<option value="-">&lt;@Localizer["Select Container"]&gt;</option>
<option value="">&lt;@Localizer["Default Admin Container"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="siteTemplate" HelpText="Select the site template" ResourceKey="SiteTemplate">Site Template: </Label>
</td>
<td>
<select id="siteTemplate" class="form-control" @bind="@_sitetemplatetype">
<option value="-">&lt;@Localizer["Select Site Template"]&gt;</option>
@foreach (SiteTemplate siteTemplate in _siteTemplates)
{
<option value="@siteTemplate.TypeName">@siteTemplate.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="tenant" HelpText="Select the tenant for the site" ResourceKey="Tenant">Tenant: </Label>
</td>
<td>
<select id="tenant" class="form-control" @onchange="(e => TenantChanged(e))">
<option value="-">&lt;@Localizer["Select Tenant"]&gt;</option>
<option value="+">&lt;@Localizer["Create New Tenant"]&gt;</option>
@foreach (Tenant tenant in _tenants)
{
<option value="@tenant.TenantId">@tenant.Name</option>
}
</select>
</td>
</tr>
@if (_tenantid == "+")
{
<tr>
<td colspan="2">
<hr class="app-rule" />
</td>
</tr>
<tr>
<td>
<Label For="name" HelpText="Enter the name for the tenant" ResourceKey="TenantName">Tenant Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_tenantname" />
</td>
</tr>
<tr>
<td>
<Label For="databaseType" HelpText="Select the database type for the tenant" ResourceKey="DatabaseType">Database Type: </Label>
</td>
<td>
<select id="databaseType" class="custom-select" @bind="@_databasetype">
<option value="LocalDB">@Localizer["Local Database"]</option>
<option value="SQLServer">@Localizer["SQL Server"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="server" HelpText="Enter the server for the tenant" ResourceKey="DatabaseServer">Server: </Label>
</td>
<td>
<input id="server" type="text" class="form-control" @bind="@_server" />
</td>
</tr>
<tr>
<td>
<Label For="database" HelpText="Enter the database for the tenant" ResourceKey="Database">Database: </Label>
</td>
<td>
<input id="database" type="text" class="form-control" @bind="@_database" />
</td>
</tr>
<tr>
<td>
<Label For="integratedSecurity" HelpText="Select if you want integrated security or not" ResourceKey="IntegratedSecurity">Integrated Security: </Label>
</td>
<td>
<select id="integratedSecurity" class="custom-select" @onchange="SetIntegratedSecurity">
<option value="true" selected>@Localizer["True"]</option>
<option value="false">@Localizer["False"]</option>
</select>
</td>
</tr>
@if (!_integratedsecurity)
{
<tr>
<td>
<Label For="username" HelpText="Enter the username for the integrated security" ResourceKey="DatabaseUsername">Database Username: </Label>
</td>
<td>
<input id="username" type="text" class="form-control" @bind="@_username" />
</td>
</tr>
<tr>
<td>
<Label For="password" HelpText="Enter the password for the integrated security" ResourceKey="DatabasePassword">Database Password: </Label>
</td>
<td>
<input id="password" type="password" class="form-control" @bind="@_password" />
</td>
</tr>
}
<tr>
<td>
<Label For="hostUsername" HelpText="Enter the username of the host for this site" ResourceKey="HostUsername">Host Username:</Label>
</td>
<td>
<input id="hostUsername" class="form-control" @bind="@_hostusername" readonly />
</td>
</tr>
<tr>
<td>
<Label For="hostPassword" HelpText="Enter the password for the host of this site" ResourceKey="HostPassword">Host Password:</Label>
</td>
<td>
<input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" />
</td>
</tr>
}
</table>
<button type="button" class="btn btn-success" @onclick="SaveSite">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
}
@code {
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants;
private string _tenantid = "-";
private string _tenantname = string.Empty;
private string _databasetype = "LocalDB";
private string _server = "(LocalDb)\\MSSQLLocalDB";
private string _database = "Oqtane-" + DateTime.UtcNow.ToString("yyyyMMddHHmm");
private string _username = string.Empty;
private string _password = string.Empty;
private bool _integratedsecurity = true;
private string _hostusername = UserNames.Host;
private string _hostpassword = string.Empty;
private string _name = string.Empty;
private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _admincontainertype = "";
private string _sitetemplatetype = "-";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
_tenants = await TenantService.GetTenantsAsync();
_urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
}
private void TenantChanged(ChangeEventArgs e)
{
_tenantid = (string)e.Value;
if (string.IsNullOrEmpty(_tenantname))
{
_tenantname = _name;
}
StateHasChanged();
}
private void SetIntegratedSecurity(ChangeEventArgs e)
{
if (Convert.ToBoolean((string)e.Value))
{
_integratedsecurity = true;
}
else
{
_integratedsecurity = false;
}
StateHasChanged();
}
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
}
else
{
_containers = new List<ThemeControl>();
}
_containertype = "-";
_admincontainertype = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error Loading Containers For Theme"], MessageType.Error);
}
}
private async Task SaveSite()
{
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-")
{
var duplicates = new List<string>();
var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (aliases.Exists(item => item.Name == name))
{
duplicates.Add(name);
}
}
if (duplicates.Count == 0)
{
InstallConfig config = new InstallConfig();
if (_tenantid == "+")
{
if (!string.IsNullOrEmpty(_tenantname) && _tenants.FirstOrDefault(item => item.Name == _tenantname) == null)
{
// validate host credentials
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = UserNames.Host;
user.Password = _hostpassword;
user = await UserService.LoginUserAsync(user, false, false);
if (user.IsAuthenticated)
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the name of the site" ResourceKey="Name">Site Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="200" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="Enter 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 can 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="defaultTheme" HelpText="Select the default theme for the website" ResourceKey="DefaultTheme">Default Theme: </Label>
<div class="col-sm-9">
<select id="defaultTheme" class="form-select" @onchange="(e => ThemeChanged(e))" required>
<option value="-">&lt;@Localizer["Theme.Select"]&gt;</option>
@foreach (var theme in _themes)
{
if (!string.IsNullOrEmpty(_server) && !string.IsNullOrEmpty(_database))
<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="adminContainer" HelpText="Select the admin container for the site" ResourceKey="AdminContainer">Admin Container: </Label>
<div class="col-sm-9">
<select id="adminContainer" class="form-select" @bind="@_admincontainertype" required>
<option value="-">&lt;@Localizer["Container.Select"]&gt;</option>
<option value="">&lt;@Localizer["DefaultContainer.Admin"]&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="siteTemplate" HelpText="Select the site template" ResourceKey="SiteTemplate">Site Template: </Label>
<div class="col-sm-9">
<select id="siteTemplate" class="form-select" @bind="@_sitetemplatetype" required>
<option value="-">&lt;@Localizer["SiteTemplate.Select"]&gt;</option>
@foreach (SiteTemplate siteTemplate in _siteTemplates)
{
<option value="@siteTemplate.TypeName">@siteTemplate.Name</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The runtime hosting model" ResourceKey="Runtime">Runtime: </Label>
<div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="prerender" HelpText="Specifies if the site should be prerendered (for search crawlers, etc...)" ResourceKey="Prerender">Prerender? </Label>
<div class="col-sm-9">
<select id="prerender" class="form-select" @bind="@_prerender" required>
<option value="Prerendered">@SharedLocalizer["Yes"]</option>
<option value="">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the tenant for the site" ResourceKey="Tenant">Tenant: </Label>
<div class="col-sm-9">
<select id="tenant" class="form-select" @onchange="(e => TenantChanged(e))" required>
<option value="-">&lt;@Localizer["Tenant.Select"]&gt;</option>
<option value="+">&lt;@Localizer["Tenant.Add"]&gt;</option>
@foreach (Tenant tenant in _tenants)
{
<option value="@tenant.TenantId">@tenant.Name</option>
}
</select>
</div>
</div>
@if (_tenantid == "+")
{
<div class="row mb-1 align-items-center">
<hr class="app-rule" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the name for the tenant" ResourceKey="TenantName">Tenant Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_tenantName" maxlength="100" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databaseType" HelpText="Select the database type for the tenant" ResourceKey="DatabaseType">Database Type: </Label>
<div class="col-sm-9">
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
@foreach (var database in _databases)
{
var connectionString = string.Empty;
if (_databasetype == "LocalDB")
if (database.IsDefault)
{
connectionString = "Data Source=" + _server + ";AttachDbFilename=|DataDirectory|\\" + _database + ".mdf;Initial Catalog=" + _database + ";Integrated Security=SSPI;";
<option value="@database.Name" selected>@Localizer[@database.Name]</option>
}
else
{
connectionString = "Data Source=" + _server + ";Initial Catalog=" + _database + ";";
if (_integratedsecurity)
{
connectionString += "Integrated Security=SSPI;";
}
else
{
connectionString += "User ID=" + _username + ";Password=" + _password;
}
<option value="@database.Name">@Localizer[@database.Name]</option>
}
}
</select>
</div>
</div>
if (_databaseConfigType != null)
{
@DatabaseConfigComponent;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="hostUsername" HelpText="Enter the username of an existing host user" ResourceKey="HostUsername">Host Username:</Label>
<div class="col-sm-9">
<input id="hostUsername" class="form-control" @bind="@_hostusername" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="hostPassword" HelpText="Enter the password of an existing host user" ResourceKey="HostPassword">Host Password:</Label>
<div class="col-sm-9">
<input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" autocomplete="new-password" required />
</div>
</div>
}
</div>
<br />
<br />
<button type="button" class="btn btn-success" @onclick="SaveSite">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
}
config.ConnectionString = connectionString;
config.HostPassword = _hostpassword;
config.HostEmail = user.Email;
config.HostName = user.DisplayName;
config.TenantName = _tenantname;
config.IsNewTenant = true;
@code {
private List<Database> _databases;
private ElementReference form;
private bool validated = false;
private string _databaseName = "LocalDB";
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<SiteTemplate> _siteTemplates;
private List<Tenant> _tenants;
private string _tenantid = "-";
private string _tenantName = string.Empty;
private string _hostusername = string.Empty;
private string _hostpassword = string.Empty;
private string _name = string.Empty;
private string _urls = string.Empty;
private string _themetype = "-";
private string _containertype = "-";
private string _admincontainertype = "";
private string _sitetemplatetype = "-";
private string _runtime = "Server";
private string _prerender = "Prerendered";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
_tenants = await TenantService.GetTenantsAsync();
_urls = PageState.Alias.Name;
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
_siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync();
_databases = await DatabaseService.GetDatabasesAsync();
LoadDatabaseConfigComponent();
}
private void DatabaseChanged(ChangeEventArgs eventArgs)
{
try
{
_databaseName = (string)eventArgs.Value;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (database != null)
{
_databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder =>
{
builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent();
};
}
}
private void TenantChanged(ChangeEventArgs e)
{
_tenantid = (string)e.Value;
if (string.IsNullOrEmpty(_tenantName))
{
_tenantName = _name;
}
StateHasChanged();
}
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
}
else
{
_containers = new List<ThemeControl>();
}
_containertype = "-";
_admincontainertype = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error);
}
}
private async Task SaveSite()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-")
{
_urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>();
var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (aliases.Exists(item => item.Name == name))
{
duplicates.Add(name);
}
}
if (duplicates.Count == 0)
{
InstallConfig config = new InstallConfig();
if (_tenantid == "+")
{
if (!string.IsNullOrEmpty(_tenantName) && _tenants.FirstOrDefault(item => item.Name == _tenantName) == null)
{
// validate host credentials
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = _hostusername;
user.Password = _hostpassword;
user.LastIPAddress = PageState.RemoteIPAddress;
user = await UserService.LoginUserAsync(user);
if (user.IsAuthenticated)
{
var connectionString = String.Empty;
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (connectionString != "")
{
config.TenantName = _tenantName;
config.DatabaseType = database.DBType;
config.ConnectionString = connectionString;
config.HostUsername = _hostusername;
config.HostPassword = _hostpassword;
config.HostEmail = user.Email;
config.HostName = user.DisplayName;
config.IsNewTenant = true;
}
else
{
AddModuleMessage(Localizer["Error.Required.ServerDatabase"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["You Must Specify A Server And Database"], MessageType.Error);
AddModuleMessage(Localizer["Error.InvalidPassword"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Invalid Host Password"], MessageType.Error);
AddModuleMessage(Localizer["Error.TenantName.Exists"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Tenant Name Is Missing Or Already Exists"], MessageType.Error);
var tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid));
if (tenant != null)
{
config.TenantName = tenant.Name;
config.DatabaseType = tenant.DBType;
config.ConnectionString = tenant.DBConnectionString;
config.IsNewTenant = false;
}
}
if (!string.IsNullOrEmpty(config.TenantName))
{
config.SiteName = _name;
config.Aliases = _urls;
config.DefaultTheme = _themetype;
config.DefaultContainer = _containertype;
config.DefaultAdminContainer = _admincontainertype;
config.SiteTemplate = _sitetemplatetype;
config.Runtime = _runtime;
config.RenderMode = _runtime + _prerender;
ShowProgressIndicator();
var installation = await InstallationService.Install(config);
if (installation.Success)
{
var aliasname = config.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0];
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + aliasname, true);
}
else
{
await logger.LogError("Error Creating Site {Error}", installation.Message);
AddModuleMessage(installation.Message, MessageType.Error);
}
}
}
else
{
var tenant = _tenants.FirstOrDefault(item => item.TenantId == int.Parse(_tenantid));
if (tenant != null)
{
config.TenantName = tenant.Name;
config.ConnectionString= tenant.DBConnectionString;
config.IsNewTenant = false;
}
}
if (!string.IsNullOrEmpty(config.TenantName))
{
config.SiteName = _name;
config.Aliases = _urls.Replace("\n", ",");
config.DefaultTheme = _themetype;
config.DefaultContainer = _containertype;
config.DefaultAdminContainer = _admincontainertype;
config.SiteTemplate = _sitetemplatetype;
ShowProgressIndicator();
var installation = await InstallationService.Install(config);
if (installation.Success)
{
var aliasname = config.Aliases.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0];
var uri = new Uri(NavigationManager.Uri);
NavigationManager.NavigateTo(uri.Scheme + "://" + aliasname, true);
}
else
{
await logger.LogError("Error Creating Site {Error}", installation.Message);
AddModuleMessage(installation.Message, MessageType.Error);
}
AddModuleMessage(string.Format(Localizer["Message.SiteName.InUse"], string.Join(", ", duplicates.ToArray())), MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["{0} Already Used For Another Site", string.Join(", ", duplicates.ToArray())], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.Tenant"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide A Tenant, Site Name, Alias, Default Theme/Container, And Site Template"], MessageType.Warning);
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
}

View File

@ -1,283 +0,0 @@
@namespace Oqtane.Modules.Admin.Sites
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ISiteService SiteService
@inject ITenantService TenantService
@inject IAliasService AliasService
@inject IThemeService ThemeService
@inject IStringLocalizer<Edit> Localizer
@if (_initialized)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="Enter the name of the site" ResourceKey="Name">Name: </Label>
</td>
<td>
<input id="name" class="form-control" @bind="@_name" />
</td>
</tr>
<tr>
<td>
<Label For="alias" HelpText="Enter the alias for the server" ResourceKey="Aliases">Aliases: </Label>
</td>
<td>
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" />
</td>
</tr>
<tr>
<td>
<Label For="defaultTheme" HelpText="Select the default theme for the website" ResourceKey="DefaultTheme">Default Theme: </Label>
</td>
<td>
<select id="defaultTheme" class="form-control" value="@_themetype" @onchange="(e => ThemeChanged(e))">
<option value="-">&lt;@Localizer["Select Theme"]&gt;</option>
@foreach (var theme in _themes)
{
<option value="@theme.TypeName">@theme.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="defaultContainer" HelpText="Select the default container for the site" ResourceKey="DefaultContainer">Default Container: </Label>
</td>
<td>
<select id="defaultIdea" class="form-control" @bind="@_containertype">
<option value="-">&lt;@Localizer["Select Container"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="defaultAdminContainer" HelpText="Select the default admin container for the site" ResourceKey="DefaultAdminContainer">Default Admin Container: </Label>
</td>
<td>
<select id="defaultAdminContainer" class="form-control" @bind="@_admincontainertype">
<option value="-">&lt;@Localizer["Select Container"]&gt;</option>
<option value="">&lt;@Localizer["Default Admin Container"]&gt;</option>
@foreach (var container in _containers)
{
<option value="@container.TypeName">@container.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="isDeleted" HelpText="Has this site been deleted?" ResourceKey="IsDeleted">Is Deleted? </Label>
</td>
<td>
<select id="isDeleted" class="form-control" @bind="@_isdeleted">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</select>
</td>
</tr>
<tr>
<td>
<Label For="tenant" HelpText="The tenant for the site" ResourceKey="Tenant">Tenant: </Label>
</td>
<td>
<input id="tenant" class="form-control" @bind="@_tenant" readonly />
</td>
</tr>
<tr>
<td>
<Label For="connectionstring" HelpText="The database connection string" ResourceKey="ConnectionString">Connection String: </Label>
</td>
<td>
<textarea id="connectionstring" class="form-control" @bind="@_connectionstring" rows="3" readonly></textarea>
</td>
</tr>
</table>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSite">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
}
@code {
private bool _initialized = false;
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private Alias _alias;
private string _name = string.Empty;
private List<Alias> _aliasList;
private string _urls = string.Empty;
private string _themetype;
private string _containertype = "-";
private string _admincontainertype = "-";
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private string _deletedby;
private DateTime? _deletedon;
private string _isdeleted;
private string _tenant = string.Empty;
private string _connectionstring = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_themeList = await ThemeService.GetThemesAsync();
_aliasList = await AliasService.GetAliasesAsync();
_alias = _aliasList.Find(item => item.AliasId == Int32.Parse(PageState.QueryString["id"]));
SiteService.SetAlias(_alias);
var site = await SiteService.GetSiteAsync(_alias.SiteId);
if (site != null)
{
_name = site.Name;
foreach (Alias alias in _aliasList.Where(item => item.SiteId == site.SiteId && item.TenantId == site.TenantId).ToList())
{
_urls += alias.Name + "\n";
}
_themes = ThemeService.GetThemeControls(_themeList);
_themetype = site.DefaultThemeType;
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = site.DefaultContainerType;
_admincontainertype = site.AdminContainerType;
_createdby = site.CreatedBy;
_createdon = site.CreatedOn;
_modifiedby = site.ModifiedBy;
_modifiedon = site.ModifiedOn;
_deletedby = site.DeletedBy;
_deletedon = site.DeletedOn;
_isdeleted = site.IsDeleted.ToString();
List<Tenant> tenants = await TenantService.GetTenantsAsync();
Tenant tenant = tenants.Find(item => item.TenantId == site.TenantId);
if (tenant != null)
{
_tenant = tenant.Name;
_connectionstring = tenant.DBConnectionString;
}
_initialized = true;
}
}
catch (Exception ex)
{
await Log(_alias, LogLevel.Error, string.Empty, ex, "Error Loading Site {SiteId} {Error}", _alias.SiteId, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private async void ThemeChanged(ChangeEventArgs e)
{
try
{
_themetype = (string)e.Value;
if (_themetype != "-")
{
_containers = ThemeService.GetContainerControls(_themeList, _themetype);
}
else
{
_containers = new List<ThemeControl>();
}
_containertype = "-";
_admincontainertype = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error Loading Pane Layouts For Theme"], MessageType.Error);
}
}
private async Task SaveSite()
{
try
{
if (_name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-")
{
var unique = true;
foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (_aliasList.Exists(item => item.Name == name && item.SiteId != _alias.SiteId && item.TenantId != _alias.TenantId))
{
unique = false;
}
}
if (unique)
{
SiteService.SetAlias(_alias);
var site = await SiteService.GetSiteAsync(_alias.SiteId);
if (site != null)
{
site.Name = _name;
site.LogoFileId = null;
site.DefaultThemeType = _themetype;
site.DefaultContainerType = _containertype;
site.AdminContainerType = _admincontainertype;
site.IsDeleted = (_isdeleted == null || Boolean.Parse(_isdeleted));
site = await SiteService.UpdateSiteAsync(site);
_urls = _urls.Replace("\n", ",");
var names = _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (Alias alias in _aliasList.Where(item => item.SiteId == site.SiteId && item.TenantId == site.TenantId).ToList())
{
if (!names.Contains(alias.Name))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
}
}
foreach (string name in names)
{
if (!_aliasList.Exists(item => item.Name == name))
{
Alias alias = new Alias
{
Name = name,
TenantId = site.TenantId,
SiteId = site.SiteId
};
await AliasService.AddAliasAsync(alias);
}
}
await Log(_alias, LogLevel.Information, PermissionNames.Edit, null, "Site Saved {Site}", site);
NavigationManager.NavigateTo(NavigateUrl());
}
}
else
{
AddModuleMessage(Localizer["An Alias Specified Has Already Been Used For Another Site"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide A Site Name, Alias, And Default Theme/Container"], MessageType.Warning);
}
}
catch (Exception ex)
{
await Log(_alias, LogLevel.Error, string.Empty, ex, "Error Saving Site {SiteId} {Error}", _alias.SiteId, ex.Message);
AddModuleMessage(Localizer["Error Saving Site"], MessageType.Error);
}
}
}

View File

@ -4,6 +4,7 @@
@inject IAliasService AliasService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_sites == null)
{
@ -17,65 +18,41 @@ else
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
<th>@SharedLocalizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.AliasId.ToString())" ResourceKey="EditSite" /></td>
<td><ActionDialog Header="Delete Site" Message="@Localizer["Are You Sure You Wish To Delete The {0} Site?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteSite(context))" ResourceKey="DeleteSite" /></td>
<td><a href="@(_scheme + context.Name +"?reload")">@context.Name</a></td>
<td><button type="button" class="btn btn-primary" @onclick="@(async () => Edit(context.Name))">@SharedLocalizer["Edit"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => Browse(context.Name))">@Localizer["Browse"]</button></td>
<td>@context.Name</td>
</Row>
</Pager>
}
@code {
private List<Alias> _sites;
private string _scheme;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
{
var uri = new Uri(NavigationManager.Uri);
_scheme = uri.Scheme + "://";
var aliases = await AliasService.GetAliasesAsync();
_sites = new List<Alias>();
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);
}
}
}
private async Task DeleteSite(Alias alias)
private void Edit(string name)
{
try
{
if (alias.SiteId != PageState.Site.SiteId || alias.TenantId != PageState.Site.TenantId)
{
SiteService.SetAlias(alias);
await SiteService.DeleteSiteAsync(alias.SiteId);
await Log(alias, LogLevel.Information, "", null, "Site Deleted {SiteId}", alias.SiteId);
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name + "/admin/site", true);
}
var aliases = await AliasService.GetAliasesAsync();
foreach (Alias a in aliases.Where(item => item.SiteId == alias.SiteId && item.TenantId == alias.TenantId))
{
await AliasService.DeleteAliasAsync(a.AliasId);
}
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["You Can Not Delete The Current Site"], MessageType.Warning);
}
}
catch (Exception ex)
{
await Log(alias, LogLevel.Error, "", ex, "Error Deleting Site {SiteId} {Error}", alias.SiteId, ex.Message);
AddModuleMessage(Localizer["Error Deleting Site"], MessageType.Error);
}
private void Browse(string name)
{
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + name, true);
}
}

View File

@ -2,112 +2,150 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ITenantService TenantService
@inject IDatabaseService DatabaseService
@inject ISqlService SqlService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_tenants == null)
{
<p><em>Loading...</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<table class="table table-borderless">
<tr>
<td>
<Label For="tenant" HelpText="Select the tenant for the SQL server" ResourceKey="Tenant">Tenant: </Label>
</td>
<td>
<select id="teneant" class="form-control" @bind="_tenantid">
<option value="-1">&lt;@Localizer["Select Tenant"]&gt;</option>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the tenant for the SQL server" ResourceKey="Tenant">Tenant: </Label>
<div class="col-sm-9">
<select id="tenant" class="form-select" value="@_tenantid" @onchange="(e => TenantChanged(e))">
<option value="-1">&lt;@Localizer["Tenant.Select"]&gt;</option>
@foreach (Tenant tenant in _tenants)
{
<option value="@tenant.TenantId">@tenant.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="sqlQeury" HelpText="Enter the query for the SQL server" ResourceKey="SqlQuery">SQL Query: </Label>
</td>
<td>
<textarea id="sqlQeury" class="form-control" @bind="@_sql" rows="5"></textarea>
</td>
</tr>
</table>
</div>
</div>
@if (_tenantid != "-1")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="The database for the tenant" ResourceKey="Database">Database: </Label>
<div class="col-sm-9">
<input id="database" class="form-control" @bind="@_database" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label>
<div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionstring" rows="2" readonly></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sqlQeury" HelpText="Enter the query for the SQL server" ResourceKey="SqlQuery">SQL Query: </Label>
<div class="col-sm-9">
<textarea id="sqlQeury" class="form-control" @bind="@_sql" rows="3"></textarea>
</div>
</div>
}
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Execute">@Localizer["Execute"]</button>
<br />
<br />
@if (!string.IsNullOrEmpty(_results))
@if (_results != null)
{
@((MarkupString)_results)
@if (_results.Count > 0)
{
<Pager Class="table table-bordered" Items="@_results">
<Header>
@foreach (KeyValuePair<string, string> kvp in _results.First())
{
<th>@kvp.Key</th>
}
</Header>
<Row>
@foreach (KeyValuePair<string, string> kvp in context)
{
<td>@kvp.Value</td>
}
</Row>
</Pager>
}
else
{
@Localizer["Return.NoResult"]
}
<br />
<br />
}
}
@code {
private List<Tenant> _tenants;
private string _tenantid = "-1";
private string _database = string.Empty;
private string _connectionstring = string.Empty;
private string _sql = string.Empty;
private string _results = string.Empty;
private List<Dictionary<string, string>> _results;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
_tenants = await TenantService.GetTenantsAsync();
try
{
_tenants = await TenantService.GetTenantsAsync();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private async void TenantChanged(ChangeEventArgs e)
{
try
{
_tenantid = (string)e.Value;
var tenants = await TenantService.GetTenantsAsync();
var _databases = await DatabaseService.GetDatabasesAsync();
var tenant = tenants.Find(item => item.TenantId == int.Parse(_tenantid));
if (tenant != null)
{
_database = _databases.Find(item => item.DBType == tenant.DBType)?.Name;
_connectionstring = tenant.DBConnectionString;
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Tenant {TenantId} {Error}", _tenantid, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private async Task Execute()
{
if (_tenantid != "-1" && !string.IsNullOrEmpty(_sql))
try
{
var sqlquery = new SqlQuery { TenantId = int.Parse(_tenantid), Query = _sql };
sqlquery = await SqlService.ExecuteQueryAsync(sqlquery);
_results = DisplayResults(sqlquery.Results);
}
else
{
AddModuleMessage(Localizer["You Must Select A Tenant And Provide A SQL Query"], MessageType.Warning);
}
}
private string DisplayResults(List<Dictionary<string, string>> results)
{
var table = string.Empty;
foreach (Dictionary<string, string> item in results)
{
if (table == string.Empty)
if (_tenantid != "-1" && !string.IsNullOrEmpty(_sql))
{
table = "<div class=\"table-responsive\">";
table += "<table class=\"table table-bordered\"><thead><tr>";
foreach (KeyValuePair<string, string> kvp in item)
{
table += "<th scope=\"col\">" + kvp.Key + "</th>";
}
table += "</tr></thead><tbody>";
var sqlquery = new SqlQuery { TenantId = int.Parse(_tenantid), Query = _sql };
sqlquery = await SqlService.ExecuteQueryAsync(sqlquery);
_results = sqlquery.Results;
AddModuleMessage(Localizer["Success.QueryExecuted"], MessageType.Success);
}
table += "<tr>";
foreach (KeyValuePair<string, string> kvp in item)
else
{
table += "<td>" + kvp.Value + "</td>";
AddModuleMessage(Localizer["Message.Required.Tenant"], MessageType.Warning);
}
table += "</tr>";
}
if (table != string.Empty)
catch (Exception ex)
{
table += "</tbody></table></div>";
await logger.LogError(ex, "Error Executing SQL Query {SQL} {Error}", _sql, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
else
{
table = Localizer["No Results Returned"];
}
return table;
}
}

View File

@ -3,82 +3,214 @@
@inject ISystemService SystemService
@inject IInstallationService InstallationService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="version" HelpText="Framework Version" ResourceKey="FrameworkVersion">Framework Version: </Label>
</td>
<td>
<input id="version" class="form-control" @bind="@_version" readonly />
</td>
</tr>
<tr>
<td>
<Label For="runtime" HelpText="Blazor Runtime (Server or WebAssembly)" ResourceKey="BlazorRunime">Blazor Runtime: </Label>
</td>
<td>
<input id="runtime" class="form-control" @bind="@_runtime" readonly />
</td>
</tr>
<tr>
<td>
<Label For="clrversion" HelpText="Common Language Runtime Version" ResourceKey="ClrVerion">CLR Version: </Label>
</td>
<td>
<input id="clrversion" class="form-control" @bind="@_clrversion" readonly />
</td>
</tr>
<tr>
<td>
<Label For="osversion" HelpText="Operating System Version" ResourceKey="OsVersion">OS Version: </Label>
</td>
<td>
<input id="osversion" class="form-control" @bind="@_osversion" readonly />
</td>
</tr>
<tr>
<td>
<Label For="serverpath" HelpText="Server Path" ResourceKey="ServerPath">Server Path: </Label>
</td>
<td>
<input id="serverpath" class="form-control" @bind="@_serverpath" readonly />
</td>
</tr>
<tr>
<td>
<Label For="servertime" HelpText="Server Time" ResourceKey="ServerTime">Server Time: </Label>
</td>
<td>
<input id="servertime" class="form-control" @bind="@_servertime" readonly />
</td>
</tr>
</table>
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access Framework API"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
<TabStrip>
<TabPanel Name="Info" Heading="Info" ResourceKey="Info">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="Framework Version" ResourceKey="FrameworkVersion">Framework Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clrversion" HelpText="Common Language Runtime Version" ResourceKey="CLRVersion">CLR Version: </Label>
<div class="col-sm-9">
<input id="clrversion" class="form-control" @bind="@_clrversion" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="osversion" HelpText="Operating System Version" ResourceKey="OSVersion">OS Version: </Label>
<div class="col-sm-9">
<input id="osversion" class="form-control" @bind="@_osversion" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9">
<input id="machinename" class="form-control" @bind="@_machinename" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<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">
<input id="servertime" class="form-control" @bind="@_servertime" readonly />
</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">
<Label Class="col-sm-3" For="installationid" HelpText="The Unique Identifier For Your Installation" ResourceKey="InstallationId">Installation ID: </Label>
<div class="col-sm-9">
<input id="installationid" class="form-control" @bind="@_installationid" readonly />
</div>
</div>
</div>
<br /><br />
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel>
<TabPanel Name="Options" Heading="Options" ResourceKey="Options">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="detailederrors" HelpText="Specify If Detailed Errors Are Enabled For Blazor. This Option Should Not Not Be Enabled In Production." ResourceKey="DetailedErrors">Detailed Errors? </Label>
<div class="col-sm-9">
<select id="detailederrors" class="form-select" @bind="@_detailederrors">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logginglevel" HelpText="The Minimum Logging Level For The Event Log. This Option Can Be Used To Control The Volume Of Items Stored In Your Event Log." ResourceKey="LoggingLevel">Logging Level: </Label>
<div class="col-sm-9">
<select id="logginglevel" class="form-select" @bind="@_logginglevel">
<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>
</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>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="swagger" HelpText="Specify If Swagger Is Enabled For Your Server API" ResourceKey="Swagger">Swagger Enabled? </Label>
<div class="col-sm-9">
<select id="swagger" class="form-select" @bind="@_swagger">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageservice" HelpText="Specify If The Package Service Is Enabled For Installing Modules, Themes, And Translations" ResourceKey="PackageService">Enable Package Service? </Label>
<div class="col-sm-9">
<select id="packageservice" class="form-select" @bind="@_packageservice">
<option value="true">@SharedLocalizer["True"]</option>
<option value="false">@SharedLocalizer["False"]</option>
</select>
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel>
</TabStrip>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
private string _version = string.Empty;
private string _runtime = string.Empty;
private string _clrversion = string.Empty;
private string _osversion = string.Empty;
private string _serverpath = string.Empty;
private string _servertime = string.Empty;
private string _version = string.Empty;
private string _clrversion = string.Empty;
private string _osversion = string.Empty;
private string _machinename = string.Empty;
private string _ipaddress = 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;
protected override async Task OnInitializedAsync()
private string _detailederrors = string.Empty;
private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty;
private string _packageservice = string.Empty;
protected override async Task OnInitializedAsync()
{
_version = Constants.Version;
Dictionary<string, object> systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null)
{
_clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["OSVersion"].ToString();
_machinename = systeminfo["MachineName"].ToString();
_ipaddress = systeminfo["IPAddress"].ToString();
_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";
}
systeminfo = await SystemService.GetSystemInfoAsync();
if (systeminfo != null)
{
_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()
{
_version = Constants.Version;
_runtime = PageState.Runtime.ToString();
Dictionary<string, string> systeminfo = await SystemService.GetSystemInfoAsync();
if (systeminfo != null)
try
{
_clrversion = systeminfo["clrversion"];
_osversion = systeminfo["osversion"];
_serverpath = systeminfo["serverpath"];
_servertime = systeminfo["servertime"];
var settings = new Dictionary<string, object>();
settings.Add("DetailedErrors", _detailederrors);
settings.Add("Logging:LogLevel:Default", _logginglevel);
settings.Add("Logging:LogLevel:Notify", _notificationlevel);
settings.Add("UseSwagger", _swagger);
settings.Add("PackageService", _packageservice);
await SystemService.UpdateSystemInfoAsync(settings);
AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Configuration");
AddModuleMessage(Localizer["Error.UpdateConfig"], MessageType.Error);
}
}
@ -88,7 +220,7 @@
{
ShowProgressIndicator();
var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(""), 10);
await interop.RedirectBrowser(NavigateUrl(""), 20);
await InstallationService.RestartAsync();
}
catch (Exception ex)

View File

@ -5,50 +5,122 @@
@inject IThemeService ThemeService
@inject IPackageService PackageService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_packages != null)
{
<TabStrip>
@if (_packages.Count > 0)
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
<div class="row justify-content-center mb-3">
<div class="col-sm-6">
<div class="input-group">
<select id="price" class="form-select custom-select" @onchange="(e => PriceChanged(e))">
<option value="free">@SharedLocalizer["Free"]</option>
<option value="paid">@SharedLocalizer["Paid"]</option>
</select>
<input id="search" class="form-control" placeholder="@SharedLocalizer["Search.Hint"]" @bind="@_search" />
<button type="button" class="btn btn-primary" @onclick="Search">@SharedLocalizer["Search"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Reset"]</button>
</div>
</div>
</div>
@if (_packages != null)
{
<TabPanel Name="Download" ResourceKey="Download">
<ModuleMessage Type="MessageType.Info" Message="Download one or more themes from the list below. Once you are ready click Install to complete the installation."></ModuleMessage>
if (_packages.Count > 0)
{
<Pager Items="@_packages">
<Header>
<th>@Localizer["Name"]</th>
<th>@Localizer["Version"]</th>
<th style="width: 1px;"></th>
</Header>
<Row>
<td>@context.Name</td>
<td>@context.Version</td>
<td>
<button type="button" class="btn btn-primary" @onclick=@(async () => await DownloadTheme(context.PackageId, context.Version))>@Localizer["Download"]</button>
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_new">@context.Name</a></h3>&nbsp;&nbsp;@SharedLocalizer["Search.By"]:&nbsp;&nbsp;<strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
<strong>@(String.Format("{0:n0}", context.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@context.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@context.Version</strong>
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
<td style="width: 1px; vertical-align: middle;">
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-primary" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@context.Price.Value.ToString("$#,##0.00")</a>
}
else
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
</td>
</Row>
</Pager>
</TabPanel>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
}
}
<TabPanel Name="Upload" ResourceKey="Upload">
<table class="table table-borderless">
<tr>
<td>
<Label HelpText="Upload one or more theme packages. Once they are uploaded click Install to complete the installation." ResourceKey="Theme">Theme: </Label>
</td>
<td>
<FileManager Filter="nupkg" ShowFiles="false" Folder="Themes" UploadMultiple="@true" />
</td>
</tr>
</table>
</TabPanel>
</TabStrip>
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more theme packages. Once they are uploaded click Install to complete the installation." ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" />
</div>
</div>
</div>
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-success" @onclick="InstallThemes">@Localizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
@if (_productname != "")
{
<div class="app-actiondialog">
<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@SharedLocalizer["Review License Terms"]</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="HideModal"></button>
</div>
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3>
@if (!string.IsNullOrEmpty(_license))
{
@((MarkupString)_license)
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
}
<button type="button" class="btn btn-success" @onclick="InstallThemes">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private List<Package> _packages;
private string _price = "free";
private string _search = "";
private string _productname = "";
private string _license = "";
private string _packageid = "";
private string _version = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -56,21 +128,118 @@
{
try
{
var themes = await ThemeService.GetThemesAsync();
_packages = await PackageService.GetPackagesAsync("theme");
await LoadThemes();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Package.Load"], MessageType.Error);
}
}
private async Task LoadThemes()
{
var themes = await ThemeService.GetThemesAsync();
_packages = await PackageService.GetPackagesAsync("theme", _search, _price, "");
if (_packages != null)
{
foreach (Package package in _packages.ToArray())
{
if (themes.Exists(item => Utilities.GetTypeName(item.ThemeName) == package.PackageId))
if (themes.Exists(item => item.PackageName == package.PackageId))
{
_packages.Remove(package);
}
}
}
}
private async void PriceChanged(ChangeEventArgs e)
{
try
{
_price = (string)e.Value;
_search = "";
await LoadThemes();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Packages"], MessageType.Error);
await logger.LogError(ex, "Error On PriceChanged");
}
}
private async Task Search()
{
try
{
await LoadThemes();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Search");
}
}
private async Task Reset()
{
try
{
_search = "";
await LoadThemes();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Reset");
}
}
private void HideModal()
{
_productname = "";
_license = "";
StateHasChanged();
}
private async Task GetPackage(string packageid, string version)
{
try
{
var package = await PackageService.GetPackageAsync(packageid, version);
if (package != null)
{
_productname = package.Name;
if (!string.IsNullOrEmpty(package.License))
{
_license = package.License.Replace("\n", "<br />");
}
_packageid = package.PackageId;
_version = package.Version;
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packageid, version);
AddModuleMessage(Localizer["Error.Theme.Download"], MessageType.Error);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(Localizer["Success.Theme.Download"], MessageType.Success);
_productname = "";
_license = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _version);
AddModuleMessage(Localizer["Error.Theme.Download"], MessageType.Error);
}
}
@ -79,27 +248,11 @@
try
{
await ThemeService.InstallThemesAsync();
AddModuleMessage(Localizer["Theme Installed Successfully. You Must <a href=\"{0}\">Restart</a> Your Application To Apply These Changes.", NavigateUrl("admin/system")], MessageType.Success);
AddModuleMessage(string.Format(Localizer["Success.Theme.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Theme");
}
}
private async Task DownloadTheme(string packageid, string version)
{
try
{
await PackageService.DownloadPackageAsync(packageid, version, "Themes");
await logger.LogInformation("Theme {ThemeName} {Version} Downloaded Successfully", packageid, version);
AddModuleMessage(Localizer["Themes Downloaded Successfully. Click Install To Complete Installation."], MessageType.Success);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ThemeName} {Version}", packageid, version);
AddModuleMessage(Localizer["Error Downloading Theme"], MessageType.Error);
}
}
}

View File

@ -1,100 +1,94 @@
@namespace Oqtane.Modules.Admin.Themes
@inherits ModuleBase
@using System.Text.RegularExpressions
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IModuleService ModuleService
@inject IPageModuleService PageModuleService
@inject ISystemService SystemService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@using System.Text.RegularExpressions
@using System.IO;
@inject IStringLocalizer<Create> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_systeminfo != null && _templates != null)
@if (_templates != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="owner" HelpText="Enter the name of the organization who is developing this theme. It should not contain spaces or punctuation." ResourceKey="OwnerName">Owner Name: </Label>
</td>
<td>
<input id="owner" class="form-control" @bind="@_owner" />
</td>
</tr>
<tr>
<td>
<Label For="module" HelpText="Enter a name for this theme. It should not contain spaces or punctuation." ResourceKey="ThemeName">Theme Name: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_theme" />
</td>
</tr>
<tr>
<td>
<Label For="template" HelpText="Select a theme template. Templates are located in the wwwroot/Themes/Templates folder on the server." ResourceKey="Template">Template: </Label>
</td>
<td>
<select id="template" class="form-control" @onchange="(e => TemplateChanged(e))">
<option value="-">&lt;@Localizer["Select Template"]&gt;</option>
@foreach (string template in _templates)
{
<option value="@template">@template</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
</td>
<td>
<select id="reference" class="form-control" @bind="@_reference">
@foreach (string version in Constants.ReleaseVersions.Split(','))
{
if (Version.Parse(version).CompareTo(Version.Parse("2.0.0")) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@Localizer["Local Version"]</option>
</select>
</td>
</tr>
@if (!string.IsNullOrEmpty(_location))
{
<tr>
<td>
<Label For="location" HelpText="Location where the theme will be created" ResourceKey="Location">Location: </Label>
</td>
<td>
<input id="module" class="form-control" @bind="@_location" readonly />
</td>
</tr>
}
</table>
<button type="button" class="btn btn-success" @onclick="CreateTheme">@Localizer["Create Theme"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="Enter the name of the organization who is developing this theme. It should not contain spaces or punctuation." ResourceKey="OwnerName">Owner Name: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="module" HelpText="Enter a name for this theme. It should not contain spaces or punctuation." ResourceKey="ThemeName">Theme Name: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_theme" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="template" HelpText="Select a theme template. Templates are located in the wwwroot/Themes/Templates folder on the server." ResourceKey="Template">Template: </Label>
<div class="col-sm-9">
<select id="template" class="form-select" @onchange="(e => TemplateChanged(e))">
<option value="-">&lt;@Localizer["Template.Select"]&gt;</option>
@foreach (Template template in _templates)
{
<option value="@template.Name">@template.Title</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reference" HelpText="Select a framework reference version" ResourceKey="FrameworkReference">Framework Reference: </Label>
<div class="col-sm-9">
<select id="reference" class="form-select" @bind="@_reference">
@foreach (string version in _versions)
{
if (Version.Parse(version).CompareTo(Version.Parse(_minversion)) >= 0)
{
<option value="@(version)">@(version)</option>
}
}
<option value="local">@SharedLocalizer["LocalVersion"]</option>
</select>
</div>
</div>
@if (!string.IsNullOrEmpty(_location))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="location" HelpText="Location where the theme will be created" ResourceKey="Location">Location: </Label>
<div class="col-sm-9">
<input id="module" class="form-control" @bind="@_location" readonly />
</div>
</div>
}
</div>
<br />
<button type="button" class="btn btn-success" @onclick="CreateTheme">@Localizer["Theme.Create"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
}
@code {
private string _owner = string.Empty;
private string _theme = string.Empty;
private List<Template> _templates;
private string _template = "-";
private string _reference = Constants.Version;
private string[] _versions;
private string _reference = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;
private Dictionary<string, string> _systeminfo;
private List<string> _templates;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
protected override void OnInitialized()
{
AddModuleMessage(Localizer["Info.Theme.CreatorIntent"], MessageType.Info);
}
protected override async Task OnParametersSetAsync()
{
try
{
_systeminfo = await SystemService.GetSystemInfoAsync();
_templates = await ThemeService.GetThemeTemplatesAsync();
AddModuleMessage(Localizer["Please Note That The Theme Creator Is Only Intended To Be Used In A Development Environment"], MessageType.Info);
_versions = Constants.ReleaseVersions.Split(',').Where(item => Version.Parse(item).CompareTo(Version.Parse("2.0.0")) >= 0).ToArray();
}
catch (Exception ex)
{
@ -111,12 +105,11 @@
var theme = new Theme { Owner = _owner, Name = _theme, Template = _template, Version = _reference };
theme = await ThemeService.CreateThemeAsync(theme);
GetLocation();
AddModuleMessage(Localizer["The Source Code For Your Theme Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href=\"{0}\">Restart</a> Your Application To Activate The Module.", NavigateUrl("admin/system")], MessageType.Success);
AddModuleMessage(string.Format(Localizer["Success.Theme.Create"], NavigateUrl("admin/system")), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["You Must Provide A Valid Owner Name And Theme Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.ValidName"], MessageType.Warning);
}
}
catch (Exception ex)
@ -128,23 +121,29 @@
private bool IsValid(string name)
{
// must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
return !string.IsNullOrEmpty(name) && name.ToLower() != "theme" && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
}
private void TemplateChanged(ChangeEventArgs e)
{
_template = (string)e.Value;
_minversion = "2.0.0";
if (_template != "-")
{
var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version;
}
GetLocation();
}
private void GetLocation()
{
_location = string.Empty;
if (_template != "-" && _systeminfo != null && _systeminfo.ContainsKey("serverpath"))
if (_owner != "" && _theme != "" && _template != "-")
{
string[] path = _systeminfo["serverpath"].Split(Path.DirectorySeparatorChar);
_location = string.Join(Path.DirectorySeparatorChar, path, 0, path.Length - 2) +
Path.DirectorySeparatorChar + _owner + "." + _theme;
var template = _templates.FirstOrDefault(item => item.Name == _template);
_location = template.Location + _owner + "." + _theme;
}
StateHasChanged();
}

View File

@ -5,23 +5,25 @@
@inject IThemeService ThemeService
@inject IPackageService PackageService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_themes == null)
{
<p><em>Loading...</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<ActionLink Action="Add" Text="Install Theme" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary" />
<Pager Items="@_themes">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th scope="col">@Localizer["Name"]</th>
<th scope="col">@Localizer["Version"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@SharedLocalizer["Version"]</th>
<th>@SharedLocalizer["Expires"]</th>
<th>&nbsp;</th>
</Header>
<Row>
@ -29,16 +31,22 @@ else
<td>
@if (context.AssemblyName != "Oqtane.Client")
{
<ActionDialog Header="Delete Theme" Message="@Localizer["Are You Sure You Wish To Delete The {0} Theme?", context.Name]" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
<ActionDialog Header="Delete Theme" Message="@string.Format(Localizer["Confirm.Theme.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteTheme(context))" ResourceKey="DeleteTheme" />
}
</td>
<td>@context.Name</td>
<td>@context.Version</td>
<td>
@if (UpgradeAvailable(context.ThemeName, context.Version))
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.ThemeName, context.Version))>@Localizer["Upgrade"]</button>
}
@((MarkupString)PurchaseLink(context.PackageName))
</td>
<td>
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
}
</td>
<td></td>
</Row>
@ -63,38 +71,58 @@ else
if (_themes == null)
{
await logger.LogError(ex, "Error Loading Themes {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Themes"], MessageType.Error);
AddModuleMessage(Localizer["Error.Theme.Load"], MessageType.Error);
}
}
}
private bool UpgradeAvailable(string themename, string version)
private string PurchaseLink(string packagename)
{
var upgradeavailable = false;
if (_packages != null)
string link = "";
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == Utilities.GetTypeName(themename)).FirstOrDefault();
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
if (package.ExpiryDate != null && package.ExpiryDate.Value.Date != DateTime.MaxValue.Date)
{
link += "<span>" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + "</span>";
if (!string.IsNullOrEmpty(package.PaymentUrl))
{
link += "&nbsp;&nbsp;<a class=\"btn btn-primary\" style=\"text-decoration: none !important\" href=\"" + package.PaymentUrl + "\" target=\"_new\">" + SharedLocalizer["Extend"] + "</a>";
}
}
}
}
return upgradeavailable;
return link;
}
private async Task DownloadTheme(string themename, string version)
private string UpgradeAvailable(string packagename, string version)
{
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return package.Version;
}
}
return version;
}
private async Task DownloadTheme(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(themename, version, "Themes");
await logger.LogInformation("Theme Downloaded {ThemeName} {Version}", themename, version);
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Theme Downloaded {ThemeName} {Version}", packagename, version);
await ThemeService.InstallThemesAsync();
AddModuleMessage(Localizer["Theme Installed Successfully. You Must <a href=\"{0}\">Restart</a> Your Application To Apply These Changes.", NavigateUrl("admin/system")], MessageType.Success);
AddModuleMessage(string.Format(Localizer["Success.Theme.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Theme {ThemeName} {Version} {Error}", themename, version, ex.Message);
AddModuleMessage(Localizer["Error Downloading Theme"], MessageType.Error);
await logger.LogError(ex, "Error Downloading Theme {ThemeName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Theme.Download"], MessageType.Error);
}
}
@ -103,13 +131,13 @@ else
try
{
await ThemeService.DeleteThemeAsync(Theme.ThemeName);
AddModuleMessage(Localizer["Theme Deleted Successfully"], MessageType.Success);
StateHasChanged();
AddModuleMessage(Localizer["Success.Theme.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Theme {Theme} {Error}", Theme, ex.Message);
AddModuleMessage(Localizer["Error Deleting Theme"], MessageType.Error);
AddModuleMessage(Localizer["Error.Theme.Delete"], MessageType.Error);
}
}
}

View File

@ -4,66 +4,53 @@
@inject IThemeService ThemeService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<View> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="name" HelpText="The name of the theme" ResourceKey="Name">Name: </Label>
</td>
<td>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="The name of the theme" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" disabled />
</td>
</tr>
<tr>
<td>
<Label For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="themename" HelpText="The internal name of the module" ResourceKey="InternalName">Internal Name: </Label>
<div class="col-sm-9">
<input id="themename" class="form-control" @bind="@_themeName" disabled />
</td>
</tr>
<tr>
<td>
<Label For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="version" HelpText="The version of the theme" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<input id="version" class="form-control" @bind="@_version" disabled />
</td>
</tr>
<tr>
<td>
<Label For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="owner" HelpText="The owner or creator of the theme" ResourceKey="Owner">Owner: </Label>
<div class="col-sm-9">
<input id="owner" class="form-control" @bind="@_owner" disabled />
</td>
</tr>
<tr>
<td>
<Label For="url" HelpText="The reference url of the theme" ResourceKey="ReferenceUrl">Reference Url: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="url" HelpText="The reference url of the theme" ResourceKey="ReferenceUrl">Reference Url: </Label>
<div class="col-sm-9">
<input id="url" class="form-control" @bind="@_url" disabled />
</td>
</tr>
<tr>
<td>
<Label For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="contact" HelpText="The contact for the theme" ResourceKey="Contact">Contact: </Label>
<div class="col-sm-9">
<input id="contact" class="form-control" @bind="@_contact" disabled />
</td>
</tr>
<tr>
<td>
<Label For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="license" HelpText="The license of the theme" ResourceKey="License">License: </Label>
<div class="col-sm-9">
<textarea id="license" class="form-control" @bind="@_license" rows="5" disabled></textarea>
</td>
</tr>
</table>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
</div>
</div>
</div>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _themeName = "";
@ -96,7 +83,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Theme {ThemeName} {Error}", _themeName, ex.Message);
AddModuleMessage(Localizer["Error Loading Theme"], MessageType.Error);
AddModuleMessage(Localizer["Error.Theme.Loading"], MessageType.Error);
}
}
}

View File

@ -5,36 +5,34 @@
@inject IPackageService PackageService
@inject IInstallationService InstallationService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_package != null)
{
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
@if (_upgradeavailable)
{
<ModuleMessage Type="MessageType.Info" Message="Select The Upgrade Button To Install a New Framework Version"></ModuleMessage>
<button type="button" class="btn btn-success" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@Localizer["Upgrade To"] @_package.Version</button>
}
else
{
<ModuleMessage Type="MessageType.Info" Message="Framework Is Already Up To Date"></ModuleMessage>
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<table class="table table-borderless">
<tr>
<td>
<Label HelpText="Upload a framework package and select Install to complete the installation" ResourceKey="Framework">Framework: </Label>
</td>
<td>
<FileManager Filter="nupkg" ShowFiles="false" Folder="Framework" />
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="Upgrade">@Localizer["Install"]</button>
</TabPanel>
</TabStrip>
}
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable)
{
<ModuleMessage Type="MessageType.Info" Message="Select The Download Button To Download The Framework Upgrade Package And Then Select Upgrade"></ModuleMessage>
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button>
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
}
else
{
<ModuleMessage Type="MessageType.Info" Message="Framework Is Already Up To Date"></ModuleMessage>
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message="Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade"></ModuleMessage>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>
<div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" />
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
</TabPanel>
</TabStrip>
@code {
private Package _package;
@ -46,10 +44,10 @@
{
try
{
List<Package> packages = await PackageService.GetPackagesAsync("framework");
List<Package> packages = await PackageService.GetPackagesAsync("framework", "", "", "");
if (packages != null)
{
_package = packages.FirstOrDefault();
_package = packages.Where(item => item.PackageId.StartsWith(Constants.PackageId)).FirstOrDefault();
if (_package != null)
{
_upgradeavailable = (Version.Parse(_package.Version).CompareTo(Version.Parse(Constants.Version)) > 0);
@ -70,6 +68,7 @@
{
try
{
AddModuleMessage(Localizer["Info.Upgrade.Wait"], MessageType.Info);
ShowProgressIndicator();
var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(), 10);
@ -78,7 +77,7 @@
catch (Exception ex)
{
await logger.LogError(ex, "Error Executing Upgrade {Error}", ex.Message);
AddModuleMessage(Localizer["Error Executing Upgrade"], MessageType.Error);
AddModuleMessage(Localizer["Error.Upgrade.Execute"], MessageType.Error);
}
}
@ -86,16 +85,14 @@
{
try
{
await PackageService.DownloadPackageAsync(packageid, version, "Framework");
ShowProgressIndicator();
var interop = new Interop(JSRuntime);
await interop.RedirectBrowser(NavigateUrl(), 10);
await InstallationService.Upgrade();
await PackageService.DownloadPackageAsync(packageid, version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version, Constants.PackagesFolder);
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Framework {Error}", ex.Message);
AddModuleMessage(Localizer["Error Downloading Framework"], MessageType.Error);
await logger.LogError(ex, "Error Downloading Framework Package {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Framework.Download"], MessageType.Error);
}
}
}

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

@ -1,73 +1,69 @@
@namespace Oqtane.Modules.Admin.UserProfile
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@namespace Oqtane.Modules.Admin.UserProfile
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject INotificationService NotificationService
@inject IStringLocalizer<Add> Localizer
@if (PageState.User != null)
{
<table class="table table-borderless">
<tr>
<td>
<Label For="to" HelpText="Enter the username you wish to send a message to" ResourceKey="To">To: </Label>
</td>
<td>
<input id="to" class="form-control" @bind="@username" />
</td>
</tr>
<tr>
<td>
<Label For="subject" HelpText="Enter the subject of the message" ResourceKey="Subject">Subject: </Label>
</td>
<td>
<input id="subject" class="form-control" @bind="@subject" />
</td>
</tr>
<tr>
<td>
<Label For="message" HelpText="Enter the message" ResourceKey="Message">Message: </Label>
</td>
<td>
<textarea id="message" class="form-control" @bind="@body" rows="5" />
</td>
</tr>
</table>
<button type="button" class="btn btn-primary" @onclick="Send">@Localizer["Send"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
}
@code {
private string username = "";
private string subject = "";
private string body = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Title => "Send Notification";
private async Task Send()
{
try
{
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user != null)
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.User != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<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">
<input id="to" class="form-control" @bind="@username" />
</div>
</div>
<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>
<div class="col-sm-9">
<input id="subject" class="form-control" @bind="@subject" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="message" HelpText="Enter the message" ResourceKey="Message">Message: </Label>
<div class="col-sm-9">
<textarea id="message" class="form-control" @bind="@body" rows="5" />
</div>
</div>
</div>
<br/>
<button type="button" class="btn btn-primary" @onclick="Send">@SharedLocalizer["Send"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
}
@code {
private string username = "";
private string subject = "";
private string body = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Title => "Send Notification";
private async Task Send()
{
try
{
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user != null)
{
var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, null);
notification = await NotificationService.AddNotificationAsync(notification);
await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["User Does Not Exist. Please Verify That The Username Provided Is Correct."], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Notification {Error}", ex.Message);
AddModuleMessage(Localizer["Error Adding Notification"], MessageType.Error);
}
}
}
var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body);
notification = await NotificationService.AddNotificationAsync(notification);
await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.User.Invalid"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Notification {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Notification.Add"], MessageType.Error);
}
}
}

View File

@ -5,11 +5,14 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject INotificationService NotificationService
@inject IFileService FileService
@inject IFolderService FolderService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.User != null && photofileid != -1)
@if (PageState.User != null && photo != null)
{
<img src="@(ContentUrl(photofileid))" 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
{
@ -17,119 +20,124 @@ else
}
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
@if (PageState.User != null)
@if (profiles != null && settings != null)
{
<table class="table table-borderless">
<tr>
<td>
<label for="Name" class="control-label">@Localizer["Username:"] </label>
</td>
<td>
<input class="form-control" @bind="@username" readonly />
</td>
</tr>
<tr>
<td>
<label for="Name" class="control-label">@Localizer["Password:"] </label>
</td>
<td>
<input type="password" class="form-control" @bind="@password" autocomplete="new-password" />
</td>
</tr>
<tr>
<td>
<label for="Name" class="control-label">@Localizer["Confirm Password:"] </label>
</td>
<td>
<input type="password" class="form-control" @bind="@confirm" autocomplete="new-password" />
</td>
</tr>
<tr>
<td>
<label for="Name" class="control-label">@Localizer["Email:"] </label>
</td>
<td>
<input class="form-control" @bind="@email" />
</td>
</tr>
<tr>
<td>
<label for="Name" class="control-label">@Localizer["Full Name:"] </label>
</td>
<td>
<input class="form-control" @bind="@displayname" />
</td>
</tr>
<tr>
<td>
<label for="Name" class="control-label">@Localizer["Photo:"] </label>
</td>
<td>
<FileManager FileId="@photofileid" @ref="filemanager" />
</td>
</tr>
</table>
<button type="button" class="btn btn-primary" @onclick="Save">@Localizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@Localizer["Cancel"]</button>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="If you wish to change your password you can enter it here. Please choose a sufficiently secure password." ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="If you are changing your password you must enter it again to confirm it matches" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</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">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@photofileid" Filter="@Constants.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
}
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
@if (profiles != null && settings != null)
{
<table class="table table-borderless">
@foreach (Profile profile in profiles)
{
var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (p.Category != category)
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
{
<tr>
<th colspan="2" style="text-align: center;">
@p.Category
</th>
</tr>
category = p.Category;
}
<tr>
<td>
<Label For="@p.Name" HelpText="@p.Description">@p.Title</Label>
</td>
<td>
@if (!string.IsNullOrEmpty(p.Options))
var p = profile;
if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (p.Category != category)
{
<select id="@p.Name" class="form-control" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
<div class="col text-center pb-2">
@p.Category
</div>
category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
@if(GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option))
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option))
{
<option value="@option" selected>@option</option>
}
else
{
<option value="@option">@option</option>
}
}
</select>
}
else
{
@if (p.IsRequired)
{
<option value="@option" selected>@option</option>
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
}
else
{
<option value="@option">@option</option>
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
}
</select>
}
else
{
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
}
</td>
</tr>
}
}
</table>
<button type="button" class="btn btn-primary" @onclick="Save">@Localizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@Localizer["Cancel"]</button>
</div>
</div>
}
}
</div>
</div>
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
}
</TabPanel>
<TabPanel Name="Notifications" ResourceKey="Notifications">
@ -152,18 +160,19 @@ else
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.FromDisplayName</td>
<td>@context.Subject</td>
<td>@context.CreatedOn</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input)){
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
</td>
</Detail>
@ -184,40 +193,50 @@ else
<td><ActionDialog Header="Delete Notification" Message="Are You Sure You Wish To Delete This Notification?" Action="Delete" Security="SecurityAccessLevel.View" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" EditMode="false" ResourceKey="DeleteNotification" /></td>
<td>@context.ToDisplayName</td>
<td>@context.Subject</td>
<td>@context.CreatedOn</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", @context.CreatedOn)</td>
</Row>
<Detail>
<td colspan="2"></td>
<td colspan="3">
@{
string input = "___";
if (context.Body.Contains(input)){
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@{
string input = "___";
if (context.Body.Contains(input))
{
context.Body = context.Body.Split(input)[0];
context.Body = context.Body.Replace("\n", "");
context.Body = context.Body.Replace("\r", "");
} }
@(context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body)
</td>
</Detail>
</Pager>
}
<br />
<ActionDialog Header="Clear Notifications" Message="Are You Sure You Wish To Permanently Delete All Notifications ?" Action="Delete All Notifications" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllNotifications())" ResourceKey="DeleteAllNotifications" />
<br /><hr />
<select class="form-control" @onchange="(e => FilterChanged(e))">
<select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
<option value="from">@Localizer["Sent Items"]</option>
<option value="from">@Localizer["Items.Sent"]</option>
</select>
}
</TabPanel>
</TabStrip>
<br /><br />
@code {
private string username = string.Empty;
private string password = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty;
private bool allowtwofactor = false;
private string twofactor = "False";
private string email = string.Empty;
private string displayname = string.Empty;
private FileManager filemanager;
private int folderid = -1;
private int photofileid = -1;
private File photo = null;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
@ -226,19 +245,40 @@ else
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync()
protected override async Task OnParametersSetAsync()
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{
allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
}
if (PageState.User != null)
{
username = PageState.User.Username;
twofactor = PageState.User.TwoFactorRequired.ToString();
email = PageState.User.Email;
displayname = PageState.User.DisplayName;
// get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
{
folderid = folder.FolderId;
}
if (PageState.User.PhotoFileId != null)
{
photofileid = PageState.User.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
@ -248,13 +288,13 @@ else
}
else
{
AddModuleMessage(Localizer["Current User Is Not Logged In"], MessageType.Warning);
AddModuleMessage(Localizer["Message.User.NoLogIn"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading User Profile"], MessageType.Error);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
@ -273,40 +313,51 @@ else
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
if (_password == confirm)
{
var user = PageState.User;
user.Username = username;
user.Password = password;
user.Password = _password;
user.TwoFactorRequired = bool.Parse(twofactor);
user.Email = email;
user.DisplayName = (displayname == string.Empty ? username : displayname);
user.PhotoFileId = null;
photofileid = filemanager.GetFileId();
if (photofileid != -1)
user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = photofileid;
user.PhotoFileId = null;
}
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
await UserService.UpdateUserAsync(user);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
AddModuleMessage(Localizer["User Profile Updated Successfully"], MessageType.Success);
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
}
else
{
AddModuleMessage(Localizer["Passwords Entered Do Not Match"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide A Username and Email Address As Well As All Required Profile Information"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error Saving User Profile"], MessageType.Error);
AddModuleMessage(Localizer["Error.Profile.Save"], MessageType.Error);
}
}
@ -374,4 +425,47 @@ else
StateHasChanged();
}
private async Task DeleteAllNotifications()
{
try
{
foreach(var Notification in notifications)
{
if (!Notification.IsDeleted)
{
Notification.IsDeleted = true;
await NotificationService.UpdateNotificationAsync(Notification);
}
else
{
await NotificationService.DeleteNotificationAsync(Notification.NotificationId);
}
await logger.LogInformation("Notification Deleted {Notification}", Notification);
}
await logger.LogInformation("Notifications Permanently Deleted");
await LoadNotificationsAsync();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Notifications {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -1,201 +1,198 @@
@namespace Oqtane.Modules.Admin.UserProfile
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@namespace Oqtane.Modules.Admin.UserProfile
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject INotificationService NotificationService
@inject IStringLocalizer<View> Localizer
@if (PageState.User != null)
{
<table class="table table-borderless">
<tr>
<td>
<label class="control-label">@Localizer["Title:"] </label>
</td>
@if (title == "From")
{
<td>
<input class="form-control" @bind="@username" readonly />
</td>
}
@if (title == "To")
{
<td>
<input class="form-control" @bind="@username" />
</td>
}
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Subject:"] </label>
</td>
@if (title == "From")
{
<td>
<input class="form-control" @bind="@subject" readonly />
</td>
}
@if (title == "To")
{
<td>
<input class="form-control" @bind="@subject" />
</td>
}
</tr>
@if (title == "From")
{
<tr>
<td>
<label class="control-label">@Localizer["Date:"] </label>
</td>
<td>
<input class="form-control" @bind="@createdon" readonly />
</td>
</tr>
}
@if (title == "From")
{
<tr>
<td>
<label class="control-label">@Localizer["Message:"] </label>
</td>
<td>
<textarea class="form-control" @bind="@body" rows="5" readonly />
</td>
</tr>
}
@if (title == "To")
{
<tr>
<td>
<label class="control-label">@Localizer["Message:"] </label>
</td>
<td>
<textarea class="form-control" @bind="@body" rows="5" />
</td>
</tr>
}
</table>
@if (reply != string.Empty)
{
<button type="button" class="btn btn-primary" @onclick="Send">@Localizer["Send"]</button>
}
else
{
if (title == "From")
{
@inject IStringLocalizer<View> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.User != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<label Class="col-sm-3">@Localizer["Title"] </label>
@if (title == "From")
{
<div class="col-sm-3">
<input class="form-control" @bind="@username" readonly />
</div>
}
@if (title == "To")
{
<div class="col-sm-3">
<input class="form-control" @bind="@username" />
</div>
}
</div>
<div class="row mb-1 align-items-center">
<label Class="col-sm-3">@Localizer["Subject"] </label>
@if (title == "From")
{
<div class="col-sm-3">
<input class="form-control" @bind="@subject" readonly />
</div>
}
@if (title == "To")
{
<div class="col-sm-3">
<input class="form-control" @bind="@subject" />
</div>
}
</div>
</div>
<div class="container">
@if (title == "From")
{
<div class="row mb-1 align-items-center">
<label class="col-sm-3">@Localizer["Date"] </label>
<div class="col-sm-9">
<input class="form-control" @bind="@createdon" readonly />
</div>
</div>
}
@if (title == "From")
{
<div class="row mb-1 align-items-center">
<label class="col-sm-3">@Localizer["Message"] </label>
<div class="col-sm-9">
<textarea class="form-control" @bind="@body" rows="5" readonly />
</div>
</div>
}
@if (title == "To")
{
<div class="row mb-1 align-items-center">
<label class="col-sm-3">@Localizer["Message"] </label>
<div class="col-sm-9">
<textarea class="form-control" @bind="@body" rows="5" readonly />
</div>
</div>
}
</div>
@if (reply != string.Empty)
{
<button type="button" class="btn btn-primary" @onclick="Send">@SharedLocalizer["Send"]</button>
}
else
{
if (title == "From")
{
<button type="button" class="btn btn-primary" @onclick="Reply">@Localizer["Reply"]</button>
}
}
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<br />
<br />
@if (title == "To")
{
<div class="control-group">
<label class="control-label">@Localizer["Original Message"] </label>
<textarea class="form-control" @bind="@reply" rows="5" readonly />
</div>
}
}
@code {
private int notificationid;
private string title = string.Empty;
private string username = "";
private string subject = string.Empty;
private string createdon = string.Empty;
private string body = string.Empty;
private string reply = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Title => "View Notification";
protected override async Task OnInitializedAsync()
{
try
{
notificationid = Int32.Parse(PageState.QueryString["id"]);
Notification notification = await NotificationService.GetNotificationAsync(notificationid);
if (notification != null)
{
int userid = -1;
if (notification.ToUserId == PageState.User.UserId)
{
title = "From";
if (notification.FromUserId != null)
{
userid = notification.FromUserId.Value;
}
}
else
{
title = "To";
if (notification.ToUserId != null)
{
userid = notification.ToUserId.Value;
}
}
if (userid != -1)
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
if (user != null)
{
username = user.Username;
}
}
if (username == "")
{
username = "System";
}
subject = notification.Subject;
createdon = notification.CreatedOn.ToString();
body = notification.Body;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Users"], MessageType.Error);
}
}
private void Reply()
{
title = "To";
if (!subject.Contains("RE:"))
{
subject = "RE: " + subject;
}
reply = body;
body = "\n\n____________________________________________\nSent: " + createdon + "\nSubject: " + subject + "\n\n" + body;
StateHasChanged();
}
private async Task Send()
{
try
{
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user != null)
}
}
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
@if (title == "To")
{
<div class="control-group">
<label class="control-label">@Localizer["OriginalMessage"] </label>
<textarea class="form-control" @bind="@reply" rows="5" readonly />
</div>
}
}
@code {
private int notificationid;
private string title = string.Empty;
private string username = "";
private string subject = string.Empty;
private string createdon = string.Empty;
private string body = string.Empty;
private string reply = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
public override string Title => "View Notification";
protected override async Task OnInitializedAsync()
{
try
{
notificationid = Int32.Parse(PageState.QueryString["id"]);
Notification notification = await NotificationService.GetNotificationAsync(notificationid);
if (notification != null)
{
int userid = -1;
if (notification.ToUserId == PageState.User.UserId)
{
title = "From";
if (notification.FromUserId != null)
{
userid = notification.FromUserId.Value;
}
}
else
{
title = "To";
if (notification.ToUserId != null)
{
userid = notification.ToUserId.Value;
}
}
if (userid != -1)
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
if (user != null)
{
username = user.Username;
}
}
if (username == "")
{
username = "System";
}
subject = notification.Subject;
createdon = notification.CreatedOn.ToString();
body = notification.Body;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {Error}", ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
private void Reply()
{
title = "To";
if (!subject.Contains("RE:"))
{
subject = "RE: " + subject;
}
reply = body;
body = "\n\n____________________________________________\nSent: " + createdon + "\nSubject: " + subject + "\n\n" + body;
StateHasChanged();
}
private async Task Send()
{
try
{
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user != null)
{
var notification = new Notification(PageState.Site.SiteId, PageState.User, user, subject, body, notificationid);
notification = await NotificationService.AddNotificationAsync(notification);
await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["User Does Not Exist. Please Verify That The Username Provided Is Correct."], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Notification {Error}", ex.Message);
AddModuleMessage(Localizer["Error Adding Notification"], MessageType.Error);
}
}
}
notification = await NotificationService.AddNotificationAsync(notification);
await logger.LogInformation("Notification Created {NotificationId}", notification.NotificationId);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.User.Invalid"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding Notification {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Notification.Add"], MessageType.Error);
}
}
}

View File

@ -5,98 +5,98 @@
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip>
<TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null)
{
<table class="table table-borderless">
<tr>
<td>
<label class="control-label">@Localizer["Username:"] </label>
</td>
<td>
<input class="form-control" @bind="@username" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Password:"] </label>
</td>
<td>
<input type="password" class="form-control" @bind="@password" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Confirm Password:"] </label>
</td>
<td>
<input type="password" class="form-control" @bind="@confirm" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Email:"] </label>
</td>
<td>
<input class="form-control" @bind="@email" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Full Name:"] </label>
</td>
<td>
<input class="form-control" @bind="@displayname" />
</td>
</tr>
</table>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="A unique username for a user. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
</div>
</div>
</div>
}
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
@if (profiles != null)
{
<table class="table table-borderless">
@foreach (Profile profile in profiles)
{
var p = profile;
if (p.Category != category)
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
{
<tr>
<th colspan="2" style="text-align: center;">
@p.Category
</th>
</tr>
category = p.Category;
var p = profile;
if (p.Category != category)
{
<div class="col text-center pb-2">
<strong>@p.Category</strong>
</div>
category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
</div>
</div>
}
<tr>
<td>
<Label For="@p.Name" HelpText="@p.Description">@p.Title</Label>
</td>
<td>
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
</td>
</tr>
}
</table>
</div>
</div>
}
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-primary" @onclick="SaveUser">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<br />
<br />
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string username = string.Empty;
private string password = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty;
private string email = string.Empty;
private string displayname = string.Empty;
@ -110,13 +110,14 @@
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Profile {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading User Profile"], MessageType.Error);
AddModuleMessage(Localizer["Error.Profile.Load"], MessageType.Error);
}
}
@ -127,46 +128,54 @@
{
try
{
if (username != string.Empty && password != string.Empty && confirm != string.Empty && email != string.Empty && ValidateProfiles())
if (username != string.Empty && _password != string.Empty && confirm != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
if (_password == confirm)
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Password = password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null;
user = await UserService.AddUserAsync(user);
if (user != null)
var user = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user == null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null;
user = await UserService.AddUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Created {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", username, email);
AddModuleMessage(Localizer["Error.User.AddCheckPass"], MessageType.Error);
}
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", username, email);
AddModuleMessage(Localizer["Error Adding User. Please Ensure Password Meets Complexity Requirements And Username Is Not Already In Use."], MessageType.Error);
AddModuleMessage(Localizer["Message.Username.Exists"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Passwords Entered Do Not Match"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide A Username, Password, Email Address And All Required Profile Information"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding User {Username} {Email} {Error}", username, email, ex.Message);
AddModuleMessage(Localizer["Error Adding User"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.Add"], MessageType.Error);
}
}
@ -193,4 +202,17 @@
settings = SettingService.SetSetting(settings, SettingName, value);
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -4,11 +4,13 @@
@inject IUserService UserService
@inject IProfileService ProfileService
@inject ISettingService SettingService
@inject IFileService FileService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (PageState.User != null && photofileid != -1)
@if (PageState.User != null && photo != null)
{
<img src="@(ContentUrl(photofileid))" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
<img src="@photo.Url" alt="@displayname" style="max-width: 400px" class="rounded-circle mx-auto d-block">
}
else
{
@ -18,167 +20,205 @@ else
<TabPanel Name="Identity" ResourceKey="Identity">
@if (profiles != null)
{
<table class="table table-borderless">
<tr>
<td>
<label class="control-label">@Localizer["Username:"] </label>
</td>
<td>
<input class="form-control" @bind="@username" readonly />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Password:"] </label>
</td>
<td>
<input type="password" class="form-control" @bind="@password" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Confirm Password:"] </label>
</td>
<td>
<input type="password" class="form-control" @bind="@confirm" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Email:"] </label>
</td>
<td>
<input class="form-control" @bind="@email" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Full Name:"] </label>
</td>
<td>
<input class="form-control" @bind="@displayname" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Photo:"] </label>
</td>
<td>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of the user" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@photofileid" @ref="filemanager" />
</td>
</tr>
<tr>
<td>
<label class="control-label">@Localizer["Is Deleted?"] </label>
</td>
<td>
<select class="form-control" @bind="@isdeleted">
<option value="True">@Localizer["Yes"]</option>
<option value="False">@Localizer["No"]</option>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@isdeleted">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
</tr>
</table>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@lastlogin" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label>
<div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@lastipaddress" readonly />
</div>
</div>
</div>
}
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
@if (profiles != null)
{
<table class="table table-borderless">
@foreach (Profile profile in profiles)
{
var p = profile;
if (p.Category != category)
<div class="container">
<div class="row mb-1 align-items-center">
@foreach (Profile profile in profiles)
{
<tr>
<th colspan="2" style="text-align: center;">
@p.Category
</th>
</tr>
category = p.Category;
var p = profile;
if (p.Category != category)
{
<div class="col text-center pb-2">
<strong>@p.Category</strong>
</div>
category = p.Category;
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option))
{
<option value="@option" selected>@option</option>
}
else
{
<option value="@option">@option</option>
}
}
</select>
}
else
{
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
}
</div>
</div>
}
<tr>
<td>
<Label For="@p.Name" HelpText="@p.Description">@p.Title</Label>
</td>
<td>
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
}
else
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
</td>
</tr>
}
</table>
</div>
</div>
}
</TabPanel>
</TabStrip>
<button type="button" class="btn btn-primary" @onclick="SaveUser">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
@code {
private int userid;
private string username = string.Empty;
private string password = string.Empty;
private string confirm = string.Empty;
private string email = string.Empty;
private string displayname = string.Empty;
private FileManager filemanager;
private int photofileid = -1;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private string deletedby;
private DateTime? deletedon;
private string isdeleted;
private int userid;
private string username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty;
private string email = string.Empty;
private string displayname = string.Empty;
private FileManager filemanager;
private int photofileid = -1;
private File photo = null;
private string isdeleted;
private string lastlogin;
private string lastipaddress;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
protected override async Task OnInitializedAsync()
{
try
{
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private string deletedby;
private DateTime? deletedon;
userid = Int32.Parse(PageState.QueryString["id"]);
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
if (user != null)
{
username = user.Username;
email = user.Email;
displayname = user.DisplayName;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
protected override async Task OnParametersSetAsync()
{
try
{
if (PageState.QueryString.ContainsKey("id"))
{
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
userid = Int32.Parse(PageState.QueryString["id"]);
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
if (user != null)
{
username = user.Username;
email = user.Email;
displayname = user.DisplayName;
if (user.PhotoFileId != null)
{
photofileid = user.PhotoFileId.Value;
photo = await FileService.GetFileAsync(photofileid);
}
else
{
photofileid = -1;
photo = null;
}
isdeleted = user.IsDeleted.ToString();
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
lastipaddress = user.LastIPAddress;
settings = await SettingService.GetUserSettingsAsync(user.UserId);
createdby = user.CreatedBy;
createdon = user.CreatedOn;
modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy;
deletedon = user.DeletedOn;
}
settings = await SettingService.GetUserSettingsAsync(user.UserId);
createdby = user.CreatedBy;
createdon = user.CreatedOn;
modifiedby = user.ModifiedBy;
modifiedon = user.ModifiedOn;
deletedby = user.DeletedBy;
deletedon = user.DeletedOn;
isdeleted = user.IsDeleted.ToString();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message);
AddModuleMessage(Localizer["Error Loading User"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
}
@ -191,20 +231,19 @@ else
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
{
if (password == confirm)
if (_password == confirm)
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Password = password;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.PhotoFileId = null;
photofileid = filemanager.GetFileId();
if (photofileid != -1)
user.PhotoFileId = filemanager.GetFileId();
if (user.PhotoFileId == -1)
{
user.PhotoFileId = photofileid;
user.PhotoFileId = null;
}
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
@ -217,18 +256,18 @@ else
}
else
{
AddModuleMessage(Localizer["Passwords Entered Do Not Match"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["You Must Provide A Username, Password, Email Address, And All Required Profile Information"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", username, email, ex.Message);
AddModuleMessage(Localizer["Error Saving User"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error);
}
}
@ -255,4 +294,17 @@ else
settings = SettingService.SetSetting(settings, SettingName, value);
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -3,113 +3,638 @@
@inject IUserRoleService UserRoleService
@inject IUserService UserService
@inject ISettingService SettingService
@inject ISiteService SiteService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject SiteState SiteState
@if (userroles == null)
@if (users == null)
{
<p>
<em>@Localizer["Loading..."]</em>
<em>@SharedLocalizer["Loading"]</em>
</p>
}
else
{
<ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" />
<div class="d-flex p-1">
<input class="form-control mr-4" @bind="@_search" /><button class="btn btn-outline-primary ml-1" @onclick="OnSearch">@Localizer["Search"]</button>
</div>
<Pager Items="@userroles">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
</Header>
<Row>
<td>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" />
</td>
<td>
<ActionDialog Header="Delete User" Message="@Localizer["Are You Sure You Wish To Delete {0}?", context.User.DisplayName]" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" ResourceKey="DeleteUser" />
</td>
<td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" />
</td>
<td>@context.User.DisplayName</td>
</Row>
</Pager>
<TabStrip>
<TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-4">
<ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" />
</div>
<div class="col-sm-4">
<input class="form-control" @bind="@_search" />
</div>
<div class="col-sm-4">
<button type="button" class="btn btn-secondary" @onclick="OnSearch">@SharedLocalizer["Search"]</button>
</div>
</div>
</div>
<Pager Items="@users" RowClass="align-middle">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Username"]</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["LastLoginOn"]</th>
</Header>
<Row>
<td>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" />
</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" />
</td>
<td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" />
</td>
<td>@context.User.Username</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td>
<td>@string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn)</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>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
<option value="true">@Localizer["Optional"]</option>
<option value="required">@Localizer["Required"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cookiename" HelpText="You can choose to use a custom authentication cookie name for each site. However please be aware that if you want to share an authentication cookie between sites on the same domain they need to use a consistent cookie name. Also be aware that changing the authentication cookie name will logout all current users." ResourceKey="CookieName">Cookie Name:</Label>
<div class="col-sm-9">
<input id="cookiename" class="form-control" @bind="@_cookiename" />
</div>
</div>
}
</Section>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
<div class="col-sm-9">
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
<div class="col-sm-9">
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
<div class="col-sm-9">
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
<div class="col-sm-9">
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
<div class="col-sm-9">
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
<div class="col-sm-9">
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</Section>
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
<div class="col-sm-9">
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
<div class="col-sm-9">
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
</div>
</div>
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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="parameters" HelpText="Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple)." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="The name of the unique user identifier claim provided by the provider" ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The name of the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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 {
private List<UserRole> allroles;
private List<UserRole> userroles;
private string _search;
private List<UserRole> allusers;
private List<UserRole> users;
private string _search = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private string _allowregistration;
private string _allowsitelogin;
private string _twofactor;
private string _cookiename;
protected override async Task OnInitializedAsync()
{
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
await LoadSettingsAsync();
userroles = Search(_search);
}
private string _minimumlength;
private string _uniquecharacters;
private string _requiredigit;
private string _requireupper;
private string _requirelower;
private string _requirepunctuation;
private string _maximumfailures;
private string _lockoutduration;
private List<UserRole> Search(string search)
{
if (string.IsNullOrEmpty(_search))
{
return allroles.Where(item => item.Role.Name == RoleNames.Registered).ToList();
}
return allroles
.Where(item => item.Role.Name == RoleNames.Registered &&
(
item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)
)
)
.ToList();
}
private string _providertype;
private string _providername;
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 _parameters;
private string _pkce;
private string _redirecturl;
private string _identifierclaimtype;
private string _emailclaimtype;
private string _domainfilter;
private string _createusers;
private async Task OnSearch()
{
userroles = Search(_search);
await UpdateSettingsAsync();
}
private string _secret;
private string _secrettype = "password";
private string _togglesecret = string.Empty;
private string _issuer;
private string _audience;
private string _lifetime;
private string _token;
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);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private string settingSearch = "AU-search";
protected override async Task OnInitializedAsync()
{
await LoadUserSettingsAsync();
await LoadUsersAsync(true);
private async Task LoadSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, "");
}
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
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);
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
_requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true");
_requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true");
_requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true");
_requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true");
_maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
_lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
_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 = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
_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 = SharedLocalizer["ShowPassword"];
_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 async Task LoadUsersAsync(bool load)
{
if (load)
{
allusers = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var hosts = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Host);
allusers.AddRange(hosts);
allusers = allusers.OrderBy(u => u.User.DisplayName).ToList();
}
}
users = allusers;
if (!string.IsNullOrEmpty(_search))
{
users = users.Where(item =>
(
item.User.Username.Contains(_search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(_search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(_search, StringComparison.OrdinalIgnoreCase)
)
).ToList();
}
}
private async Task OnSearch()
{
await UpdateUserSettingsAsync();
await LoadUsersAsync(false);
}
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);
await LoadUsersAsync(true);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
private string settingSearch = "AU-search";
private async Task LoadUserSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, "");
}
private async Task UpdateUserSettingsAsync()
{
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);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, 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:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, 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();
if (!string.IsNullOrEmpty(_secret))
{
SiteState.AuthorizationToken = await UserService.GetTokenAsync();
}
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 (string.IsNullOrEmpty(_providername))
{
if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
_scopes = "openid,profile,email";
_identifierclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
_emailclaimtype = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
}
else
{
_scopes = "";
_identifierclaimtype = "sub";
_emailclaimtype = "email";
}
}
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
StateHasChanged();
}
private async Task CreateToken()
{
_token = await UserService.GetPersonalAccessTokenAsync();
}
private void ToggleClientSecret()
{
if (_clientsecrettype == "password")
{
_clientsecrettype = "text";
_toggleclientsecret = SharedLocalizer["HidePassword"];
}
else
{
_clientsecrettype = "password";
_toggleclientsecret = SharedLocalizer["ShowPassword"];
}
}
private void ToggleSecret()
{
if (_secrettype == "password")
{
_secrettype = "text";
_togglesecret = SharedLocalizer["HidePassword"];
}
else
{
_secrettype = "password";
_togglesecret = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -4,70 +4,66 @@
@inject IUserService UserService
@inject IUserRoleService UserRoleService
@inject IStringLocalizer<Roles> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (userroles == null)
{
<p><em>@Localizer["Loading..."]</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<table class="table table-borderless">
<tr>
<td>
<Label For="user" HelpText="The user you are assigning roles to" ResourceKey="User">User: </Label>
</td>
<td>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="The user you are assigning roles to" ResourceKey="User">User: </Label>
<div class="col-sm-9">
<input id="user" class="form-control" @bind="@name" disabled />
</td>
</tr>
<tr>
<td>
<Label For="role" HelpText="Select a role" ResourceKey="Role">Role: </Label>
</td>
<td>
<select id="role" class="form-control" @bind="@roleid">
<option value="-1">&lt;@Localizer["Select Role"]&gt;</option>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="role" HelpText="Select a role" ResourceKey="Role">Role: </Label>
<div class="col-sm-9">
<select id="role" class="form-select" @bind="@roleid">
<option value="-1">&lt;@Localizer["Role.Select"]&gt;</option>
@foreach (Role role in roles)
{
<option value="@(role.RoleId)">@role.Name</option>
}
</select>
</td>
</tr>
<tr>
<td>
<Label For="effectiveDate" HelpText="The date that this role assignment is active" ResourceKey="EffectiveDate">Effective Date: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this role assignment is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input id="effectiveDate" class="form-control" @bind="@effectivedate" />
</td>
</tr>
<tr>
<td>
<Label For="expiryDate" HelpText="The date that this role assignment expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
</td>
<td>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this role assignment expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input id="expiryDate" class="form-control" @bind="@expirydate" />
</td>
</tr>
</table>
<button type="button" class="btn btn-success" @onclick="SaveUserRole">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
</div>
</div>
</div>
<br />
<br />
<button type="button" class="btn btn-success" @onclick="SaveUserRole">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<hr class="app-rule" />
<p align="center">
<Pager Items="@userroles">
<Header>
<th>@Localizer["Roles"]</th>
<th>&nbsp;</th>
<th>@Localizer["Roles"]</th>
<th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th>
<th>&nbsp;</th>
</Header>
<Row>
<td>@context.Role.Name</td>
<td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td>
<td>
@if (context.Role.Name != RoleNames.Registered)
{
<button type="button" class="btn btn-danger" @onclick=@(async () => await DeleteUserRole(context.UserRoleId))>@Localizer["Delete"]</button>
}
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
</td>
</Row>
</Pager>
@ -92,13 +88,21 @@ else
userid = Int32.Parse(PageState.QueryString["id"]);
User user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
name = user.DisplayName;
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true);
roles = roles.Where(item => item.Name != RoleNames.Everyone).ToList();
}
else
{
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId);
}
await GetUserRoles();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Roles {Error}", ex.Message);
AddModuleMessage(Localizer["Error Loading Roles"], MessageType.Error);
AddModuleMessage(Localizer["Error.LoadRole"], MessageType.Error);
}
}
@ -106,13 +110,12 @@ else
{
try
{
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = userroles.Where(item => item.UserId == userid).ToList();
userroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, userid);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading User Roles {UserId} {Error}", userid, ex.Message);
AddModuleMessage(Localizer["Error Loading User Roles"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.LoadRole"], MessageType.Error);
}
}
@ -171,19 +174,20 @@ else
await UserRoleService.AddUserRoleAsync(userrole);
}
await GetUserRoles();
await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["User Assigned To Role"], MessageType.Success);
AddModuleMessage(Localizer["Success.User.AssignRole"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
else
{
AddModuleMessage(Localizer["You Must Select A Role"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.Role"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving User Roles {UserId} {Error}", userid, ex.Message);
AddModuleMessage(Localizer["Error Saving User Roles"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.SaveRole"], MessageType.Error);
}
}
@ -192,14 +196,15 @@ else
try
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await GetUserRoles();
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId);
AddModuleMessage(Localizer["User Removed From Role"], MessageType.Success);
AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error Removing User From Role"], MessageType.Error);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
}
}
}

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

@ -9,7 +9,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@Header</h5>
<button type="button" class="close" @onclick="DisplayModal" aria-label="Close">&times;</button>
<button type="button" class="btn-close" aria-label="Close" @onclick="DisplayModal"></button>
</div>
<div class="modal-body">
<p>@Message</p>
@ -17,7 +17,7 @@
<div class="modal-footer">
@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>
</div>
@ -30,16 +30,17 @@
{
if (Disabled)
{
<button class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button>
<button type="button" class="@Class" disabled>@((MarkupString)_iconSpan) @Text</button>
}
else
{
<button class="@Class" @onclick="DisplayModal">@((MarkupString)_iconSpan) @Text</button>
<button type="button" class="@Class" @onclick="DisplayModal">@((MarkupString)_iconSpan) @Text</button>
}
}
@code {
private bool _visible = false;
private string _permissions = string.Empty;
private bool _editmode = false;
private bool _authorized = false;
private string _iconSpan = string.Empty;
@ -59,6 +60,9 @@
[Parameter]
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]
public string Class { get; set; } // optional
@ -105,6 +109,7 @@
Header = Localize(nameof(Header), Header);
Message = Localize(nameof(Message), Message);
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions;
_authorized = IsAuthorized();
}
@ -138,10 +143,10 @@
authorized = true;
break;
case SecurityAccessLevel.View:
authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.View, ModuleState.Permissions);
authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.View, _permissions);
break;
case SecurityAccessLevel.Edit:
authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.Edit, ModuleState.Permissions);
authorized = UserSecurity.IsAuthorized(PageState.User,PermissionNames.Edit, _permissions);
break;
case SecurityAccessLevel.Admin:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin);

View File

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

View File

@ -35,6 +35,9 @@
[Parameter]
public string Style { get; set; }
[Parameter]
public string DateTimeFormat { get; set; } = "MMM dd yyyy HH:mm:ss";
protected override void OnParametersSet()
{
_text = string.Empty;
@ -44,12 +47,12 @@
if (!String.IsNullOrEmpty(CreatedBy))
{
_text += $" {Localizer["by"]} <b>{CreatedBy}</b>";
_text += $" {Localizer["By"]} <b>{CreatedBy}</b>";
}
if (CreatedOn != null)
{
_text += $" {Localizer["on"]} <b>{CreatedOn.Value.ToString("MMM dd yyyy HH:mm:ss")}</b>";
_text += $" {Localizer["On"]} <b>{CreatedOn.Value.ToString(DateTimeFormat)}</b>";
}
_text += "</p>";
@ -57,7 +60,7 @@
if (!String.IsNullOrEmpty(ModifiedBy) || ModifiedOn.HasValue)
{
_text += $"<p style=\"{Style}\">{Localizer["Last modified"]} ";
_text += $"<p style=\"{Style}\">{Localizer["LastModified"]} ";
if (!String.IsNullOrEmpty(ModifiedBy))
{
@ -66,7 +69,7 @@
if (ModifiedOn != null)
{
_text += $" {Localizer["on"]} <b>{ModifiedOn.Value.ToString("MMM dd yyyy HH:mm:ss")}</b>";
_text += $" {Localizer["on"]} <b>{ModifiedOn.Value.ToString(DateTimeFormat)}</b>";
}
_text += "</p>";
@ -78,12 +81,12 @@
if (!String.IsNullOrEmpty(DeletedBy))
{
_text += $" {Localizer["by"]} <b>{DeletedBy}</b>";
_text += $" {Localizer["By"]} <b>{DeletedBy}</b>";
}
if (DeletedOn != null)
{
_text += $" {Localizer["on"]} <b>{DeletedOn.Value.ToString("MMM dd yyyy HH:mm:ss")}</b>";
_text += $" {Localizer["On"]} <b>{DeletedOn.Value.ToString(DateTimeFormat)}</b>";
}
_text += "</p>";

View File

@ -3,61 +3,69 @@
@inject IFolderService FolderService
@inject IFileService FileService
@inject IStringLocalizer<FileManager> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_folders != null)
{
<div id="@Id" class="container-fluid px-0">
<div class="row">
<div class="col">
@if (ShowFolders || FolderId <= 0)
{
<div>
<select class="form-control" value="@FolderId" @onchange="(e => FolderChanged(e))">
@if (string.IsNullOrEmpty(Folder))
{
<option value="-1">&lt;@Localizer["Select Folder"]&gt;</option>
}
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
}
@if (ShowFiles)
{
<div>
<select class="form-control" value="@FileId" @onchange="(e => FileChanged(e))">
<option value="-1">&lt;@Localizer["Select File"]&gt;</option>
@foreach (File file in _files)
{
<option value="@(file.FileId)">@(file.Name)</option>
}
</select>
</div>
}
@if (ShowUpload && _haseditpermission)
{
<div>
@if (UploadMultiple)
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" multiple />
}
else
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" />
}
<span id="@_progressinfoid"></span><progress id="@_progressbarid" style="width: 150px; visibility: hidden;"></progress>
<span class="float-right">
<button type="button" class="btn btn-success" @onclick="UploadFile">@Localizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1)
{
<button type="button" class="btn btn-danger" @onclick="DeleteFile">@Localizer["Delete"]</button>
}
</span>
</div>
}
<ModuleMessage Message="@_message" Type="@_messagetype"></ModuleMessage>
<div class="container-fluid px-0">
@if (ShowFolders)
{
<div class="row">
<div class="col">
<select class="form-select" value="@FolderId" @onchange="(e => FolderChanged(e))">
<option value="-1">&lt;@Localizer["Folder.Select"]&gt;</option>
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
</div>
}
@if (ShowFiles)
{
<div class="row mt-1">
<div class="col">
<select class="form-select" value="@FileId" @onchange="(e => FileChanged(e))">
<option value="-1">&lt;@Localizer["File.Select"]&gt;</option>
@foreach (File file in _files)
{
<option value="@(file.FileId)">@(file.Name)</option>
}
</select>
</div>
</div>
}
@if (ShowUpload && _haseditpermission)
{
<div class="row">
<div class="col mt-2">
@if (UploadMultiple)
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" multiple />
}
else
{
<input type="file" id="@_fileinputid" name="file" accept="@_filter" />
}
</div>
<div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1)
{
<button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button>
}
</div>
</div>
<div class="row">
<div class="col mt-1"><span id="@_progressinfoid" style="display: none;"></span></div>
<div class="col text-center mt-1"><progress id="@_progressbarid" class="mt-1" style="display: none;"></progress></div>
</div>
}
</div>
</div>
@if (_image != string.Empty)
{
@ -66,6 +74,14 @@
</div>
}
</div>
@if (!string.IsNullOrEmpty(_message))
{
<div class="row mt-1">
<div class="col">
<ModuleMessage Message="@_message" Type="@_messagetype" />
</div>
</div>
}
</div>
}
@ -79,6 +95,7 @@
private string _filter = "*";
private bool _haseditpermission = false;
private string _image = string.Empty;
private File _file = null;
private string _guid;
private string _message = string.Empty;
private MessageType _messagetype;
@ -87,10 +104,16 @@
public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility
[Parameter]
public string Folder { get; set; } // optional - for setting a specific folder by default
public int FolderId { get; set; } = -1; // optional - for setting a specific default folder by folderid
[Parameter]
public int FolderId { get; set; } = -1; // optional - for setting a specific folderid by default
public string Folder { get; set; } = ""; // optional - for setting a specific default folder by folder path
[Parameter]
public int FileId { get; set; } = -1; // optional - for selecting a specific file by default
[Parameter]
public string Filter { get; set; } // optional - comma delimited list of file types that can be selected or uploaded ie. "jpg,gif"
[Parameter]
public bool ShowFiles { get; set; } = true; // optional - for indicating whether a list of files should be displayed - default is true
@ -102,14 +125,23 @@
public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true
[Parameter]
public int FileId { get; set; } = -1; // optional - for setting a specific file by default
public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true
[Parameter]
public string Filter { get; set; } // optional - comma delimited list of file types that can be selected or uploaded ie. "jpg,gif"
public bool ShowSuccess { get; set; } = false; // optional - for indicating whether a success message should be displayed upon successful upload - default is false
[Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
[Parameter]
public EventCallback<int> OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected
[Parameter]
public EventCallback<int> OnDelete { get; set; } // optional - executes a method in the calling component when a file is deleted
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Id))
@ -117,14 +149,35 @@
_id = Id;
}
if (!string.IsNullOrEmpty(Folder))
// packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder)
{
_folders = new List<Folder> { new Folder { FolderId = -1, Name = Folder } };
FolderId = -1;
ShowFiles = false;
ShowFolders = false;
Filter = "nupkg";
ShowSuccess = true;
}
else
if (!ShowFiles)
{
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
ShowImage = false;
}
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder)
{
Folder folder = await FolderService.GetFolderAsync(ModuleState.SiteId, Folder);
if (folder != null)
{
FolderId = folder.FolderId;
}
else
{
FolderId = -1;
_message = "Folder Path " + Folder + "Does Not Exist";
_messagetype = MessageType.Error;
}
}
if (FileId != -1)
@ -133,12 +186,16 @@
if (file != null)
{
FolderId = file.FolderId;
await OnSelect.InvokeAsync(FileId);
}
else
{
FileId = -1; // file does not exist
_message = "FileId " + FileId.ToString() + "Does Not Exist";
_messagetype = MessageType.Error;
}
}
await SetImage();
if (!string.IsNullOrEmpty(Filter))
@ -158,10 +215,10 @@
private async Task GetFiles()
{
_haseditpermission = false;
if (!string.IsNullOrEmpty(Folder))
if (Folder == Constants.PackagesFolder)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
_files = await FileService.GetFilesAsync(Folder);
_files = new List<File>();
}
else
{
@ -199,14 +256,14 @@
FolderId = int.Parse((string)e.Value);
await GetFiles();
FileId = -1;
_file = null;
_image = string.Empty;
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Files {Error}", ex.Message);
_message = Localizer["Error Loading Files"];
_message = Localizer["Error.File.Load"];
_messagetype = MessageType.Error;
}
}
@ -215,6 +272,10 @@
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
if (FileId != -1)
{
await OnSelect.InvokeAsync(FileId);
}
await SetImage();
StateHasChanged();
@ -223,21 +284,22 @@
private async Task SetImage()
{
_image = string.Empty;
_file = null;
if (FileId != -1)
{
File file = await FileService.GetFileAsync(FileId);
if (file != null && file.ImageHeight != 0 && file.ImageWidth != 0)
_file = await FileService.GetFileAsync(FileId);
if (_file != null && ShowImage && _file.ImageHeight != 0 && _file.ImageWidth != 0)
{
var maxwidth = 200;
var maxheight = 200;
var ratioX = (double)maxwidth / (double)file.ImageWidth;
var ratioY = (double)maxheight / (double)file.ImageHeight;
var ratioX = (double)maxwidth / (double)_file.ImageWidth;
var ratioY = (double)maxheight / (double)_file.ImageHeight;
var ratio = ratioX < ratioY ? ratioX : ratioY;
_image = "<img src=\"" + ContentUrl(FileId) + "\" alt=\"" + file.Name +
"\" width=\"" + Convert.ToInt32(file.ImageWidth * ratio).ToString() +
"\" height=\"" + Convert.ToInt32(file.ImageHeight * ratio).ToString() + "\" />";
_image = "<img src=\"" + _file.Url + "\" alt=\"" + _file.Name +
"\" width=\"" + Convert.ToInt32(_file.ImageWidth * ratio).ToString() +
"\" height=\"" + Convert.ToInt32(_file.ImageHeight * ratio).ToString() + "\" />";
}
}
}
@ -252,7 +314,7 @@
try
{
string result;
if (!string.IsNullOrEmpty(Folder))
if (Folder == Constants.PackagesFolder)
{
result = await FileService.UploadFilesAsync(Folder, upload, _guid);
}
@ -264,20 +326,20 @@
if (result == string.Empty)
{
await logger.LogInformation("File Upload Succeeded {Files}", upload);
_message = Localizer["File Upload Succeeded"];
_messagetype = MessageType.Success;
await GetFiles();
if (upload.Length == 1)
if (ShowSuccess)
{
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
}
_message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success;
}
// set FileId to first file in upload collection
await GetFiles();
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
StateHasChanged();
}
@ -285,7 +347,7 @@
{
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", "));
_message = Localizer["File Upload Failed"];
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
@ -293,13 +355,13 @@
{
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["File Upload Failed"];
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
else
{
_message = Localizer["You Have Not Selected A File To Upload"];
_message = Localizer["Message.File.NotSelected"];
_messagetype = MessageType.Warning;
}
}
@ -311,8 +373,9 @@
{
await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId);
await OnDelete.InvokeAsync(FileId);
_message = Localizer["File Deleted"];
_message = Localizer["Success.File.Delete"];
_messagetype = MessageType.Success;
await GetFiles();
@ -324,11 +387,14 @@
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
_message = Localizer["Error Deleting File"];
_message = Localizer["Error.File.Delete"];
_messagetype = MessageType.Error;
}
}
public int GetFileId() => FileId;
public int GetFolderId() => FolderId;
public File GetFile() => _file;
}

View File

@ -3,17 +3,16 @@
@if (!string.IsNullOrEmpty(HelpText))
{
<span class="app-tooltip" data-tip="@((MarkupString)HelpText)">@((MarkupString)_openLabel)@ChildContent@((MarkupString)_closeLabel) <img src="images/help.png" /></span>
<span class="@_spanclass" data-tip="@((MarkupString)@_helptext)">
<label for="@For" class="@_labelclass">@ChildContent</label> <img src="images/help.png" />
</span>
}
else
{
@((MarkupString)_openLabel)@ChildContent@((MarkupString)_closeLabel)
<label for="@For" class="@_labelclass">@ChildContent</label>
}
@code {
private string _openLabel = string.Empty;
private string _closeLabel = "</label>";
[Parameter]
public RenderFragment ChildContent { get; set; }
@ -21,34 +20,37 @@ else
public string For { get; set; } // optional - the id of the associated input control for accessibility
[Parameter]
public string Class { get; set; } // optional - the class for the label ( ie. control-label )
public string Class { get; set; } // optional - CSS classes
[Parameter]
public string HelpText { get; set; } // optional - tooltip for this label
private string _spanclass;
private string _labelclass;
private string _helptext = string.Empty;
protected override void OnParametersSet()
{
base.OnParametersSet();
_openLabel = "<label";
if (!string.IsNullOrEmpty(For))
if (!string.IsNullOrEmpty(HelpText))
{
_openLabel += " for=\"" + For + "\"";
}
_helptext = Localize(nameof(HelpText), HelpText);
_labelclass = "form-label";
if (!string.IsNullOrEmpty(Class))
var spanclass = (!string.IsNullOrEmpty(Class)) ? " " + Class : "";
_spanclass = "app-tooltip" + spanclass;
}
else
{
_openLabel += " class=\"" + Class + "\"";
var labelclass = (!string.IsNullOrEmpty(Class)) ? " " + Class : "";
_labelclass = "form-label" + labelclass;
}
_openLabel += ">";
var text = Localize("Text", String.Empty);
if (text != String.Empty)
if (!string.IsNullOrEmpty(text))
{
ChildContent =@<text>@text</text>;
}
HelpText = Localize(nameof(HelpText), HelpText);
}
}

View File

@ -13,6 +13,9 @@ namespace Oqtane.Modules.Controls
[Parameter]
public string ResourceKey { get; set; }
[Parameter]
public string ResourceType { get; set; }
protected bool IsLocalizable { get; private set; }
protected string Localize(string name) => _localizer?[name] ?? name;
@ -50,9 +53,14 @@ namespace Oqtane.Modules.Controls
{
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)
{
using (var scope = ServiceActivator.GetScope())

View File

@ -4,14 +4,14 @@
@if (!string.IsNullOrEmpty(_message))
{
<div class="@_classname" role="alert">
<div class="@_classname alert-dismissible fade show mb-3" role="alert">
@((MarkupString)_message)
@if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@((MarkupString)"&nbsp;&nbsp;")<NavLink href="@NavigateUrl("admin/log")">View Details</NavLink>
}
<button type="button" class="btn-close" aria-label="Close" @onclick="DismissModal"></button>
</div>
<br />
}
@code {
@ -54,4 +54,10 @@
return classname;
}
private void DismissModal()
{
_message = "";
StateHasChanged();
}
}

View File

@ -2,149 +2,216 @@
@inherits ModuleControlBase
@typeparam TableItem
<p>
@if (Toolbar == "Top")
@if (ItemList != null)
{
@if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{
<div class="mx-auto text-center">
@if (_endPage > 1)
<ul class="pagination justify-content-center my-2">
<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>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="first" aria-hidden="true"></span></button>
<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>
</li>
}
@if (_page > _maxPages)
<li class="page-item@((_page > 1) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li>
@for (int i = _startPage; i <= _endPage; i++)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => SetPagerSize("back"))><span class="oi oi-media-skip-backward" title="back" aria-hidden="true"></span></button>
}
@if (_endPage > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></button>
@for (int i = _startPage; i <= _endPage; i++)
var pager = i;
if (pager == _page)
{
var pager = i;
<button class="btn @((pager == _page) ? "btn-primary" : "btn-link")" @onclick=@(async () => UpdateList(pager))>
@pager
</button>
<li class="page-item active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li>
}
<button class="btn btn-secondary mr-1" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></button>
}
@if (_endPage < _pages)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => SetPagerSize("forward"))><span class="oi oi-media-skip-forward" title="forward" aria-hidden="true"></span></button>
}
@if (_endPage > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="last" aria-hidden="true"></span></button>
}
@if (_endPage > 1)
{
<span class="btn btn-link disabled">Page @_page of @_pages</span>
}
</div>
}
@if (Format == "Table")
{
<table class="@Class">
<thead>
<tr>@Header</tr>
</thead>
<tbody>
@foreach (var item in ItemList)
else
{
<tr>@Row(item)</tr>
@if (Detail != null)
{
<tr>@Detail(item)</tr>
}
<li class="page-item">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li>
}
</tbody>
</table>
}
<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>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<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>
</li>
}
<li class="page-item@((_page < _pages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li>
<li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">Page @_page of @_pages</a>
</li>
</ul>
}
@if (Format == "Grid")
@if (Format == "Table" && Row != null)
{
<div class="table-responsive">
<table class="@Class">
<thead>
<tr class="@RowClass">@Header</tr>
</thead>
<tbody>
@foreach (var item in ItemList)
{
<tr class="@RowClass">@Row(item)</tr>
@if (Detail != null)
{
<tr>@Detail(item)</tr>
}
}
</tbody>
</table>
</div>
}
@if (Format == "Grid" && Row != null)
{
int count = 0;
int rows = 0;
int cols = 0;
if (ItemList != null)
{
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="row">@Header</div>
@foreach (var item in ItemList)
@for (int row = 0; row < rows; row++)
{
<div class="row">@Row(item)</div>
@if (Detail != null)
{
<div class="row">@Detail(item)</div>
}
<div class="@RowClass">
@for (int col = 0; col < cols; col++)
{
int index = (row * _columns) + col;
if (index < ItemList.Count())
{
<div class="@ColumnClass">@Row(ItemList.ElementAt(index))</div>
}
else
{
<div>&nbsp;</div>
}
}
</div>
}
</div>
}
@if (Toolbar == "Bottom")
@if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{
<div class="mx-auto text-center">
@if (_endPage > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="first" aria-hidden="true"></span></button>
<ul class="pagination justify-content-center my-2">
<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>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<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>
</li>
}
@if (_page > _maxPages)
<li class="page-item@((_page > 1) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li>
@for (int i = _startPage; i <= _endPage; i++)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => SetPagerSize("back"))><span class="oi oi-media-skip-backward" title="back" aria-hidden="true"></span></button>
}
@if (_endPage > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></button>
@for (int i = _startPage; i <= _endPage; i++)
var pager = i;
if (pager == _page)
{
var pager = i;
<button class="btn @((pager == _page) ? "btn-primary" : "btn-link")" @onclick=@(async () => UpdateList(pager))>
@pager
</button>
<li class="page-item active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li>
}
else
{
<li class="page-item">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li>
}
<button class="btn btn-secondary mr-1" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></button>
}
@if (_endPage < _pages)
<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>
</li>
@if (_pages > _displayPages && _displayPages > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => SetPagerSize("forward"))><span class="oi oi-media-skip-forward" title="forward" aria-hidden="true"></span></button>
<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>
</li>
}
@if (_endPage > 1)
{
<button class="btn btn-secondary mr-1" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="last" aria-hidden="true"></span></button>
}
@if (_endPage > 1)
{
<span class="btn btn-link disabled">Page @_page of @_pages</span>
}
</div>
<li class="page-item@((_page < _pages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li>
<li class="page-item disabled">
<a class="page-link" style="white-space: nowrap;">Page @_page of @_pages</a>
</li>
</ul>
}
</p>
}
@code {
private int _pages = 0;
private int _page = 1;
private int _maxItems = 10;
private int _maxPages = 5;
private int _displayPages = 5;
private int _startPage = 0;
private int _endPage = 0;
private int _columns = 0;
[Parameter]
public string Format { get; set; }
public string Format { get; set; } // Table or Grid
[Parameter]
public string Toolbar { get; set; }
public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter]
public RenderFragment Header { get; set; }
public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
[Parameter]
public RenderFragment<TableItem> Row { get; set; }
public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter]
public RenderFragment<TableItem> Detail { get; set; }
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter]
public IEnumerable<TableItem> Items { get; set; }
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter]
public string PageSize { get; set; }
public string PageSize { get; set; } // number of items to display on a page
[Parameter]
public string DisplayPages { get; set; }
public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter]
public string Class { get; set; }
public string CurrentPage { get; set; } // sets the initial page to display
[Parameter]
public string DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter]
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; }
@ -168,95 +235,123 @@
}
else
{
Class = "container";
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);
}
if (!string.IsNullOrEmpty(DisplayPages))
if (!string.IsNullOrEmpty(Columns))
{
_maxPages = int.Parse(DisplayPages);
_columns = int.Parse(Columns);
}
if (!string.IsNullOrEmpty(DisplayPages))
{
_displayPages = int.Parse(DisplayPages);
}
if (!string.IsNullOrEmpty(CurrentPage))
{
_page = int.Parse(CurrentPage);
}
else
{
_page = 1;
}
_page = 1;
_startPage = 0;
_endPage = 0;
if (Items != null)
{
ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
_pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems);
if (_page > _pages)
{
_page = _pages;
}
ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
SetPagerSize();
}
SetPagerSize("forward");
}
public void UpdateList(int currentPage)
public void SetPagerSize()
{
ItemList = Items.Skip((currentPage - 1) * _maxItems).Take(_maxItems);
_page = currentPage;
_startPage = ((_page - 1) / _displayPages) * _displayPages + 1;
_endPage = _startPage + _displayPages - 1;
if (_endPage > _pages)
{
_endPage = _pages;
}
OnPageChange?.Invoke(_page);
StateHasChanged();
}
public void SetPagerSize(string direction)
public void UpdateList(int page)
{
if (direction == "forward")
{
if (_endPage + 1 < _pages)
{
_startPage = _endPage + 1;
}
else
{
_startPage = 1;
}
ItemList = Items.Skip((page - 1) * _maxItems).Take(_maxItems);
_page = page;
SetPagerSize();
}
if (_endPage + _maxPages < _pages)
{
_endPage = _startPage + _maxPages - 1;
}
else
{
_endPage = _pages;
}
StateHasChanged();
}
else if (direction == "back")
public void SkipPages(string direction)
{
switch (direction)
{
_endPage = _startPage - 1;
_startPage = _startPage - _maxPages;
case "forward":
_page = _endPage + 1;
break;
case "back":
_page = _startPage - 1;
break;
}
SetPagerSize();
}
public void NavigateToPage(string direction)
{
if (direction == "next")
switch (direction)
{
if (_page < _pages)
{
if (_page == _endPage)
case "next":
if (_page < _pages)
{
SetPagerSize("forward");
_page += 1;
}
_page += 1;
}
}
else if (direction == "previous")
{
if (_page > 1)
{
if (_page == _startPage)
break;
case "previous":
if (_page > 1)
{
SetPagerSize("back");
_page -= 1;
}
_page -= 1;
}
break;
}
UpdateList(_page);

View File

@ -2,234 +2,270 @@
@inherits ModuleControlBase
@inject IRoleService RoleService
@inject IUserService UserService
@inject IStringLocalizer<PermissionGrid> Localizer
@inject IStringLocalizer<PermissionGrid> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_permissions != null)
{
<br />
<table class="table" style="width: 50%; min-width: 250px;">
<tbody>
<tr>
<th scope="col">@Localizer["Role"]</th>
@foreach (PermissionString permission in _permissions)
{
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th>
}
</tr>
@foreach (Role role in _roles)
{
<tr>
<td>@role.Name</td>
@foreach (PermissionString permission in _permissions)
{
var p = permission;
<td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled=@GetPermissionDisabled(role.Name) OnChange="@(e => PermissionChanged(e, p.PermissionName, role.Name))" />
</td>
}
</tr>
}
</tbody>
</table>
@if (_users.Count != 0)
{
<table class="table" style="width: 50%; min-width: 250px;">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (PermissionString permission in _permissions)
{
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
{
string userid = "[" + user.UserId.ToString() + "]";
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<td>@user.DisplayName</td>
<th scope="col">@Localizer["Role"]</th>
@foreach (PermissionString permission in _permissions)
{
var p = permission;
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled=false OnChange="@(e => PermissionChanged(e, p.PermissionName, userid))" />
</td>
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th>
}
</tr>
}
</tbody>
</table>
}
<table class="table" style="width: 50%; min-width: 250px;">
<tbody>
<tr>
<td class="input-group">
<input type="text" name="Username" class="form-control" placeholder="@Localizer["Enter Username"]" @bind="@_username" />
<button type="button" class="btn btn-primary" @onclick="AddUser">@Localizer["Add"]</button>
</td>
</tr>
</tbody>
</table>
<br />
<ModuleMessage Type="MessageType.Error" Message="@_message" />
@foreach (Role role in _roles)
{
<tr>
<td>@role.Name</td>
@foreach (PermissionString permission in _permissions)
{
var p = permission;
<td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled=@GetPermissionDisabled(role.Name) OnChange="@(e => PermissionChanged(e, p.PermissionName, role.Name))" />
</td>
}
</tr>
}
</tbody>
</table>
<br />
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (PermissionString permission in _permissions)
{
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
{
string userid = "[" + user.UserId.ToString() + "]";
<tr>
<td>@user.DisplayName</td>
@foreach (PermissionString permission in _permissions)
{
var p = permission;
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled=false OnChange="@(e => PermissionChanged(e, p.PermissionName, userid))" />
</td>
}
</tr>
}
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<td class="input-group">
<input type="text" name="Username" class="form-control" placeholder="@Localizer["Username.Enter"]" @bind="@_username" />
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</td>
</tr>
</tbody>
</table>
<br />
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Error" Message="@_message" />
</div>
</div>
</div>
}
@code {
private string _permissionnames = string.Empty;
private List<Role> _roles;
private List<PermissionString> _permissions;
private List<User> _users = new List<User>();
private string _username = string.Empty;
private string _message = string.Empty;
private string _permissionnames = string.Empty;
private List<Role> _roles;
private List<PermissionString> _permissions;
private List<User> _users = new List<User>();
private string _username = string.Empty;
private string _message = string.Empty;
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string Permissions { get; set; }
[Parameter]
public string Permissions { get; set; }
protected override async Task OnInitializedAsync()
{
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = Shared.PermissionNames.View + "," + Shared.PermissionNames.Edit;
}
else
{
_permissionnames = PermissionNames;
}
protected override async Task OnInitializedAsync()
{
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = Shared.PermissionNames.View + "," + Shared.PermissionNames.Edit;
}
else
{
_permissionnames = PermissionNames;
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId);
_roles.Insert(0, new Role { Name = RoleNames.Everyone });
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId);
_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))
{
// initialize with admin role
_permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin });
}
foreach (string permissionname in _permissionnames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
// initialize with admin role
_permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin });
}
if (!string.IsNullOrEmpty(Permissions))
{
// populate permissions
foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions))
{
if (_permissions.Find(item => item.PermissionName == permissionstring.PermissionName) != null)
{
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionstring.PermissionName)].Permissions = permissionstring.Permissions;
}
if (!string.IsNullOrEmpty(Permissions))
{
// populate permissions
foreach (PermissionString permissionstring in UserSecurity.GetPermissionStrings(Permissions))
{
if (_permissions.Find(item => item.PermissionName == permissionstring.PermissionName) != null)
{
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionstring.PermissionName)].Permissions = permissionstring.Permissions;
}
if (permissionstring.Permissions.Contains("["))
{
foreach (string user in permissionstring.Permissions.Split(new char[] { '[' }, StringSplitOptions.RemoveEmptyEntries))
{
if (user.Contains("]"))
{
var userid = int.Parse(user.Substring(0, user.IndexOf("]")));
if (_users.Where(item => item.UserId == userid).FirstOrDefault() == null)
{
_users.Add(await UserService.GetUserAsync(userid, ModuleState.SiteId));
}
}
}
}
}
}
}
if (permissionstring.Permissions.Contains("["))
{
foreach (string user in permissionstring.Permissions.Split(new char[] { '[' }, StringSplitOptions.RemoveEmptyEntries))
{
if (user.Contains("]"))
{
var userid = int.Parse(user.Substring(0, user.IndexOf("]")));
if (_users.Where(item => item.UserId == userid).FirstOrDefault() == null)
{
_users.Add(await UserService.GetUserAsync(userid, ModuleState.SiteId));
}
}
}
}
}
}
}
private bool? GetPermissionValue(string permissions, string securityKey)
{
if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";"))
{
return false; // deny permission
}
else
{
if ((";" + permissions + ";").Contains(";" + securityKey + ";"))
{
return true; // grant permission
}
else
{
return null; // not specified
}
}
}
private bool? GetPermissionValue(string permissions, string securityKey)
{
if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";"))
{
return false; // deny permission
}
else
{
if ((";" + permissions + ";").Contains(";" + securityKey + ";"))
{
return true; // grant permission
}
else
{
return null; // not specified
}
}
}
private bool GetPermissionDisabled(string roleName)
=> roleName == RoleNames.Admin
? true
: false;
private bool GetPermissionDisabled(string roleName)
=> (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) ? true : false;
private async Task AddUser()
{
if (_users.Where(item => item.Username == _username).FirstOrDefault() == null)
{
try
{
var user = await UserService.GetUserAsync(_username, ModuleState.SiteId);
if (user != null)
{
_users.Add(user);
}
}
catch
{
_message = Localizer["Username Does Not Exist"];
}
}
private async Task AddUser()
{
if (_users.Where(item => item.Username == _username).FirstOrDefault() == null)
{
try
{
var user = await UserService.GetUserAsync(_username, ModuleState.SiteId);
if (user != null)
{
_users.Add(user);
}
}
catch
{
_message = Localizer["Message.Username.DontExist"];
}
}
_username = string.Empty;
}
_username = string.Empty;
}
private void PermissionChanged(bool? value, string permissionName, string securityId)
{
var selected = value;
var permission = _permissions.Find(item => item.PermissionName == permissionName);
if (permission != null)
{
var ids = permission.Permissions.Split(';').ToList();
private void PermissionChanged(bool? value, string permissionName, string securityId)
{
var selected = value;
var permission = _permissions.Find(item => item.PermissionName == permissionName);
if (permission != null)
{
var ids = permission.Permissions.Split(';').ToList();
ids.Remove(securityId); // remove grant permission
ids.Remove("!" + securityId); // remove deny permission
ids.Remove(securityId); // remove grant permission
ids.Remove("!" + securityId); // remove deny permission
switch (selected)
{
case true:
ids.Add(securityId); // add grant permission
break;
case false:
ids.Add("!" + securityId); // add deny permission
break;
case null:
break; // permission not specified
}
switch (selected)
{
case true:
ids.Add(securityId); // add grant permission
break;
case false:
ids.Add("!" + securityId); // add deny permission
break;
case null:
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()
{
ValidatePermissions();
return UserSecurity.SetPermissionStrings(_permissions);
}
public string GetPermissions()
{
ValidatePermissions();
return UserSecurity.SetPermissionStrings(_permissions);
}
private void ValidatePermissions()
{
PermissionString permission;
for (int i = 0; i < _permissions.Count; i++)
{
permission = _permissions[i];
List<string> ids = permission.Permissions.Split(';').ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Registered); // remove deny registered users
permission.Permissions = string.Join(";", ids.ToArray());
private void ValidatePermissions()
{
PermissionString permission;
for (int i = 0; i < _permissions.Count; i++)
{
permission = _permissions[i];
List<string> ids = permission.Permissions.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList();
ids.Remove("!" + RoleNames.Everyone); // remove deny all users
ids.Remove("!" + RoleNames.Registered); // remove deny registered users
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;
}
}

View File

@ -14,9 +14,9 @@
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="row justify-content-center" style="margin-bottom: 20px;">
<button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["Synchronize Content"]</button>&nbsp;&nbsp;
<button type="button" class="btn btn-primary" @onclick="InsertImage">@Localizer["Insert Image"]</button>
<div class="d-flex justify-content-center mb-2">
<button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["SynchronizeContent"]</button>&nbsp;&nbsp;
<button type="button" class="btn btn-primary" @onclick="InsertImage">@Localizer["InsertImage"]</button>
@if (_filemanagervisible)
{
@((MarkupString)"&nbsp;&nbsp;")
@ -66,16 +66,16 @@
</div>
</TabPanel>
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
<div class="row justify-content-center" style="margin-bottom: 20px;">
<button type="button" class="btn btn-secondary" @onclick="RefreshRawHtml">@Localizer["Synchronize Content"]</button>
<div class="d-flex justify-content-center mb-2">
<button type="button" class="btn btn-secondary" @onclick="RefreshRawHtml">@Localizer["SynchronizeContent"]</button>
</div>
@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
{
<textarea class="form-control" placeholder="@Placeholder" @bind="@_content" rows="10"></textarea>
<textarea class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
</TabStrip>
@ -83,107 +83,121 @@
</div>
@code {
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _filemanagervisible = false;
private FileManager _fileManager;
private string _content = string.Empty;
private string _original = string.Empty;
private string _message = string.Empty;
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _filemanagervisible = false;
private FileManager _fileManager;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
[Parameter]
public string Content { get; set; }
[Parameter]
public string Content { get; set; }
[Parameter]
public bool ReadOnly { get; set; } = false;
[Parameter]
public bool ReadOnly { get; set; } = false;
[Parameter]
public string Placeholder { get; set; } = "Enter Your Content...";
[Parameter]
public string Placeholder { get; set; } = "Enter Your Content...";
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public string DebugLevel { get; set; } = "info";
[Parameter]
public string DebugLevel { get; set; } = "info";
[Parameter]
public bool AllowFileManagement { get; set; } = true;
[Parameter]
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-interop.js" }
};
protected override void OnInitialized()
{
_content = Content; // raw HTML
}
protected override void OnParametersSet()
{
_richhtml = Content;
_rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await base.OnAfterRenderAsync(firstRender);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
var interop = new RichTextEditorInterop(JSRuntime);
var interop = new RichTextEditorInterop(JSRuntime);
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
if (firstRender)
{
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
await interop.LoadEditorContent(_editorElement, Content);
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 )
_original = await interop.GetHtml(_editorElement);
}
}
// 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);
}
}
public void CloseFileManager()
{
_filemanagervisible = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseFileManager()
{
_filemanagervisible = false;
_message = string.Empty;
StateHasChanged();
}
public async Task RefreshRichText()
{
var interop = new RichTextEditorInterop(JSRuntime);
await interop.LoadEditorContent(_editorElement, _content);
}
public void RefreshRichText()
{
_richhtml = _rawhtml;
StateHasChanged();
}
public async Task RefreshRawHtml()
{
var interop = new RichTextEditorInterop(JSRuntime);
_content = await interop.GetHtml(_editorElement);
StateHasChanged();
}
public async Task RefreshRawHtml()
{
var interop = new RichTextEditorInterop(JSRuntime);
_rawhtml = await interop.GetHtml(_editorElement);
StateHasChanged();
}
public async Task<string> GetHtml()
{
// get rich text content
var interop = new RichTextEditorInterop(JSRuntime);
string content = await interop.GetHtml(_editorElement);
if (_original != content)
{
// rich text content has changed - return it
return content;
}
else
{
// return raw html content
return _content;
}
public async Task<string> GetHtml()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
// return rich text content if it has changed
var interop = new RichTextEditorInterop(JSRuntime);
var richhtml = await interop.GetHtml(_editorElement);
if (richhtml != _originalrichhtml)
{
return richhtml;
}
else
{
// return original raw html content
return _originalrawhtml;
}
}
}
public async Task InsertImage()
@ -191,16 +205,16 @@
_message = string.Empty;
if (_filemanagervisible)
{
var fileid = _fileManager.GetFileId();
if (fileid != -1)
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new RichTextEditorInterop(JSRuntime);
await interop.InsertImage(_editorElement, ContentUrl(fileid));
await interop.InsertImage(_editorElement, file.Url, file.Name);
_filemanagervisible = false;
}
else
{
_message = Localizer["You Must Select An Image To Insert"];
_message = Localizer["Message.Require.Image"];
}
}
else
@ -209,23 +223,4 @@
}
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

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Threading.Tasks;
@ -105,13 +105,13 @@ namespace Oqtane.Modules.Controls
}
}
public Task InsertImage(ElementReference quillElement, string imageUrl)
public Task InsertImage(ElementReference quillElement, string imageUrl, string altText)
{
try
{
_jsRuntime.InvokeAsync<object>(
"Oqtane.RichTextEditor.insertQuillImage",
quillElement, imageUrl);
quillElement, imageUrl, altText);
return Task.CompletedTask;
}
catch

View File

@ -1,14 +1,14 @@
@namespace Oqtane.Modules.Controls
@inherits LocalizableComponent
<div class="d-flex">
<div class="d-flex mt-2">
<div>
<a data-toggle="collapse" class="app-link-unstyled" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<a data-bs-toggle="collapse" class="app-link-unstyled" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<h5>@_heading</h5>
</a>
</div>
<div class="ml-auto">
<a data-toggle="collapse" class="app-link-unstyled float-right" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<div class="ms-auto">
<a data-bs-toggle="collapse" class="app-link-unstyled float-right" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<i class="oi oi-chevron-bottom"></i>&nbsp;
</a>
</div>

View File

@ -38,7 +38,7 @@ else
if (string.IsNullOrEmpty(Heading))
{
Name = Localize(nameof(Name), Name);
Heading = Localize(nameof(Name), Name);
}
else
{

View File

@ -7,23 +7,20 @@
<ul class="nav nav-tabs" role="tablist">
@foreach (TabPanel tabPanel in _tabPanels)
{
@if (IsAuthorized(tabPanel))
{
<li class="nav-item" @key="tabPanel.Name">
@if (tabPanel.Name == ActiveTab)
{
<a class="nav-link active" data-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()
</a>
}
else
{
<a class="nav-link" data-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()
</a>
}
</li>
}
<li class="nav-item" @key="tabPanel.Name">
@if (tabPanel.Name == ActiveTab)
{
<a class="nav-link active" data-bs-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()
</a>
}
else
{
<a class="nav-link" data-bs-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading()
</a>
}
</li>
}
</ul>
<div class="tab-content">
@ -43,30 +40,32 @@
[Parameter]
public string ActiveTab { get; set; } // optional - defaults to first TabPanel if not specified. Can also be set using a "tab=" querystring parameter.
protected override void OnInitialized()
[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.
protected override void OnParametersSet()
{
if (PageState.QueryString.ContainsKey("tab"))
{
ActiveTab = PageState.QueryString["tab"];
}
}
protected override void OnParametersSet()
{
_tabPanels = new List<TabPanel>();
if (_tabPanels == null || Refresh)
{
_tabPanels = new List<TabPanel>();
}
}
internal void AddTabPanel(TabPanel tabPanel)
{
if (!_tabPanels.Exists(item => item.Name == tabPanel.Name))
if (!_tabPanels.Exists(item => item.Name == tabPanel.Name) && IsAuthorized(tabPanel))
{
_tabPanels.Add(tabPanel);
if (string.IsNullOrEmpty(ActiveTab))
{
ActiveTab = tabPanel.Name;
}
StateHasChanged();
}
if (string.IsNullOrEmpty(ActiveTab))
{
ActiveTab = tabPanel.Name;
}
}
private bool IsAuthorized(TabPanel tabPanel)

View File

@ -52,11 +52,11 @@
{
case true:
_src = "images/checked.png";
_title = Localizer["Permission Granted"];
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["Permission Denied"];
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";

View File

@ -7,96 +7,188 @@
@inject ISettingService SettingService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_content != null)
{
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" @ref="@RichTextEditorHtml"></RichTextEditor>
<button type="button" class="btn btn-success" @onclick="SaveContent">@Localizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@Localizer["Cancel"]</NavLink>
@if (!string.IsNullOrEmpty(_content))
{
<br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon"></AuditInfo>
}
}
<TabStrip>
<TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit">
@if (_content != null)
{
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" @ref="@RichTextEditorHtml"></RichTextEditor>
<br />
<button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@if (!string.IsNullOrEmpty(_content))
{
<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 {
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 = "css/quill/quill1.3.6.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill1.3.6.snow.css" }
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
};
private RichTextEditor RichTextEditorHtml;
private bool _allowfilemanagement;
private string _content = null;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private RichTextEditor RichTextEditorHtml;
private bool _allowfilemanagement;
private string _content = null;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private List<Models.HtmlText> _htmltexts;
private string _view = "";
protected override async Task OnInitializedAsync()
{
try
{
_allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"));
protected override async Task OnInitializedAsync()
{
try
{
_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);
if (htmltext != null)
{
_content = htmltext.Content;
_content = _content.Replace(Constants.ContentUrl, "/" + PageState.Alias.AliasId.ToString() + Constants.ContentUrl);
_createdby = htmltext.CreatedBy;
_createdon = htmltext.CreatedOn;
_modifiedby = htmltext.ModifiedBy;
_modifiedon = htmltext.ModifiedOn;
}
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 LoadContent()
{
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
_content = htmltext.Content;
_content = Utilities.FormatContent(_content, PageState.Alias, "render");
_createdby = htmltext.CreatedBy;
_createdon = htmltext.CreatedOn;
_modifiedby = htmltext.ModifiedBy;
_modifiedon = htmltext.ModifiedOn;
}
else
{
_content = string.Empty;
}
private async Task SaveContent()
{
string content = await RichTextEditorHtml.GetHtml();
content = content.Replace("/" + PageState.Alias.AliasId.ToString() + Constants.ContentUrl, Constants.ContentUrl);
_htmltexts = await HtmlTextService.GetHtmlTextsAsync(ModuleState.ModuleId);
_htmltexts = _htmltexts.OrderByDescending(item => item.CreatedOn).ToList();
try
{
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
htmltext.Content = content;
await HtmlTextService.UpdateHtmlTextAsync(htmltext);
}
else
{
htmltext = new HtmlTextInfo();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
}
_view = "";
}
await logger.LogInformation("Html/Text Content Saved {HtmlText}", htmltext);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error Saving Content"], MessageType.Error);
}
}
private async Task SaveContent()
{
string content = await RichTextEditorHtml.GetHtml();
content = Utilities.FormatContent(content, PageState.Alias, "save");
try
{
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

@ -2,37 +2,41 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject IHtmlTextService HtmlTextService
@inject IStringLocalizer<Index> Localizer
@((MarkupString)content)
@if (PageState.EditMode)
{
<br /><ActionLink Action="Edit" EditMode="true" ResourceKey="Edit" /><br /><br />
<br />
<ActionLink Action="Edit" EditMode="true" ResourceKey="Edit" />
<br />
<br />
}
@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" }
};
private string content = "";
private string content = "";
protected override async Task OnParametersSetAsync()
{
try
protected override async Task OnParametersSetAsync()
{
try
{
var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId);
if (htmltext != null)
{
content = htmltext.Content;
content = content.Replace(Constants.ContentUrl, "/" + PageState.Alias.AliasId.ToString() + Constants.ContentUrl);
content = Utilities.FormatContent(content, PageState.Alias, "render");
}
}
catch (Exception ex)
{
await logger.LogError(ex, "An Error Occurred Loading Html/Text Content. " + ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
await logger.LogError(ex, "Error Loading Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.Load"], MessageType.Error);
}
}
}

View File

@ -1,16 +1,18 @@
using Oqtane.Documentation;
using Oqtane.Models;
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 ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "HtmlText",
Description = "Renders HTML or Text Content",
Version = "1.0.0",
Version = "1.0.1",
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
ReleaseVersions = "1.0.0",
ReleaseVersions = "1.0.0,1.0.1",
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"
};
}

View File

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

View File

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

View File

@ -3,22 +3,22 @@
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<table class="table table-borderless">
<tr>
<td>
<Label For="files" ResourceKey="Allow File Management" HelpText="Specify If Editors Can Upload and Select Files">Allow File Management: </Label>
</td>
<td>
<select id="files" class="form-control" @bind="@_allowfilemanagement">
<option value="true">@Localizer["Yes"]</option>
<option value="false">@Localizer["No"]</option>
<div class="container">
<div class="row mb-1 align-items-center">
<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">
<select id="files" class="form-select" @bind="@_allowfilemanagement">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</td>
</tr>
</table>
</div>
</div>
</div>
@code {
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private string _allowfilemanagement;
protected override void OnInitialized()
@ -37,7 +37,7 @@
{
try
{
var settings = ModuleState.Settings;
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}

View File

@ -30,8 +30,8 @@ namespace Oqtane.Modules
[CascadingParameter]
protected Module ModuleState { get; set; }
[CascadingParameter]
protected ModuleInstance ModuleInstance { get; set; }
[Parameter]
public ModuleInstance ModuleInstance { get; set; }
// optional interface properties
public virtual SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.View; } set { } } // default security
@ -53,9 +53,9 @@ namespace Oqtane.Modules
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{
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())
{
@ -84,11 +84,21 @@ namespace Oqtane.Modules
return NavigateUrl(path, "");
}
public string NavigateUrl(bool refresh)
{
return NavigateUrl(PageState.Page.Path, refresh);
}
public string NavigateUrl(string path, string parameters)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, parameters);
}
public string NavigateUrl(string path, bool refresh)
{
return Utilities.NavigateUrl(PageState.Alias.Path, path, refresh ? "refresh" : "");
}
public string EditUrl(string action)
{
return EditUrl(ModuleState.ModuleId, action);
@ -124,6 +134,21 @@ namespace Oqtane.Modules
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)
{
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 = "")
{
var urlParameters = new Dictionary<string, string>();
@ -195,6 +220,38 @@ namespace Oqtane.Modules
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{
LogFunction logFunction;
if (string.IsNullOrEmpty(function))
{
// try to infer from page action
function = PageState.Action;
}
if (!Enum.TryParse(function, out logFunction))
{
switch (function.ToLower())
{
case "add":
logFunction = LogFunction.Create;
break;
case "edit":
logFunction = LogFunction.Update;
break;
case "delete":
logFunction = LogFunction.Delete;
break;
case "":
logFunction = LogFunction.Read;
break;
default:
logFunction = LogFunction.Other;
break;
}
}
await Log(alias, level, logFunction, exception, message, args);
}
public async Task Log(Alias alias, LogLevel level, LogFunction function, Exception exception, string message, params object[] args)
{
int pageId = ModuleState.PageId;
int moduleId = ModuleState.ModuleId;
@ -205,34 +262,8 @@ namespace Oqtane.Modules
}
string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1);
LogFunction logFunction;
if (string.IsNullOrEmpty(function))
{
function = PageState.Action;
}
switch (function.ToLower())
{
case "add":
logFunction = LogFunction.Create;
break;
case "edit":
logFunction = LogFunction.Update;
break;
case "delete":
logFunction = LogFunction.Delete;
break;
default:
logFunction = LogFunction.Read;
break;
}
if (feature == "Login")
{
logFunction = LogFunction.Security;
}
await LoggingService.Log(alias, pageId, moduleId, userId, category, feature, logFunction, level, exception, message, args);
await LoggingService.Log(alias, pageId, moduleId, userId, category, feature, function, level, exception, message, args);
}
public class Logger
@ -249,6 +280,11 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Trace, "", null, message, args);
}
public async Task LogTrace(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, function, null, message, args);
}
public async Task LogTrace(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Trace, "", exception, message, args);
@ -259,6 +295,11 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Debug, "", null, message, args);
}
public async Task LogDebug(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, function, null, message, args);
}
public async Task LogDebug(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Debug, "", exception, message, args);
@ -269,6 +310,11 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Information, "", null, message, args);
}
public async Task LogInformation(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, function, null, message, args);
}
public async Task LogInformation(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Information, "", exception, message, args);
@ -279,6 +325,11 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Warning, "", null, message, args);
}
public async Task LogWarning(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, function, null, message, args);
}
public async Task LogWarning(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Warning, "", exception, message, args);
@ -289,6 +340,11 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Error, "", null, message, args);
}
public async Task LogError(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, function, null, message, args);
}
public async Task LogError(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Error, "", exception, message, args);
@ -299,6 +355,11 @@ namespace Oqtane.Modules
await _moduleBase.Log(null, LogLevel.Critical, "", null, message, args);
}
public async Task LogCritical(LogFunction function, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, function, null, message, args);
}
public async Task LogCritical(Exception exception, string message, params object[] args)
{
await _moduleBase.Log(null, LogLevel.Critical, "", exception, message, args);

View File

@ -1,35 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations>
<Version>2.0.2</Version>
<Version>3.1.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
<Description>Modular Application Framework for Blazor</Description>
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<RepositoryUrl>https://github.com/oqtane</RepositoryUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v2.0.2</PackageReleaseNotes>
<RootNamespace>Oqtane</RootNamespace>
<IsPackable>true</IsPackable>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="5.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="5.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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<TrimmerRootAssembly Include="System.Runtime" />
<TrimmerRootAssembly Include="System.Linq.Parallel" />
<TrimmerRootAssembly Include="System.Runtime.CompilerServices.VisualC" />
</ItemGroup>
<PropertyGroup>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -8,105 +8,55 @@ using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Oqtane.Documentation;
using Oqtane.Modules;
using Oqtane.Providers;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Client
{
[PrivateApi("Mark Entry-Program as private, since it's not very useful in the public docs")]
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
HttpClient httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
builder.Services.AddSingleton(httpClient);
var httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)};
builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase
builder.Services.AddOptions();
// Register localization services
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
// register auth services
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<IdentityAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<IdentityAuthenticationStateProvider>());
builder.Services.AddOqtaneAuthorization();
// register scoped core services
builder.Services.AddScoped<SiteState>();
builder.Services.AddScoped<IInstallationService, InstallationService>();
builder.Services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>();
builder.Services.AddScoped<IThemeService, ThemeService>();
builder.Services.AddScoped<IAliasService, AliasService>();
builder.Services.AddScoped<ITenantService, TenantService>();
builder.Services.AddScoped<ISiteService, SiteService>();
builder.Services.AddScoped<IPageService, PageService>();
builder.Services.AddScoped<IModuleService, ModuleService>();
builder.Services.AddScoped<IPageModuleService, PageModuleService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IProfileService, ProfileService>();
builder.Services.AddScoped<IRoleService, RoleService>();
builder.Services.AddScoped<IUserRoleService, UserRoleService>();
builder.Services.AddScoped<ISettingService, SettingService>();
builder.Services.AddScoped<IPackageService, PackageService>();
builder.Services.AddScoped<ILogService, LogService>();
builder.Services.AddScoped<IJobService, JobService>();
builder.Services.AddScoped<IJobLogService, JobLogService>();
builder.Services.AddScoped<INotificationService, NotificationService>();
builder.Services.AddScoped<IFolderService, FolderService>();
builder.Services.AddScoped<IFileService, FileService>();
builder.Services.AddScoped<ISiteTemplateService, SiteTemplateService>();
builder.Services.AddScoped<ISqlService, SqlService>();
builder.Services.AddScoped<ISystemService, SystemService>();
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
builder.Services.AddScoped<ILanguageService, LanguageService>();
builder.Services.AddOqtaneScopedServices();
await LoadClientAssemblies(httpClient);
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (var assembly in assemblies)
{
// dynamically register module services
var implementationTypes = assembly.GetInterfaces<IService>();
foreach (var implementationType in implementationTypes)
{
if (implementationType.AssemblyQualifiedName != null)
{
var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}"));
builder.Services.AddScoped(serviceType ?? implementationType, implementationType);
}
}
// dynamically register module services
RegisterModuleServices(assembly, builder.Services);
// register client startup services
var startUps = assembly.GetInstances<IClientStartup>();
foreach (var startup in startUps)
{
startup.ConfigureServices(builder.Services);
}
RegisterClientStartups(assembly, builder.Services);
}
var host = builder.Build();
var jsRuntime = host.Services.GetRequiredService<IJSRuntime>();
var interop = new Interop(jsRuntime);
var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName);
var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie).UICultures[0].Value;
var localizationService = host.Services.GetRequiredService<ILocalizationService>();
var cultures = await localizationService.GetCulturesAsync();
if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase)))
{
culture = cultures.Single(c => c.IsDefault).Name;
}
SetCulture(culture);
await SetCultureFromLocalizationCookie(host.Services);
ServiceActivator.Configure(host.Services);
@ -115,11 +65,11 @@ namespace Oqtane.Client
private static async Task LoadClientAssemblies(HttpClient http)
{
// get list of loaded assemblies on the client
// get list of loaded assemblies on the client
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList();
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/~/api/Installation/load");
var zip = await http.GetByteArrayAsync($"/api/Installation/load");
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
@ -162,6 +112,45 @@ namespace Oqtane.Client
}
}
private static void RegisterModuleServices(Assembly assembly, IServiceCollection services)
{
var implementationTypes = assembly.GetInterfaces<IService>();
foreach (var implementationType in implementationTypes)
{
if (implementationType.AssemblyQualifiedName != null)
{
var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}"));
services.AddScoped(serviceType ?? implementationType, implementationType);
}
}
}
private static void RegisterClientStartups(Assembly assembly, IServiceCollection services)
{
var startUps = assembly.GetInstances<IClientStartup>();
foreach (var startup in startUps)
{
startup.ConfigureServices(services);
}
}
private static async Task SetCultureFromLocalizationCookie(IServiceProvider serviceProvider)
{
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
var interop = new Interop(jsRuntime);
var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName);
var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value;
var localizationService = serviceProvider.GetRequiredService<ILocalizationService>();
var cultures = await localizationService.GetCulturesAsync();
if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase)))
{
culture = cultures.Single(c => c.IsDefault).Name;
}
SetCulture(culture);
}
private static void SetCulture(string culture)
{
var cultureInfo = CultureInfo.GetCultureInfo(culture);

View File

@ -1,49 +1,42 @@
using System;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Security;
using Oqtane.Shared;
namespace Oqtane.Providers
{
public class IdentityAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly NavigationManager _navigationManager;
private readonly SiteState _siteState;
private readonly IServiceProvider _serviceProvider;
public IdentityAuthenticationStateProvider(NavigationManager navigationManager, SiteState siteState, IServiceProvider serviceProvider)
private readonly NavigationManager _navigationManager;
public IdentityAuthenticationStateProvider(IServiceProvider serviceProvider, NavigationManager navigationManager)
{
_navigationManager = navigationManager;
_siteState = siteState;
_serviceProvider = serviceProvider;
_navigationManager = navigationManager;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// get HttpClient lazily from IServiceProvider as you cannot use standard dependency injection due to the AuthenticationStateProvider being initialized prior to NavigationManager ( https://github.com/aspnet/AspNetCore/issues/11867 )
var http = _serviceProvider.GetRequiredService<HttpClient>();
string apiurl = "/~/api/User/authenticate";
User user = await http.GetFromJsonAsync<User>(apiurl);
ClaimsIdentity identity = new ClaimsIdentity();
// get HttpClient lazily from IServiceProvider as you cannot use standard dependency injection due to the AuthenticationStateProvider being initialized prior to NavigationManager(https://github.com/aspnet/AspNetCore/issues/11867 )
var http = _serviceProvider.GetRequiredService<HttpClient>();
var siteState = _serviceProvider.GetRequiredService<SiteState>();
User user = await http.GetFromJsonAsync<User>(Utilities.TenantUrl(siteState.Alias, "/api/User/authenticate"));
if (user.IsAuthenticated)
{
identity = new ClaimsIdentity("Identity.Application");
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.PrimarySid, user.UserId.ToString()));
foreach (string role in user.Roles.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
identity = UserSecurity.CreateClaimsIdentity(siteState.Alias, user);
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}

View File

@ -0,0 +1,132 @@
<?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 xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<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="Server.Text" xml:space="preserve">
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>
</data>
<data name="Database.HelpText" xml:space="preserve">
<value>Enter the name of the database</value>
</data>
</root>

View File

@ -0,0 +1,150 @@
<?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 xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<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="Server.Text" xml:space="preserve">
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server</value>
</data>
<data name="Port.Text" xml:space="preserve">
<value>Port:</value>
</data>
<data name="Port.HelpText" xml:space="preserve">
<value>Enter the port used to connect to the server</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>
</data>
<data name="Database.HelpText" xml:space="preserve">
<value>Enter the name of the database</value>
</data>
<data name="Uid.Text" xml:space="preserve">
<value>User Id:</value>
</data>
<data name="Uid.HelpText" xml:space="preserve">
<value>Enter the username to use for the database</value>
</data>
<data name="Pwd.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Pwd.HelpText" xml:space="preserve">
<value>Enter the password to use for the database</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="Server.Text" xml:space="preserve">
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server</value>
</data>
<data name="Port.Text" xml:space="preserve">
<value>Port:</value>
</data>
<data name="Port.HelpText" xml:space="preserve">
<value>Enter the port used to connect to the server</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>
</data>
<data name="Database.HelpText" xml:space="preserve">
<value>Enter the name of the database</value>
</data>
<data name="IntegratedSecurity.Text" xml:space="preserve">
<value>Integrated Security:</value>
</data>
<data name="IntegratedSecurity.HelpText" xml:space="preserve">
<value>Select if you want integrated security or not</value>
</data>
<data name="Uid.Text" xml:space="preserve">
<value>User Id:</value>
</data>
<data name="Uid.HelpText" xml:space="preserve">
<value>Enter the username to use for the database</value>
</data>
<data name="Pwd.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Pwd.HelpText" xml:space="preserve">
<value>Enter the password to use for the database</value>
</data>
<data name="Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="Integrated" xml:space="preserve">
<value>Integrated</value>
</data>
</root>

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Server.Text" xml:space="preserve">
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server</value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>
</data>
<data name="Database.HelpText" xml:space="preserve">
<value>Enter the name of the database</value>
</data>
<data name="IntegratedSecurity.Text" xml:space="preserve">
<value>Integrated Security:</value>
</data>
<data name="IntegratedSecurity.HelpText" xml:space="preserve">
<value>Select if you want integrated security or not</value>
</data>
<data name="Uid.Text" xml:space="preserve">
<value>User Id:</value>
</data>
<data name="Uid.HelpText" xml:space="preserve">
<value>Enter the username to use for the database</value>
</data>
<data name="Pwd.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Pwd.HelpText" xml:space="preserve">
<value>Enter the password to use for the database</value>
</data>
<data name="Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="Integrated" xml:space="preserve">
<value>Integrated</value>
</data>
<data name="Encryption,Text" xml:space="preserve">
<value>Encryption:</value>
</data>
<data name="Encryption.HelpText" xml:space="preserve">
<value>Specify if you are using an encrypted database connection. It is highly recommended to use encryption in a production environment.</value>
</data>
<data name="Self Signed" xml:space="preserve">
<value>Self Signed</value>
</data>
<data name="TrustServerCertificate.HelpText" xml:space="preserve">
<value>Specify the type of certificate you are using for encryption</value>
</data>
<data name="TrustServerCertificate.Text" xml:space="preserve">
<value>Trust Server Certificate:</value>
</data>
<data name="Verifiable" xml:space="preserve">
<value>Verifiable</value>
</data>
</root>

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