Compare commits

...

460 Commits

Author SHA1 Message Date
802ee8a1ff Merge pull request #2419 from oqtane/master
Merge pull request #2418 from oqtane/dev
2022-09-13 09:18:35 -04:00
e312970212 Merge pull request #2418 from oqtane/dev
3.2.0 release
2022-09-13 09:18:16 -04:00
c0f4069a9b Merge pull request #2417 from sbwalker/dev
refactor IndexedDB interop functions
2022-09-13 07:44:10 -04:00
654352827e refactor IndexedDB interop functions 2022-09-13 07:42:27 -04:00
7dd210976d Merge pull request #2416 from sbwalker/dev
optimize assembly list retrieval
2022-09-12 16:21:11 -04:00
5302be8bc1 optimize assembly list retrieval 2022-09-12 16:19:32 -04:00
9ed7181e28 Merge pull request #2415 from sbwalker/dev
remove unnecessary using statements
2022-09-12 14:56:35 -04:00
23ae4b01cb remove unnecessary using statements 2022-09-12 14:54:31 -04:00
59764d3378 Merge pull request #2414 from sbwalker/dev
cache assemblies in IndexedDB on WebAssembly
2022-09-12 14:48:17 -04:00
b8e2c729c1 cache assemblies in IndexedDB on WebAssembly 2022-09-12 14:46:46 -04:00
530d80a011 Merge pull request #2412 from sbwalker/dev
optimize assembly loading for MAUI to use client storage
2022-09-11 10:50:20 -04:00
2d306e8fda optimize assembly loading for MAUI to use client storage 2022-09-11 10:48:40 -04:00
b3d9a70fd1 Merge pull request #2410 from sbwalker/dev
remove Oqtane.Server from Oqtane.Maui solution
2022-09-09 11:40:06 -04:00
b880207f61 remove Oqtane.Server from Oqtane.Maui solution 2022-09-09 11:37:33 -04:00
ee76d02999 Merge pull request #2409 from sbwalker/dev
improvements to run in Android Emulator
2022-09-09 10:27:45 -04:00
804c33a375 improvements to run in Android Emulator 2022-09-09 10:26:13 -04:00
de784714d9 Merge pull request #2408 from sbwalker/dev
fix issue in upgrade logic for making folder paths cross platform
2022-09-08 15:44:34 -04:00
2404e26b61 fix issue in upgrade logic for making folder paths cross platform 2022-09-08 15:43:03 -04:00
b191fdda2c Merge pull request #2407 from sbwalker/dev
prepare for 3.2.0
2022-09-08 15:30:05 -04:00
e8adfd45d2 prepare for 3.2.0 2022-09-08 15:28:25 -04:00
7158595801 Merge pull request #2406 from orionlaw/dev
Make sure Job date times are stored in the database as UTC.
2022-09-08 09:14:31 -04:00
ba97f63338 Make sure Job date times are stored in the database as UTC. This is required if using Postgres or you will get an exception with a message of “Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported.”. 2022-09-07 12:46:24 -06:00
62eca2aedc Merge pull request #2401 from chlupac/BackslashFix
Backslash fix.
2022-09-06 10:53:48 -04:00
b15f6b1fa7 Merge pull request #2402 from chlupac/GitignoreUpdate
Gitignore update
2022-09-06 10:53:05 -04:00
5b22de589c Merge pull request #2403 from sbwalker/dev
Fix #2399 - page paths not being validated for deleted pages
2022-09-06 10:52:34 -04:00
d1f50f12af Fix #2399 - page paths not being validated for deleted pages 2022-09-06 10:50:53 -04:00
d40c1d9b31 Backslash fix. 2022-09-06 09:14:58 +02:00
b69041d4af Gitignore update 2022-09-06 09:14:42 +02:00
64c5d9a09f Merge pull request #2400 from sbwalker/dev
more changes to support Default pane
2022-09-05 15:51:22 -04:00
dd170bb41a more changes to support Default pane 2022-09-05 15:49:38 -04:00
1e6e4033f8 Merge pull request #2398 from sbwalker/dev
changed UrlParameterTemplate name for consistency
2022-09-04 09:48:48 -04:00
01fabc8d9e changed UrlParameterTemplate name for consistency 2022-09-04 09:47:03 -04:00
2ca2539b53 Merge pull request #2397 from sbwalker/dev
fix #2366 - populate new UrlParameters property
2022-09-04 09:37:09 -04:00
51e2e2966f fix #2366 - populate new UrlParameters property 2022-09-04 09:35:18 -04:00
55d02d2db5 Merge pull request #2396 from sbwalker/dev
Fix #2382 - Admin pane improvements
2022-09-02 18:11:51 -04:00
282a0b0c44 Fix #2382 - Admin pane improvements 2022-09-02 18:10:13 -04:00
8432779b23 Merge pull request #2395 from sbwalker/dev
added public Refresh method to FileManager
2022-09-02 09:12:41 -04:00
13b9982461 added public Refresh method to FileManager 2022-09-02 09:11:00 -04:00
d76b8cebdc Merge pull request #2389 from sbwalker/dev
Changes for .NET MAUI on Android
2022-08-31 16:35:53 -04:00
80315ae6d4 Changes for .NET MAUI on Android 2022-08-31 16:34:14 -04:00
95e7344286 Merge pull request #2385 from sbwalker/dev
moved hierarchical ordering logic to server for pages and folders
2022-08-30 07:33:46 -04:00
28f73727b5 moved hierarchical ordering logic to server for pages and folders 2022-08-30 07:31:56 -04:00
68f5bf5759 Merge pull request #2384 from sbwalker/dev
made folder paths cross platform, introduced file handler for abstracting the serving of files, enabled url mapping for broken file links, resolved public folder deletion issue
2022-08-30 07:23:50 -04:00
075748d697 made folder paths cross platform, introduced file handler for abstracting the serving of files, enabled url mapping for broken file links, resolved public folder deletion issue 2022-08-30 07:21:52 -04:00
e8d86f94f2 Merge pull request #2376 from sbwalker/dev
fix rootnamespace
2022-08-19 16:12:32 -04:00
d6bb802892 fix rootnamespace 2022-08-19 16:10:27 -04:00
52680e9002 Merge pull request #2375 from sbwalker/dev
prepare for 3.2.0
2022-08-19 15:59:35 -04:00
d6385d82ae prepare for 3.2.0 2022-08-19 15:57:31 -04:00
d058de067c Merge pull request #2374 from sbwalker/dev
Prepare for 3.2.0 release
2022-08-19 15:56:39 -04:00
32d6d143dd Prepare for 3.2.0 release 2022-08-19 15:54:33 -04:00
b49432802b Merge pull request #2373 from sbwalker/dev
Improvements to richtexteditor to allow file management in raw html editor. Also allow disabling of raw html editor which can be utilized via new setting in Html/Text module.
2022-08-19 15:34:43 -04:00
99d4d75d8e Improvements to richtexteditor to allow file management in raw html editor. Also allow disabling of raw html editor which can be utilized via new setting in Html/Text module. 2022-08-19 15:32:30 -04:00
1f584d57ac Merge pull request #2372 from sbwalker/dev
optimize Url Parameters and implement in Event Log
2022-08-18 16:06:34 -04:00
2c1543aa82 optimize Url Parameters and implement in Event Log 2022-08-18 16:04:30 -04:00
4390cbbfae Update README.md 2022-08-18 08:35:49 -04:00
bbf9e5717e Merge pull request #2370 from sbwalker/dev
improve support for module content editors
2022-08-16 17:27:54 -04:00
c7edc28bd9 improve support for module content editors 2022-08-16 17:25:46 -04:00
6e0de6f7bf Merge pull request #2369 from sbwalker/dev
check for existence of appsettings.json on Maui
2022-08-16 09:42:14 -04:00
3659422165 check for existence of appsettings.json on Maui 2022-08-16 09:40:03 -04:00
1af30da44e Merge pull request #2368 from sbwalker/dev
trim list of pages allowed to be Home Page
2022-08-16 08:44:53 -04:00
56c082cb26 trim list of pages allowed to be Home Page 2022-08-16 08:42:47 -04:00
e8eca582de Merge pull request #2363 from sbwalker/dev
added ability to specify a site home page, updated default template content to include .NET MAUI
2022-08-15 17:03:34 -04:00
4084b352de added ability to specify a site home page, updated default template content to include .NET MAUI 2022-08-15 17:01:20 -04:00
633e4acf0e Merge pull request #2362 from sbwalker/dev
add Site option for specifying a Hosting Model of Blazor Hybrid
2022-08-15 09:32:42 -04:00
468df15d80 add Site option for specifying a Hosting Model of Blazor Hybrid 2022-08-15 09:30:36 -04:00
f4537b4fcb Merge pull request #2361 from sbwalker/dev
optimize site router
2022-08-14 11:24:43 -04:00
8bca345b45 optimize site router 2022-08-14 11:22:39 -04:00
ee80712c77 Merge pull request #2360 from sbwalker/dev
resolve issue with deleted pages and modules caused by refactoring
2022-08-12 18:04:51 -04:00
3cf7153f44 resolve issue with deleted pages and modules caused by refactoring 2022-08-12 18:02:45 -04:00
8e2fc75e48 Update README.md 2022-08-12 17:09:15 -04:00
4aa51c8583 Merge pull request #2359 from sbwalker/dev
performance improvements to reduce http and database interactions
2022-08-12 16:49:59 -04:00
3c6ebd7742 performance improvements to reduce http and database interactions 2022-08-12 16:47:51 -04:00
b85539dc17 Merge pull request #2358 from sbwalker/dev
add ability to dynamically set module title and visible from components
2022-08-12 13:08:14 -04:00
4cae3f02ed add ability to dynamically set module title and visible from components 2022-08-12 13:05:48 -04:00
469b436f10 Merge pull request #2356 from dkoeder/dev
Some methods failing in BaseEntityBuilder if Schema is not null.
2022-08-12 10:45:14 -04:00
66e3e6729b Merge pull request #2357 from sbwalker/dev
add support for preserving state when loading admin components
2022-08-12 10:45:05 -04:00
fc6a794714 add support for preserving state when loading admin components 2022-08-12 10:43:00 -04:00
d75ed3d5ac Update BaseEntityBuilder.cs
Some methods failing in BaseEntityBuilder if Schema is not null.
2022-08-11 16:15:09 -06:00
bd0a218214 Merge pull request #2355 from sbwalker/dev
Blazor Hybrid / .NET MAUI support
2022-08-11 17:11:45 -04:00
f96129fa37 Blazor Hybrid / .NET MAUI support 2022-08-11 17:09:32 -04:00
920418618a Update README.md 2022-08-10 08:30:38 -04:00
29247481a6 Merge pull request #2351 from ajahangard/patch-1
#Bug in passing Lifetime property to GenerateToken
2022-08-09 10:47:28 -04:00
773710aeef #Bug in passing Lifetime property to GenerateToken
Audience is passed to GenerateToken instead of Lifetime.
2022-08-09 15:32:52 +04:30
d0c8ee57e6 Merge pull request #2348 from sbwalker/dev
Fix satellite assembly loading issue when running on WebAssembly
2022-08-08 10:49:42 -04:00
cf2adc7f6a Fix satellite assembly loading issue when running on WebAssembly 2022-08-08 10:47:33 -04:00
b621f24540 Merge pull request #2342 from sbwalker/dev
Fix #2336 - error.png path incorrect
2022-08-06 16:27:45 -04:00
99be638525 Fix #2336 - error.png path incorrect 2022-08-06 16:27:24 -04:00
d35c204e07 Merge pull request #2341 from sbwalker/dev
Fix #2339 - refactor module upgrade logic to  remove requirement on ServerManagerType for modules which have no backend
2022-08-06 16:13:53 -04:00
d8b4267668 Fix #2339 - refactor module upgrade logic to remove requirement on ServerManagerType for modules which have no backend 2022-08-06 16:13:28 -04:00
3c2f3be451 Merge pull request #2338 from chlupac/TruncateAgent
Truncate UserAgent for save to Visitors table #2337
2022-08-05 09:16:54 -04:00
e846cf8672 Truncate UserAgent for save to Visitors table 2022-08-04 16:14:39 +02:00
8804bce6c0 Merge pull request #2331 from sbwalker/dev
add proper translation keys for ActionLink and ActionDialog into RESX for Module Creator template
2022-08-03 08:50:12 -04:00
83acda6d05 add proper translation keys for ActionLink and ActionDialog into RESX for Module Creator template 2022-08-03 08:49:47 -04:00
063719532f Merge pull request #2328 from sbwalker/dev
include ResourceType attribute in Settings component for external module template
2022-08-02 07:55:38 -04:00
7b1b061355 include ResourceType attribute in Settings component for external module template 2022-08-02 07:55:11 -04:00
ed4540887e Merge pull request #2326 from leigh-pointer/Bootstrap5.2
Formating issues with Bootstrap 5.2
2022-08-02 07:51:33 -04:00
6968476ed0 Merge pull request #2327 from leigh-pointer/ScreenProgess
Added Progress Indicator
2022-08-02 07:50:59 -04:00
5d2c7c3058 Added Progress Indicator
When deleting large blocks of Pages, Modules or Notifications there was currently no visual feedback so added the ModuleInstance.ShowProgressIndicator() and ModuleInstance.HideProgressIndicator() calls to these processes.
2022-08-02 10:37:04 +02:00
e6cb90e545 Formating issues with Bootstrap 5.2 2022-08-02 08:55:42 +02:00
ec73f4dbea Merge pull request #2322 from leigh-pointer/Bootstrap5.2
Updated Bootstrap to 5.2
2022-08-01 17:36:10 -04:00
4f41a52ee7 Merge pull request #2325 from sbwalker/dev
fix upgrade issue for framework translations, improvements for managing module translations
2022-08-01 17:05:57 -04:00
c097956fcb fix upgrade issue for framework translations, improvements for managing module translations 2022-08-01 17:05:18 -04:00
8cbc17ed98 Theme Creator updated to Bootstrap 5.2.0 2022-07-28 20:59:52 +02:00
50d89d0f13 Updated Bootstrap to 5.2
Replaced Bootstrap cloudflare versions and Integrity keys to match 5.2.0
2022-07-28 20:52:11 +02:00
7b4d13b73e Merge pull request #2318 from oqtane/master
Merge pull request #2317 from oqtane/dev
2022-07-27 16:16:01 -04:00
2909aa1656 Merge pull request #2317 from oqtane/dev
3.1.4 release
2022-07-27 16:15:43 -04:00
64131b6764 Update README.md 2022-07-27 08:54:48 -04:00
0b78d75a21 Merge pull request #2314 from sbwalker/dev
prepare for 3.1.4 release
2022-07-26 17:22:27 -04:00
b35c342960 prepare for 3.1.4 release 2022-07-26 17:22:06 -04:00
24c858d379 Merge pull request #2313 from sbwalker/dev
support for module translation download/install
2022-07-26 14:44:25 -04:00
b8a31a8be9 support for module translation download/install 2022-07-26 14:44:06 -04:00
02c30d6454 Merge pull request #2312 from sbwalker/dev
add ability to supply connection string in Add Site
2022-07-26 10:13:14 -04:00
985f003e6d add ability to supply connection string in Add Site 2022-07-26 10:12:54 -04:00
98045e1e2e Merge pull request #2311 from sbwalker/dev
introduce ITransientService interface for auto registration of transient services (for DBContexts and Repositories)
2022-07-26 09:42:12 -04:00
5762ce58a4 introduce ITransientService interface for auto registration of transient services (for DBContexts and Repositories) 2022-07-26 09:41:42 -04:00
2787ee71fc Merge pull request #2310 from sbwalker/dev
Allow for entry of raw connection string during installation
2022-07-26 07:48:29 -04:00
e61a6df4d7 Allow for entry of raw connection string during installation 2022-07-26 07:48:04 -04:00
0a5c2ecbf5 Merge pull request #2304 from sbwalker/dev
optimize satellite assembly loading based on the new model where all cultures are available
2022-07-21 16:02:48 -04:00
6bfab696ad optimize satellite assembly loading based on the new model where all cultures are available 2022-07-21 16:02:23 -04:00
928f2dd496 Merge pull request #2302 from chlupac/FileServiceFix
FileService fix
2022-07-20 08:23:41 -04:00
bcf75892f7 FileService fix 2022-07-20 10:39:13 +02:00
594761385f Merge pull request #2301 from sbwalker/dev
add Environment to System Info
2022-07-19 14:34:10 -04:00
d05fba06ec add Environment to System Info 2022-07-19 14:33:51 -04:00
25155b1b38 Merge pull request #2295 from chlupac/AppSettingsFix
Fixed loading of alternative appsettings "appsettings.{env.EnvironmentName}.json"
2022-07-19 14:20:08 -04:00
ded6c9c199 Merge pull request #2299 from chlupac/InstallManFix
Fix - InstallationManager crash when package folders are missing
2022-07-19 13:12:43 -04:00
c62e6c0045 Merge pull request #2300 from sbwalker/dev
performance optimization for permissions
2022-07-19 10:49:52 -04:00
b3feda9fd1 performance optimization for permissions 2022-07-19 10:49:33 -04:00
7ef8e2c8b8 Fix - InstallationManager crash when package folders are missing 2022-07-19 09:42:12 +02:00
d5ff211871 Fixed loading of alternative appsettings "appsettings.{env.EnvironmentName}.json" 2022-07-18 21:34:59 +02:00
51c23e3842 Merge pull request #2294 from sbwalker/dev
use package name as a convention for identifying satellite assemblies
2022-07-18 13:14:53 -04:00
557b30815e use package name as a convention for identifying satellite assemblies 2022-07-18 13:14:34 -04:00
145459bfc3 Merge pull request #2292 from sbwalker/dev
Added version to Language Management, improved framework performance by loading languages into PageState, include all supported cultures and allow Administrator to add any language to a site regardless of translation availability, fix translation upgrade issue
2022-07-16 10:00:20 -04:00
f97a6a2bee Added version to Language Management, improved framework performance by loading languages into PageState, include all supported cultures and allow Administrator to add any language to a site regardless of translation availability, fix translation upgrade issue 2022-07-16 09:59:47 -04:00
1134422891 Merge pull request #2291 from sbwalker/dev
Fix #2282 - dynamically determine framework path when scaffolding project references
2022-07-15 16:00:23 -04:00
6012275c7b Fix #2282 - dynamically determine framework path when scaffolding project references 2022-07-15 15:59:55 -04:00
2f07063375 Merge pull request #2288 from sbwalker/dev
Fix #2285 - handle scenario where the module definition associated to a module instance does not exist
2022-07-14 16:58:43 -04:00
310d1ed485 Fix #2285 - handle scenario where the module definition associated to a module instance does not exist 2022-07-14 16:58:16 -04:00
a48edbb16e Merge pull request #2287 from sbwalker/dev
fixed issue in default site template where MIT License module was being created in invalid pane
2022-07-14 09:11:16 -04:00
d6258409fc fixed issue in default site template where MIT License module was being created in invalid pane 2022-07-14 09:10:51 -04:00
1b94b1247b Merge pull request #2284 from sbwalker/dev
Fix #2280 - add 404 page on upgrade, Fix #2279 add message indicating a restart is required to activate scheduled jobs after installation, add Package Name to Module and Theme management
2022-07-13 15:19:07 -04:00
9ef63ae60e Fix #2280 - add 404 page on upgrade, Fix #2279 add message indicating a restart is required to activate scheduled jobs after installation, add Package Name to Module and Theme management 2022-07-13 15:18:41 -04:00
f99de4be48 Merge pull request #2272 from sbwalker/dev
Support for module editors by exposing Edit Mode in the Control Panel
2022-07-06 17:25:43 -04:00
80fd1820c2 Support for module editors by exposing Edit Mode in the Control Panel 2022-07-06 17:25:08 -04:00
0a4a983d20 Merge pull request #2269 from leigh-pointer/Resx
Added Missing Resource from Oqtane Theme Settings
2022-07-06 08:37:25 -04:00
c2cc830691 Added Missing Resource from Oqtane Theme Settings
Missing Resource string added
2022-07-04 21:37:42 +02:00
4d0490d1c6 Merge pull request #2260 from sbwalker/dev
FIx issue with redirect after site delete and remove tenant if it is empty
2022-06-28 08:17:39 -04:00
02d1838547 FIx issue with redirect after site delete and remove tenant if it is empty 2022-06-28 08:17:06 -04:00
5c6edff778 Update README.md 2022-06-27 16:35:50 -04:00
7cbca32ddd Update README.md 2022-06-27 16:16:58 -04:00
8950e315f8 Merge pull request #2258 from oqtane/master
Merge pull request #2257 from oqtane/dev
2022-06-27 16:12:27 -04:00
a703df40c0 Merge pull request #2257 from oqtane/dev
3.1.3 release
2022-06-27 16:12:05 -04:00
225fce8810 Merge pull request #2256 from sbwalker/dev
3.1.3 data provider packages
2022-06-27 15:43:42 -04:00
bdc0f0fcdd 3.1.3 data provider packages 2022-06-27 15:43:24 -04:00
5bbb8c4858 Merge pull request #2255 from sbwalker/dev
Add schema support to BaseEntityBuilder
2022-06-27 13:55:08 -04:00
35b9551bfb Add schema support to BaseEntityBuilder 2022-06-27 13:54:52 -04:00
c8c5a05b39 Merge pull request #2254 from sbwalker/dev
Fix #2249 Fix #2250 - issues with site deletion
2022-06-27 13:46:02 -04:00
2771f0301a Fix #2249 Fix #2250 - issues with site deletion 2022-06-27 13:45:42 -04:00
c4f04edc59 Merge pull request #2253 from sbwalker/dev
Fix #2252 - unable to insert images into rich text editor
2022-06-27 12:24:16 -04:00
5530422846 Fix #2252 - unable to insert images into rich text editor 2022-06-27 12:23:55 -04:00
ce77c81fb5 Merge pull request #2248 from sbwalker/dev
prepare for 3.1.3
2022-06-21 09:25:33 -04:00
bc488d4ac2 prepare for 3.1.3 2022-06-21 09:25:14 -04:00
fe72a10346 Merge pull request #2247 from sbwalker/dev
fix #2239 - email notification encoding to support all cultures
2022-06-20 19:42:50 -04:00
0da88398b4 fix #2239 - email notification encoding to support all cultures 2022-06-20 19:42:32 -04:00
c42de3da20 Merge pull request #2246 from sbwalker/dev
fix #2245 - default database type not set correctly when adding new site for any DB other than LocalDB, added Source: info to all extension installation scenarios now that the Registry supports both Nuget and GitHub locations
2022-06-20 17:45:10 -04:00
4bf9f36baa fix #2245 - default database type not set correctly when adding new site for any DB other than LocalDB, added Source: info to all extension installation scenarios now that the Registry supports both Nuget and GitHub locations 2022-06-20 17:44:49 -04:00
380cb192c7 Merge pull request #2244 from sbwalker/dev
allow multiple aliases to be defined as default
2022-06-18 09:18:47 -04:00
8882e19ec5 allow multiple aliases to be defined as default 2022-06-18 09:18:23 -04:00
7f52059b98 Merge pull request #2243 from sbwalker/dev
added extension method for creating a LocalizerFactory using a type name, refactored Pager and LocalizableComponent to use LocalizerFactory
2022-06-15 16:19:51 -04:00
1ce3cc4d7c added extension method for creating a LocalizerFactory using a type name, refactored Pager and LocalizableComponent to use LocalizerFactory 2022-06-15 16:19:22 -04:00
657c71e94d Merge pull request #2242 from leigh-pointer/Issue#2234
Fix for Recycle bin not showing Deleted Date (Issue #2234)
2022-06-14 10:26:50 -04:00
c8cfb3c7b7 Fix for Recycle bin not showing Deleted Date (Issue #2234)
Added the Deleted data to Module from the PageModule
2022-06-14 09:22:12 +02:00
6e7e90acf4 Merge pull request #2241 from sbwalker/dev
additional changes for #2228
2022-06-13 09:10:20 -04:00
6d3a556d34 additional changes for #2228 2022-06-13 09:10:01 -04:00
7d9188b659 Merge pull request #2238 from chlupac/LogExceptions
Exception is not saved to log
2022-06-09 15:13:10 -04:00
4f0a805c79 Exception is not saved to log 2022-06-09 10:24:13 +02:00
53f3320492 Merge pull request #2228 from chlupac/Log_notification_improvement
Log notification improvement
2022-06-08 15:48:17 -04:00
f9ce51b4a5 Merge pull request #2222 from hishamco/pager
Pager should inherits from LocalizableComponent
2022-06-08 15:47:25 -04:00
f8bf432c0d Merge pull request #2237 from sbwalker/dev
Improvements for #2229 - relax userrole restrictions
2022-06-08 15:47:12 -04:00
a822482172 Improvements for #2229 - relax userrole restrictions 2022-06-08 15:46:36 -04:00
b22f8a0b02 Remove generic type suffix properly 2022-06-08 00:12:10 +03:00
227331bf24 Merge branch 'dev' into pager 2022-06-07 23:37:08 +03:00
744688cbe1 Merge pull request #2235 from sbwalker/dev
Fix #2230 - add support for an Unauthenticated User global role
2022-06-07 15:26:05 -04:00
79c8126c4a Fix #2230 - add support for an Unauthenticated User global role 2022-06-07 15:25:44 -04:00
0b7c8e4ef7 Merge pull request #2232 from ijaz-saeed/dev
missing translation keys for module names
2022-06-07 15:23:08 -04:00
35e00f61d8 Update README.md 2022-06-07 09:38:42 -04:00
aba3d58df8 missing translation keys for module names 2022-06-06 10:12:00 +05:00
45984a8166 Merge pull request #2231 from sbwalker/dev
Improvements for #2221 - validate file extensions client-side before initiating upload, validate file extension server-side before writing part to disk, optimize part cleanup logic, add error handling to JavaScript XMLHttpRequest, ensure FileInput gets initialized after upload
2022-06-04 15:41:38 -04:00
ea5655ae42 Improvements for #2221 - validate file extensions client-side before initiating upload, valid file extension server-side before writing part to disk, optimize cleanup logic, add error handling to JavaScript XMLHttpRequest, ensure FileInput gets initialized after upload 2022-06-04 15:40:26 -04:00
f06cb0dfbb Log notification improvement 2022-05-31 11:33:42 +02:00
1abae55976 Remove magic string 2022-05-31 11:59:17 +03:00
a83ed40ec4 Avoid breaking changes 2022-05-31 10:40:42 +03:00
75ecae4672 Merge pull request #2223 from hishamco/toggle-password
Avoid toggle password & confirm password as same time when one button is clicked
2022-05-28 21:59:40 -04:00
16a6f942c5 Avoid toggle password & confirm password as same time when one button is clicked 2022-05-27 16:20:35 +03:00
aa98508e57 Remove the entry from SharedResources 2022-05-27 15:37:13 +03:00
583383aee1 Pager should inherits from LocalizableComponent 2022-05-27 15:33:48 +03:00
13f69f81d7 Merge pull request #2219 from sbwalker/dev
fix #2213 - disabling show on all pages
2022-05-26 01:19:35 -04:00
43c34fcd64 fix #2213 - disabling show on all pages 2022-05-26 01:19:14 -04:00
c272238539 Merge pull request #2217 from leigh-pointer/MissingRes2215
Fix for missing Resx entries #2215
2022-05-25 07:23:01 -04:00
6e0d2706a8 Fix for missing Resx entries #2215
Updated the Resx File with the missing entries.  

However the UserProfile\Add cant be Localized at present as the IStringLocalizer is not loaded when that property assignment is executed.
2022-05-25 11:13:38 +02:00
25a7289ce8 Merge pull request #2210 from leigh-pointer/PageOf
Fix for #2209 Localization Pager component (Page @_page of @_pages)
2022-05-24 21:47:12 -04:00
1ba7d045e4 Merge pull request #2211 from elgransan/dev
* Collapse menu after click on a page (mobile version)
2022-05-24 21:46:11 -04:00
a4d75befe7 * Collapse menu after click on a page (mobile version) 2022-05-22 20:05:01 -03:00
88377529bc Fix for #2209 Localization Pager component (Page @_page of @_pages)
Added fixe for issue.  Added the resource "PageOfPages" to SharedResources as trying to inject IStringLocalizer<Pager<TableItem>> and adding Pager.resx the resource failed to load.
2022-05-22 20:23:17 +02:00
91b9a0280f Merge pull request #2207 from sbwalker/dev
Add filtering by Category to Module Management - default to Common
2022-05-21 10:11:24 -04:00
25173ae85c Ddd filtering by Category to Module Management - default to Common 2022-05-21 10:10:57 -04:00
ad3350705e Update README.md 2022-05-19 11:44:38 -04:00
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
384 changed files with 15991 additions and 5257 deletions

13
.gitignore vendored
View File

@ -10,11 +10,11 @@ msbuild.binlog
*.zip
*.idea
_ReSharper.Caches
.DS_Store
Oqtane.Server/appsettings.json
Oqtane.Server/Data/*.mdf
Oqtane.Server/Data/*.ldf
Oqtane.Server/Data/*.db
Oqtane.Server/Data
/Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml
Oqtane.Server/Content
@ -22,3 +22,10 @@ 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

@ -45,6 +45,9 @@
[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 = "" };
@ -55,17 +58,13 @@
{
SiteState.RemoteIPAddress = RemoteIPAddress;
SiteState.AntiForgeryToken = AntiForgeryToken;
InstallationService.SetAntiForgeryTokenHeader(AntiForgeryToken);
SiteState.AuthorizationToken = AuthorizationToken;
_installation = await InstallationService.IsInstalled();
if (_installation.Alias != null)
{
SiteState.Alias = _installation.Alias;
}
else
{
_installation.Message = "Site Not Configured Correctly - No Matching Alias Exists For Host Name";
}
_initialized = true;
}

View File

@ -1,3 +1,5 @@
using System;
namespace Microsoft.Extensions.Localization
{
public static class OqtaneLocalizationExtensions
@ -18,5 +20,42 @@ namespace Microsoft.Extensions.Localization
}
return localizedValue;
}
/// <summary>
/// Creates an IStringLocalizer based on a type name. This extension method is useful in scenarios where the default IStringLocalizer is unable to locate the resources.
/// </summary>
/// <param name="localizerFactory"></param>
/// <param name="fullTypeName">the full type name ie. GetType().FullName</param>
/// <returns></returns>
public static IStringLocalizer Create(this IStringLocalizerFactory localizerFactory, string fullTypeName)
{
var typename = fullTypeName;
// handle generic types
var type = Type.GetType(fullTypeName);
if (type.IsGenericType)
{
typename = type.GetGenericTypeDefinition().FullName;
typename = typename.Substring(0, typename.IndexOf("`")); // remove generic type info
}
// format typename
if (typename.Contains(","))
{
typename = typename.Substring(0, typename.IndexOf(",")); // remove assembly info
}
// remove rootnamespace
var rootnamespace = "";
var attributes = type.Assembly.GetCustomAttributes(typeof(RootNamespaceAttribute), false);
if (attributes.Length > 0)
{
rootnamespace = ((RootNamespaceAttribute)attributes[0]).RootNamespace;
}
typename = typename.Replace(rootnamespace + ".", "");
// create IStringLocalizer using factory
return localizerFactory.Create(typename, type.Assembly.GetName().Name);
}
}
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection
return services;
}
internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services)
public static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services)
{
services.AddScoped<SiteState>();
services.AddScoped<IInstallationService, InstallationService>();

View File

@ -28,7 +28,7 @@
<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" />
<input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div>
</div>

View File

@ -40,7 +40,7 @@
<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" />
<input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div>
</div>
}

View File

@ -4,7 +4,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="server" HelpText="Enter the database server" ResourceKey="Server">Server:</Label>
<Label Class="col-sm-3" For="server" HelpText="Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) " ResourceKey="Server">Server:</Label>
<div class="col-sm-9">
<input id="server" type="text" class="form-control" @bind="@_server" />
</div>
@ -35,7 +35,7 @@
<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" />
<input id="pwd" type="password" class="form-control" @bind="@_pwd" autocomplete="new-password" />
</div>
</div>
}
@ -51,7 +51,7 @@
@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>
<Label Class="col-sm-3" For="trustservercertificate" HelpText="Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True." ResourceKey="TrustServerCertificate">Certificate:</Label>
<div class="col-sm-9">
<select id="encryption" class="form-select custom-select" @bind="@_trustservercertificate">
<option value="true">@Localizer["Self Signed"]</option>

View File

@ -26,29 +26,42 @@
<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 class="input-group">
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
@foreach (var database in _databases)
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
</select>
@if (!_showConnectionString)
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionString"]</button>
}
else
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionParameters"]</button>
}
</div>
}
</div>
</div>
@{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent;
}
}
</div>
@if (!_showConnectionString)
{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent
}
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">String:</Label>
<div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div>
</div>
}
</div>
</div>
<div class="col text-center">
<h2>@Localizer["ApplicationAdmin"]</h2><br />
@ -62,13 +75,19 @@
<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">
<input id="password" type="password" class="form-control" @bind="@_hostPassword" />
<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">
<input id="confirm" type="password" class="form-control" @bind="@_confirmPassword" />
<div class="input-group">
<input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword">@_toggleConfirmPassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
@ -101,9 +120,15 @@
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _connectionString = string.Empty;
private string _hostUsername = string.Empty;
private string _hostPassword = string.Empty;
private string _passwordType = "password";
private string _confirmPasswordType = "password";
private string _togglePassword = string.Empty;
private string _toggleConfirmPassword = string.Empty;
private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty;
private bool _register = true;
@ -112,6 +137,9 @@
protected override async Task OnInitializedAsync()
{
_togglePassword = SharedLocalizer["ShowPassword"];
_toggleConfirmPassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault))
{
@ -121,94 +149,146 @@
{
_databaseName = "LocalDB";
}
LoadDatabaseConfigComponent();
}
LoadDatabaseConfigComponent();
}
private void DatabaseChanged(ChangeEventArgs eventArgs)
{
try
{
_databaseName = (string)eventArgs.Value;
private void DatabaseChanged(ChangeEventArgs eventArgs)
{
try
{
_databaseName = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
_message = Localizer["Error.DbConfig.Load"];
}
}
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();
};
}
}
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.2.0/css/bootstrap.min.css", "text/css", "sha512-XWTTruHZEYJsxV3W/lSXG1n3Q39YIWOstqvmFsdNEEQfHoZ6vm6E9GK2OrF6DSJSpIbRbi+Nn0WDPID9O7xB2Q==", "anonymous", "");
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.0/js/bootstrap.bundle.min.js", "sha512-9GacT4119eY3AcosfWtHMsT5JyZudrexyEVzTBWV3viP/YfB9e2pEy3N7WXL3SV6ASXpTU0vzzSxsbfsuUH4sQ==", "anonymous", "", "head");
}
}
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 (_showConnectionString)
{
connectionString = _connectionString;
}
else
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
}
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();
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && _hostPassword.Length >= 6 && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
{
_loadingDisplay = "";
StateHasChanged();
Uri uri = new Uri(NavigationManager.Uri);
Uri uri = new Uri(NavigationManager.Uri);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var config = new InstallConfig
{
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 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"];
}
}
private void ToggleConfirmPassword()
{
if (_confirmPasswordType == "password")
{
_confirmPasswordType = "text";
_toggleConfirmPassword = SharedLocalizer["HidePassword"];
}
else
{
_confirmPasswordType = "password";
_toggleConfirmPassword = SharedLocalizer["ShowPassword"];
}
}
private void ToggleConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionString = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
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.Require.DbInfo"];
}
}
}

View File

@ -10,8 +10,8 @@
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions))
{
string url = NavigateUrl(p.Path);
<div class="col-md-2 mx-auto text-center">
<NavLink class="nav-link" href="@url" Match="NavLinkMatch.All">
<div class="col-md-2 mx-auto text-center mb-3">
<NavLink class="nav-link text-primary" href="@url" Match="NavLinkMatch.All">
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>@SharedLocalizer[p.Name]
</NavLink>
</div>
@ -27,6 +27,6 @@
protected override void OnInitialized()
{
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId && !item.IsDeleted).ToList();
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList();
}
}

View File

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

View File

@ -132,22 +132,10 @@
_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");
}
(_startDate, _startTime) = Utilities.UtcAsLocalDateAndTime(job.StartDate);
(_endDate, _endTime) = Utilities.UtcAsLocalDateAndTime(job.EndDate);
_retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution;
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{
_nextTime = job.NextExecution.Value.ToString("HH:mm");
}
(_nextDate, _nextTime) = Utilities.UtcAsLocalDateAndTime(job.NextExecution);
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
@ -180,50 +168,27 @@
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate;
if (job.StartDate != null)
{
job.StartDate = job.StartDate.Value.Date;
if (!string.IsNullOrEmpty(_startTime))
{
job.StartDate = DateTime.Parse(job.StartDate.Value.ToShortDateString() + " " + _startTime);
}
}
job.EndDate = _endDate;
if (job.EndDate != null)
{
job.EndDate = job.EndDate.Value.Date;
if (!string.IsNullOrEmpty(_endTime))
{
job.EndDate = DateTime.Parse(job.EndDate.Value.ToShortDateString() + " " + _endTime);
}
}
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);
}
}
job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime);
job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime);
job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime);
try
{
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
try
{
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
}

View File

@ -33,7 +33,7 @@ else
<td>@context.Name</td>
<td>@DisplayStatus(context.IsEnabled, context.IsExecuting)</td>
<td>@DisplayFrequency(context.Interval, context.Frequency)</td>
<td>@context.NextExecution</td>
<td>@context.NextExecution?.ToLocalTime()</td>
<td>
@if (context.IsStarted)
{
@ -56,6 +56,10 @@ else
protected override async Task OnParametersSetAsync()
{
_jobs = await JobService.GetJobsAsync();
if (_jobs.Count == 0)
{
AddModuleMessage(string.Format(Localizer["Message.NoJobs"], NavigateUrl("admin/system")), MessageType.Warning);
}
}
private string DisplayStatus(bool isEnabled, bool isExecuting)

View File

@ -20,6 +20,7 @@ else
@if (_availableCultures.Count() == 0)
{
<ModuleMessage Type="MessageType.Info" Message="@_message"></ModuleMessage>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
}
else
{
@ -31,7 +32,7 @@ else
<select id="_code" class="form-select" @bind="@_code" required>
@foreach (var culture in _availableCultures)
{
<option value="@culture.Name">@culture.DisplayName</option>
<option value="@culture.Name">@(culture.DisplayName + " (" + culture.Name + ")")</option>
}
</select>
</div>
@ -47,11 +48,11 @@ else
</div>
</div>
<button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
}
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="Download" ResourceKey="Download" Security="SecurityAccessLevel.Host">
<TabPanel Name="Translations" Heading="Translations" ResourceKey="Download" Security="SecurityAccessLevel.Host">
<div class="row justify-content-center mb-3">
<div class="col-sm-6">
<div class="input-group">
@ -78,6 +79,7 @@ else
<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)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@ -108,12 +110,16 @@ else
}
<button type="button" class="btn btn-success" @onclick="InstallLanguages">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
}
</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>
<Label Class="col-sm-3" HelpText="Upload one or more translations. Once they are uploaded click Install to complete the installation." ResourceKey="LanguageUpload">Translation: </Label>
<div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" />
</div>
@ -159,33 +165,32 @@ else
}
@code {
private ElementReference form;
private bool validated = false;
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 = "";
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;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
var languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
var languagesCodes = languages.Select(l => l.Code).ToList();
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();
_availableCultures = _supportedCultures
.Where(c => !c.Name.Equals(Constants.DefaultCulture) && !languagesCodes.Contains(c.Name));
await LoadTranslations();
_supportedCultures = await LocalizationService.GetCulturesAsync();
_availableCultures = _supportedCultures.Where(c => !c.Name.Equals(Constants.DefaultCulture) && !languagesCodes.Contains(c.Name));
await LoadTranslations();
if (_supportedCultures.Count() == 1)
{

View File

@ -19,16 +19,18 @@ else
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th>
<th>@Localizer["Translation"]</th>
<th>@Localizer["Default"]</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
<Row>
<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))
<td>@context.Version</td>
<td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td>
<td>
@if (UpgradeAvailable(context.Code, context.Version))
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadLanguage(context.Code))>@SharedLocalizer["Upgrade"]</button>
}
@ -38,24 +40,21 @@ else
}
@code {
private List<Language> _languages;
private List<Package> _packages;
private List<Language> _languages;
private List<Package> _packages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync()
{
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId);
protected override async Task OnParametersSetAsync()
{
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.PackageId);
var cultures = await LocalizationService.GetCulturesAsync();
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");
_packages = await PackageService.GetPackagesAsync("translation");
}
}
@ -76,15 +75,16 @@ else
}
}
private bool UpgradeAvailable(string code)
private bool UpgradeAvailable(string code, string version)
{
var upgradeavailable = false;
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == (Constants.PackageId + ".Client." + code)).FirstOrDefault();
if (package != null)
var package = _packages.Where(item => item.PackageId == (Constants.PackageId + "." + code)).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) > 0);
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(Constants.Version)) == 0) &&
(Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
}
}
@ -97,7 +97,7 @@ else
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await PackageService.DownloadPackageAsync(Constants.PackageId + ".Client." + code, Constants.Version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(Constants.PackageId + "." + 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);

View File

@ -3,14 +3,9 @@
@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 Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
@ -19,185 +14,313 @@
<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">@SharedLocalizer["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">@SharedLocalizer["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["RememberMe"]</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">@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>
</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)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
_message = Localizer["Success.Account.Verified"];
}
else
{
await logger.LogError(LogFunction.Security, "Email Verification Failed For Username {Username}", _username);
_message = Localizer["Message.Account.NotVerfied"];
_type = MessageType.Warning;
}
}
}
if (PageState.QueryString.ContainsKey("returnurl"))
{
_returnUrl = PageState.QueryString["returnurl"];
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if(PageState.User == null)
{
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)
{
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(LogFunction.Security, "Login Successful For Username {Username}", _username);
// server-side Blazor needs to post to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
else
{
await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], 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(LogFunction.Security, "Login Successful For Username {Username}", _username);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
else
{
await logger.LogError(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Message.Required.UserInfo"], MessageType.Warning);
}
}
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);
await logger.LogInformation(LogFunction.Security, "Password Reset Notification Sent For Username {Username}", _username);
_message = Localizer["Message.ForgotUser"];
}
else
{
_message = Localizer["Message.UserDoesNotExist"];
_type = MessageType.Warning;
}
}
else
{
_message = Localizer["Message.ForgotPassword"];
}
private async Task Login()
{
try
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(login))
{
var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid);
var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress};
if (!twofactor)
{
user = await UserService.LoginUserAsync(user, hybrid, _remember);
}
else
{
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
StateHasChanged();
}
if (user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider
.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true));
}
else
{
// 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

@ -9,86 +9,88 @@
@inject IStringLocalizer<Detail> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<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>
@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>
@ -107,81 +109,89 @@
</div>
</div>
</div>
}
<NavLink class="btn btn-secondary" href="@CloseUrl()">@SharedLocalizer["Cancel"]</NavLink>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private bool _initialized = false;
private int _logId;
private string _logDate = string.Empty;
private string _level = string.Empty;
private string _feature = string.Empty;
private string _function = string.Empty;
private string _category = string.Empty;
private string _pageName = string.Empty;
private string _moduleTitle = string.Empty;
private string _username = string.Empty;
private string _url = string.Empty;
private string _template = string.Empty;
private string _message = string.Empty;
private string _exception = string.Empty;
private string _properties = string.Empty;
private string _server = string.Empty;
@code {
private int _logId;
private string _logDate = string.Empty;
private string _level = string.Empty;
private string _feature = string.Empty;
private string _function = string.Empty;
private string _category = string.Empty;
private string _pageName = string.Empty;
private string _moduleTitle = string.Empty;
private string _username = string.Empty;
private string _url = string.Empty;
private string _template = string.Empty;
private string _message = string.Empty;
private string _exception = string.Empty;
private string _properties = string.Empty;
private string _server = string.Empty;
public override string UrlParametersTemplate => "/{id}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
protected override async Task OnInitializedAsync()
{
try
{
try
_logId = Int32.Parse(UrlParameters["id"]);
var log = await LogService.GetLogAsync(_logId);
if (log != null)
{
_logId = Int32.Parse(PageState.QueryString["id"]);
var log = await LogService.GetLogAsync(_logId);
if (log != null)
_logDate = log.LogDate.ToString(CultureInfo.CurrentCulture);
_level = log.Level;
_feature = log.Feature;
_function = log.Function;
_category = log.Category;
if (log.PageId != null)
{
_logDate = log.LogDate.ToString(CultureInfo.CurrentCulture);
_level = log.Level;
_feature = log.Feature;
_function = log.Function;
_category = log.Category;
if (log.PageId != null)
var page = await PageService.GetPageAsync(log.PageId.Value);
if (page != null)
{
var page = await PageService.GetPageAsync(log.PageId.Value);
if (page != null)
{
_pageName = page.Name;
}
_pageName = page.Name;
}
if (log.PageId != null && log.ModuleId != null)
{
var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value);
if (pagemodule != null)
{
_moduleTitle = pagemodule.Title;
}
}
if (log.UserId != null)
{
var user = await UserService.GetUserAsync(log.UserId.Value, PageState.Site.SiteId);
if (user != null)
{
_username = user.Username;
}
}
_url = log.Url;
_template = log.MessageTemplate;
_message = log.Message;
_exception = log.Exception;
_properties = log.Properties;
_server = log.Server;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Log {LogId} {Error}", _logId, ex.Message);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
if (log.PageId != null && log.ModuleId != null && log.ModuleId != -1)
{
var pagemodule = await PageModuleService.GetPageModuleAsync(log.PageId.Value, log.ModuleId.Value);
if (pagemodule != null)
{
_moduleTitle = pagemodule.Title;
}
}
if (log.UserId != null)
{
var user = await UserService.GetUserAsync(log.UserId.Value, PageState.Site.SiteId);
if (user != null)
{
_username = user.Username;
}
}
_url = log.Url;
_template = log.MessageTemplate;
_message = log.Message;
_exception = log.Exception;
_properties = log.Properties;
_server = log.Server;
_initialized = true;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Log {LogId} {Error}", _logId, ex.Message);
AddModuleMessage(Localizer["Error.Log.Load"], MessageType.Error);
}
}
private string CloseUrl()
{
return (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl();
}
}

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Admin.Logs
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject ILogService LogService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer
@ -17,7 +18,7 @@ else
<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" @onchange="(e => LevelChanged(e))">
<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>
@ -29,7 +30,7 @@ else
</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" @onchange="(e => FunctionChanged(e))">
<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>
@ -41,7 +42,7 @@ else
</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" @onchange="(e => RowsChanged(e))">
<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>
@ -53,7 +54,7 @@ else
@if (_logs.Any())
{
<Pager Items="@_logs">
<Pager Items="@_logs" CurrentPage="@_page.ToString()" OnPageChange="OnPageChange">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Date"]</th>
@ -62,7 +63,7 @@ else
<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)"><ActionLink Action="Detail" Parameters="@($"/{context.LogId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, AddUrlParameters(_level, _function, _rows, _page)))" ResourceKey="LogDetails" /></td>
<td class="@GetClass(context.Function)">@context.LogDate</td>
<td class="@GetClass(context.Function)">@context.Level</td>
<td class="@GetClass(context.Function)">@context.Feature</td>
@ -91,20 +92,47 @@ else
}
@code {
private string _level = "-";
private string _function = "-";
private string _rows = "10";
private List<Log> _logs;
private string _retention = "";
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;
public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnParametersSetAsync()
{
try
{
// external link to log item will display Details component
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int id))
{
NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, ModuleState.ModuleId, "Detail", $"id={id}"));
}
if (UrlParameters.ContainsKey("level"))
{
_level = UrlParameters["level"];
}
if (UrlParameters.ContainsKey("function"))
{
_function = UrlParameters["function"];
}
if (UrlParameters.ContainsKey("rows"))
{
_rows = UrlParameters["rows"];
}
if (UrlParameters.ContainsKey("page") && int.TryParse(UrlParameters["page"], out int page))
{
_page = page;
}
protected override async Task OnInitializedAsync()
{
try
{
await GetLogs();
_retention = SettingService.GetSetting(PageState.Site.Settings, "LogRetention", "30");
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = SettingService.GetSetting(settings, "LogRetention", "30");
}
catch (Exception ex)
{
@ -208,4 +236,9 @@ else
}
}
private void OnPageChange(int page)
{
_page = page;
}
}

View File

@ -76,64 +76,71 @@ else
}
@code {
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 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 = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;
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 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
{
_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");
}
}
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");
}
}
private async Task CreateModule()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
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);
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);
GetLocation();
AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Require.ValidName"], MessageType.Warning);
}
}
catch (Exception ex)
{

View File

@ -35,6 +35,7 @@
<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)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@ -90,9 +91,9 @@
<div class="modal-body">
<p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3>
@if (!string.IsNullOrEmpty(_license))
@if (!string.IsNullOrEmpty(_packagelicense))
{
@((MarkupString)_license)
@((MarkupString)_packagelicense)
}
else
{
@ -113,146 +114,150 @@
<button type="button" class="btn btn-success" @onclick="InstallModules">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
@code {
private List<Package> _packages;
private string _price = "free";
private string _search = "";
private string _productname = "";
private string _license = "";
private string _packageid = "";
private string _version = "";
private List<Package> _packages;
private string _price = "free";
private string _search = "";
private string _productname = "";
private string _packageid = "";
private string _packagelicense = "";
private string _packageversion = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
await LoadModuleDefinitions();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Packages {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Package.Load"], MessageType.Error);
}
}
protected override async Task OnInitializedAsync()
{
try
{
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, "");
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 => item.PackageName == package.PackageId))
{
_packages.Remove(package);
}
}
}
}
if (_packages != null)
{
foreach (Package package in _packages.ToArray())
{
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 On PriceChanged");
}
}
private async void PriceChanged(ChangeEventArgs e)
{
try
{
_price = (string)e.Value;
_search = "";
await LoadModuleDefinitions();
StateHasChanged();
}
catch (Exception ex)
{
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 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 async Task Reset()
{
try
{
_search = "";
await LoadModuleDefinitions();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error On Reset");
}
}
private void HideModal()
{
_productname = "";
_license = "";
StateHasChanged();
}
private void HideModal()
{
_productname = "";
_packagelicense = "";
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 GetPackage(string packageid, string version)
{
try
{
var package = await PackageService.GetPackageAsync(packageid, version);
if (package != null)
{
_productname = package.Name;
_packageid = package.PackageId;
if (!string.IsNullOrEmpty(package.License))
{
_packagelicense = package.License.Replace("\n", "<br />");
}
_packageversion = 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);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _packageversion, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _packageversion);
AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success);
_productname = "";
_packagelicense = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packageid, _packageversion);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task InstallModules()
{
try
{
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
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 InstallModules()
{
try
{
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Modules");
}
}
}

View File

@ -81,7 +81,7 @@
private List<Template> _templates;
private string _template = "-";
private string[] _versions;
private string _reference = Constants.Version;
private string _reference = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;
@ -151,7 +151,7 @@
var template = _templates.FirstOrDefault(item => item.Name == _template);
_minversion = template.Version;
}
GetLocation();
GetLocation();
}
private void GetLocation()

View File

@ -1,6 +1,10 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService
@inject ILanguageService LanguageService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -43,7 +47,13 @@
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed" ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</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 />
@ -74,7 +84,13 @@
</div>
</div>
</div>
</Section>
</Section>
<br />
<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>
</TabPanel>
<TabPanel Name="Permissions" ResourceKey="Permissions">
<div class="container">
@ -82,107 +98,278 @@
<PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" Permissions="@_permissions" @ref="_permissionGrid" />
</div>
</div>
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="Translations" ResourceKey="Translations">
@if (_languages != null)
{
@if (_languages.Count > 0)
{
<Pager Items="@_languages">
<Header>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th>
<th>@Localizer["Version"]</th>
<th style="width: 1px;">&nbsp;</th>
</Header>
<Row>
<td>@context.Name</td>
<td>@context.Code</td>
<td>@context.Version</td>
<td>
@if (context.IsDefault)
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(_packagename + "." + context.Code))>@SharedLocalizer["Download"]</button>
}
else
{
if (UpgradeAvailable(_packagename + "." + context.Code, context.Version))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await DownloadPackage(_packagename + "." + context.Code))>@SharedLocalizer["Upgrade"]</button>
}
}
</td>
</Row>
</Pager>
<button type="button" class="btn btn-success" @onclick="InstallTranslations">@SharedLocalizer["Install"]</button>
}
else
{
<br />
<div class="mx-auto text-center">
@Localizer["Search.NoResults"]
</div>
<br />
}
}
</TabPanel>
</TabStrip>
<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>
@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(_packagelicense))
{
@((MarkupString)_packagelicense)
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadPackage(_packageid))>@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 int _moduleDefinitionId;
private string _name;
private string _version;
private string _categories;
private string _moduledefinitionname = "";
private string _description = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
private string _license = "";
private string _runtimes = "";
private string _permissions;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private ElementReference form;
private bool validated = false;
private int _moduleDefinitionId;
private string _name;
private string _description = "";
private string _categories;
private string _moduledefinitionname = "";
private string _version;
private string _packagename = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
private string _license = "";
private string _runtimes = "";
private string _permissions;
private string _createdby;
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
#pragma warning disable 649
private PermissionGrid _permissionGrid;
private PermissionGrid _permissionGrid;
#pragma warning restore 649
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private List<Package> _packages;
private List<Language> _languages;
private string _productname = "";
private string _packagelicense = "";
private string _packageid = "";
protected override async Task OnInitializedAsync()
{
try
{
_moduleDefinitionId = Int32.Parse(PageState.QueryString["id"]);
var moduleDefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduleDefinition != null)
{
_name = moduleDefinition.Name;
_version = moduleDefinition.Version;
_categories = moduleDefinition.Categories;
_moduledefinitionname = moduleDefinition.ModuleDefinitionName;
_description = moduleDefinition.Description;
_owner = moduleDefinition.Owner;
_url = moduleDefinition.Url;
_contact = moduleDefinition.Contact;
_license = moduleDefinition.License;
_runtimes = moduleDefinition.Runtimes;
_permissions = moduleDefinition.Permissions;
_createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn;
_modifiedby = moduleDefinition.ModifiedBy;
_modifiedon = moduleDefinition.ModifiedOn;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private async Task SaveModuleDefinition()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
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());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
protected override async Task OnInitializedAsync()
{
try
{
_moduleDefinitionId = Int32.Parse(PageState.QueryString["id"]);
var moduleDefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduleDefinition != null)
{
_name = moduleDefinition.Name;
_description = moduleDefinition.Description;
_categories = moduleDefinition.Categories;
_moduledefinitionname = moduleDefinition.ModuleDefinitionName;
_version = moduleDefinition.Version;
_packagename = moduleDefinition.PackageName;
_owner = moduleDefinition.Owner;
_url = moduleDefinition.Url;
_contact = moduleDefinition.Contact;
_license = moduleDefinition.License;
_runtimes = moduleDefinition.Runtimes;
_permissions = moduleDefinition.Permissions;
_createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn;
_modifiedby = moduleDefinition.ModifiedBy;
_modifiedon = moduleDefinition.ModifiedOn;
_packages = await PackageService.GetPackagesAsync("translation", "", "", _packagename);
_languages = await LanguageService.GetLanguagesAsync(-1, _packagename);
foreach (var package in _packages)
{
var code = package.PackageId.Split('.').Last();
if (!_languages.Any(item => item.Code == code))
{
_languages.Add(new Language { Code = code, Name = CultureInfo.GetCultureInfo(code).DisplayName, Version = package.Version, IsDefault = true });
}
}
_languages = _languages.OrderBy(item => item.Name).ToList();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Load"], MessageType.Error);
}
}
private async Task SaveModuleDefinition()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
try
{
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());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving ModuleDefinition {ModuleDefinitionId} {Error}", _moduleDefinitionId, ex.Message);
AddModuleMessage(Localizer["Error.Module.Save"], MessageType.Error);
}
}
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void HideModal()
{
_productname = "";
_packagelicense = "";
StateHasChanged();
}
private bool UpgradeAvailable(string packagename, string version)
{
var upgradeavailable = false;
if (_packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
}
}
return upgradeavailable;
}
private async Task GetPackage(string packagename)
{
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
try
{
var package = await PackageService.GetPackageAsync(packagename, version);
if (package != null)
{
_productname = package.Name;
if (!string.IsNullOrEmpty(package.License))
{
_packagelicense = package.License.Replace("\n", "<br />");
}
_packageid = package.PackageId;
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", packagename, version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private async Task DownloadPackage(string packagename)
{
try
{
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", packagename, version);
AddModuleMessage(Localizer["Success.Translation.Download"], MessageType.Success);
_productname = "";
_packagelicense = "";
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private async Task InstallTranslations()
{
try
{
await PackageService.InstallPackagesAsync();
AddModuleMessage(string.Format(Localizer["Success.Translation.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Translations");
}
}
}

View File

@ -12,11 +12,32 @@
}
else
{
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" />
<Pager Items="@_moduleDefinitions">
<div class="container">
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
@((MarkupString)"&nbsp;")
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary" />
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">
@foreach (var category in _categories)
{
if (category == _category)
{
<option value="@category" selected>@category @Localizer["Modules"]</option>
}
else
{
<option value="@category">@category @Localizer["Modules"]</option>
}
}
</select>
</div>
</div>
</div>
<Pager Items="@_moduleDefinitions.Where(item => item.Categories.Contains(_category))">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
@ -30,14 +51,14 @@ else
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ModuleDefinitionId.ToString())" ResourceKey="EditModule" /></td>
<td>
@if (context.AssemblyName != "Oqtane.Client")
{
{
<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(context.AssemblyName == "Oqtane.Client" || PageState.Modules.Where(m => m.ModuleDefinition.ModuleDefinitionId == context.ModuleDefinitionId).Count() > 0)
@if(context.AssemblyName == "Oqtane.Client" || PageState.Modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null)
{
<span>@SharedLocalizer["Yes"]</span>
}
@ -50,9 +71,12 @@ else
@((MarkupString)PurchaseLink(context.PackageName))
</td>
<td>
@if (UpgradeAvailable(context.PackageName, context.Version))
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, context.Version))>@SharedLocalizer["Upgrade"]</button>
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadModule(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
}
</td>
</Row>
@ -60,92 +84,99 @@ else
}
@code {
private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages;
private List<ModuleDefinition> _moduleDefinitions;
private List<Package> _packages;
private List<string> _categories = new List<string>();
private string _category = "Common";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
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);
}
}
}
protected override async Task OnParametersSetAsync()
{
try
{
_moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
_packages = await PackageService.GetPackagesAsync("module");
_categories = _moduleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList();
}
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 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 bool UpgradeAvailable(string packagename, string version)
{
var upgradeavailable = false;
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
}
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;
}
}
return upgradeavailable;
}
private async Task DownloadModule(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DownloadModule(string packagename, string version)
{
try
{
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message);
AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error);
}
}
private async Task DeleteModule(ModuleDefinition moduleDefinition)
{
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private async Task DeleteModule(ModuleDefinition moduleDefinition)
{
try
{
await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
private void CategoryChanged(ChangeEventArgs e)
{
_category = (string)e.Value;
StateHasChanged();
}
}

View File

@ -19,7 +19,7 @@
@code {
private string _content = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Export Content";
@ -27,7 +27,7 @@
{
try
{
_content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId);
_content = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId);
AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success);
}
catch (Exception ex)

View File

@ -25,7 +25,7 @@
private ElementReference form;
private bool validated = false;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Import Content";
private async Task ImportModule()
@ -38,7 +38,7 @@
{
try
{
bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, _content);
bool success = await ModuleService.ImportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, _content);
if (success)
{
AddModuleMessage(Localizer["Success.Content.Import"], MessageType.Success);

View File

@ -90,116 +90,125 @@
</form>
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Title => "Module Settings";
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;
private string _containerType;
private string _allPages = "false";
private string _permissionNames = "";
private string _permissions = null;
private string _pageId;
private PermissionGrid _permissionGrid;
private Type _moduleSettingsType;
private object _moduleSettings;
private string _moduleSettingsTitle = "Module Settings";
private RenderFragment ModuleSettingsComponent { get; set; }
private Type _containerSettingsType;
private object _containerSettings;
private RenderFragment ContainerSettingsComponent { get; set; }
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private ElementReference form;
private bool validated = false;
private List<Theme> _themes;
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _title;
private string _containerType;
private string _allPages = "false";
private string _permissionNames = "";
private string _permissions = null;
private string _pageId;
private PermissionGrid _permissionGrid;
private Type _moduleSettingsType;
private object _moduleSettings;
private string _moduleSettingsTitle = "Module Settings";
private RenderFragment ModuleSettingsComponent { get; set; }
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()
{
_title = ModuleState.Title;
_themes = await ThemeService.GetThemesAsync();
_containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.Permissions;
_permissionNames = ModuleState.ModuleDefinition.PermissionNames;
_pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn;
protected override async Task OnInitializedAsync()
{
_title = ModuleState.Title;
_themes = await ThemeService.GetThemesAsync();
_containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.Permissions;
_pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn;
modifiedby = ModuleState.ModifiedBy;
modifiedon = ModuleState.ModifiedOn;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
}
else
{
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
}
if (_moduleSettingsType != null)
{
var moduleobject = Activator.CreateInstance(_moduleSettingsType) as IModuleControl;
if (!string.IsNullOrEmpty(moduleobject.Title))
{
_moduleSettingsTitle = moduleobject.Title;
}
if (ModuleState.ModuleDefinition != null)
{
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
ModuleSettingsComponent = builder =>
{
builder.OpenComponent(0, _moduleSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _moduleSettings = Convert.ChangeType(inst, _moduleSettingsType); });
builder.CloseComponent();
};
}
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType);
}
else
{
// legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module )
_moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true);
}
if (_moduleSettingsType != null)
{
var moduleobject = Activator.CreateInstance(_moduleSettingsType) as IModuleControl;
if (!string.IsNullOrEmpty(moduleobject.Title))
{
_moduleSettingsTitle = moduleobject.Title;
}
var theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
_containerSettingsType = Type.GetType(theme.ContainerSettingsType);
if (_containerSettingsType != null)
{
ContainerSettingsComponent = builder =>
{
builder.OpenComponent(0, _containerSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _containerSettings = Convert.ChangeType(inst, _containerSettingsType); });
builder.CloseComponent();
};
}
}
}
ModuleSettingsComponent = builder =>
{
builder.OpenComponent(0, _moduleSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _moduleSettings = Convert.ChangeType(inst, _moduleSettingsType); });
builder.CloseComponent();
};
}
}
else
{
AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error);
}
private async Task SaveModule()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (!string.IsNullOrEmpty(_title))
{
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 theme = _themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType)));
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
_containerSettingsType = Type.GetType(theme.ContainerSettingsType);
if (_containerSettingsType != null)
{
ContainerSettingsComponent = builder =>
{
builder.OpenComponent(0, _containerSettingsType);
builder.AddComponentReferenceCapture(1, inst => { _containerSettings = Convert.ChangeType(inst, _containerSettingsType); });
builder.CloseComponent();
};
}
}
}
var module = ModuleState;
module.AllPages = bool.Parse(_allPages);
module.Permissions = _permissionGrid.GetPermissions();
await ModuleService.UpdateModuleAsync(module);
private async Task SaveModule()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (!string.IsNullOrEmpty(_title))
{
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.PageModuleId = ModuleState.PageModuleId;
module.Permissions = _permissionGrid.GetPermissions();
await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null)
{

View File

@ -24,7 +24,7 @@
<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)
@foreach (Page page in PageState.Pages)
{
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
}
@ -95,6 +95,12 @@
<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">
@ -156,220 +162,225 @@
</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 = "-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;
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
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.Page.Initialize"], 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;
_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))
{
_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 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 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 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;
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 (string.IsNullOrEmpty(_path))
{
_path = _name;
}
if (_path.Contains("/"))
{
if (_path.EndsWith("/") && _path != "/")
{
_path = _path.Substring(0, _path.Length - 1);
}
_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 (_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;
}
var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (_pages.Any(item => item.Path == page.Path))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
return;
}
if (!PagePathIsUnique(page.Path, page.SiteId, _pageList))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Exists"], _path), MessageType.Warning);
return;
}
if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower()))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), 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 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.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);
@ -413,14 +424,4 @@
NavigationManager.NavigateTo(NavigateUrl());
}
}
private static bool PagePathIsUnique(string pagePath, int siteId, List<Page> existingPages)
{
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);
}
}

View File

@ -25,7 +25,7 @@
<div class="col-sm-9">
<select id="parent" class="form-select" value="@_parentid" @onchange="(e => ParentChanged(e))" required>
<option value="-1">&lt;@Localizer["SiteRoot"]&gt;</option>
@foreach (Page page in _pageList)
@foreach (Page page in PageState.Pages)
{
if (page.PageId != _pageId)
{
@ -102,6 +102,12 @@
<input id="title" class="form-control" @bind="@_title" maxlength="200"/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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">
@ -195,11 +201,11 @@
private List<Theme> _themeList;
private List<ThemeControl> _themes = new List<ThemeControl>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private List<Page> _pageList;
private List<Module> _pageModules;
private int _pageId;
private string _name;
private string _title;
private string _meta;
private string _path;
private string _currentparentid;
private string _parentid = "-1";
@ -231,7 +237,6 @@
{
try
{
_pageList = PageState.Pages;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_themeList = await ThemeService.GetThemesAsync();
_themes = ThemeService.GetThemeControls(_themeList);
@ -243,8 +248,9 @@
{
_name = page.Name;
_title = page.Title;
_meta = page.Meta;
_path = page.Path;
_pageModules = PageState.Modules.Where(m => m.PageId == page.PageId && m.IsDeleted == false).ToList();
_pageModules = PageState.Modules.Where(m => m.PageId == page.PageId).ToList();
if (string.IsNullOrEmpty(_path))
{
@ -313,183 +319,198 @@
_pageModules.RemoveAll(item => item.PageModuleId == pagemodule.PageModuleId);
StateHasChanged();
NavigationManager.NavigateTo(NavigationManager.Uri + "&tab=PageModules");
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {Title} {Error}", module.Title, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error);
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Module {Title} {Error}", module.Title, ex.Message);
AddModuleMessage(Localizer["Error.Module.Delete"], 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))
{
_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);
}
}
}
if (_parentid == _currentparentid)
{
_insert = "=";
}
else
{
_insert = ">>";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
}
}
private async void 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);
}
}
}
if (_parentid == _currentparentid)
{
_insert = "=";
}
else
{
_insert = ">>";
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Child Pages For Parent {PageId} {Error}", _parentid, ex.Message);
AddModuleMessage(Localizer["Error.ChildPage.Load"], MessageType.Error);
}
}
private async void ThemeChanged(ChangeEventArgs e)
{
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 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 void ThemeSettings()
{
_themeSettingsType = null;
if (PageState.QueryString.ContainsKey("cp")) // can only be displayed if invoked from Control Panel
{
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 = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId);
string currentPath = page.Path;
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 = PageState.Pages.FirstOrDefault(item => item.PageId == _pageId);
string currentPath = page.Path;
page.Name = _name;
page.Title = _title;
page.Name = _name;
page.Title = _title;
if (string.IsNullOrEmpty(_path))
{
_path = _name;
}
if (_path.Contains("/"))
{
_path = _path.Substring(_path.LastIndexOf("/") + 1);
}
if (string.IsNullOrEmpty(_path))
{
_path = _name;
}
if (_path.Contains("/"))
{
if (_path.EndsWith("/") && _path != "/")
{
_path = _path.Substring(0, _path.Length - 1);
}
_path = _path.Substring(_path.LastIndexOf("/") + 1);
}
if (_parentid == "-1")
{
page.ParentId = null;
page.Path = Utilities.GetFriendlyUrl(_path);
}
else
{
page.ParentId = Int32.Parse(_parentid);
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
if (_parentid == "-1")
{
page.ParentId = null;
page.Path = Utilities.GetFriendlyUrl(_path);
}
else
{
page.ParentId = Int32.Parse(_parentid);
Page parent = PageState.Pages.FirstOrDefault(item => item.PageId == page.ParentId);
if (parent.Path == string.Empty)
{
page.Path = Utilities.GetFriendlyUrl(parent.Name) + "/" + Utilities.GetFriendlyUrl(_path);
}
else
{
page.Path = parent.Path + "/" + Utilities.GetFriendlyUrl(_path);
}
}
if (!PagePathIsUnique(page.Path, page.SiteId, page.PageId, _pageList))
{
AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning);
return;
}
var _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
if (_pages.Any(item => item.Path == page.Path && item.PageId != page.PageId))
{
AddModuleMessage(string.Format(Localizer["Mesage.Page.PathExists"], _path), MessageType.Warning);
return;
}
if (_insert != "=")
{
Page child;
switch (_insert)
{
case "<<":
page.Order = 0;
break;
case "<":
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid);
if (child != null) page.Order = child.Order - 1;
break;
case ">":
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid);
if (child != null) page.Order = child.Order + 1;
break;
case ">>":
page.Order = int.MaxValue;
break;
}
}
page.IsNavigation = (_isnavigation == null || 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 ?? string.Empty;
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable));
page.UserId = null;
if (page.ParentId == null && Constants.ReservedRoutes.Contains(page.Name.ToLower()))
{
AddModuleMessage(string.Format(Localizer["Message.Page.Reserved"], page.Name), MessageType.Warning);
return;
}
if (_insert != "=")
{
Page child;
switch (_insert)
{
case "<<":
page.Order = 0;
break;
case "<":
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid);
if (child != null) page.Order = child.Order - 1;
break;
case ">":
child = PageState.Pages.FirstOrDefault(item => item.PageId == _childid);
if (child != null) page.Order = child.Order + 1;
break;
case ">>":
page.Order = int.MaxValue;
break;
}
}
page.IsNavigation = (_isnavigation == null || 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 ?? string.Empty;
page.Permissions = _permissionGrid.GetPermissions();
page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable));
page.UserId = null;
page.Meta = _meta;
page = await PageService.UpdatePageAsync(page);
await PageService.UpdatePageOrderAsync(page.SiteId, page.PageId, page.ParentId);
@ -555,9 +576,4 @@
NavigationManager.NavigateTo(NavigateUrl());
}
}
private static bool PagePathIsUnique(string pagePath, int siteId, int pageId, List<Page> existingPages)
{
return !existingPages.Any(page => page.SiteId == siteId && page.Path == pagePath && page.PageId != pageId);
}
}

View File

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

View File

@ -25,7 +25,7 @@
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-info" title="Restore">Restore</button></td>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">Restore</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td>
<td>@context.DeletedBy</td>
@ -56,9 +56,9 @@
<th>@Localizer["DeletedOn"]</th>
</Header>
<Row>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-info" title="Restore">@Localizer["Restore"]</button></td>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@PageState.Pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td>
<td>@context.DeletedBy</td>
<td>@context.DeletedOn</td>
@ -140,6 +140,7 @@
{
try
{
ModuleInstance.ShowProgressIndicator();
foreach (Page page in _pages)
{
await PageService.DeletePageAsync(page.PageId);
@ -148,6 +149,7 @@
await logger.LogInformation("Pages Permanently Deleted");
await Load();
ModuleInstance.HideProgressIndicator();
StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
}
@ -155,6 +157,7 @@
{
await logger.LogError(ex, "Error Permanently Deleting Pages {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
ModuleInstance.HideProgressIndicator();
}
}
@ -204,6 +207,7 @@
{
try
{
ModuleInstance.ShowProgressIndicator();
foreach (Module module in _modules)
{
await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
@ -218,12 +222,14 @@
await logger.LogInformation("Modules Permanently Deleted");
await Load();
ModuleInstance.HideProgressIndicator();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting Modules {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Modules.Delete"], MessageType.Error);
ModuleInstance.HideProgressIndicator();
}
}
}

View File

@ -27,19 +27,25 @@
<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">
<input id="password" type="password" class="form-control" @bind="@_password" autocomplete="new-password" required />
<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">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<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 />
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -62,15 +68,22 @@ else
}
@code {
private string _username = string.Empty;
private ElementReference form;
private bool validated = false;
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()
{
@ -90,9 +103,10 @@ else
{
SiteId = PageState.Site.SiteId,
Username = _username,
DisplayName = (_displayname == string.Empty ? _username : _displayname),
Password = _password,
Email = _email,
Password = _password
DisplayName = (_displayname == string.Empty ? _username : _displayname),
PhotoFileId = null
};
user = await UserService.AddUserAsync(user);
@ -133,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

@ -16,13 +16,19 @@
<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">
<input id="password" type="password" class="form-control" @bind="@_password" required />
<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">
<input id="confirm" type="password" class="form-control" @bind="@_confirm" required />
<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>
@ -36,12 +42,16 @@
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;
protected override async Task OnInitializedAsync()
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{
_username = PageState.QueryString["name"];
@ -110,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

@ -59,7 +59,7 @@ else
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true);
_roles = _roles.Where(item => item.Name != RoleNames.Everyone).ToList();
_roles.RemoveAll(item => item.Name == RoleNames.Everyone || item.Name == RoleNames.Unauthenticated);
}
else
{

View File

@ -53,12 +53,14 @@ else
<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>
@ -93,11 +95,7 @@ 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)
@ -111,8 +109,7 @@ 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)
{

View File

@ -70,6 +70,21 @@
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="homepage" HelpText="Select the home page for the site (to be used if there is no page with a path of '/')" ResourceKey="HomePage">Home Page: </Label>
<div class="col-sm-9">
<select id="homepage" class="form-select" @bind="@_homepageid" required>
<option value="-">&lt;@Localizer["Not Specified"]&gt;</option>
@foreach (Page page in PageState.Pages)
{
if (UserSecurity.ContainsRole(page.Permissions, PermissionNames.View, RoleNames.Everyone))
{
<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="isDeleted" HelpText="Is this site deleted?" ResourceKey="IsDeleted">Deleted? </Label>
@ -120,7 +135,10 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<input id="password" type="password" class="form-control" @bind="@_smtppassword" />
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword">@_togglesmtppassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
@ -129,6 +147,12 @@
<input id="sender" class="form-control" @bind="@_smtpsender" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
</div>
@ -161,24 +185,54 @@
@if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<Section Name="Aliases" Heading="Aliases" ResourceKey="Aliases">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="alias" HelpText="The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="aliases" HelpText="The list of aliases for this site" ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<button type="button" class="btn btn-primary" @onclick="AddAlias">@SharedLocalizer["Add"]</button>
<Pager Items="@_aliases">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["AliasName"]</th>
<th>@Localizer["AliasDefault"]</th>
</Header>
<Row>
@if (context.AliasId != _aliasid)
{
<td>
@if (_aliasid == -1)
{
<button type="button" class="btn btn-primary" @onclick="@(() => EditAlias(context))">@SharedLocalizer["Edit"]</button>
}
</td>
<td>
@if (_aliasid == -1)
{
<ActionDialog Action="Delete" OnClick="@(async () => await DeleteAlias(context))" ResourceKey="DeleteModule" Class="btn btn-danger" Header="Delete Alias" Message="@string.Format(Localizer["Confirm.Alias.Delete", context.Name])" />
}
</td>
<td>@context.Name</td>
<td>@context.IsDefault</td>
}
else
{
<td><button type="button" class="btn btn-success" @onclick="@(async () => await SaveAlias())">@SharedLocalizer["Save"]</button></td>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => await CancelAlias())">@SharedLocalizer["Cancel"]</button></td>
<td>
<input id="aliasname" class="form-control" @bind="@_aliasname" />
</td>
<td>
<select id="defaultaias" class="form-select" @bind="@_defaultalias" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</td>
}
</Row>
</Pager>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultalias" HelpText="The default alias for the site. Requests for non-default aliases will be redirected to the default alias." ResourceKey="DefaultAlias">Default Alias: </Label>
<div class="col-sm-9">
<select id="defaultalias" class="form-select" @bind="@_defaultalias" required>
@foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
{
<option value="@name">@name</option>
}
</select>
</div>
</div>
</div>
</Section>
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
@ -189,6 +243,7 @@
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
<option value="Hybrid">@SharedLocalizer["BlazorHybrid"]</option>
</select>
</div>
</div>
@ -244,8 +299,9 @@
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _name = string.Empty;
private List<Alias> _aliases;
private string _defaultalias = string.Empty;
private string _urls = string.Empty;
private int _aliasid = -1;
private string _aliasname;
private string _defaultalias;
private string _runtime = "";
private string _prerender = "";
private int _logofileid = -1;
@ -255,12 +311,16 @@
private string _themetype = "-";
private string _containertype = "-";
private string _admincontainertype = "-";
private string _homepageid = "-";
private string _smtphost = string.Empty;
private string _smtpport = string.Empty;
private string _smtpssl = "False";
private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty;
private string _smtpsender = string.Empty;
private string _retention = string.Empty;
private string _pwaisenabled;
private int _pwaappiconfileid = -1;
private FileManager _pwaappiconfilemanager;
@ -292,10 +352,7 @@
_prerender = site.RenderMode.Replace(_runtime, "");
_isdeleted = site.IsDeleted.ToString();
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await GetAliases();
}
await GetAliases();
if (site.LogoFileId != null)
{
@ -313,6 +370,11 @@
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
if (site.HomePageId != null)
{
_homepageid = site.HomePageId.Value.ToString();
}
_pwaisenabled = site.PwaIsEnabled.ToString();
if (site.PwaAppIconFileId != null)
{
@ -329,7 +391,9 @@
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@ -393,153 +457,95 @@
{
try
{
if (_name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-")
if (_name != string.Empty && _themetype != "-" && _containertype != "-")
{
var unique = true;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
var site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
{
_urls = Regex.Replace(_urls, @"\r\n?|\n", ","); // convert line breaks to commas
var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
bool refresh = false;
bool reload = false;
site.Name = _name;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var alias = aliases.Where(item => item.Name == name).FirstOrDefault();
if (alias != null && unique)
if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender)
{
unique = (alias.TenantId == PageState.Site.TenantId && alias.SiteId == PageState.Site.SiteId);
site.Runtime = _runtime;
site.RenderMode = _runtime + _prerender;
reload = true; // needs to be reloaded on server
}
}
if (unique && string.IsNullOrEmpty(_defaultalias)) unique = false;
}
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
if (unique)
{
var site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
site.LogoFileId = null;
var logofileid = _logofilemanager.GetFileId();
if (logofileid != -1)
{
bool refresh = false;
bool reload = false;
site.Name = _name;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender)
{
site.Runtime = _runtime;
site.RenderMode = _runtime + _prerender;
reload = true; // needs to be reloaded on server
}
}
site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted));
site.LogoFileId = null;
var logofileid = _logofilemanager.GetFileId();
if (logofileid != -1)
{
site.LogoFileId = logofileid;
}
int? faviconFieldId = _faviconfilemanager.GetFileId();
if (faviconFieldId == -1) faviconFieldId = null;
if (site.FaviconFileId != faviconFieldId)
{
site.FaviconFileId = faviconFieldId;
reload = true; // needs to be reloaded on server
}
if (site.DefaultThemeType != _themetype)
{
site.DefaultThemeType = _themetype;
refresh = true; // needs to be refreshed on client
}
if (site.DefaultContainerType != _containertype)
{
site.DefaultContainerType = _containertype;
refresh = true; // needs to be refreshed on client
}
site.AdminContainerType = _admincontainertype;
if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{
site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
reload = true; // needs to be reloaded on server
}
int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId();
if (pwaappiconfileid == -1) pwaappiconfileid = null;
if (site.PwaAppIconFileId != pwaappiconfileid)
{
site.PwaAppIconFileId = pwaappiconfileid;
reload = true; // needs to be reloaded on server
}
int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
if (pwasplashiconfileid == -1) pwasplashiconfileid = null;
if (site.PwaSplashIconFileId != pwasplashiconfileid)
{
site.PwaSplashIconFileId = pwasplashiconfileid;
reload = true; // needs to be reloaded on server
}
site = await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var names = _urls.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(sValue => sValue.Trim()).ToArray();
foreach (Alias alias in _aliases)
{
if (!names.Contains(alias.Name.Trim()))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
}
}
foreach (string name in names)
{
var alias = _aliases.Find(item => item.Name.Trim() == name);
if (alias == null)
{
alias = new Alias();
alias.Name = name;
alias.TenantId = site.TenantId;
alias.SiteId = site.SiteId;
alias.IsDefault = (name == _defaultalias);
await AliasService.AddAliasAsync(alias);
}
else
{
if (alias.Name != name || alias.IsDefault != (alias.Name.Trim() == _defaultalias))
{
alias.Name = name;
alias.IsDefault = (name == _defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
}
await GetAliases();
}
await logger.LogInformation("Site Settings Saved {Site}", site);
if (refresh || reload)
{
NavigationManager.NavigateTo(NavigateUrl(true), reload); // refresh/reload
}
else
{
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
await interop.ScrollTo(0, 0, "smooth");
}
site.LogoFileId = logofileid;
}
int? faviconFieldId = _faviconfilemanager.GetFileId();
if (faviconFieldId == -1) faviconFieldId = null;
if (site.FaviconFileId != faviconFieldId)
{
site.FaviconFileId = faviconFieldId;
reload = true; // needs to be reloaded on server
}
if (site.DefaultThemeType != _themetype)
{
site.DefaultThemeType = _themetype;
refresh = true; // needs to be refreshed on client
}
if (site.DefaultContainerType != _containertype)
{
site.DefaultContainerType = _containertype;
refresh = true; // needs to be refreshed on client
}
site.AdminContainerType = _admincontainertype;
site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null);
if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{
site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
reload = true; // needs to be reloaded on server
}
int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId();
if (pwaappiconfileid == -1) pwaappiconfileid = null;
if (site.PwaAppIconFileId != pwaappiconfileid)
{
site.PwaAppIconFileId = pwaappiconfileid;
reload = true; // needs to be reloaded on server
}
int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
if (pwasplashiconfileid == -1) pwasplashiconfileid = null;
if (site.PwaSplashIconFileId != pwasplashiconfileid)
{
site.PwaSplashIconFileId = pwasplashiconfileid;
reload = true; // needs to be reloaded on server
}
site = await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await logger.LogInformation("Site Settings Saved {Site}", site);
if (refresh || reload)
{
NavigationManager.NavigateTo(NavigateUrl(true), reload); // refresh/reload
}
else
{
AddModuleMessage(Localizer["Success.Settings.SaveSite"], MessageType.Success);
await interop.ScrollTo(0, 0, "smooth");
}
}
else // deuplicate alias or default alias not specified
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
}
}
else
@ -563,19 +569,19 @@
{
try
{
var sites = await SiteService.GetSitesAsync();
if (sites.Count > 1)
var aliases = await AliasService.GetAliasesAsync();
if (aliases.Any(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId))
{
await SiteService.DeleteSiteAsync(PageState.Site.SiteId);
await logger.LogInformation("Site Deleted {SiteId}", PageState.Site.SiteId);
var aliases = await AliasService.GetAliasesAsync();
foreach (Alias a in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId))
foreach (Alias alias in aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId))
{
await AliasService.DeleteAliasAsync(a.AliasId);
await AliasService.DeleteAliasAsync(alias.AliasId);
}
NavigationManager.NavigateTo(NavigateUrl("admin/sites"));
var redirect = aliases.First(item => item.SiteId != PageState.Site.SiteId || item.TenantId != PageState.Site.TenantId);
NavigationManager.NavigateTo(PageState.Uri.Scheme + "://" + redirect.Name, true);
}
else
{
@ -605,8 +611,10 @@
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User.DisplayName, PageState.User.Email, PageState.User.DisplayName, PageState.User.Email, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
var interop = new Interop(JSRuntime);
await interop.ScrollTo(0, 0, "smooth");
}
catch (Exception ex)
{
@ -616,21 +624,101 @@
}
else
{
AddModuleMessage(Localizer["Message.required.Smtp"], MessageType.Warning);
AddModuleMessage(Localizer["Message.Required.Smtp"], MessageType.Warning);
}
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases()
{
_urls = string.Empty;
_defaultalias = string.Empty;
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
foreach (Alias alias in _aliases)
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_urls += (_urls == string.Empty) ? alias.Name.Trim() : ", " + alias.Name.Trim();
if (alias.IsDefault && string.IsNullOrEmpty(_defaultalias)) _defaultalias = alias.Name.Trim();
_aliases = await AliasService.GetAliasesAsync();
_aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Site.TenantId).OrderBy(item => item.AliasId).ToList();
}
if (string.IsNullOrEmpty(_defaultalias)) _defaultalias = _aliases.First().Name.Trim();
}
private void AddAlias()
{
_aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false });
_aliasid = 0;
_aliasname = "";
_defaultalias = "False";
StateHasChanged();
}
private void EditAlias(Alias alias)
{
_aliasid = alias.AliasId;
_aliasname = alias.Name;
_defaultalias = alias.IsDefault.ToString();
StateHasChanged();
}
private async Task DeleteAlias(Alias alias)
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
await AliasService.DeleteAliasAsync(alias.AliasId);
await GetAliases();
StateHasChanged();
}
}
private async Task SaveAlias()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
if (!string.IsNullOrEmpty(_aliasname))
{
var aliases = await AliasService.GetAliasesAsync();
var alias = aliases.Where(item => item.Name == _aliasname).FirstOrDefault();
bool unique = (alias == null || alias.AliasId == _aliasid);
if (unique)
{
if (_aliasid == 0)
{
alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Site.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) };
await AliasService.AddAliasAsync(alias);
}
else
{
alias = _aliases.Single(item => item.AliasId == _aliasid);
alias.Name = _aliasname;
alias.IsDefault = bool.Parse(_defaultalias);
await AliasService.UpdateAliasAsync(alias);
}
}
else // duplicate alias
{
AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning);
}
}
await GetAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
}
private async Task CancelAlias()
{
await GetAliases();
_aliasid = -1;
_aliasname = "";
StateHasChanged();
}
}

View File

@ -29,7 +29,7 @@ else
</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>
<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)." ResourceKey="Aliases">Aliases: </Label>
<div class="col-sm-9">
<textarea id="alias" class="form-control" @bind="@_urls" rows="3" required></textarea>
</div>
@ -89,7 +89,8 @@ else
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
</select>
<option value="Hybrid">@SharedLocalizer["BlazorHybrid"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
@ -128,25 +129,43 @@ else
<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)
{
if (database.IsDefault)
{
<option value="@database.Name" selected>@Localizer[@database.Name]</option>
}
else
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
}
</select>
</div>
@if (_databases != null)
{
<div class="input-group">
<select id="databaseType" class="form-select" value="@_databaseName" @onchange="(e => DatabaseChanged(e))" required>
@foreach (var database in _databases)
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
</select>
@if (!_showConnectionString)
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionString"]</button>
}
else
{
<button type="button" class="btn btn-secondary" @onclick="ToggleConnectionString">@Localizer["EnterConnectionParameters"]</button>
}
</div>
}
</div>
</div>
if (_databaseConfigType != null)
{
@DatabaseConfigComponent;
}
@if (!_showConnectionString)
{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent
}
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">String:</Label>
<div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div>
</div>
}
<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">
@ -156,7 +175,7 @@ else
<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" required />
<input id="hostPassword" type="password" class="form-control" @bind="@_hostpassword" autocomplete="new-password" required />
</div>
</div>
}
@ -169,153 +188,171 @@ else
}
@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<Database> _databases;
private ElementReference form;
private bool validated = false;
private string _databaseName;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _connectionString = string.Empty;
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 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 _tenantName = string.Empty;
private string _hostusername = string.Empty;
private string _hostpassword = 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";
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;
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();
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();
}
_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;
private void DatabaseChanged(ChangeEventArgs eventArgs)
{
try
{
_databaseName = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
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 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 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 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 != "-")
{
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);
}
}
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 (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 = await UserService.LoginUserAsync(user, false, false);
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, false, false);
if (user.IsAuthenticated)
{
var connectionString = String.Empty;
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var connectionString = String.Empty;
if (_showConnectionString)
{
connectionString = _connectionString;
}
else
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionString = databaseConfigControl.GetConnectionString();
}
}
if (connectionString != "")
{
@ -396,4 +433,13 @@ else
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private void ToggleConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionString = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
}

View File

@ -27,9 +27,33 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="serverpath" HelpText="Server Path" ResourceKey="ServerPath">Server Path: </Label>
<Label Class="col-sm-3" For="machinename" HelpText="Machine Name" ResourceKey="MachineName">Machine Name: </Label>
<div class="col-sm-9">
<input id="serverpath" class="form-control" @bind="@_serverpath" readonly />
<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="environment" HelpText="Environment name" ResourceKey="Environment">Environment: </Label>
<div class="col-sm-9">
<input id="environment" class="form-control" @bind="@_environment" 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">
@ -38,6 +62,18 @@
<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">
@ -69,6 +105,21 @@
<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>
@ -97,6 +148,7 @@
<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>
<br /><br />
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -104,12 +156,19 @@
private string _version = string.Empty;
private string _clrversion = string.Empty;
private string _osversion = string.Empty;
private string _serverpath = string.Empty;
private string _machinename = string.Empty;
private string _ipaddress = string.Empty;
private string _environment = string.Empty;
private string _contentrootpath = string.Empty;
private string _webrootpath = string.Empty;
private string _servertime = string.Empty;
private string _tickcount = string.Empty;
private string _workingset = string.Empty;
private string _installationid = string.Empty;
private string _detailederrors = string.Empty;
private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty;
private string _swagger = string.Empty;
private string _packageservice = string.Empty;
@ -117,31 +176,43 @@
{
_version = Constants.Version;
Dictionary<string, string> systeminfo = await SystemService.GetSystemInfoAsync();
Dictionary<string, object> systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null)
{
_clrversion = systeminfo["clrversion"];
_osversion = systeminfo["osversion"];
_serverpath = systeminfo["serverpath"];
_servertime = systeminfo["servertime"] + " UTC";
_installationid = systeminfo["installationid"];
_clrversion = systeminfo["CLRVersion"].ToString();
_osversion = systeminfo["OSVersion"].ToString();
_machinename = systeminfo["MachineName"].ToString();
_ipaddress = systeminfo["IPAddress"].ToString();
_environment = systeminfo["Environment"].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";
}
_detailederrors = systeminfo["detailederrors"];
_logginglevel = systeminfo["logginglevel"];
_swagger = systeminfo["swagger"];
_packageservice = systeminfo["packageservice"];
}
}
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()
{
try
{
var settings = new Dictionary<string, string>();
settings.Add("detailederrors", _detailederrors);
settings.Add("logginglevel", _logginglevel);
settings.Add("swagger", _swagger);
settings.Add("packageservice", _packageservice);
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);
}

View File

@ -35,6 +35,7 @@
<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)(!string.IsNullOrEmpty(context.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(context.PackageUrl).Host + "</strong>" : ""))
@((MarkupString)(context.TrialPeriod > 0 ? "&nbsp;&nbsp;|&nbsp;&nbsp;<strong>" + context.TrialPeriod + " " + @SharedLocalizer["Trial"] + "</strong>" : ""))
</td>
<td style="width: 1px; vertical-align: middle;">
@ -113,6 +114,10 @@
<button type="button" class="btn btn-success" @onclick="InstallThemes">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
@code {
private List<Package> _packages;
private string _price = "free";

View File

@ -72,7 +72,7 @@
private List<Template> _templates;
private string _template = "-";
private string[] _versions;
private string _reference = Constants.Version;
private string _reference = "local";
private string _minversion = "2.0.0";
private string _location = string.Empty;

View File

@ -40,9 +40,12 @@ else
@((MarkupString)PurchaseLink(context.PackageName))
</td>
<td>
@if (UpgradeAvailable(context.PackageName, context.Version))
@{
var version = UpgradeAvailable(context.PackageName, context.Version);
}
@if (version != context.Version)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, context.Version))>@SharedLocalizer["Upgrade"]</button>
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadTheme(context.PackageName, version))>@SharedLocalizer["Upgrade"]</button>
}
</td>
<td></td>
@ -94,18 +97,17 @@ else
return link;
}
private bool UpgradeAvailable(string packagename, string version)
{
var upgradeavailable = false;
if (!string.IsNullOrEmpty(packagename) && _packages != null)
{
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null)
{
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
}
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 upgradeavailable;
return version;
}
private async Task DownloadTheme(string packagename, string version)

View File

@ -25,7 +25,13 @@
<input id="version" class="form-control" @bind="@_version" disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed" ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
</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 />
@ -53,28 +59,30 @@
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@code {
private string _themeName = "";
private string _name;
private string _version;
private string _owner = "";
private string _url = "";
private string _contact = "";
private string _license = "";
private string _themeName = "";
private string _name;
private string _version;
private string _packagename;
private string _owner = "";
private string _url = "";
private string _contact = "";
private string _license = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_themeName = WebUtility.UrlDecode(PageState.QueryString["name"]);
var themes = await ThemeService.GetThemesAsync();
var theme = themes.FirstOrDefault(item => item.ThemeName == _themeName);
if (theme != null)
{
_name = theme.Name;
_version = theme.Version;
_owner = theme.Owner;
protected override async Task OnInitializedAsync()
{
try
{
_themeName = WebUtility.UrlDecode(PageState.QueryString["name"]);
var themes = await ThemeService.GetThemesAsync();
var theme = themes.FirstOrDefault(item => item.ThemeName == _themeName);
if (theme != null)
{
_name = theme.Name;
_version = theme.Version;
_packagename = theme.PackageName;
_owner = theme.Owner;
_url = theme.Url;
_contact = theme.Contact;
_license = theme.License;

View File

@ -13,7 +13,7 @@
<Label Class="col-sm-3" For="to" HelpText="Enter the username you wish to send a message to" ResourceKey="To">To: </Label>
<div class="col-sm-9">
<input id="to" class="form-control" @bind="@username" />
</div> >
</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>
@ -49,7 +49,7 @@
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);
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());

View File

@ -31,16 +31,34 @@ else
</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">
<input id="password" type="password" class="form-control" @bind="@password" autocomplete="new-password" />
<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">
<input id="confirm" type="password" class="form-control" @bind="@confirm" autocomplete="new-password" />
<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">
@ -142,7 +160,7 @@ 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>
@ -175,7 +193,7 @@ 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>
@ -193,6 +211,8 @@ else
</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-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option>
@ -201,11 +221,16 @@ else
}
</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;
@ -224,9 +249,17 @@ else
{
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;
@ -280,11 +313,12 @@ 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 = filemanager.GetFileId();
@ -292,12 +326,23 @@ else
{
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");
NavigationManager.NavigateTo(NavigateUrl());
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
}
else
{
@ -380,4 +425,51 @@ else
StateHasChanged();
}
private async Task DeleteAllNotifications()
{
try
{
ModuleInstance.ShowProgressIndicator();
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();
ModuleInstance.HideProgressIndicator();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting Notifications {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
ModuleInstance.HideProgressIndicator();
}
}
private void TogglePassword()
{
if (_passwordtype == "password")
{
_passwordtype = "text";
_togglepassword = SharedLocalizer["HidePassword"];
}
else
{
_passwordtype = "password";
_togglepassword = SharedLocalizer["ShowPassword"];
}
}
}

View File

@ -20,14 +20,20 @@
</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">
<input id="password" type="password" class="form-control" @bind="@password" />
<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">
<input id="confirm" type="password" class="form-control" @bind="@confirm" />
<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">
@ -88,7 +94,9 @@
@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;
@ -102,6 +110,7 @@
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
settings = new Dictionary<string, string>();
}
@ -119,9 +128,9 @@
{
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 = await UserService.GetUserAsync(username, PageState.Site.SiteId);
if (user == null)
@ -129,7 +138,7 @@
user = new User();
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;
@ -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

@ -29,14 +29,20 @@ else
</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">
<input id="password" type="password" class="form-control" @bind="@password" />
<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">
<input id="confirm" type="password" class="form-control" @bind="@confirm" />
<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">
@ -66,8 +72,19 @@ else
</select>
</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">
@ -131,61 +148,70 @@ else
<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 File photo = null;
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 OnParametersSetAsync()
{
try
{
// OnParametersSetAsync is called when the edit modal is closed - in which case there is no id parameter
if (PageState.QueryString.ContainsKey("id"))
{
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;
}
settings = await SettingService.GetUserSettingsAsync(user.UserId);
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private string deletedby;
private DateTime? deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
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;
isdeleted = user.IsDeleted.ToString();
}
}
}
@ -205,12 +231,12 @@ 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;
@ -268,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

@ -7,7 +7,7 @@
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (userroles == null)
@if (users == null)
{
<p>
<em>@SharedLocalizer["Loading"]</em>
@ -30,12 +30,14 @@ else
</div>
</div>
</div>
<Pager Items="@userroles">
<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["Name"]</th>
<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>
@ -47,21 +49,299 @@ else
<td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" />
</td>
<td>@context.User.DisplayName</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">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowregistration" HelpText="Do you want to allow visitors to be able to register for a user account on the site" ResourceKey="AllowRegistration">Allow User Registration? </Label>
<div class="col-sm-9">
<select id="allowregistration" class="form-select" @bind="@_allowregistration" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<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>
</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>
@ -70,79 +350,170 @@ else
}
@code {
private List<UserRole> allroles;
private List<UserRole> userroles;
private string _search;
private List<UserRole> allusers;
private List<UserRole> users;
private string _search = "";
private string _allowregistration;
private string _allowsitelogin;
private string _twofactor;
private string _cookiename;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private string _minimumlength;
private string _uniquecharacters;
private string _requiredigit;
private string _requireupper;
private string _requirelower;
private string _requirepunctuation;
private string _maximumfailures;
private string _lockoutduration;
protected override async Task OnInitializedAsync()
{
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
await LoadSettingsAsync();
userroles = Search(_search);
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 string _secret;
private string _secrettype = "password";
private string _togglesecret = string.Empty;
private string _issuer;
private string _audience;
private string _lifetime;
private string _token;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync()
{
await LoadUserSettingsAsync();
await LoadUsersAsync(true);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowregistration = PageState.Site.AllowRegistration.ToString();
}
_allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true");
private List<UserRole> Search(string search)
{
var results = allroles.Where(item => item.Role.Name == RoleNames.Registered || (item.Role.Name == RoleNames.Host && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)));
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false");
_cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application");
if (!string.IsNullOrEmpty(_search))
{
results = results.Where(item =>
(
item.User.Username.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(search, StringComparison.OrdinalIgnoreCase)
)
);
}
return results.ToList();
}
_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");
private async Task OnSearch()
{
userroles = Search(_search);
await UpdateSettingsAsync();
}
_maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
_lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
private async Task DeleteUser(UserRole UserRole)
{
try
{
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null)
{
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User);
allroles = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId);
userroles = Search(_search);
StateHasChanged();
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error);
}
}
_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");
private string settingSearch = "AU-search";
_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 LoadSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, "");
}
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();
}
}
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);
}
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()
{
@ -151,7 +522,58 @@ else
var site = PageState.Site;
site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
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)
{
@ -160,4 +582,58 @@ else
}
}
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

@ -88,15 +88,17 @@ else
userid = Int32.Parse(PageState.QueryString["id"]);
User user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
name = user.DisplayName;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId, true);
roles = roles.Where(item => item.Name != RoleNames.Everyone).ToList();
roles.RemoveAll(item => item.Name == RoleNames.Everyone || item.Name == RoleNames.Unauthenticated);
}
else
{
roles = await RoleService.GetRolesAsync(PageState.Site.SiteId);
}
await GetUserRoles();
}
catch (Exception ex)
@ -110,8 +112,7 @@ 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)
{

View File

@ -7,69 +7,73 @@
@inject IStringLocalizer<Detail> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<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>
@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="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<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;
@ -108,6 +112,7 @@
_user = user.DisplayName;
}
}
_initialized = true;
}
else
{
@ -120,4 +125,9 @@
AddModuleMessage(Localizer["Error.LoadVisitor"], MessageType.Error);
}
}
}
private string CloseUrl()
{
return (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : NavigateUrl();
}
}

View File

@ -17,13 +17,13 @@ else
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-6">
<select id="type" class="form-select custom-select" @onchange="(e => TypeChanged(e))">
<option value="false">@Localizer["AllVisitors"]</option>
<option value="true">@Localizer["UsersOnly"]</option>
<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="type" class="form-select custom-select" @onchange="(e => DateChanged(e))">
<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>
@ -32,7 +32,7 @@ else
</div>
</div>
<br/>
<Pager Items="@_visitors">
<Pager Items="@_visitors" CurrentPage="@_page.ToString()" OnPageChange="OnPageChange">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["IP"]</th>
@ -43,7 +43,7 @@ else
<th>@Localizer["Created"]</th>
</Header>
<Row>
<td><ActionLink Action="Detail" Parameters="@($"id=" + context.VisitorId.ToString())" ResourceKey="Details" /></td>
<td><ActionLink Action="Detail" Parameters="@($"id={context.VisitorId}")" ReturnUrl="@(NavigateUrl(PageState.Page.Path, $"type={_type}&days={_days}&page={_page}"))" ResourceKey="Details" /></td>
<td>@context.IPAddress</td>
<td>
@if (context.UserId != null)
@ -70,7 +70,7 @@ else
</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 which identify visitors which should not be tracked (ie. bots)" ResourceKey="Filter">Filter: </Label>
<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>
@ -81,6 +81,15 @@ else
<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>
@ -89,28 +98,46 @@ else
}
@code {
private bool _users = false;
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();
_filter = SettingService.GetSetting(PageState.Site.Settings, "VisitorFilter", "");
_retention = SettingService.GetSetting(PageState.Site.Settings, "VisitorRetention", "30");
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
{
_users = bool.Parse(e.Value.ToString());
_type = e.Value.ToString();
await GetVisitors();
StateHasChanged();
}
@ -120,7 +147,7 @@ else
}
}
private async void DateChanged(ChangeEventArgs e)
private async void DaysChanged(ChangeEventArgs e)
{
try
{
@ -137,7 +164,7 @@ else
private async Task GetVisitors()
{
_visitors = await VisitorService.GetVisitorsAsync(PageState.Site.SiteId, DateTime.UtcNow.AddDays(-_days));
if (_users)
if (_type == "users")
{
_visitors = _visitors.Where(item => item.UserId != null).ToList();
}
@ -154,6 +181,7 @@ else
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);
@ -164,4 +192,9 @@ else
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
private void OnPageChange(int page)
{
_page = page;
}
}

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Controls
@using System.Net
@inherits LocalizableComponent
@inject IUserService UserService
@ -71,6 +72,9 @@
[Parameter]
public bool IconOnly { get; set; } // optional - specifies only icon in link
[Parameter]
public string ReturnUrl { get; set; } // optional - used to set a url to redirect to
protected override void OnParametersSet()
{
base.OnParametersSet();
@ -116,9 +120,13 @@
}
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions;
_text = Localize(nameof(Text), _text);
_url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters);
_authorized = IsAuthorized();
_text = Localize(nameof(Text), _text);
_url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters);
if (!string.IsNullOrEmpty(ReturnUrl))
{
_url += ((_url.Contains("?")) ? "&" : "?") + $"returnurl={WebUtility.UrlEncode(ReturnUrl)}";
}
_authorized = IsAuthorized();
}
private bool IsAuthorized()

View File

@ -52,7 +52,7 @@
<input type="file" id="@_fileinputid" name="file" accept="@_filter" />
}
</div>
<div class="col mt-2 text-center">
<div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1)
{
@ -86,315 +86,352 @@
}
@code {
private string _id;
private List<Folder> _folders;
private List<File> _files = new List<File>();
private string _fileinputid = string.Empty;
private string _progressinfoid = string.Empty;
private string _progressbarid = string.Empty;
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;
private string _id;
private List<Folder> _folders;
private List<File> _files = new List<File>();
private string _fileinputid = string.Empty;
private string _progressinfoid = string.Empty;
private string _progressbarid = string.Empty;
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;
[Parameter]
public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility
[Parameter]
public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility
[Parameter]
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 default folder by folderid
[Parameter]
public string Folder { get; set; } = ""; // optional - for setting a specific default folder by folder path
[Parameter]
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 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 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
[Parameter]
public bool ShowFiles { get; set; } = true; // optional - for indicating whether a list of files should be displayed - default is true
[Parameter]
public bool ShowUpload { get; set; } = true; // optional - for indicating whether a Upload controls should be displayed - default is true
[Parameter]
public bool ShowUpload { get; set; } = true; // optional - for indicating whether a Upload controls should be displayed - default is true
[Parameter]
public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true
[Parameter]
public bool ShowFolders { get; set; } = true; // optional - for indicating whether a list of folders should be displayed - default is true
[Parameter]
public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true
[Parameter]
public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true
[Parameter]
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 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 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> 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> 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
[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))
{
_id = Id;
}
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Id))
{
_id = Id;
}
// packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder)
{
ShowFiles = false;
ShowFolders = false;
Filter = "nupkg";
ShowSuccess = true;
}
// packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder)
{
ShowFiles = false;
ShowFolders = false;
Filter = "nupkg";
ShowSuccess = true;
}
if (!ShowFiles)
{
ShowImage = false;
}
if (!ShowFiles)
{
ShowImage = false;
}
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
_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 (!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)
{
File file = await FileService.GetFileAsync(FileId);
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;
}
}
if (FileId != -1)
{
File file = await FileService.GetFileAsync(FileId);
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();
await SetImage();
if (!string.IsNullOrEmpty(Filter))
{
_filter = "." + Filter.Replace(",", ",.");
}
if (!string.IsNullOrEmpty(Filter))
{
_filter = "." + Filter.Replace(",", ",.");
}
await GetFiles();
await GetFiles();
// create unique id for component
_guid = Guid.NewGuid().ToString("N");
_fileinputid = _guid + "FileInput";
_progressinfoid = _guid + "ProgressInfo";
_progressbarid = _guid + "ProgressBar";
}
// create unique id for component
_guid = Guid.NewGuid().ToString("N");
_fileinputid = _guid + "FileInput";
_progressinfoid = _guid + "ProgressInfo";
_progressbarid = _guid + "ProgressBar";
}
private async Task GetFiles()
{
_haseditpermission = false;
if (Folder == Constants.PackagesFolder)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
_files = new List<File>();
}
else
{
Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId);
if (folder != null)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.Permissions);
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
_haseditpermission = false;
_files = new List<File>();
}
}
if (_filter != "*")
{
List<File> filtered = new List<File>();
foreach (File file in _files)
{
if (_filter.ToUpper().IndexOf("." + file.Extension.ToUpper()) != -1)
{
filtered.Add(file);
}
}
_files = filtered;
}
}
private async Task GetFiles()
{
_haseditpermission = false;
if (Folder == Constants.PackagesFolder)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
_files = new List<File>();
}
else
{
Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId);
if (folder != null)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.Permissions);
_files = await FileService.GetFilesAsync(FolderId);
}
else
{
_haseditpermission = false;
_files = new List<File>();
}
}
if (_filter != "*")
{
List<File> filtered = new List<File>();
foreach (File file in _files)
{
if (_filter.ToUpper().IndexOf("." + file.Extension.ToUpper()) != -1)
{
filtered.Add(file);
}
}
_files = filtered;
}
}
private async Task FolderChanged(ChangeEventArgs e)
{
_message = string.Empty;
try
{
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.File.Load"];
_messagetype = MessageType.Error;
}
}
private async Task FolderChanged(ChangeEventArgs e)
{
_message = string.Empty;
try
{
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.File.Load"];
_messagetype = MessageType.Error;
}
}
private async Task FileChanged(ChangeEventArgs e)
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
if (FileId != -1)
{
await OnSelect.InvokeAsync(FileId);
}
private async Task FileChanged(ChangeEventArgs e)
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
if (FileId != -1)
{
await OnSelect.InvokeAsync(FileId);
}
await SetImage();
StateHasChanged();
}
await SetImage();
StateHasChanged();
}
private async Task SetImage()
{
_image = string.Empty;
_file = null;
if (FileId != -1)
{
_file = await FileService.GetFileAsync(FileId);
if (_file != null && ShowImage && _file.ImageHeight != 0 && _file.ImageWidth != 0)
{
var maxwidth = 200;
var maxheight = 200;
private async Task SetImage()
{
_image = string.Empty;
_file = null;
if (FileId != -1)
{
_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 ratio = ratioX < ratioY ? ratioX : ratioY;
var ratioX = (double)maxwidth / (double)_file.ImageWidth;
var ratioY = (double)maxheight / (double)_file.ImageHeight;
var ratio = ratioX < ratioY ? ratioX : ratioY;
_image = "<img src=\"" + _file.Url + "\" 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() + "\" />";
}
}
}
private async Task UploadFile()
{
_message = string.Empty;
var interop = new Interop(JSRuntime);
var upload = await interop.GetFiles(_fileinputid);
if (upload.Length > 0)
{
try
{
string result;
if (Folder == Constants.PackagesFolder)
{
result = await FileService.UploadFilesAsync(Folder, upload, _guid);
}
else
{
result = await FileService.UploadFilesAsync(FolderId, upload, _guid);
}
private async Task UploadFile()
{
_message = string.Empty;
var interop = new Interop(JSRuntime);
var upload = await interop.GetFiles(_fileinputid);
if (upload.Length > 0)
{
string restricted = "";
foreach (var file in upload)
{
var extension = (file.LastIndexOf(".") != -1) ? file.Substring(file.LastIndexOf(".") + 1) : "";
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
{
restricted += (restricted == "" ? "" : ",") + extension;
}
}
if (restricted == "")
{
try
{
string result;
if (Folder == Constants.PackagesFolder)
{
result = await FileService.UploadFilesAsync(Folder, upload, _guid);
}
else
{
result = await FileService.UploadFilesAsync(FolderId, upload, _guid);
}
if (result == string.Empty)
{
await logger.LogInformation("File Upload Succeeded {Files}", upload);
if (ShowSuccess)
{
_message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success;
}
if (result == string.Empty)
{
await logger.LogInformation("File Upload Succeeded {Files}", upload);
if (ShowSuccess)
{
_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();
}
else
{
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", "));
// 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();
}
else
{
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", "));
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
else
{
_message = Localizer["Message.File.NotSelected"];
_messagetype = MessageType.Warning;
}
}
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
}
else
{
_message = string.Format(Localizer["Message.File.Restricted"], restricted);
_messagetype = MessageType.Warning;
}
}
else
{
_message = Localizer["Message.File.NotSelected"];
_messagetype = MessageType.Warning;
}
}
private async Task DeleteFile()
{
_message = string.Empty;
try
{
await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId);
await OnDelete.InvokeAsync(FileId);
private async Task DeleteFile()
{
_message = string.Empty;
try
{
await FileService.DeleteFileAsync(FileId);
await logger.LogInformation("File Deleted {File}", FileId);
await OnDelete.InvokeAsync(FileId);
_message = Localizer["Success.File.Delete"];
_messagetype = MessageType.Success;
_message = Localizer["Success.File.Delete"];
_messagetype = MessageType.Success;
await GetFiles();
FileId = -1;
await SetImage();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
await GetFiles();
FileId = -1;
await SetImage();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message);
_message = Localizer["Error.File.Delete"];
_messagetype = MessageType.Error;
}
}
_message = Localizer["Error.File.Delete"];
_messagetype = MessageType.Error;
}
}
public int GetFileId() => FileId;
public int GetFileId() => FileId;
public int GetFolderId() => FolderId;
public int GetFolderId() => FolderId;
public File GetFile() => _file;
public File GetFile() => _file;
public async Task Refresh()
{
await Refresh(-1);
}
public async Task Refresh(int fileId)
{
await GetFiles();
if (fileId != -1)
{
var file = _files.Where(item => item.FileId == fileId).FirstOrDefault();
if (file != null)
{
FileId = file.FileId;
await SetImage();
}
}
StateHasChanged();
}
}

View File

@ -1,13 +1,13 @@
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Oqtane.Shared;
namespace Oqtane.Modules.Controls
{
public class LocalizableComponent : ModuleControlBase
{
[Inject] public IStringLocalizerFactory LocalizerFactory { get; set; }
private IStringLocalizer _localizer;
[Parameter]
@ -30,22 +30,15 @@ namespace Oqtane.Modules.Controls
var key = $"{ResourceKey}.{propertyName}";
var value = Localize(key);
if (value == key)
if (value == key || value == String.Empty)
{
// Returns default property value (English version) instead of ResourceKey.PropertyName
// return default property value if key does not exist in resource file or value is empty
return propertyValue;
}
else
{
if (value == String.Empty)
{
// Returns default property value (English version)
return propertyValue;
}
else
{
return value;
}
// return localized value
return value;
}
}
@ -53,24 +46,15 @@ namespace Oqtane.Modules.Controls
{
IsLocalizable = false;
if (string.IsNullOrEmpty(ResourceType))
if (String.IsNullOrEmpty(ResourceType))
{
ResourceType = ModuleState?.ModuleType;
}
if (!String.IsNullOrEmpty(ResourceKey) && !string.IsNullOrEmpty(ResourceType))
if (!String.IsNullOrEmpty(ResourceKey) && !String.IsNullOrEmpty(ResourceType))
{
var moduleType = Type.GetType(ResourceType);
if (moduleType != null)
{
using (var scope = ServiceActivator.GetScope())
{
var localizerFactory = scope.ServiceProvider.GetService<IStringLocalizerFactory>();
_localizer = localizerFactory.Create(moduleType);
IsLocalizable = true;
}
}
_localizer = LocalizerFactory.Create(ResourceType);
IsLocalizable = true;
}
}
}

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@inject IStringLocalizerFactory LocalizerFactory
@typeparam TableItem
@if (ItemList != null)
@ -48,7 +49,7 @@
<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>
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li>
</ul>
}
@ -57,12 +58,12 @@
<div class="table-responsive">
<table class="@Class">
<thead>
<tr>@Header</tr>
<tr class="@RowClass">@Header</tr>
</thead>
<tbody>
@foreach (var item in ItemList)
{
<tr>@Row(item)</tr>
<tr class="@RowClass">@Row(item)</tr>
@if (Detail != null)
{
<tr>@Detail(item)</tr>
@ -75,28 +76,37 @@
@if (Format == "Grid" && Row != null)
{
int count = 0;
if (ItemList != null)
int rows = 0;
int cols = 0;
if (ItemList != null)
{
count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns;
if (_columns == 0)
{
count = ItemList.Count();
rows = 1;
cols = count;
}
else
{
count = (int)Math.Ceiling(ItemList.Count() / (decimal)_columns) * _columns;
rows = count / _columns;
cols = _columns;
}
}
<div class="@Class">
@if (Header != null)
@for (int row = 0; row < rows; row++)
{
<div class="row"><div class="col">@Header</div></div>
}
@for (int row = 0; row < (count / _columns); row++)
{
<div class="row">
@for (int col = 0; col < _columns; col++)
<div class="@RowClass">
@for (int col = 0; col < cols; col++)
{
int index = (row * _columns) + col;
if (index < ItemList.Count())
{
<div class="col">@Row(ItemList.ElementAt(index))</div>
<div class="@ColumnClass">@Row(ItemList.ElementAt(index))</div>
}
else
{
<div class="col">&nbsp;</div>
<div>&nbsp;</div>
}
}
</div>
@ -147,58 +157,73 @@
<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>
<a class="page-link" style="white-space: nowrap;">@Localizer["PageOfPages", _page, _pages]</a>
</li>
</ul>
}
}
@code {
private int _pages = 0;
private int _page = 1;
private int _maxItems = 10;
private int _displayPages = 5;
private int _startPage = 0;
private int _endPage = 0;
private int _columns = 1;
private IStringLocalizer Localizer;
private int _pages = 0;
private int _page = 1;
private int _maxItems = 10;
private int _displayPages = 5;
private int _startPage = 0;
private int _endPage = 0;
private int _columns = 0;
[Parameter]
public string Format { get; set; } // Table or Grid
[Parameter]
public string Format { get; set; } // Table or Grid
[Parameter]
public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter]
public string Toolbar { get; set; } // Top, Bottom or Both
[Parameter]
public RenderFragment Header { get; set; } = null;
[Parameter]
public RenderFragment Header { get; set; } = null; // only applicable to Table layouts
[Parameter]
public RenderFragment<TableItem> Row { get; set; } = null;
[Parameter]
public RenderFragment<TableItem> Row { get; set; } = null; // required
[Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter]
public RenderFragment<TableItem> Detail { get; set; } = null; // only applicable to Table layouts
[Parameter]
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter]
public IEnumerable<TableItem> Items { get; set; } // the IEnumerable data source
[Parameter]
public string PageSize { get; set; } // number of items to display on a page
[Parameter]
public string PageSize { get; set; } // number of items to display on a page
[Parameter]
public string Columns { get; set; } // only applicable to Grid layouts
[Parameter]
public string Columns { get; set; } // only applicable to Grid layouts - default is zero indicating use responsive behavior
[Parameter]
public string CurrentPage { get; set; } // optional property to set the initial page to display
[Parameter]
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 DisplayPages { get; set; } // maximum number of page numbers to display for user selection
[Parameter]
public string Class { get; set; }
[Parameter]
public string Class { get; set; } // class for the containing element - ie. <table> for Table or <div> for Grid
private IEnumerable<TableItem> ItemList { get; set; }
[Parameter]
public string RowClass { get; set; } // class for row element - ie. <tr> for Table or <div> for Grid
protected override void OnParametersSet()
{
[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; }
protected override void OnInitialized()
{
Localizer = LocalizerFactory.Create(GetType().FullName);
}
protected override void OnParametersSet()
{
if (string.IsNullOrEmpty(Format))
{
Format = "Table";
@ -217,11 +242,35 @@
}
else
{
Class = "container-fluid px-0";
Class = "container-fluid";
}
}
if (!string.IsNullOrEmpty(PageSize))
if (string.IsNullOrEmpty(RowClass))
{
if (Format == "Table")
{
RowClass = "";
}
else
{
RowClass = "row";
}
}
if (string.IsNullOrEmpty(ColumnClass))
{
if (Format == "Table")
{
ColumnClass = "";
}
else
{
ColumnClass = "col";
}
}
if (!string.IsNullOrEmpty(PageSize))
{
_maxItems = int.Parse(PageSize);
}
@ -268,6 +317,7 @@
{
_endPage = _pages;
}
OnPageChange?.Invoke(_page);
StateHasChanged();
}

View File

@ -100,160 +100,172 @@
}
@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, true);
if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles.RemoveAll(item => item.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["Message.Username.DontExist"];
}
}
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.Unauthenticated); // remove deny unauthenticated
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

@ -5,230 +5,276 @@
<div class="row" style="margin-bottom: 50px;">
<div class="col">
<TabStrip>
<TabPanel Name="Rich" Heading="Rich Text Editor">
@if (AllowFileManagement)
{
@if (_filemanagervisible)
{
<FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<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;")
<button type="button" class="btn btn-secondary" @onclick="CloseFileManager">@Localizer["Close"]</button>
}
</div>
}
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement">
</div>
</div>
</div>
</TabPanel>
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
<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>
}
else
{
<textarea class="form-control" placeholder="@Placeholder" @bind="@_content" rows="10"></textarea>
}
</TabPanel>
<TabPanel Name="Rich" Heading="Rich Text Editor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowRawHtml)
{
<button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["SynchronizeContent"]</button>@((MarkupString)"&nbsp;&nbsp;")
}
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement">
</div>
</div>
</div>
</TabPanel>
@if (AllowRawHtml)
{
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager)
{
<FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
<button type="button" class="btn btn-secondary" @onclick="RefreshRawHtml">@Localizer["SynchronizeContent"]</button>&nbsp;&nbsp;
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
}
@if (_rawfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRawFileManager">@Localizer["Close"]</button>
}
</div>
@if (ReadOnly)
{
<textarea id="rawhtmleditor" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10" readonly></textarea>
}
else
{
<textarea id="rawhtmleditor" class="form-control" placeholder="@Placeholder" @bind="@_rawhtml" rows="10"></textarea>
}
</TabPanel>
}
</TabStrip>
</div>
</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 _richfilemanager = false;
private FileManager _fileManager;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
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; }
[Parameter]
public bool AllowFileManagement { get; set; } = true;
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public bool AllowRawHtml { get; set; } = true;
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
[Parameter]
public string DebugLevel { get; set; } = "info";
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public bool AllowFileManagement { get; set; } = true;
[Parameter]
public string DebugLevel { get; set; } = "info";
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.7.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 async Task OnParametersSetAsync()
{
_content = Content; // raw HTML
await RefreshRichText();
}
protected override void OnParametersSet()
{
_richhtml = Content;
_rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
var interop = new RichTextEditorInterop(JSRuntime);
if (firstRender)
{
await base.OnAfterRenderAsync(firstRender);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
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);
_content = Content; // raw HTML
// preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor)
_originalrichhtml = await interop.GetHtml(_editorElement);
}
}
}
// 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);
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseFileManager()
{
_filemanagervisible = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = 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);
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;
}
}
}
if (_original != content)
{
// rich text content has changed - return it
return content;
}
else
{
// return raw html content
return _content;
}
}
public async Task InsertRichImage()
{
_message = string.Empty;
if (_richfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new RichTextEditorInterop(JSRuntime);
await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name));
_richfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_richfilemanager = true;
}
StateHasChanged();
}
public async Task InsertImage()
{
_message = string.Empty;
if (_filemanagervisible)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new RichTextEditorInterop(JSRuntime);
await interop.InsertImage(_editorElement, file.Url, file.Name);
_filemanagervisible = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_filemanagervisible = true;
}
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);
}
public async Task InsertRawImage()
{
_message = string.Empty;
if (_rawfilemanager)
{
var file = _fileManager.GetFile();
if (file != null)
{
var interop = new Interop(JSRuntime);
int pos = await interop.GetCaretPosition("rawhtmleditor");
var image = "<img src=\"" + file.Url + "\" alt=\"" + ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name) + "\">";
_rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos);
_rawfilemanager = false;
}
else
{
_message = Localizer["Message.Require.Image"];
}
}
else
{
_rawfilemanager = true;
}
StateHasChanged();
}
}

View File

@ -9,95 +9,188 @@
@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">@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>
}
}
<TabStrip>
<TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit">
@if (_content != null)
{
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" AllowRawHtml="@_allowrawhtml" @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.7.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill1.3.7.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 bool _allowrawhtml;
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"));
_allowrawhtml = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "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 = Utilities.FormatContent(_content, PageState.Alias, "render");
_createdby = htmltext.CreatedBy;
_createdon = htmltext.CreatedOn;
_modifiedby = htmltext.ModifiedBy;
_modifiedon = htmltext.ModifiedOn;
}
else
{
_content = string.Empty;
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Content {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Content.Load"], 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 = Utilities.FormatContent(content, PageState.Alias, "save");
_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 HtmlText();
htmltext.ModuleId = ModuleState.ModuleId;
htmltext.Content = content;
await HtmlTextService.AddHtmlTextAsync(htmltext);
}
_view = "";
}
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 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

@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
@ -13,24 +15,29 @@ namespace Oqtane.Modules.HtmlText.Services
private string ApiUrl => CreateApiUrl("HtmlText");
public async Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId)
{
return await GetJsonAsync<List<Models.HtmlText>>(CreateAuthorizationPolicyUrl($"{ApiUrl}?moduleid={moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int moduleId)
{
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId)
{
return await GetJsonAsync<Models.HtmlText>(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
public async Task AddHtmlTextAsync(Models.HtmlText htmlText)
{
await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText);
}
public async Task UpdateHtmlTextAsync(Models.HtmlText htmlText)
public async Task DeleteHtmlTextAsync(int htmlTextId, int moduleId)
{
await PutJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlText.HtmlTextId}", EntityNames.Module, htmlText.ModuleId), htmlText);
}
public async Task DeleteHtmlTextAsync(int moduleId)
{
await DeleteAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId));
await DeleteAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId));
}
}
}

View File

@ -1,19 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Modules.HtmlText.Models;
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<Models.HtmlText> GetHtmlTextAsync(int ModuleId);
Task<List<Models.HtmlText>> GetHtmlTextsAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int moduleId);
Task<Models.HtmlText> GetHtmlTextAsync(int htmlTextId, int moduleId);
Task AddHtmlTextAsync(Models.HtmlText htmltext);
Task UpdateHtmlTextAsync(Models.HtmlText htmltext);
Task DeleteHtmlTextAsync(int ModuleId);
Task DeleteHtmlTextAsync(int htmlTextId, int moduleId);
}
}

View File

@ -15,18 +15,29 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="files" ResourceKey="AllowRawHtml" ResourceType="@resourceType" HelpText="Specify If Editors Can Enter Raw HTML">Allow Raw HTML: </Label>
<div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowrawhtml">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private string _allowfilemanagement;
private string _allowrawhtml;
protected override void OnInitialized()
{
try
{
_allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
}
_allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
_allowrawhtml = SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true");
}
catch (Exception ex)
{
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
@ -39,7 +50,8 @@
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowRawHtml", _allowrawhtml);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{

View File

@ -15,6 +15,8 @@ namespace Oqtane.Modules
public abstract class ModuleBase : ComponentBase, IModuleControl
{
private Logger _logger;
private string _urlparametersstate;
private Dictionary<string, string> _urlparameters;
protected Logger logger => _logger ?? (_logger = new Logger(this));
@ -24,6 +26,9 @@ namespace Oqtane.Modules
[Inject]
protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected SiteState SiteState { get; set; }
[CascadingParameter]
protected PageState PageState { get; set; }
@ -44,6 +49,21 @@ namespace Oqtane.Modules
public virtual List<Resource> Resources { get; set; }
// url parameters
public virtual string UrlParametersTemplate { get; set; }
public Dictionary<string, string> UrlParameters {
get
{
if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters)
{
_urlparametersstate = PageState.UrlParameters;
_urlparameters = GetUrlParameters(UrlParametersTemplate);
}
return _urlparameters;
}
}
// base lifecycle method for handling JSInterop script registration
protected override async Task OnAfterRenderAsync(bool firstRender)
@ -53,9 +73,10 @@ 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 ?? "" });
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + "/" + resource.Url;
scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
}
if (scripts.Any())
{
@ -149,15 +170,26 @@ namespace Oqtane.Modules
return Utilities.ImageUrl(PageState.Alias, fileid, width, height, mode, position, background, rotate, recreate);
}
public virtual Dictionary<string, string> GetUrlParameters(string parametersTemplate = "")
public string AddUrlParameters(params object[] parameters)
{
var url = "";
for (var i = 0; i < parameters.Length; i++)
{
url += "/" + parameters[i].ToString();
}
return url;
}
// template is in the form of a standard route template ie. "/{id}/{name}" and produces dictionary of key/value pairs
// if url parameters belong to a specific module you should embed a unique key into the route (ie. /!/blog/1) and validate the url parameter key in the module
public virtual Dictionary<string, string> GetUrlParameters(string template = "")
{
var urlParameters = new Dictionary<string, string>();
string[] templateSegments;
var parameters = PageState.UrlParameters.Split('/', StringSplitOptions.RemoveEmptyEntries);
var parameterId = 0;
var parameters = _urlparametersstate.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (string.IsNullOrEmpty(parametersTemplate))
if (string.IsNullOrEmpty(template))
{
// no template will populate dictionary with generic "parameter#" keys
for (int i = 0; i < parameters.Length; i++)
{
urlParameters.TryAdd("parameter" + i, parameters[i]);
@ -165,39 +197,37 @@ namespace Oqtane.Modules
}
else
{
templateSegments = parametersTemplate.Split('/', StringSplitOptions.RemoveEmptyEntries);
var segments = template.Split('/', StringSplitOptions.RemoveEmptyEntries);
string key;
if (parameters.Length == templateSegments.Length)
for (int i = 0; i < parameters.Length; i++)
{
for (int i = 0; i < parameters.Length; i++)
if (i < segments.Length)
{
if (parameters.Length > i)
key = segments[i];
if (key.StartsWith("{") && key.EndsWith("}"))
{
if (templateSegments[i] == parameters[i])
{
urlParameters.TryAdd("parameter" + parameterId, parameters[i]);
parameterId++;
}
else if (templateSegments[i].StartsWith("{") && templateSegments[i].EndsWith("}"))
{
var key = templateSegments[i].Replace("{", "");
key = key.Replace("}", "");
urlParameters.TryAdd(key, parameters[i]);
}
else
{
i = parameters.Length;
urlParameters.Clear();
}
// dynamic segment
key = key.Substring(1, key.Length - 2);
}
else
{
// static segments use generic "parameter#" keys
key = "parameter" + i.ToString();
}
}
else // unspecified segments use generic "parameter#" keys
{
key = "parameter" + i.ToString();
}
urlParameters.TryAdd(key, parameters[i]);
}
}
return urlParameters;
}
// user feedback methods
// UI methods
public void AddModuleMessage(string message, MessageType type)
{
ModuleInstance.AddModuleMessage(message, type);
@ -218,6 +248,18 @@ namespace Oqtane.Modules
ModuleInstance.HideProgressIndicator();
}
public void SetModuleTitle(string title)
{
var obj = new { PageModuleId = ModuleState.PageModuleId, Title = title };
SiteState.Properties.ModuleTitle = obj;
}
public void SetModuleVisibility(bool visible)
{
var obj = new { PageModuleId = ModuleState.PageModuleId, Visible = visible };
SiteState.Properties.ModuleVisibility = obj;
}
// logging methods
public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args)
{

View File

@ -5,15 +5,15 @@
<OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations>
<Version>3.0.2</Version>
<Version>3.2.0</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
<Description>Modular Application Framework for Blazor</Description>
<Description>Modular Application Framework for Blazor and MAUI</Description>
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.2</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@ -22,11 +22,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.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="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.3" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup>
@ -34,13 +35,8 @@
<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>
<PublishTrimmed>false</PublishTrimmed>
<BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization;
@ -15,7 +16,6 @@ using Microsoft.JSInterop;
using Oqtane.Documentation;
using Oqtane.Modules;
using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Client
@ -28,11 +28,12 @@ namespace Oqtane.Client
var builder = WebAssemblyHostBuilder.CreateDefault(args);
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.AddSingleton(httpClient);
builder.Services.AddOptions();
// Register localization services
// register localization services
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
// register auth services
@ -41,7 +42,9 @@ namespace Oqtane.Client
// register scoped core services
builder.Services.AddOqtaneScopedServices();
await LoadClientAssemblies(httpClient);
var serviceProvider = builder.Services.BuildServiceProvider();
await LoadClientAssemblies(httpClient, serviceProvider);
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (var assembly in assemblies)
@ -53,37 +56,105 @@ namespace Oqtane.Client
RegisterClientStartups(assembly, builder.Services);
}
var host = builder.Build();
await SetCultureFromLocalizationCookie(host.Services);
ServiceActivator.Configure(host.Services);
await host.RunAsync();
await builder.Build().RunAsync();
}
private static async Task LoadClientAssemblies(HttpClient http)
private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider)
{
// get list of loaded assemblies on the client
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList();
var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
var filter = new List<string>();
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load");
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
var interop = new Interop(jsRuntime);
var files = await interop.GetIndexedDBKeys(".dll");
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
if (files.Count() != 0)
{
var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
// get list of assemblies from server
var json = await http.GetStringAsync("/api/Installation/list");
var assemblies = JsonSerializer.Deserialize<List<string>>(json);
foreach (ZipArchiveEntry entry in archive.Entries)
// determine which assemblies need to be downloaded
foreach (var assembly in assemblies)
{
if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.FullName)))
var file = files.FirstOrDefault(item => item.Contains(assembly));
if (file == null)
{
filter.Add(assembly);
}
else
{
// check if newer version available
if (GetFileDate(assembly) > GetFileDate(file))
{
filter.Add(assembly);
}
}
}
// get assemblies already downloaded
foreach (var file in files)
{
if (assemblies.Contains(file) && !filter.Contains(file))
{
try
{
dlls.Add(file, await interop.GetIndexedDBItem<byte[]>(file));
var pdb = file.Replace(".dll", ".pdb");
if (files.Contains(pdb))
{
pdbs.Add(pdb, await interop.GetIndexedDBItem<byte[]>(pdb));
}
}
catch
{
// ignore
}
}
else // file is deprecated
{
try
{
await interop.RemoveIndexedDBItem(file);
}
catch
{
// ignore
}
}
}
}
else
{
filter.Add("*");
}
if (filter.Count != 0)
{
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", filter));
// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
using (var memoryStream = new MemoryStream())
{
entry.Open().CopyTo(memoryStream);
byte[] file = memoryStream.ToArray();
// save assembly to indexeddb
try
{
await interop.SetIndexedDBItem(entry.FullName, file);
}
catch
{
// ignore
}
switch (Path.GetExtension(entry.FullName))
{
case ".dll":
@ -96,23 +167,31 @@ namespace Oqtane.Client
}
}
}
}
foreach (var item in dlls)
// load assemblies into app domain
foreach (var item in dlls)
{
if (pdbs.ContainsKey(item.Key.Replace(".dll", ".pdb")))
{
if (pdbs.ContainsKey(item.Key))
{
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key]));
}
else
{
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value));
}
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key.Replace(".dll", ".pdb")]));
}
else
{
AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value));
}
}
}
private static DateTime GetFileDate(string filepath)
{
var segments = filepath.Split('.');
return DateTime.ParseExact(segments[segments.Length - 2], "yyyyMMddHHmmss", CultureInfo.InvariantCulture);
}
private static void RegisterModuleServices(Assembly assembly, IServiceCollection services)
{
// dynamically register module scoped services
var implementationTypes = assembly.GetInterfaces<IService>();
foreach (var implementationType in implementationTypes)
{

View File

@ -121,7 +121,7 @@
<value>Server:</value>
</data>
<data name="Server.HelpText" xml:space="preserve">
<value>Enter the database server</value>
<value>Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) </value>
</data>
<data name="Database.Text" xml:space="preserve">
<value>Database:</value>
@ -133,7 +133,7 @@
<value>Integrated Security:</value>
</data>
<data name="IntegratedSecurity.HelpText" xml:space="preserve">
<value>Select if you want integrated security or not</value>
<value>Select if you are using integrated security</value>
</data>
<data name="Uid.Text" xml:space="preserve">
<value>User Id:</value>
@ -163,7 +163,7 @@
<value>Self Signed</value>
</data>
<data name="TrustServerCertificate.HelpText" xml:space="preserve">
<value>Specify the type of certificate you are using for encryption</value>
<value>Specify the type of certificate you are using for encryption. Verifiable is equivalent to False. Self Signed is equivalent to True.</value>
</data>
<data name="TrustServerCertificate.Text" xml:space="preserve">
<value>Trust Server Certificate:</value>

View File

@ -130,10 +130,13 @@
<value>Install Now</value>
</data>
<data name="Error.DbConfig.Load" xml:space="preserve">
<value>Error loading Database Configuration Control</value>
<value>Error Loading Database Configuration Control</value>
</data>
<data name="Message.Require.DbInfo" xml:space="preserve">
<value>Please Enter All Required Fields. Ensure Passwords Match And Are Greater Than 5 Characters In Length. Ensure Email Address Provided Is Valid.</value>
<value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value>
</data>
<data name="Message.Password.Invalid" xml:space="preserve">
<value>The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements.</value>
</data>
<data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value>
@ -165,4 +168,16 @@
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value>
</data>
<data name="ConnectionString.Text" xml:space="preserve">
<value>String:</value>
</data>
<data name="EnterConnectionParameters" xml:space="preserve">
<value>Enter Connection Parameters</value>
</data>
<data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -118,6 +118,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Error.Module.Load" xml:space="preserve">
<value>A Problem Was Encountered Loading Module {0}</value>
<value>A Problem Was Encountered Loading Module {0}. The Module Is Either Invalid Or Does Not Exist.</value>
</data>
</root>

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -117,23 +117,26 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="RememberMe" xml:space="preserve">
<value>Remember Me?</value>
</data>
<data name="ForgotPassword" xml:space="preserve">
<value>Forgot Password</value>
</data>
<data name="Success.Account.Verified" xml:space="preserve">
<value>User Account Verified Successfully. You Can Now Login With Your Username And Password Below.</value>
</data>
<data name="Message.Account.NotVerfied" xml:space="preserve">
<data name="Message.Account.NotVerified" xml:space="preserve">
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value>
</data>
<data name="Message.Account.NotLinked" xml:space="preserve">
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive And User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email.</value>
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value>
</data>
<data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide Your Username And Password</value>
<value>Please Provide All Required Fields</value>
</data>
<data name="Info.SignedIn" xml:space="preserve">
<value>You Are Already Signed In</value>
@ -147,4 +150,79 @@
<data name="Message.UserDoesNotExist" xml:space="preserve">
<value>User Does Not Exist</value>
</data>
<data name="Code.HelpText" xml:space="preserve">
<value>Please Enter The Secure Verification Code Which Was Sent To You By Email.</value>
</data>
<data name="Code.Placeholder" xml:space="preserve">
<value>Verification Code</value>
</data>
<data name="Code.Text" xml:space="preserve">
<value>Verification Code:</value>
</data>
<data name="Error.TwoFactor.Fail" xml:space="preserve">
<value>Verification Failed. Please Ensure You Entered The Code Exactly In The Form Provided In Your Email. If You Wish To Request A New Verification Code Please Select The Cancel Option And Sign In Again. </value>
</data>
<data name="Message.TwoFactor" xml:space="preserve">
<value>A Secure Verification Code Has Been Sent To Your Email Address. Please Enter The Code That You Received. If You Do Not Receive The Code Or You Have Lost Access To Your Email, Please Contact Your Administrator.</value>
</data>
<data name="Password.HelpText" xml:space="preserve">
<value>Please Enter The Password Related To Your Account. Remember That Passwords Are Case Sensitive. If You Attempt Unsuccessfully To Log In To Your Account Multiple Times, You Will Be Locked Out For A Period Of Time.</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="Password.Text" xml:space="preserve">
<value>Password:</value>
</data>
<data name="Remember.HelpText" xml:space="preserve">
<value>Specify If You Would Like To Be Signed Back In Automatically The Next Time You Visit This Site</value>
</data>
<data name="Remember.Text" xml:space="preserve">
<value>Remember Me?</value>
</data>
<data name="Username.HelpText" xml:space="preserve">
<value>Please Enter The Username Related To Your Account</value>
</data>
<data name="Username.Placeholder" xml:space="preserve">
<value>Username</value>
</data>
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="Use" xml:space="preserve">
<value>Use</value>
</data>
<data name="Error.LoadLogin" xml:space="preserve">
<value>Error Loading Login</value>
</data>
<data name="Error.Login" xml:space="preserve">
<value>Error Performing Login</value>
</data>
<data name="Error.ResetPassword" xml:space="preserve">
<value>Error Resetting Password</value>
</data>
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.InvalidEmail" xml:space="preserve">
<value>The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.ProviderKeyMismatch" xml:space="preserve">
<value>An Error Occurred Verifying Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.UserDoesNotExist" xml:space="preserve">
<value>A User Account Matching The Email Address Of Your External Login Does Not Exist. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.UserNotCreated" xml:space="preserve">
<value>A User Account Could Not Be Created For Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.VerificationRequired" xml:space="preserve">
<value>In Order To Link Your External Login With Your User Account You Must Verify Your Identity. Please Check Your Email For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.AccessDenied" xml:space="preserve">
<value>Your External Login Was Denied Access. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.RemoteFailure" xml:space="preserve">
<value>Your External Login Failed. Please Contact Your Administrator For Further Instructions.</value>
</data>
</root>

View File

@ -195,4 +195,25 @@
<data name="Information.Text" xml:space="preserve">
<value>Information</value>
</data>
<data name="PackageName.HelpText" xml:space="preserve">
<value>The unique name of the package from which this module was installed</value>
</data>
<data name="PackageName.Text" xml:space="preserve">
<value>Package Name:</value>
</data>
<data name="Error.Translation.Download" xml:space="preserve">
<value>Error Downloading Translation</value>
</data>
<data name="Search.NoResults" xml:space="preserve">
<value>No Translations Exist For This Module Or Package Service Is Disabled</value>
</data>
<data name="Success.Translation.Download" xml:space="preserve">
<value>Translation Downloaded Successfully. Click Install To Complete Installation.</value>
</data>
<data name="Success.Translation.Install" xml:space="preserve">
<value>Translation Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
</data>
<data name="Translations.Heading" xml:space="preserve">
<value>Translations</value>
</data>
</root>

View File

@ -150,4 +150,7 @@
<data name="EditModule.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Modules" xml:space="preserve">
<value>Modules</value>
</data>
</root>

View File

@ -147,4 +147,7 @@
<data name="Message.Required.Title" xml:space="preserve">
<value>You Must Provide A Title For The Module</value>
</data>
<data name="Error.Module.Load" xml:space="preserve">
<value>A Problem Was Encountered Loading Module {0}. The Module Is Either Invalid Or Does Not Exist.</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -160,7 +160,7 @@
<value>Error Loading Pane Layouts For Theme</value>
</data>
<data name="Message.Page.Exists" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page. The page path needs to be unique for the selected parent.</value>
<value>A page with path '{0}' already exists for this site. Page paths must be unique. You may need to check if a page with this path exists in the Recycle Bin.</value>
</data>
<data name="Message.Required.PageInfo" xml:space="preserve">
<value>You Must Provide Page Name, Theme, and Container</value>
@ -228,7 +228,13 @@
<data name="Appearance.Name" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="Message.Page.Deleted" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page in the Recycle Bin. Either recover the page or remove from the Recycle Bin and create it again.</value>
<data name="Meta.HelpText" xml:space="preserve">
<value>Optionally enter meta tags (in exactly the form you want them to be included in the page output).</value>
</data>
<data name="Meta.Text" xml:space="preserve">
<value>Meta:</value>
</data>
<data name="Message.Page.Reserved" xml:space="preserve">
<value>The page name {0} is reserved. Please enter a different name for your page.</value>
</data>
</root>

View File

@ -151,7 +151,7 @@
<value>Error Loading Pane Layouts For Theme</value>
</data>
<data name="Mesage.Page.PathExists" xml:space="preserve">
<value>A page with path {0} already exists for the selected parent page. The page path needs to be unique for the selected parent.</value>
<value>A page with path '{0}' already exists for this site. Page paths must be unique. You may need to check if a page with this path exists in the Recycle Bin.</value>
</data>
<data name="Message.Required.PageInfo" xml:space="preserve">
<value>You Must Provide Page Name, Theme, and Container</value>
@ -264,4 +264,13 @@
<data name="Clickable.Text" xml:space="preserve">
<value>Clickable?</value>
</data>
<data name="Meta.HelpText" xml:space="preserve">
<value>Optionally enter meta tags (in exactly the form you want them to be included in the page output).</value>
</data>
<data name="Meta.Text" xml:space="preserve">
<value>Meta:</value>
</data>
<data name="Message.Page.Reserved" xml:space="preserve">
<value>The page name {0} is reserved. Please enter a different name for your page.</value>
</data>
</root>

View File

@ -142,7 +142,7 @@
<value>Site Settings Saved</value>
</data>
<data name="Message.Aliases.Taken" xml:space="preserve">
<value>The Default Alias Has Not Been Specified Or An Alias Was Specified That Has Already Been Used For Another Site</value>
<value>An Alias Was Specified That Has Already Been Used For Another Site</value>
</data>
<data name="Message.Required.SiteName" xml:space="preserve">
<value>You Must Provide A Site Name, Alias, And Default Theme/Container</value>
@ -166,7 +166,7 @@
<value>Enter the tenant for the site</value>
</data>
<data name="Aliases.HelpText" xml:space="preserve">
<value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder). If a site has multiple aliases they should be separated by commas.</value>
<value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder).</value>
</data>
<data name="IsDeleted.HelpText" xml:space="preserve">
<value>Is this site deleted?</value>
@ -318,10 +318,25 @@
<data name="DefaultAlias.HelpText" xml:space="preserve">
<value>The default alias for the site. Requests for non-default aliases will be redirected to the default alias.</value>
</data>
<data name="DefaultAlias.Text" xml:space="preserve">
<data name="DefaultAlias.Text" xml:space="preserve">
<value>Default Alias: </value>
</data>
<data name="Aliases.Heading" xml:space="preserve">
<value>Aliases</value>
</data>
<data name="AliasName" xml:space="preserve">
<value>Name</value>
</data>
<data name="AliasDefault" xml:space="preserve">
<value>Default?</value>
</data>
<data name="Confirm.Alias.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete {0}?</value>
</data>
<data name="HomePage.HelpText" xml:space="preserve">
<value>Select the home page for the site (to be used if there is no page with a path of '/')</value>
</data>
<data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value>
</data>
</root>

View File

@ -270,4 +270,16 @@
<data name="Runtime.Text" xml:space="preserve">
<value>Runtime: </value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>Enter a complete connection string including all parameters and delimiters</value>
</data>
<data name="ConnectionString.Text" xml:space="preserve">
<value>String:</value>
</data>
<data name="EnterConnectionParameters" xml:space="preserve">
<value>Enter Connection Parameters</value>
</data>
<data name="EnterConnectionString" xml:space="preserve">
<value>Enter Connection String</value>
</data>
</root>

View File

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

View File

@ -162,4 +162,10 @@
<data name="License.HelpText" xml:space="preserve">
<value>The license of the theme</value>
</data>
<data name="PackageName.HelpText" xml:space="preserve">
<value>The unique name of the package from which this module was installed</value>
</data>
<data name="PackageName.Text" xml:space="preserve">
<value>Package Name:</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -169,10 +169,10 @@
<value>Identity</value>
</data>
<data name="Confirm.HelpText" xml:space="preserve">
<value>If you are changing your password you must enter it again to confirm it matches</value>
<value>If you are changing your password you must enter it again to confirm it matches the value entered above</value>
</data>
<data name="Confirm.Text" xml:space="preserve">
<value>Confirm Password:</value>
<value>Confirmation:</value>
</data>
<data name="DisplayName.HelpText" xml:space="preserve">
<value>Your full name</value>
@ -204,4 +204,25 @@
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="TwoFactor.HelpText" xml:space="preserve">
<value>Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value>
</data>
<data name="DeleteAllNotifications.Header" xml:space="preserve">
<value>Clear Notifications</value>
</data>
<data name="DeleteAllNotifications.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete All Notifications?</value>
</data>
<data name="DeleteAllNotifications.Text" xml:space="preserve">
<value>Delete ALL Notifications</value>
</data>
<data name="Notifications.Heading" xml:space="preserve">
<value>Notifications</value>
</data>
<data name="Profile.Heading" xml:space="preserve">
<value>Profile</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -168,4 +168,7 @@
<data name="Username.Text" xml:space="preserve">
<value>Username:</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -183,4 +183,19 @@
<data name="Profile.Heading" xml:space="preserve">
<value>Profile</value>
</data>
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="LastIPAddress.HelpText" xml:space="preserve">
<value>The IP Address of the user recorded during their last login</value>
</data>
<data name="LastIPAddress.Text" xml:space="preserve">
<value>Last IP Address: </value>
</data>
<data name="LastLogin.HelpText" xml:space="preserve">
<value>The date and time when the user last signed in</value>
</data>
<data name="LastLogin.Text" xml:space="preserve">
<value>Last Login:</value>
</data>
</root>

View File

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

View File

@ -175,7 +175,7 @@
<value>Details</value>
</data>
<data name="Filter.HelpText" xml:space="preserve">
<value>Comma delimited list of terms which may exist in IP addresses, user agents, or languages which identify visitors which should not be tracked (ie. bots)</value>
<value>Comma delimited list of terms which may exist in IP addresses, user agents, or languages identifying visitors which should not be tracked</value>
</data>
<data name="Filter.Text" xml:space="preserve">
<value>Filter:</value>
@ -186,4 +186,10 @@
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="Correlation.HelpText" xml:space="preserve">
<value>Indicate if new visitors to this site should be correlated based on their IP Address</value>
</data>
<data name="Correlation.Text" xml:space="preserve">
<value>Correlate Visitors?</value>
</data>
</root>

View File

@ -141,4 +141,7 @@
<data name="Success.File.Upload" xml:space="preserve">
<value>File Upload Succeeded</value>
</data>
<data name="Message.File.Restricted" xml:space="preserve">
<value>Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information.</value>
</data>
</root>

View File

@ -0,0 +1,123 @@
<?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="PageOfPages" xml:space="preserve">
<value>Page {0} of {1}</value>
</data>
</root>

View File

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

View File

@ -123,4 +123,10 @@
<data name="AllowFileManagement.Text" xml:space="preserve">
<value>Allow File Management: </value>
</data>
<data name="AllowRawHtml.HelpText" xml:space="preserve">
<value>Specify If Editors Can Enter Raw HTML</value>
</data>
<data name="AllowRawHtml.Text" xml:space="preserve">
<value>Allow Raw HTML:</value>
</data>
</root>

View File

@ -259,7 +259,7 @@
<value>Upgrade</value>
</data>
<data name="Username" xml:space="preserve">
<value>Username:</value>
<value>Username</value>
</data>
<data name="Version" xml:space="preserve">
<value>Version</value>
@ -274,7 +274,7 @@
<value>Full Name:</value>
</data>
<data name="LocalVersion" xml:space="preserve">
<value>Local Version</value>
<value>Installed Version</value>
</data>
<data name="Search.Source" xml:space="preserve">
<value>source</value>
@ -318,7 +318,28 @@
<data name="BlazorWebAssembly" xml:space="preserve">
<value>Blazor WebAssembly</value>
</data>
<data name="BlazorHybrid" xml:space="preserve">
<value>Blazor Hybrid</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="HidePassword" xml:space="preserve">
<value>Hide</value>
</data>
<data name="ShowPassword" xml:space="preserve">
<value>Show</value>
</data>
<data name="PageOfPages" xml:space="preserve">
<value>Page {0} of {1}</value>
</data>
<data name="Url Mappings" xml:space="preserve">
<value>Url Mappings</value>
</data>
<data name="Visitor Management" xml:space="preserve">
<value>Visitor Management</value>
</data>
<data name="Oqtane.Marketplace" xml:space="preserve">
<value>Please note that the third party extensions displayed above have been registered in the &lt;a href="https://www.oqtane.net" target="_new"&gt;Oqtane Marketplace&lt;/a&gt; which enables them to be seamlessly downloaded and installed into the framework.</value>
</data>
</root>

View File

@ -138,6 +138,12 @@
<data name="Register.Text" xml:space="preserve">
<value>Show Register?</value>
</data>
<data name="Scope.HelpText" xml:space="preserve">
<value>Specify if the settings are applicable to this page or the entire site.</value>
</data>
<data name="Scope.Text" xml:space="preserve">
<value>Setting Scope:</value>
</data>
<data name="Site" xml:space="preserve">
<value>Site</value>
</data>

View File

@ -12,18 +12,12 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class AliasService : ServiceBase, IAliasService
{
private readonly SiteState _siteState;
/// <summary>
/// Constructor - should only be used by Dependency Injection
/// </summary>
public AliasService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
public AliasService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string ApiUrl => CreateApiUrl("Alias", _siteState.Alias);
private string ApiUrl => CreateApiUrl("Alias");
/// <inheritdoc />
public async Task<List<Alias>> GetAliasesAsync()

View File

@ -11,15 +11,9 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class DatabaseService : ServiceBase, IDatabaseService
{
public DatabaseService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly SiteState _siteState;
public DatabaseService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string Apiurl => CreateApiUrl("Database", _siteState.Alias);
private string Apiurl => CreateApiUrl("Database");
public async Task<List<Database>> GetDatabasesAsync()
{

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
@ -17,13 +18,13 @@ namespace Oqtane.Services
private readonly SiteState _siteState;
private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http)
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState)
{
_siteState = siteState;
_jsRuntime = jsRuntime;
}
private string Apiurl => CreateApiUrl("File", _siteState.Alias);
private string Apiurl => CreateApiUrl("File");
public async Task<List<File>> GetFilesAsync(int folderId)
{
@ -32,7 +33,8 @@ namespace Oqtane.Services
public async Task<List<File>> GetFilesAsync(string folder)
{
return await GetJsonAsync<List<File>>($"{Apiurl}?folder={folder}");
List<File> files = await GetJsonAsync<List<File>>($"{Apiurl}?folder={folder}");
return files.OrderBy(item => item.Name).ToList();
}
public async Task<List<File>> GetFilesAsync(int siteId, string folderPath)
@ -44,7 +46,8 @@ namespace Oqtane.Services
var path = WebUtility.UrlEncode(folderPath);
return await GetJsonAsync<List<File>>($"{Apiurl}/{siteId}/{path}");
List<File> files = await GetJsonAsync<List<File>>($"{Apiurl}/{siteId}/{path}");
return files?.OrderBy(item => item.Name).ToList();
}
public async Task<File> GetFileAsync(int fileId)
@ -82,7 +85,7 @@ namespace Oqtane.Services
string result = "";
var interop = new Interop(_jsRuntime);
await interop.UploadFiles($"{Apiurl}/upload", folder, id);
await interop.UploadFiles($"{Apiurl}/upload", folder, id, _siteState.AntiForgeryToken);
// uploading files is asynchronous so we need to wait for the upload to complete
bool success = false;

View File

@ -1,10 +1,8 @@
using Oqtane.Models;
using System.Threading.Tasks;
using System.Linq;
using System.Net.Http;
using System.Collections.Generic;
using Oqtane.Shared;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using Oqtane.Documentation;
@ -14,20 +12,13 @@ namespace Oqtane.Services
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class FolderService : ServiceBase, IFolderService
{
private readonly SiteState _siteState;
public FolderService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public FolderService(HttpClient http, SiteState siteState) : base(http)
{
_siteState = siteState;
}
private string ApiUrl => CreateApiUrl("Folder", _siteState.Alias);
private string ApiUrl => CreateApiUrl("Folder");
public async Task<List<Folder>> GetFoldersAsync(int siteId)
{
List<Folder> folders = await GetJsonAsync<List<Folder>>($"{ApiUrl}?siteid={siteId}");
folders = GetFoldersHierarchy(folders);
return folders;
return await GetJsonAsync<List<Folder>>($"{ApiUrl}?siteid={siteId}");
}
public async Task<Folder> GetFolderAsync(int folderId)
@ -63,48 +54,5 @@ namespace Oqtane.Services
{
await DeleteAsync($"{ApiUrl}/{folderId}");
}
private static List<Folder> GetFoldersHierarchy(List<Folder> folders)
{
List<Folder> hierarchy = new List<Folder>();
Action<List<Folder>, Folder> getPath = null;
var folders1 = folders;
getPath = (folderList, folder) =>
{
IEnumerable<Folder> children;
int level;
if (folder == null)
{
level = -1;
children = folders1.Where(item => item.ParentId == null);
}
else
{
level = folder.Level;
children = folders1.Where(item => item.ParentId == folder.FolderId);
}
foreach (Folder child in children)
{
child.Level = level + 1;
child.HasChildren = folders1.Any(item => item.ParentId == child.FolderId);
hierarchy.Add(child);
if (getPath != null) getPath(folderList, child);
}
};
folders = folders.OrderBy(item => item.Order).ToList();
getPath(folders, null);
// add any non-hierarchical items to the end of the list
foreach (Folder folder in folders)
{
if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null)
{
hierarchy.Add(folder);
}
}
return hierarchy;
}
}
}

View File

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

View File

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

View File

@ -17,6 +17,14 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<Language>> GetLanguagesAsync(int siteId);
/// <summary>
/// Returns a list of all available languages for the given <see cref="Site" /> and package
/// </summary>
/// <param name="siteId"></param>
/// <param name="packageName"></param>
/// <returns></returns>
Task<List<Language>> GetLanguagesAsync(int siteId, string packageName);
/// <summary>
/// Returns the given language
/// </summary>

View File

@ -50,13 +50,13 @@ namespace Oqtane.Services
/// <param name="moduleId"></param>
/// <param name="content">module in JSON format</param>
/// <returns></returns>
Task<bool> ImportModuleAsync(int moduleId, string content);
Task<bool> ImportModuleAsync(int moduleId, int pageId, string content);
/// <summary>
/// Exports a given module
/// </summary>
/// <param name="moduleId"></param>
/// <returns>module in JSON</returns>
Task<string> ExportModuleAsync(int moduleId);
Task<string> ExportModuleAsync(int moduleId, int pageId);
}
}

View File

@ -38,6 +38,12 @@ namespace Oqtane.Services
/// <returns></returns>
Task UpdateSiteSettingsAsync(Dictionary<string, string> siteSettings, int siteId);
/// <summary>
/// Clears site option cache
/// </summary>
/// <returns></returns>
Task ClearSiteSettingsCacheAsync();
/// <summary>
/// Returns a key-value dictionary of all page settings for the given page
/// </summary>
@ -149,7 +155,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId);
/// <summary>
/// Updates settings for a given entityName and Id
/// </summary>
@ -166,7 +171,6 @@ namespace Oqtane.Services
/// <returns></returns>
Task<Setting> GetSettingAsync(string entityName, int settingId);
/// <summary>
/// Creates a new setting
/// </summary>

View File

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

View File

@ -16,6 +16,31 @@ namespace Oqtane.Services
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId);
/// <summary>
/// Get all <see cref="UserRole"/>s on a <see cref="Site"/>
/// </summary>
/// <param name="siteId">ID-reference to a <see cref="Site"/></param>
/// <param name="userId">ID-reference to a <see cref="User"/></param>
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId, int userId);
/// <summary>
/// Get all <see cref="UserRole"/>s on a <see cref="Site"/>
/// </summary>
/// <param name="siteId">ID-reference to a <see cref="Site"/></param>
/// <param name="roleName">Name reference a <see cref="Role"/></param>
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId, string roleName);
/// <summary>
/// Get all <see cref="UserRole"/>s on a <see cref="Site"/>
/// </summary>
/// <param name="siteId">ID-reference to a <see cref="Site"/></param>
/// <param name="userId">ID-reference to a <see cref="User"/></param>
/// <param name="roleName">Name reference a <see cref="Role"/></param>
/// <returns></returns>
Task<List<UserRole>> GetUserRolesAsync(int siteId, int userId, string roleName);
/// <summary>
/// Get one specific <see cref="UserRole"/>
/// </summary>

View File

@ -54,8 +54,8 @@ namespace Oqtane.Services
/// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled.
/// </summary>
/// <param name="user">A <see cref="User"/> object which should have at least the <see cref="User.Username"/> and <see cref="User.Password"/> set.</param>
/// <param name="setCookie">Determines if the login should be stored in the cookie.</param>
/// <param name="isPersistent">Determines if the login should be persisted in the cookie for a long time.</param>
/// <param name="setCookie">Determines if the login cookie should be set (only relevant for Hybrid scenarios)</param>
/// <param name="isPersistent">Determines if the login cookie should be persisted for a long time.</param>
/// <returns></returns>
Task<User> LoginUserAsync(User user, bool setCookie, bool isPersistent);
@ -88,5 +88,45 @@ namespace Oqtane.Services
/// <param name="token"></param>
/// <returns></returns>
Task<User> ResetPasswordAsync(User user, string token);
/// <summary>
/// Verify the two factor verification code <see cref="User"/>
/// </summary>
/// <param name="user"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<User> VerifyTwoFactorAsync(User user, string token);
/// <summary>
/// Validate a users password against the password policy
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
Task<bool> ValidatePasswordAsync(string password);
/// <summary>
/// Get token for current user
/// </summary>
/// <returns></returns>
Task<string> GetTokenAsync();
/// <summary>
/// Get personal access token for current user (administrators only)
/// </summary>
/// <returns></returns>
Task<string> GetPersonalAccessTokenAsync();
/// <summary>
/// Link an external login with a local user account
/// </summary>
/// <param name="user">The <see cref="User"/> we're verifying</param>
/// <param name="token">A Hash value in the URL which verifies this user got the e-mail (containing this token)</param>
/// <param name="type">External Login provider type</param>
/// <param name="key">External Login provider key</param>
/// <param name="name">External Login provider display name</param>
/// <returns></returns>
Task<User> LinkUserAsync(User user, string token, string type, string key, string name);
}
}

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