Compare commits

..

338 Commits

Author SHA1 Message Date
ec0a5377b3 Merge pull request #2784 from oqtane/master
Merge pull request #2783 from oqtane/dev
2023-05-03 14:59:27 -04:00
b876da069d Merge pull request #2783 from oqtane/dev
3.4.3 release
2023-05-03 14:59:09 -04:00
f06063d7eb Merge pull request #2782 from sbwalker/dev
fix #2763 - prevent module definitions from having duplicate names
2023-05-03 12:46:44 -04:00
c6ba4f4bee fix #2763 - prevent module definitions from having duplicate names 2023-05-03 12:46:29 -04:00
89da4ab2a1 Merge pull request #2781 from sbwalker/dev
add defensive logic in case list of pages is empty
2023-05-03 12:32:44 -04:00
99ac0a3cab add defensive logic in case list of pages is empty 2023-05-03 12:32:28 -04:00
a964c30705 Merge pull request #2776 from thabaum/patch-24
Updated Dashboard Index
2023-05-03 12:27:32 -04:00
2334d0297d Merge pull request #2780 from sbwalker/dev
fix #2777 - module rendering order within pane - moved default module ordering logic to server API for consistency and better performance
2023-05-03 12:26:17 -04:00
e444c6bcf0 fix #2777 - module rendering order within pane - moved default module ordering logic to server API for consistency and better performance 2023-05-03 12:25:52 -04:00
601582fc98 Update Index.razor 2023-05-03 08:19:24 -07:00
e939dbe24e Updated SecurityAccessLevel.Admin 2023-05-02 17:14:46 -07:00
143ad85fd5 Update README.md 2023-05-02 16:00:23 -04:00
20377e9789 Merge pull request #2775 from sbwalker/dev
prepare for 3.4.3 release
2023-05-02 15:54:49 -04:00
e4a24df7b4 prepare for 3.4.3 release 2023-05-02 15:54:36 -04:00
e88ca00658 Merge pull request #2770 from thabaum/patch-23
Remove Admin Page/Module Registered User View
2023-05-02 15:41:27 -04:00
2312c612d7 Merge pull request #2774 from sbwalker/dev
remove message items as these are handled by MessageType enum and UX styles
2023-05-02 15:14:34 -04:00
3aee52482d remove message items as these are handled by MessageType enum and UX styles 2023-05-02 15:14:17 -04:00
32248e0be6 Merge pull request #2745 from thabaum/patch-18
Add support for new language translation values for #2732.
2023-05-02 15:05:36 -04:00
6c18c320bd Merge pull request #2773 from sbwalker/dev
fixed compilation error and improved UTF8 support
2023-05-02 15:03:43 -04:00
e31f32e5aa fixed compilation error and improved UTF8 support 2023-05-02 15:03:26 -04:00
a856390566 Merge pull request #2772 from sbwalker/dev
improve module/theme/translation upload user experience to be consistent with download
2023-05-02 14:22:56 -04:00
64b8291487 improve module/theme/translation upload user experience to be consistent with download 2023-05-02 14:22:34 -04:00
2ebd1310c9 Merge pull request #2747 from thabaum/patch-19
Add content-type to sitemap: Fixes issues 2749 2764
2023-05-02 14:20:39 -04:00
09118fcb42 Updates lastmod date 2023-05-02 07:15:22 -07:00
5d5167abbc Merge pull request #2771 from vnetonline/dev
Invalid template when I try and use the Deploy to Azure button
2023-05-02 09:01:41 -04:00
bf43dea060 Merge branch 'oqtane:dev' into dev 2023-05-02 15:53:56 +10:00
6ebab830c5 Invalid template when I try and use the Deploy to Azure button
1. removed depends on connection strings
2. Moved sku outside properties for type Microsoft.Web/serverfarms and added various app plans
3. Removed worker size and replaced with instance capacity
4. Replaced database and sqlserver to use various editions
5. Updated ApiVersions for various types
2023-05-02 15:47:31 +10:00
9a9a78e0bd Remove Admin Page/Module Registered User View 2023-05-01 20:01:49 -07:00
418eff7d21 Merge pull request #2754 from pepsinio/dev
Add ability to use environment variables in order to set them as app settings in Azure
2023-05-01 16:14:47 -04:00
d39869ca8e Merge pull request #2759 from leigh-pointer/MenuHorizontal#2757
Fix for MenuHorizontal #2757
2023-05-01 16:14:16 -04:00
7829168ea7 Merge pull request #2768 from sbwalker/dev
fix #2761 - updating Module Definition name, description, category not invalidating cache
2023-05-01 15:38:46 -04:00
dd83e3ee67 fix #2761 - updating Module Definition name, description, category not invalidating cache 2023-05-01 15:38:18 -04:00
c3ac0e365d Removed PR comments 2023-04-27 16:32:48 -07:00
2986625605 Update content type to XML and include UTF-8 2023-04-27 16:22:35 -07:00
8beaeabf09 Include utf-8 encoding. 2023-04-27 16:21:14 -07:00
fa9b4b6112 Fix for MenuHorizontal #2757
Add the css class to MenuHorizontal to handle scrolling when hamburger Menu is in use.
2023-04-26 09:43:11 +02:00
d81fbe4585 Fixed missing logic from PR 2023-04-19 10:18:27 -07:00
536c044139 Add environment settings needed for Azure deployment 2023-04-19 19:18:19 +02:00
376531195e Add environment settings needed for Azure deployment 2023-04-19 19:17:33 +02:00
abf4ff71d7 re-add missing settings 2023-04-19 10:13:31 -07:00
d25debcea3 cleanup using 2023-04-19 10:11:16 -07:00
948c186cb5 fixed formatting 2023-04-19 10:09:00 -07:00
ba27e70fe3 Removed unnecessary cache comments. 2023-04-19 10:05:43 -07:00
c93d2576af Updates content-type to "application/xml"
removes sitemap cache from previous commits.
2023-04-19 10:00:33 -07:00
e0b0156640 allow module and theme dependencies setting to include .dll file extension, added testmode config setting for validating list of assemblies sent to client 2023-04-19 08:48:52 -07:00
a3ca6a3071 Merge pull request #2753 from sbwalker/dev
allow module and theme dependencies setting to include .dll file extension, added testmode config setting for validating list of assemblies sent to client
2023-04-19 08:46:12 -07:00
b20157450b Add caching and content-type 2023-04-14 18:08:39 -07:00
7e4f0923d7 Add support for new language translation values. 2023-04-13 07:42:04 -07:00
e0c2b2982f improvements to #2736 to support scenarios where module is not explicitly assigned to a page 2023-04-11 13:01:34 -04:00
9a231c28af Merge pull request #2742 from sbwalker/dev
improvements to #2736 to support scenarios where module is not explicitly assigned to a page
2023-04-11 12:59:33 -04:00
94a02b7bf9 add filter to exclude orphaned permissions 2023-04-11 10:35:23 -04:00
22c8fb411a Merge pull request #2741 from sbwalker/dev
add filter to exclude orphaned permissions
2023-04-11 10:33:09 -04:00
cf46210ff8 Merge pull request #2725 from thabaum/patch-17
Fixes null reference permissions issue: Fixes #2724
2023-04-11 10:22:08 -04:00
f32d988297 Merge pull request #2740 from sbwalker/dev
Routes with Module ID and no Action can be displayed on any page regardless of whether a PageModule record exists (ie. Admin Dashboard)
2023-04-11 10:19:28 -04:00
7fe4577158 Routes with Module ID and no Action can be displayed on any page regardless of whether a PageModule record exists (ie. Admin Dashboard) 2023-04-11 10:21:37 -04:00
8985dcb4c0 fix #2736 - UI not loading correct module instance in scenarios where a module exists on multiple pages 2023-04-10 08:37:35 -04:00
d346444c51 Merge pull request #2739 from sbwalker/dev
fix #2736 - UI not loading correct module instance in scenarios where a module exists on multiple pages
2023-04-10 08:35:42 -04:00
ef27a0d6b0 Merge pull request #2727 from leigh-pointer/ModulePassSettings
Fix for #2718 SiteMap functionality missing Settings
2023-04-05 10:53:09 -04:00
627158cb5d Merge pull request #2729 from leigh-pointer/PwdControlCancelFocus
Fix for #2728 Toggle Password button focus
2023-04-05 10:51:27 -04:00
648edcabba Merge pull request #2731 from sbwalker/dev
fix #2720 - module definition permissions not being created properly for new sites
2023-04-05 10:27:44 -04:00
0f34c6efc5 fix #2720 - module definition permissions not being created properly for new sites 2023-04-05 10:29:51 -04:00
cc3cc55269 consolidated package installation so that it always occurs during startup and added logging in case of errors 2023-04-05 10:26:21 -04:00
a503a9d5bc Merge pull request #2730 from sbwalker/dev
consolidated package installation so that it always occurs during startup and added logging in case of errors
2023-04-05 10:24:15 -04:00
789baf99ff Fix for #2728 Toggle Password button focus
Added  tabindex="-1" to the Button control so that focus is not received and passed the the following control.
2023-04-05 07:51:32 +02:00
036279a54c Fix for #2718 SiteMap functionality missing Settings
Added the Module Settings to the Module parameter passed in the GetUrls interface call,
2023-04-05 07:31:57 +02:00
481f18cf1c Fixes null reference permissions issue 2023-04-04 11:19:25 -07:00
2f1e386554 Update README.md 2023-03-29 14:13:27 -04:00
1e0c7cf43d Merge pull request #2706 from oqtane/master
Merge pull request #2705 from oqtane/dev
2023-03-29 14:06:22 -04:00
7978c89731 Merge pull request #2705 from oqtane/dev
3.4.2 release
2023-03-29 14:05:57 -04:00
82221f54c5 add defensive logic to package installer 2023-03-29 08:47:54 -04:00
2e23e6e4d5 Merge pull request #2704 from sbwalker/dev
add defensive logic to package installer
2023-03-29 08:45:46 -04:00
3a79fa074a fix #2700 - translation installation 2023-03-28 15:52:07 -04:00
696c63c6d2 Merge pull request #2703 from sbwalker/dev
fix #2700 - translation installation
2023-03-28 15:49:54 -04:00
8f6dc52430 prepare for 3.4.2 release 2023-03-28 14:29:57 -04:00
c8a9ad9807 Merge pull request #2702 from sbwalker/dev
prepare for 3.4.2 release
2023-03-28 14:27:45 -04:00
a0933d07d8 made word casing consistent with other module messages and reduced length of message 2023-03-28 08:53:26 -04:00
6bf61e2008 Merge pull request #2701 from sbwalker/dev
made word casing consistent with other module messages and reduced length of message
2023-03-28 08:51:24 -04:00
36ecc55578 Merge pull request #2697 from leigh-pointer/PwdConstra
Modified Registration to display the Password requirments
2023-03-28 08:17:00 -04:00
47065299ca Merge pull request #2691 from Behnam-Emamian/dev
Code Cleanups
2023-03-28 08:14:09 -04:00
0f707a7607 Moved message to the notification.
To be honest, the message about the password should be visible at all times.
2023-03-25 11:47:15 +01:00
7590c5550f Merge pull request #2698 from sbwalker/dev
Fix #2696 - PermissionNames not appearing in PermissionGrid
2023-03-24 12:52:35 -04:00
3d23a5c79a Fix #2696 - PermissionNames not appearing in PermissionGrid 2023-03-24 12:54:46 -04:00
9aa0374dc2 Removed unused Using 2023-03-24 12:41:27 +01:00
058a191673 Modified Registration to display the Password requirments 2023-03-24 12:38:27 +01:00
5fbb9160f1 Code Cleanups 2023-03-21 00:45:17 +11:00
2c3dad0592 Merge pull request #2689 from sbwalker/dev
Fix #2687 - add Setters to Permissions property to provide improved backward compatibility
2023-03-16 13:24:27 -04:00
00f039d31e Fix #2687 - add Setters to Permissions property to provide improved backward compatibility 2023-03-16 13:26:18 -04:00
497ef1750b add defensive logic to Oqtane.Client for loading modules on which have not declared all dependencies on WebAssembly 2023-03-14 20:40:49 -04:00
7d4cd04ce9 Merge pull request #2684 from sbwalker/dev
add defensive logic to Oqtane.Client for loading modules on which have not declared all dependencies on WebAssembly
2023-03-14 20:39:23 -04:00
04c0b9d37d add defensive logic to Oqtane.Maui for loading modules which have not declared all dependencies 2023-03-14 20:37:42 -04:00
b9c16c0727 Merge pull request #2683 from sbwalker/dev
add defensive logic to Oqtane.Maui for loading modules which have not declared all dependencies
2023-03-14 20:35:55 -04:00
0a30f2b7e8 fix #2679 - fixed issue where ModuleDefinition cache properties were being overwritten (same issue as #2674 however implemented in ModuleController) 2023-03-14 11:49:38 -04:00
dbb1d53202 Merge pull request #2682 from sbwalker/dev
fix #2679 - fixed issue where ModuleDefinition cache properties were being overwritten (same issue as #2674 however implemented in ModuleController)
2023-03-14 11:48:01 -04:00
2c88f36e3d fix #2680 - issue when adding new site to existing installation 2023-03-14 10:26:51 -04:00
d91dcad774 Merge pull request #2681 from sbwalker/dev
fix #2680 - issue when adding new site to existing installation
2023-03-14 10:24:53 -04:00
6eb4ea2a2d Update README.md 2023-03-13 22:41:30 -04:00
ff6187c336 Merge pull request #2678 from oqtane/master
Merge pull request #2677 from oqtane/dev
2023-03-13 22:35:52 -04:00
1253dfe0c8 Merge pull request #2677 from oqtane/dev
3.4.1 release
2023-03-13 22:35:34 -04:00
c1f2f9a970 prepare for 3.4.1 2023-03-13 22:21:04 -04:00
8d5e7ed69f Merge pull request #2676 from sbwalker/dev
prepare for 3.4.1
2023-03-13 22:19:07 -04:00
3d3540f090 fixed issue where ModuleDefinition cache properties were being overwritten 2023-03-13 10:04:37 -04:00
61f1fcb99c Merge pull request #2674 from sbwalker/dev
fixed issue where ModuleDefinition cache properties were being overwritten
2023-03-13 10:02:53 -04:00
c7b576d6d3 Update README.md 2023-03-12 10:44:17 -04:00
ab3b1d5e46 Update README.md 2023-03-12 10:42:23 -04:00
ddff3faba9 Update README.md 2023-03-12 10:37:42 -04:00
f7bb8444da Merge pull request #2670 from oqtane/master
Merge pull request #2669 from oqtane/dev
2023-03-12 10:12:26 -04:00
92128974bb Merge pull request #2669 from oqtane/dev
3.4.0 release
2023-03-12 10:11:44 -04:00
4eb15d4806 include Site Map field in Site Settings 2023-03-11 17:42:37 -05:00
39cb3780c8 Merge pull request #2668 from sbwalker/dev
include Site Map field in Site Settings
2023-03-11 17:40:44 -05:00
81030f468b improve user profile error logging for external login 2023-03-11 14:03:24 -05:00
2032cb1ace Merge pull request #2667 from sbwalker/dev
improve user profile error logging for external login
2023-03-11 14:01:38 -05:00
5e4c91440e modify editmode parameter value in url to be more intuitive 2023-03-11 11:56:43 -05:00
13bbad863f Merge pull request #2666 from sbwalker/dev
modify editmode parameter value in url to be more intuitive
2023-03-11 11:54:48 -05:00
3065ed5094 fix issue with capturing user profile information during external login 2023-03-11 11:50:02 -05:00
9eb75cfff0 Merge pull request #2665 from sbwalker/dev
fix issue with capturing user profile information during external login
2023-03-11 11:48:07 -05:00
9305c99577 exclude hidden pages by default 2023-03-10 17:22:18 -05:00
9078da6937 Merge pull request #2663 from sbwalker/dev
exclude hidden pages by default
2023-03-10 17:20:25 -05:00
4c579639b9 avoid null reference error if list is null 2023-03-10 13:57:13 -05:00
b86472ab52 Merge pull request #2662 from sbwalker/dev
avoid null reference error if list is null
2023-03-10 13:55:28 -05:00
5e1ac485a0 #2655 - add support for capturing user profile infrmation from claims during external login 2023-03-10 10:14:57 -05:00
527c1a12f4 Merge pull request #2661 from sbwalker/dev
#2655 - add support for capturing user profile infrmation from claims during external login
2023-03-10 10:13:09 -05:00
ef4e99b3a7 further optimization of permissions - removed reference to Role to reduce API payload and minimize information disclosure 2023-03-10 08:28:37 -05:00
12a9635309 Merge pull request #2660 from sbwalker/dev
further optimization of permissions - removed reference to Role to reduce API payload and minimize information disclosure
2023-03-10 08:26:53 -05:00
78adb24a75 fix new installation issue 2023-03-09 16:54:44 -05:00
49955cf642 Merge pull request #2658 from sbwalker/dev
fix new installation issue
2023-03-09 16:52:51 -05:00
af3b289331 exclude legacy Permissions properties from serialization/API payload 2023-03-09 15:51:16 -05:00
d11591e5aa Merge pull request #2657 from sbwalker/dev
exclude legacy Permissions properties from serialization/API payload
2023-03-09 15:49:29 -05:00
9c6174e3f2 rolling back CSS changes so that it remains consistent with the Oqtane theme 2023-03-09 09:46:58 -05:00
09c2f74d52 Merge pull request #2654 from sbwalker/dev
rolling back CSS changes so that it remains consistent with the Oqtane theme
2023-03-09 09:45:15 -05:00
7d7e0254cb Merge pull request #2642 from leigh-pointer/RecycleBinDelete
Fix Clearing modules from the Recycle Bin
2023-03-09 08:08:58 -05:00
fe767afe9c Merge branch 'dev' into RecycleBinDelete 2023-03-09 11:49:22 +01:00
3378f0e4ee Procedures now checks on PageModuleId 2023-03-09 11:47:19 +01:00
c0341798ea Merge pull request #2653 from sbwalker/dev
include documentation to explain logic
2023-03-08 12:09:25 -05:00
fc114dc5db include documentation to explain logic 2023-03-08 12:11:11 -05:00
7107d844e1 fix #2640 - system should remain in edit mode when editing a page 2023-03-08 11:50:30 -05:00
59af0a817e Merge pull request #2652 from sbwalker/dev
fix #2640 - system should remain in edit mode when editing a page
2023-03-08 11:48:34 -05:00
9615eded85 Merge pull request #2644 from leigh-pointer/HideDeleteAllButtons
Hide the ActionDialog Button for  "Clear Notifications"
2023-03-08 10:50:13 -05:00
c51fa23fcb Merge pull request #2649 from leigh-pointer/PageChangeRecBin
Add OnPageChangePage to the Page and the Module Pagers in Recycle Bin
2023-03-08 10:50:04 -05:00
8737fd6f1e Merge pull request #2651 from sbwalker/dev
fx #2647 - error when creating new site in existing installation
2023-03-08 10:49:43 -05:00
0f109ab93a fx #2647 - error when creating new site in existing installation 2023-03-08 10:51:38 -05:00
63df2742db initialize SiteId in Permission constructor 2023-03-08 08:43:45 -05:00
7b7811f8ad Merge pull request #2650 from sbwalker/dev
initialize SiteId in Permission constructor
2023-03-08 08:42:13 -05:00
80f74b9939 Add OnPageChangePage to the Page and the Module Pagers
Now when deleting Pages or Modules the current page is shown and not reset to the first page.
2023-03-08 13:55:27 +01:00
1f29f77f66 fix #2624 - permission grid behavior issues 2023-03-06 12:20:20 -05:00
dd7da5f354 Merge pull request #2645 from sbwalker/dev
fix #2624 - permission grid behavior issues
2023-03-06 12:19:01 -05:00
49b30da697 Hide the ActionDialog Button for "Clear Notifications"
If there are no Notifications then the buttons is not displayed.
2023-03-06 09:13:25 +01:00
90ed767d75 Fix Clearing modules from the Recycle Bin
Modules and all related records now correctly deleted from the database.
2023-03-06 06:26:01 +01:00
7871f0f3ce Merge pull request #2628 from thabaum/dev
Fixes #2627 #2631 #2630 #2629 #2632  #2635 - Dev Branch Module and Theme Template Issues
2023-03-05 17:53:20 -05:00
a60cf40a3c inverts color of text within "main" <div> 2023-03-05 09:50:56 -08:00
f4eb2f6726 Merge pull request #2637 from sbwalker/dev
fix localization issue in Scheduled Jobs
2023-03-05 08:55:06 -05:00
cfe87a802e fix localization issue in Scheduled Jobs 2023-03-05 08:56:35 -05:00
6c90ec812f Fix - unable to see module actions dropdown toggle 2023-03-04 15:24:43 -08:00
196d611c1c Fix issue with navbar 2023-03-04 13:13:49 -08:00
ff41cb2735 Missing using directive Oqtane.Repository 2023-03-04 10:45:32 -08:00
fb11674301 Missing - Using Oqtane.Repository 2023-03-04 10:44:19 -08:00
b9e7f4530c Update Theme Project to use [RootFolder] 2023-03-04 10:35:45 -08:00
27049687bf Use [RootFolder] for Oqtane.Server Project 2023-03-04 10:34:12 -08:00
13503edc63 Removed extra { } 2023-03-04 09:50:49 -08:00
d33f82d969 prepare for 3.4.0 release 2023-03-03 15:45:56 -05:00
177632eee0 Merge pull request #2622 from sbwalker/dev
prepare for 3.4.0 release
2023-03-03 15:44:08 -05:00
ca0de5258e Merge pull request #2620 from leigh-pointer/DeletePermissionModDef
Fix Correct Permission Delete when ModuleDef is deleted #2619
2023-03-02 15:33:25 -05:00
1de788bc26 Merge pull request #2621 from sbwalker/dev
#2618 - add backward compatibility for permissions optimizations
2023-03-02 15:33:11 -05:00
2b41909d47 #2618 - add backward compatibility for permissions optimizations 2023-03-02 15:34:42 -05:00
e23a9f22dd Fix Correct Permission Delete when ModuleDef is deleted #2619
Added PermissionsRepository to delete the Module permissions when the Module Definition is deleted.
2023-03-02 06:58:19 +01:00
465b7850b7 Fix #2614 - ability to add module to page 2023-03-01 10:05:14 -05:00
a0f2eedd7f Merge pull request #2615 from sbwalker/dev
Fix #2614 - ability to add module to page
2023-03-01 10:03:30 -05:00
8605e3ca5a Major refactoring replacing permission strings with permission collections. These changes will require extensive regression testing. These changes may include breaking changes which will need to be identified and resolved to provide backward compatibility. 2023-02-28 17:59:21 -05:00
dd893e6d48 Merge pull request #2612 from sbwalker/dev
Major refactoring replacing permission strings with permission collections. These changes will require extensive regression testing. These changes may include breaking changes which will need to be identified and resolved to provide backward compatibility.
2023-02-28 17:57:54 -05:00
c4cd1a5a54 Merge pull request #2610 from leigh-pointer/DeleteModDefPageMod
Fix for deleting a ModuleDefinition and related records  #2602
2023-02-24 14:21:22 -05:00
94152651fc Merge pull request #2609 from Behnam-Emamian/dev
extends watching *.dll files
2023-02-24 14:20:44 -05:00
563ea76192 Merge pull request #2611 from sbwalker/dev
explicity specify optional and reload parameters
2023-02-24 14:19:27 -05:00
4913fab0b3 explicity specify optional and reload parameters 2023-02-24 14:21:03 -05:00
b49d011edf Fix for deleting a ModuleDefinition and related records #2602
We then find all Module items that have a ModuleDefinitionName property that matches the ModuleDefinitionName of the item to be removed, and remove them one by one. For each Module item to be removed, we find the PageModule items associated with it, remove them from the pageModules list, and then remove the Module item itself from the modules list.
2023-02-24 11:44:12 +01:00
6e04281b03 extends watching dll files
extends watching group to include *.dll files and exclude the ones cause an infinite loop.
2023-02-24 11:20:05 +11:00
f2df8e96db fix #2567 - migrate tenant connection string details from database to appsettings.json 2023-02-23 16:29:15 -05:00
c6dd7605b2 Merge pull request #2608 from sbwalker/dev
fix #2567 - migrate tenant connection string details from database to appsettings.json
2023-02-23 16:28:16 -05:00
71dd00da0f Merge pull request #2605 from Behnam-Emamian/dev
add AddByteColumn to add tinyint to the database table.
2023-02-21 07:54:20 -05:00
da48ca884d Merge pull request #2606 from sbwalker/dev
add sitemap generator which outputs all public pages and also includes an ISitemap interface for modules
2023-02-20 08:34:15 -05:00
8c6c66fb11 add sitemap generator which outputs all public pages and also includes an ISitemap interface for modules 2023-02-20 08:35:46 -05:00
f333b57310 add AddByteColumn to add tinyint to the database table 2023-02-20 00:20:19 +11:00
d1d00e6c98 Merge pull request #2601 from leigh-pointer/ModDefSettingsDelete
Fix for Missing Delete ModuleDefinition settings #1966
2023-02-17 09:15:44 -05:00
52300e680a Fix for Missing Delete ModuleDefinition settings #1966
Added ISettingRepository _settings  to the public ModuleDefinitionRepository method and updated the DeleteModuleDefinition with _settings.DeleteSettings(EntityNames.ModuleDefinition, moduleDefinitionId);
2023-02-16 13:38:42 +01:00
b3f7353582 Merge pull request #2599 from sbwalker/dev
add defensive logic to querystring parser to handle duplicate parameters
2023-02-15 15:20:12 -05:00
7db6b82a1a add defensive logic to querystring parser to handle duplicate parameters 2023-02-15 15:21:50 -05:00
a50a13374f improve initialization logic in FileManager which could sometimes result in Upload button not being displayed when the component was initially loaded 2023-02-15 15:06:50 -05:00
3952fe5a72 Merge pull request #2598 from sbwalker/dev
improve initialization logic in FileManager which could sometimes result in Upload button not being displayed when the component was initially loaded
2023-02-15 15:05:17 -05:00
2e61a43e4f fix #2596 - fix EF Core tracking error when updating a file in a folder which has a Capacity specified 2023-02-15 12:43:18 -05:00
ebe03e9310 Merge pull request #2597 from sbwalker/dev
fix #2596 - fix EF Core tracking error when updating a file in a folder which has a Capacity specified
2023-02-15 12:42:00 -05:00
11dd3ce110 adding Oqtane.Server project back to module and theme external template solutions 2023-02-09 17:45:45 -05:00
1919c24959 Merge pull request #2593 from sbwalker/dev
adding Oqtane.Server project back to module and theme external template solutions
2023-02-09 17:44:27 -05:00
aa80f31e52 fix #2570 - do not allow the term "oqtane" to be used as an organization or module/theme name (to avoid namespace issues). 2023-02-09 16:26:20 -05:00
6d8400e72f Merge pull request #2592 from sbwalker/dev
fix #2570 - do not allow the term "oqtane" to be used as an organization or module/theme name (to avoid namespace issues).
2023-02-09 16:25:08 -05:00
fa8d0c91fc added new methods for managing visitor settings (for personalization) 2023-02-08 17:43:55 -05:00
e91ff95712 Merge pull request #2591 from sbwalker/dev
added new methods for managing visitor settings (for personalization)
2023-02-08 17:42:35 -05:00
0883a8dbff optimize Split() statements for consistency 2023-02-08 16:51:45 -05:00
0db297d1cd Merge pull request #2590 from sbwalker/dev
optimize Split() statements for consistency
2023-02-08 16:50:23 -05:00
db73052ee5 allow system log to be cleared 2023-02-08 14:45:20 -05:00
8b95069610 Merge pull request #2589 from sbwalker/dev
allow system log to be cleared
2023-02-08 14:43:58 -05:00
2a12744cd5 added toggle to show/hide connection string in Site Settings 2023-02-08 08:29:50 -05:00
1df4059284 Merge pull request #2588 from sbwalker/dev
added toggle to show/hide connection string in Site Settings
2023-02-08 08:28:34 -05:00
475894b680 fix #2584 - added IsDeleted columns back to Folder and File tables to preserve compatibility for SQLite 2023-02-08 08:05:25 -05:00
1663bf8e52 Merge pull request #2587 from sbwalker/dev
fix #2584 - added IsDeleted columns back to Folder and File tables to preserve compatibility for SQLite
2023-02-08 08:04:58 -05:00
ffca1d2486 refactor visitor cookie name into a shared constant 2023-02-07 16:26:23 -05:00
eb876845ff Merge pull request #2585 from sbwalker/dev
refactor visitor cookie name into a shared constant
2023-02-07 16:25:05 -05:00
02c134bf4b Merge pull request #2580 from markdav-is/patch-3
Make ActiveDatabase setter public
2023-02-06 16:50:55 -05:00
af55c11aa0 Merge pull request #2582 from sbwalker/dev
fix #2574 - check for null ModuleDefinition reference  when loding permissions in PageModuleRepository (credit @beolafsen)
2023-02-06 16:48:40 -05:00
33bc6adcb5 fix #2574 - check for null ModuleDefinition reference when loding permissions in PageModuleRepository (credit @beolafsen) 2023-02-06 16:49:45 -05:00
56e4dcc11e fix #2578 - error notification sent via email includes direct link to specific log item, however redirect was causing an infinite loop. This resolves the problem and also preserves url querystring parameters during login/logout. 2023-02-06 16:44:25 -05:00
467cf7620e Merge pull request #2581 from sbwalker/dev
fix #2578 - error notification sent via email includes direct link to specific log item, however redirect was causing an infinite loop. This resolves the problem and also preserves url querystring parameters during login/logout.
2023-02-06 16:43:23 -05:00
85ac8dd701 Make ActiveDatabase setter public
We have two cases where we need to override the active database:  Unit Testing and added GraphQL.  In both of these cases, we have a database context that is in a different scope than the automatically assigned active database during normal Oqtane startup.  Our work-around has been to make this setter public.  Unless there is a better solution to our cases, I feel this change would be useful for others as well.
2023-02-04 09:04:54 -08:00
1f2ad4e884 Suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies 2023-02-03 16:12:13 -05:00
cf2d9af664 Merge pull request #2579 from sbwalker/dev
Suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies
2023-02-03 16:10:57 -05:00
7a105047e9 Fixed issue where TenantMiddleware was not rewriting the Url path for the new File Server when running on an Alias Path which resulted in a 404 when serving files 2023-01-23 15:16:08 -05:00
bc8bdef37d Merge pull request #2571 from sbwalker/dev
Fixed issue where TenantMiddleware was not rewriting the Url path for the new File Server when running on an Alias Path which resulted in a 404 when serving files
2023-01-23 15:15:05 -05:00
fd0519b955 Update README.md 2023-01-14 15:17:38 -05:00
d5ffb56fa8 Update README.md 2023-01-14 15:15:33 -05:00
d6cce9e2d8 Update README.md 2023-01-14 15:08:20 -05:00
08ec46637f Update README.md 2023-01-14 15:04:31 -05:00
f596795792 Update README.md 2023-01-14 15:03:46 -05:00
2c56bfd4aa Merge pull request #2565 from oqtane/master
Merge pull request #2564 from oqtane/dev
2023-01-14 14:54:37 -05:00
755615da30 Merge pull request #2564 from oqtane/dev
3.3.1 release
2023-01-14 14:54:18 -05:00
4cc0060c67 prepare for 3.3.1 patch 2023-01-14 14:45:30 -05:00
52af015c2f Merge pull request #2563 from sbwalker/dev
prepare for 3.3.1 patch
2023-01-14 14:44:32 -05:00
eae6d13284 Fix #2561 - set Permission EntityName explicitly to Module when adding module to page from Control Panel 2023-01-14 12:39:34 -05:00
2a20a7cf21 Merge pull request #2562 from sbwalker/dev
Fix #2561 - set Permission EntityName explicitly to Module when adding module to page from Control Panel
2023-01-14 12:38:55 -05:00
27251005ec incorrect url in framework nusepc 2023-01-13 07:50:58 -05:00
90a9b2e5a1 Merge pull request #2560 from sbwalker/dev
incorrect url in framework nusepc
2023-01-13 07:50:04 -05:00
cd1f12e9b4 Merge pull request #2557 from oqtane/master
Merge pull request #2556 from oqtane/dev
2023-01-12 12:50:09 -05:00
734b9b6458 Merge pull request #2556 from oqtane/dev
3.3.0 Release
2023-01-12 12:49:49 -05:00
a120449c8d Update README.md 2023-01-12 12:48:54 -05:00
b005a1da56 Merge pull request #2555 from sbwalker/dev
remove extra info from Body as the From display name is now always set to the user's name
2023-01-12 11:47:32 -05:00
afc75a09d9 remove extra info from Body as the From display name is now always set to the user's name 2023-01-12 11:48:25 -05:00
c6e6c98875 improve notification job 2023-01-12 09:13:13 -05:00
fa1a5ab913 Merge pull request #2554 from sbwalker/dev
improve notification job
2023-01-12 09:12:11 -05:00
b671b590ad change Sql Manager logging level 2023-01-12 08:18:36 -05:00
a46c98eed3 Merge pull request #2553 from sbwalker/dev
change Sql Manager logging level
2023-01-12 08:17:42 -05:00
7ab5ed5bcb copyright year update 2023-01-11 14:40:16 -05:00
5e609edc31 Merge pull request #2551 from sbwalker/dev
copyright year update
2023-01-11 14:39:15 -05:00
ac466429f8 improve release batch file 2023-01-11 10:24:50 -05:00
2804c50c8a Merge pull request #2550 from sbwalker/dev
improve release batch file
2023-01-11 10:24:18 -05:00
c4315c25bc prepare for 3.3.0 release 2023-01-10 14:02:23 -05:00
e2d5fa48cf Merge pull request #2548 from sbwalker/dev
prepare for 3.3.0 release
2023-01-10 14:01:48 -05:00
c2375c897d permission updates 2023-01-10 08:20:32 -05:00
49f181583b Merge pull request #2546 from sbwalker/dev
permission updates
2023-01-10 08:20:27 -05:00
ea463a6548 fix #2534 - added Relay Configured site setting to enable sending from users email address 2023-01-09 16:37:06 -05:00
1a567dbdc1 Merge pull request #2545 from sbwalker/dev
fix #2534 - added Relay Configured site setting to enable sending from users email address
2023-01-09 16:36:30 -05:00
e4ec10ef49 format PermissionNames to be more readable 2023-01-09 15:36:41 -05:00
3db4fc687a Merge pull request #2544 from sbwalker/dev
format PermissionNames to be more readable
2023-01-09 15:35:59 -05:00
e136972cd7 add support for API permissions at the UI layer - including ability to delegate user, role, profile management 2023-01-09 11:38:25 -05:00
7b1b32e16f Merge pull request #2543 from sbwalker/dev
add support for API permissions at the UI layer - including ability to delegate user, role, profile management
2023-01-09 11:37:54 -05:00
1616f94b86 add ability to view error.log in System Info 2023-01-05 10:18:55 -05:00
7043d1f3bf Merge pull request #2542 from sbwalker/dev
add ability to view error.log in System Info
2023-01-05 10:18:10 -05:00
7bebfe1919 fix typo and help text 2023-01-05 09:55:06 -05:00
d33ded0426 Merge pull request #2541 from sbwalker/dev
fix typo and help text
2023-01-05 09:54:23 -05:00
66aa67581f improve dynamic policy registration to handle possible race conditions 2023-01-05 09:43:59 -05:00
3a95c05db9 Merge pull request #2540 from sbwalker/dev
improve dynamic policy registration to handle possible race conditions
2023-01-05 09:43:15 -05:00
67046e9d36 Merge pull request #2535 from thabaum/Readme-Glow-Logo
Update framework root to use the new dark/light theme glow logo
2023-01-04 14:51:39 -05:00
6f2965a5b0 Merge pull request #2537 from leigh-pointer/Pager-Pointer
A change to the Pager bar to set the mouse pointer to pointer
2023-01-04 14:51:29 -05:00
197db449a1 Merge pull request #2539 from sbwalker/dev
include owner in migration tag name in external module template
2023-01-04 14:51:11 -05:00
f4800bb7f0 include owner in migration tag name in external module template 2023-01-04 14:51:55 -05:00
6a213561f6 added an autocomplete component and implemented in permission grid 2023-01-04 14:50:05 -05:00
8f8d31f0c9 Merge pull request #2538 from sbwalker/dev
added an autocomplete component and implemented in permission grid
2023-01-04 14:49:23 -05:00
f8cfdacc26 A change to the Pager bar to set the mouse pointer to pointer
Currently the mouse pointer shows the Selector icon when hoovered over the page number buttons. This is an update changing the icon to the Pointer icon.

Updated the CSS class name to 'app-pager-pointer' in app.css and the Component.
2023-01-03 14:24:17 +01:00
07223e27a4 Add Oqtane Glow Logo for Dark/Light Themes 2022-12-27 10:30:00 -08:00
4c6f46ad17 Remove Dark Logo from Framework Root 2022-12-27 10:28:37 -08:00
39dc288f9c Merge pull request #2533 from sbwalker/dev
fix #2526 - support multiple TabStrip components on a page
2022-12-19 15:57:30 -05:00
467e88ef55 fix #2526 - support multiple TabStrip components on a page 2022-12-19 15:58:04 -05:00
0965db5d57 fix skip pages logic in pager where screen was not being refreshed 2022-12-19 15:11:04 -05:00
c30339d9b8 Merge pull request #2532 from sbwalker/dev
fix skip pages logic in pager where screen was not being refreshed
2022-12-19 15:10:27 -05:00
369adcb173 allow profile management to be delegated 2022-12-19 15:07:38 -05:00
d837b981d4 Merge pull request #2531 from sbwalker/dev
allow profile management to be delegated
2022-12-19 15:07:03 -05:00
f455461c1e Update README.md 2022-12-14 14:54:23 -05:00
0087b188a1 improve null handling 2022-12-07 10:33:25 -05:00
98602f49d8 Merge pull request #2522 from sbwalker/dev
improve null handling
2022-12-07 10:33:21 -05:00
70466e5626 Merge pull request #2521 from sbwalker/dev
fix #2513 - add new methods for deleting a setting and retrieving a list of settings
2022-12-07 08:59:07 -05:00
cc7f98a6fe fix #2513 - add new methods for deleting a setting and retrieving a list of settings 2022-12-07 08:59:03 -05:00
fd13ad1fca initialize API permissions based on default roles 2022-12-06 17:16:51 -05:00
5077f6fbca Merge pull request #2520 from sbwalker/dev
initialize API permissions based on default roles
2022-12-06 17:16:47 -05:00
9b15ce6d29 Merge pull request #2519 from sbwalker/dev
make casing consistent in route template definition and method parameter declation or else Swagger will not be able to resolve
2022-12-06 10:48:57 -05:00
5a8ca24566 make casing consistent in route template definition and method parameter declation or else Swagger will not be able to resolve 2022-12-06 10:48:56 -05:00
d3f982cae1 Merge pull request #2518 from sbwalker/dev
add ModuleControllerBase helper method for validating EntityId
2022-12-05 14:21:15 -05:00
28b58b9048 add ModuleControllerBase helper method for validating EntityId 2022-12-05 14:21:12 -05:00
cb10dde97d added API Management for managing site level entity permissions 2022-12-02 16:42:43 -05:00
2c179867e8 Merge pull request #2517 from sbwalker/dev
added API Management for managing site level entity permissions
2022-12-02 16:42:41 -05:00
44fc6de82b hide connection string by default in SQL Management and provide toggle for display 2022-12-02 08:16:18 -05:00
f671af43cd Merge pull request #2516 from sbwalker/dev
hide connection string by default in SQL Management and provide toggle for display
2022-12-02 08:16:18 -05:00
8c0dc6422e Merge pull request #2515 from sbwalker/dev
fix #2512 - provide guidance about password complexity policy during install, and ensure modified passwords meet complexity policy
2022-12-02 07:42:55 -05:00
c91e285475 fix #2512 - provide guidance about password complexity policy during install, and ensure modified passwords meet complexity policy 2022-12-02 07:42:49 -05:00
642e41530e Merge pull request #2514 from sbwalker/dev
enhance dynamic authorization policies to support default role specification
2022-12-02 07:34:08 -05:00
b09a3ccdae enhance dynamic authorization policies to support default role specification 2022-12-02 07:34:06 -05:00
a1aab62cea Merge pull request #2504 from leigh-pointer/PagerPointer
A change to the Pager bar to set the mouse pointer to pointer
2022-11-23 11:26:57 -05:00
e8b1aca45c Merge pull request #2507 from sbwalker/dev
fix #2502 - invalid logic checking querystring parameter
2022-11-23 11:26:21 -05:00
3de98873d6 fix #2502 - invalid logic checking querystring parameter 2022-11-23 11:26:23 -05:00
c030ede12c Merge pull request #2506 from sbwalker/dev
fix #2503 - generate password using CultureInfo.InvariantCulture to ensure it satisfies password complexity criteria
2022-11-23 11:11:05 -05:00
67f740c264 fix #2503 - generate password using CultureInfo.InvariantCulture to ensure it satisfies password complexity criteria 2022-11-23 11:10:59 -05:00
9b68337047 Merge pull request #2505 from sbwalker/dev
fix #2501 - set default Visibility to Same As Page when adding module to a page
2022-11-23 10:51:50 -05:00
2bae971b92 fix #2501 - set default Visibility to Same As Page when adding modules to a page 2022-11-23 10:51:37 -05:00
c5c5fd859f A change to the Pager bar to set the mouse pointer to pointer
Currently the mouse pointer shows the Selector icon when hoovered over the page number buttons.  This is an update changing the icon to the Pointer icon.
2022-11-21 09:44:30 +01:00
15c46fd157 Merge pull request #2496 from sbwalker/dev
Fix #2488 - add ability to include inline script resource definitions in modules and themes
2022-11-12 10:59:25 -05:00
424950bd3e Fix #2488 - add ability to include inline script resource definitions in modules and themes 2022-11-12 10:58:58 -05:00
39a9968971 Merge pull request #2493 from sbwalker/dev
fix JS Interop methods for includeScript and includeMeta
2022-11-10 14:19:52 -05:00
075a09f0df fix JS Interop methods for includeScript and includeMeta 2022-11-10 14:19:31 -05:00
26e628e189 Merge pull request #2492 from sbwalker/dev
move UI logic from FileService to FileManager, add progressive retry logic, update file attributes if uploading a new version of a file, clean up temporary artifacts on failure, improve upload efficiency
2022-11-09 21:11:42 -05:00
7489d9d186 move UI logic from FileService to FileManager, add progressive retry logic, update file attributes if uploading a new version of a file, clean up temporary artifacts on failure, improve upload efficiency 2022-11-09 21:11:02 -05:00
7db5b6c7f3 Update README.md 2022-11-08 08:59:49 -05:00
9b7fa8cac2 Update README.md 2022-11-08 08:53:33 -05:00
5028051a34 Update README.md 2022-11-08 08:52:51 -05:00
83b3767df6 Merge pull request #2490 from sbwalker/dev
Scope permissions by SiteId to support entity level authorization as well as improve caching and performance. Optimize GetTenant to use existing cache.
2022-11-07 18:17:04 -05:00
6182b96d16 Scope permissions by SiteId to support entity level authorization as well as improve caching and performance. Optimize GetTenant to use existing cache. 2022-11-07 18:16:32 -05:00
a719382563 Merge pull request #2482 from sbwalker/dev
add support for dynamic authorization policies
2022-11-04 08:08:33 -04:00
2aa6eb90e2 add support for dynamic authorization policies 2022-11-04 08:08:10 -04:00
d2495455cd Merge pull request #2477 from sbwalker/dev
added ETag / 304 Not Modified logic to File server for performance optimization
2022-10-29 10:23:58 -04:00
23d1dd23d1 added ETag / 304 Not Modified logic to File server for performance optimization 2022-10-29 10:23:04 -04:00
573acd6a5d Merge pull request #2476 from sbwalker/dev
fix File Update API to update the file size and image dimensions
2022-10-27 09:39:05 -04:00
40ddbbfbb7 fix File Update API to update the file size and image dimensions 2022-10-27 09:38:26 -04:00
1b488b298b Merge pull request #2475 from sbwalker/dev
remove IDeletable fields from Folder and File entities as they are never set and not used
2022-10-26 17:43:09 -04:00
54b45943db remove IDeletable fields from Folder and File entities as they are never set and not used 2022-10-26 17:42:26 -04:00
89b3ae4c74 Merge pull request #2474 from sbwalker/dev
fix IDeletable code documentation
2022-10-26 17:33:52 -04:00
fe97a76d00 fix IDeletable code documentation 2022-10-26 17:33:17 -04:00
78094f69d7 Merge pull request #2473 from sbwalker/dev
update models to use new ModelBase
2022-10-26 17:27:22 -04:00
4499d55464 update models to use new ModelBase 2022-10-26 17:26:46 -04:00
4c12ca0607 Merge pull request #2472 from sbwalker/dev
introduced a ModelBase to move the IAuditable properties to a base class
2022-10-26 17:12:38 -04:00
1daa9575db introduced a ModelBase to move the IAuditable properties to a base class 2022-10-26 17:12:03 -04:00
f7c9961f8c Merge pull request #2471 from sbwalker/dev
add loading icon to Maui project
2022-10-26 10:06:57 -04:00
e27a625069 add loading icon to Maui project 2022-10-26 09:04:04 -04:00
42b1669cce Merge pull request #2469 from sbwalker/dev
remove Oqtane.Server from external templates as they cause random compilation issues
2022-10-20 14:05:45 -04:00
74571afc9e remove Oqtane.Server from external templates as they cause random compilation issues 2022-10-20 14:04:53 -04:00
f678f11c59 Merge pull request #2468 from sbwalker/dev
add validation message for missing package name
2022-10-20 14:00:56 -04:00
e685252b1d add validation message for missing package name 2022-10-20 14:00:17 -04:00
bb75fcb5a3 Merge pull request #2467 from sbwalker/dev
fix language delete refresh
2022-10-20 13:43:36 -04:00
7653f36f31 fix language delete refresh 2022-10-20 13:42:54 -04:00
0670148064 Merge pull request #2466 from sbwalker/dev
fix #2464 - translation install/upgrade experience
2022-10-20 13:17:06 -04:00
368b900a6e fix #2464 - translation install/upgrade experience 2022-10-20 13:16:18 -04:00
6378a78cbd Update README.md 2022-10-18 08:10:48 -04:00
d3bcdec0f2 Update README.md 2022-10-18 08:10:02 -04:00
82c074ce2e Merge pull request #2463 from sbwalker/dev
replace assembly references with package references
2022-10-18 07:52:33 -04:00
a313e2d386 replace assembly references with package references 2022-10-18 07:51:50 -04:00
578cb2f7e3 Update README.md 2022-10-18 07:43:18 -04:00
245 changed files with 5383 additions and 3830 deletions

View File

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

View File

@ -55,7 +55,7 @@
else else
{ {
<div class="row mb-1 align-items-center"> <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> <Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">Settings:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea> <textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div> </div>
@ -77,7 +77,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordType" class="form-control" @bind="@_hostPassword" autocomplete="new-password" /> <input id="password" type="@_passwordType" class="form-control" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglePassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -86,7 +86,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" /> <input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword">@_toggleConfirmPassword</button> <button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword" tabindex="-1">@_toggleConfirmPassword</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,10 +4,12 @@
@inject IUserService UserService @inject IUserService UserService
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="row"> @if (_pages != null)
{
<div class="row">
@foreach (var p in _pages) @foreach (var p in _pages)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
string url = NavigateUrl(p.Path); string url = NavigateUrl(p.Path);
<div class="col-md-2 mx-auto text-center mb-3"> <div class="col-md-2 mx-auto text-center mb-3">
@ -17,16 +19,20 @@
</div> </div>
} }
} }
</div> </div>
}
@code { @code {
private List<Page> _pages; private List<Page> _pages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override void OnInitialized() protected override void OnInitialized()
{ {
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin"); var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
_pages = PageState.Pages.Where(item => item.ParentId == admin?.PageId).ToList(); if (admin != null)
{
_pages = PageState.Pages.Where(item => item.ParentId == admin.PageId).ToList();
}
} }
} }

View File

@ -62,8 +62,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col-sm-12"> <div class="col-sm-12">
<Label Class="col-sm-3" For="permissions" HelpText="Select the permissions you want for the folder" ResourceKey="Permissions">Permissions: </Label> <Label Class="col-sm-3" For="permissions" HelpText="Select the permissions you want for the folder" ResourceKey="Permissions">Permissions: </Label>
<PermissionGrid EntityName="@EntityNames.Folder" PermissionNames="@(PermissionNames.Browse + "," + PermissionNames.View + "," + PermissionNames.Edit)" Permissions="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.Folder" PermissionNames="@(PermissionNames.Browse + "," + PermissionNames.View + "," + PermissionNames.Edit)" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
</div> </div>
@ -99,7 +98,7 @@
private string _imagesizes = string.Empty; private string _imagesizes = string.Empty;
private string _capacity = "0"; private string _capacity = "0";
private bool _isSystem; private bool _isSystem;
private string _permissions = string.Empty; private List<Permission> _permissions = null;
private string _createdBy; private string _createdBy;
private DateTime _createdOn; private DateTime _createdOn;
private string _modifiedBy; private string _modifiedBy;
@ -131,7 +130,7 @@
_imagesizes = folder.ImageSizes; _imagesizes = folder.ImageSizes;
_capacity = folder.Capacity.ToString(); _capacity = folder.Capacity.ToString();
_isSystem = folder.IsSystem; _isSystem = folder.IsSystem;
_permissions = folder.Permissions; _permissions = folder.PermissionList;
_createdBy = folder.CreatedBy; _createdBy = folder.CreatedBy;
_createdOn = folder.CreatedOn; _createdOn = folder.CreatedOn;
_modifiedBy = folder.ModifiedBy; _modifiedBy = folder.ModifiedBy;
@ -196,7 +195,7 @@
folder.ImageSizes = _imagesizes; folder.ImageSizes = _imagesizes;
folder.Capacity = int.Parse(_capacity); folder.Capacity = int.Parse(_capacity);
folder.IsSystem = _isSystem; folder.IsSystem = _isSystem;
folder.Permissions = _permissionGrid.GetPermissions(); folder.PermissionList = _permissionGrid.GetPermissionList();
if (_folderId != -1) if (_folderId != -1)
{ {

View File

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

View File

@ -19,38 +19,95 @@ else
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th> <th>@Localizer["Code"]</th>
<th>@Localizer["Translation"]</th>
<th>@Localizer["Default"]</th> <th>@Localizer["Default"]</th>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<th style="width: 1px;">@Localizer["Translation"]</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
}
</Header> </Header>
<Row> <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><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.Name</td>
<td>@context.Code</td> <td>@context.Code</td>
<td>@context.Version</td>
<td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td> <td><TriStateCheckBox Value="@(context.IsDefault)" Disabled="true"></TriStateCheckBox></td>
<td> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@if (UpgradeAvailable(context.Code, context.Version))
{ {
<button type="button" class="btn btn-success" @onclick=@(async () => await DownloadLanguage(context.Code))>@SharedLocalizer["Upgrade"]</button> <td>@((string.IsNullOrEmpty(context.Version)) ? "---" : context.Version)</td>
<td>
@{
var translation = TranslationAvailable(context.Code, context.Version);
}
@if (translation != null)
{
if (string.IsNullOrEmpty(context.Version))
{
<button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(context.Code, translation.Version))>@SharedLocalizer["Download"]</button>
}
else
{
if (Version.Parse(translation.Version).CompareTo(Version.Parse(context.Version)) > 0)
{
<button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(context.Code, translation.Version))>@SharedLocalizer["Upgrade"]</button>
}
}
} }
</td> </td>
}
</Row> </Row>
</Pager> </Pager>
} }
@if (_package != null)
{
<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;">
<h4 style="display: inline;"><a href="@_package.ProductUrl" target="_new">@_package.Name</a></h4><br />
@SharedLocalizer["Search.By"]:&nbsp;&nbsp;<strong><a href="@_package.OwnerUrl" target="new">@_package.Owner</a></strong><br />
@(_package.Description.Length > 400 ? (_package.Description.Substring(0, 400) + "...") : _package.Description)<br />
<strong>@(String.Format("{0:n0}", _package.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@_package.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@_package.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(_package.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(_package.PackageUrl).Host + "</strong>" : ""))
<br /><br />
@if (!string.IsNullOrEmpty(_package.License))
{
@((MarkupString)_package.License.Replace("\n", "<br />"))
}
else
{
@SharedLocalizer["License Not Specified"]
}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
</div>
</div>
</div>
}
@code { @code {
private List<Language> _languages; private List<Language> _languages;
private List<Package> _packages; private List<Package> _packages;
private Package _package;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId); await GetLanguages();
var cultures = await LocalizationService.GetCulturesAsync();
var culture = cultures.First(c => c.Name.Equals(Constants.DefaultCulture));
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
@ -58,13 +115,18 @@ else
} }
} }
private async Task GetLanguages()
{
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId);
}
private async Task DeleteLanguage(Language language) private async Task DeleteLanguage(Language language)
{ {
try try
{ {
await LanguageService.DeleteLanguageAsync(language.LanguageId); await LanguageService.DeleteLanguageAsync(language.LanguageId);
await logger.LogInformation("Language Deleted {Language}", language); await logger.LogInformation("Language Deleted {Language}", language);
await GetLanguages();
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@ -75,38 +137,45 @@ else
} }
} }
private bool UpgradeAvailable(string code, string version) private Package TranslationAvailable(string code, string version)
{ {
var upgradeavailable = false; return _packages?.FirstOrDefault(item => item.PackageId == (Constants.PackageId + "." + code));
if (_packages != null && version != 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) &&
(Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0);
} }
} private async Task GetPackage(string code, string version)
return upgradeavailable;
}
private async Task DownloadLanguage(string code)
{ {
try try
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) _package = await PackageService.GetPackageAsync(Constants.PackageId + "." + code, version);
{ StateHasChanged();
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);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Downloading Translation {Code} {Version} {Error}", code, Constants.Version, ex.Message); await logger.LogError(ex, "Error Getting Package {PackageId} {Version}", Constants.PackageId + "." + code, Constants.Version);
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private async Task DownloadPackage()
{
try
{
await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version, Constants.PackagesFolder);
await logger.LogInformation("Language Package {Name} {Version} Downloaded Successfully", _package.PackageId, _package.Version);
AddModuleMessage(string.Format(Localizer["Success.Language.Download"], NavigateUrl("admin/system")), MessageType.Success);
_package = null;
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Translation {Name} {Version}", _package.PackageId, _package.Version);
AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error); AddModuleMessage(Localizer["Error.Language.Download"], MessageType.Error);
} }
} }
private void HideModal()
{
_package = null;
StateHasChanged();
}
} }

View File

@ -1,3 +1,4 @@
@using System.Net
@namespace Oqtane.Modules.Admin.Login @namespace Oqtane.Modules.Admin.Login
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@ -33,7 +34,7 @@
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label> <Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required /> <input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
<div class="form-group mt-2"> <div class="form-group mt-2">
@ -205,7 +206,7 @@
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider
.GetService(typeof(IdentityAuthenticationStateProvider)); .GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged(); authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true)); NavigationManager.NavigateTo(NavigateUrl(WebUtility.UrlDecode(_returnUrl), true));
} }
else else
{ {

View File

@ -106,12 +106,6 @@ else
{ {
try 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}"));
}
if (UrlParameters.ContainsKey("level")) if (UrlParameters.ContainsKey("level"))
{ {
_level = UrlParameters["level"]; _level = UrlParameters["level"];
@ -241,4 +235,15 @@ else
_page = page; _page = page;
} }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// 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}"));
}
}
}
} }

View File

@ -131,7 +131,7 @@ else
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition); moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName); settings = SettingService.SetSetting(settings, "ModuleDefinitionName", moduleDefinition.ModuleDefinitionName);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
GetLocation(); GetLocation();
@ -174,7 +174,7 @@ else
private bool IsValid(string name) private bool IsValid(string name)
{ {
// must contain letters, underscores and digits and first character must be letter or underscore // must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$"); return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
} }
private void TemplateChanged(ChangeEventArgs e) private void TemplateChanged(ChangeEventArgs e)

View File

@ -65,13 +65,15 @@
</div> </div>
} }
} }
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
</TabPanel> </TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload"> <TabPanel Name="Upload" ResourceKey="Upload">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Module: </Label> <Label Class="col-sm-3" HelpText="Upload one or more module packages. Once they are uploaded click Install to complete the installation." ResourceKey="Module">Module: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" /> <FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" OnUpload="OnUpload" />
</div> </div>
</div> </div>
</div> </div>
@ -111,13 +113,8 @@
</div> </div>
} }
<button type="button" class="btn btn-success" @onclick="InstallModules">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
@code { @code {
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";
@ -236,7 +233,7 @@
{ {
await PackageService.DownloadPackageAsync(_packageid, _packageversion, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(_packageid, _packageversion, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _packageversion); await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _packageversion);
AddModuleMessage(Localizer["Success.Module.Download"], MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Module.Download"], NavigateUrl("admin/system")), MessageType.Success);
_productname = ""; _productname = "";
_packagelicense = ""; _packagelicense = "";
StateHasChanged(); StateHasChanged();
@ -248,16 +245,8 @@
} }
} }
private async Task InstallModules() private void OnUpload()
{ {
try AddModuleMessage(string.Format(Localizer["Success.Module.Download"], NavigateUrl("admin/system")), MessageType.Success);
{
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

@ -139,7 +139,7 @@
private bool IsValid(string name) private bool IsValid(string name)
{ {
// must contain letters, underscores and digits and first character must be letter or underscore // must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$"); return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
} }
private void TemplateChanged(ChangeEventArgs e) private void TemplateChanged(ChangeEventArgs e)

View File

@ -9,7 +9,9 @@
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<TabStrip> @if (_initialized)
{
<TabStrip>
<TabPanel Name="Definition" ResourceKey="Definition"> <TabPanel Name="Definition" ResourceKey="Definition">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
@ -95,9 +97,10 @@
<TabPanel Name="Permissions" ResourceKey="Permissions"> <TabPanel Name="Permissions" ResourceKey="Permissions">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" Permissions="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.ModuleDefinition" PermissionNames="@PermissionNames.Utilize" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel> </TabPanel>
@ -108,43 +111,47 @@
<Header> <Header>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th> <th>@Localizer["Code"]</th>
<th>@Localizer["Version"]</th> <th style="width: 1px;">@Localizer["Version"]</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
</Header> </Header>
<Row> <Row>
<td>@context.Name</td> <td>@context.Name</td>
<td>@context.Code</td> <td>@context.Code</td>
<td>@context.Version</td> <td>@((string.IsNullOrEmpty(context.Version)) ? "---" : context.Version)</td>
<td> <td>
@if (context.IsDefault) @switch (TranslationAvailable(_packagename + "." + context.Code, context.Version))
{ {
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(_packagename + "." + context.Code))>@SharedLocalizer["Download"]</button> case "install":
} <button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(_packagename + "." + context.Code))>@SharedLocalizer["Download"]</button>
else break;
{ case "upgrade":
if (UpgradeAvailable(_packagename + "." + context.Code, context.Version)) <button type="button" class="btn btn-success" @onclick=@(async () => await GetPackage(_packagename + "." + context.Code))>@SharedLocalizer["Upgrade"]</button>
{ break;
<button type="button" class="btn btn-primary" @onclick=@(async () => await DownloadPackage(_packagename + "." + context.Code))>@SharedLocalizer["Upgrade"]</button>
}
} }
</td> </td>
</Row> </Row>
</Pager> </Pager>
<button type="button" class="btn btn-success" @onclick="InstallTranslations">@SharedLocalizer["Install"]</button>
} }
else else
{ {
<br /> <br />
<div class="mx-auto text-center"> <div class="mx-auto text-center">
@if (string.IsNullOrEmpty(_packagename))
{
@Localizer["Search.PackageNameMissing"]
}
else
{
@Localizer["Search.NoResults"] @Localizer["Search.NoResults"]
}
</div> </div>
<br /> <br />
} }
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
@if (_productname != "") @if (_package != null)
{ {
<div class="app-actiondialog"> <div class="app-actiondialog">
<div class="modal" tabindex="-1" role="dialog"> <div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
@ -155,10 +162,17 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p style="height: 200px; overflow-y: scroll;"> <p style="height: 200px; overflow-y: scroll;">
<h3>@_productname</h3> <h4 style="display: inline;"><a href="@_package.ProductUrl" target="_new">@_package.Name</a></h4><br />
@if (!string.IsNullOrEmpty(_packagelicense)) @SharedLocalizer["Search.By"]:&nbsp;&nbsp;<strong><a href="@_package.OwnerUrl" target="new">@_package.Owner</a></strong><br />
@(_package.Description.Length > 400 ? (_package.Description.Substring(0, 400) + "...") : _package.Description)<br />
<strong>@(String.Format("{0:n0}", _package.Downloads))</strong> @SharedLocalizer["Search.Downloads"]&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Released"]: <strong>@_package.ReleaseDate.ToString("MMM dd, yyyy")</strong>&nbsp;&nbsp;|&nbsp;&nbsp;
@SharedLocalizer["Search.Version"]: <strong>@_package.Version</strong>
@((MarkupString)(!string.IsNullOrEmpty(_package.PackageUrl) ? "&nbsp;&nbsp;|&nbsp;&nbsp;" + SharedLocalizer["Search.Source"] + ": <strong>" + new Uri(_package.PackageUrl).Host + "</strong>" : ""))
<br /><br />
@if (!string.IsNullOrEmpty(_package.License))
{ {
@((MarkupString)_packagelicense) @((MarkupString)_package.License.Replace("\n", "<br />"))
} }
else else
{ {
@ -167,16 +181,18 @@
</p> </p>
</div> </div>
<div class="modal-footer"> <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-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
}
} }
@code { @code {
private bool _initialized = false;
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private int _moduleDefinitionId; private int _moduleDefinitionId;
@ -191,7 +207,7 @@
private string _contact = ""; private string _contact = "";
private string _license = ""; private string _license = "";
private string _runtimes = ""; private string _runtimes = "";
private string _permissions; private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
@ -203,9 +219,7 @@
private List<Package> _packages; private List<Package> _packages;
private List<Language> _languages; private List<Language> _languages;
private string _productname = ""; private Package _package;
private string _packagelicense = "";
private string _packageid = "";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -228,7 +242,7 @@
_contact = moduleDefinition.Contact; _contact = moduleDefinition.Contact;
_license = moduleDefinition.License; _license = moduleDefinition.License;
_runtimes = moduleDefinition.Runtimes; _runtimes = moduleDefinition.Runtimes;
_permissions = moduleDefinition.Permissions; _permissions = moduleDefinition.PermissionList;
_createdby = moduleDefinition.CreatedBy; _createdby = moduleDefinition.CreatedBy;
_createdon = moduleDefinition.CreatedOn; _createdon = moduleDefinition.CreatedOn;
_modifiedby = moduleDefinition.ModifiedBy; _modifiedby = moduleDefinition.ModifiedBy;
@ -248,6 +262,8 @@
} }
_languages = _languages.OrderBy(item => item.Name).ToList(); _languages = _languages.OrderBy(item => item.Name).ToList();
} }
_initialized = true;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -264,6 +280,9 @@
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
try try
{
var moduleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId);
if (!moduleDefinitions.Any(item => item.Name.ToLower() == _name.ToLower() && item.ModuleDefinitionId != _moduleDefinitionId))
{ {
var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId); var moduledefinition = await ModuleDefinitionService.GetModuleDefinitionAsync(_moduleDefinitionId, ModuleState.SiteId);
if (moduledefinition.Name != _name) if (moduledefinition.Name != _name)
@ -278,10 +297,15 @@
{ {
moduledefinition.Categories = _categories; moduledefinition.Categories = _categories;
} }
moduledefinition.Permissions = _permissionGrid.GetPermissions(); moduledefinition.PermissionList = _permissionGrid.GetPermissionList();
await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition); await ModuleDefinitionService.UpdateModuleDefinitionAsync(moduledefinition);
await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition); await logger.LogInformation("ModuleDefinition Saved {ModuleDefinition}", moduledefinition);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.DuplicateName"], MessageType.Warning);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -297,24 +321,31 @@
private void HideModal() private void HideModal()
{ {
_productname = ""; _package = null;
_packagelicense = "";
StateHasChanged(); StateHasChanged();
} }
private bool UpgradeAvailable(string packagename, string version) private string TranslationAvailable(string packagename, string version)
{ {
var upgradeavailable = false;
if (_packages != null) if (_packages != null)
{ {
var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault(); var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault();
if (package != null) if (package != null)
{ {
upgradeavailable = (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0); if (string.IsNullOrEmpty(version))
{
return "install";
} }
else
{
if (Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0)
{
return "upgrade";
} }
return upgradeavailable; }
}
}
return "";
} }
private async Task GetPackage(string packagename) private async Task GetPackage(string packagename)
@ -322,16 +353,7 @@
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version; var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
try try
{ {
var package = await PackageService.GetPackageAsync(packagename, version); _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(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@ -341,16 +363,14 @@
} }
} }
private async Task DownloadPackage(string packagename) private async Task DownloadPackage()
{ {
try try
{ {
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version; await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder); await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _package.PackageId, _package.Version);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", packagename, version); AddModuleMessage(string.Format(Localizer["Success.Translation.Download"], NavigateUrl("admin/system")), MessageType.Success);
AddModuleMessage(Localizer["Success.Translation.Download"], MessageType.Success); _package = null;
_productname = "";
_packagelicense = "";
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
@ -359,17 +379,4 @@
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error); 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

@ -149,7 +149,6 @@ else
{ {
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version); await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
await ModuleDefinitionService.InstallModuleDefinitionsAsync();
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -46,7 +46,7 @@
<select id="page" class="form-select" @bind="@_pageId" required> <select id="page" class="form-select" @bind="@_pageId" required>
@foreach (Page p in PageState.Pages) @foreach (Page p in PageState.Pages)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
<option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option> <option value="@p.PageId">@(new string('-', p.Level * 2))@(p.Name)</option>
} }
@ -62,7 +62,7 @@
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Module" PermissionNames="@_permissionNames" Permissions="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.Module" PermissionNames="@_permissionNames" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
@ -101,7 +101,7 @@
private string _containerType; private string _containerType;
private string _allPages = "false"; private string _allPages = "false";
private string _permissionNames = ""; private string _permissionNames = "";
private string _permissions = null; private List<Permission> _permissions = null;
private string _pageId; private string _pageId;
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private Type _moduleSettingsType; private Type _moduleSettingsType;
@ -123,7 +123,7 @@
_containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType); _containers = ThemeService.GetContainerControls(_themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType; _containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString(); _allPages = ModuleState.AllPages.ToString();
_permissions = ModuleState.Permissions; _permissions = ModuleState.PermissionList;
_pageId = ModuleState.PageId.ToString(); _pageId = ModuleState.PageId.ToString();
createdby = ModuleState.CreatedBy; createdby = ModuleState.CreatedBy;
createdon = ModuleState.CreatedOn; createdon = ModuleState.CreatedOn;
@ -207,7 +207,7 @@
var module = ModuleState; var module = ModuleState;
module.AllPages = bool.Parse(_allPages); module.AllPages = bool.Parse(_allPages);
module.PageModuleId = ModuleState.PageModuleId; module.PageModuleId = ModuleState.PageModuleId;
module.Permissions = _permissionGrid.GetPermissions(); module.PermissionList = _permissionGrid.GetPermissionList();
await ModuleService.UpdateModuleAsync(module); await ModuleService.UpdateModuleAsync(module);
if (_moduleSettingsType != null) if (_moduleSettingsType != null)

View File

@ -157,6 +157,7 @@
</TabPanel> </TabPanel>
} }
</TabStrip> </TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form> </form>
@ -182,7 +183,7 @@
private string _themetype = string.Empty; private string _themetype = string.Empty;
private string _containertype = string.Empty; private string _containertype = string.Empty;
private string _icon = string.Empty; private string _icon = string.Empty;
private string _permissions = string.Empty; private string _permissions = null;
private PermissionGrid _permissionGrid; private PermissionGrid _permissionGrid;
private Type _themeSettingsType; private Type _themeSettingsType;
private object _themeSettings; private object _themeSettings;
@ -201,7 +202,6 @@
_containers = ThemeService.GetContainerControls(_themeList, _themetype); _containers = ThemeService.GetContainerControls(_themeList, _themetype);
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList(); _children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_permissions = string.Empty;
ThemeSettings(); ThemeSettings();
} }
catch (Exception ex) catch (Exception ex)
@ -221,7 +221,7 @@
{ {
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null)) foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
_children.Add(p); _children.Add(p);
} }
@ -231,7 +231,7 @@
{ {
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid))) foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
_children.Add(p); _children.Add(p);
} }
@ -377,7 +377,7 @@
page.DefaultContainerType = string.Empty; page.DefaultContainerType = string.Empty;
} }
page.Icon = (_icon == null ? string.Empty : _icon); page.Icon = (_icon == null ? string.Empty : _icon);
page.Permissions = _permissionGrid.GetPermissions(); page.PermissionList = _permissionGrid.GetPermissionList();
page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable)); page.IsPersonalizable = (_ispersonalizable == null ? false : Boolean.Parse(_ispersonalizable));
page.UserId = null; page.UserId = null;
page.Meta = _meta; page.Meta = _meta;

View File

@ -148,7 +148,8 @@
</div> </div>
</div> </div>
</Section> </Section>
<br /><br /> <br />
<br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
} }
</TabPanel> </TabPanel>
@ -157,7 +158,7 @@
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<PermissionGrid EntityName="@EntityNames.Page" Permissions="@_permissions" @ref="_permissionGrid" /> <PermissionGrid EntityName="@EntityNames.Page" PermissionList="@_permissions" @ref="_permissionGrid" />
</div> </div>
</div> </div>
} }
@ -173,8 +174,8 @@
<th>@Localizer["ModuleDefinition"]</th> <th>@Localizer["ModuleDefinition"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Settings" Text="Edit" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" Permissions="@context.Permissions" ResourceKey="ModuleSettings" /></td> <td><ActionLink Action="Settings" Text="Edit" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" Permissions="@context.Permissions" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td> <td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td> <td>@context.Title</td>
<td>@context.ModuleDefinition?.Name</td> <td>@context.ModuleDefinition?.Name</td>
</Row> </Row>
@ -189,6 +190,7 @@
<br /> <br />
} }
</TabStrip> </TabStrip>
<br />
<button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SavePage">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form> </form>
@ -219,7 +221,7 @@
private string _themetype; private string _themetype;
private string _containertype = "-"; private string _containertype = "-";
private string _icon; private string _icon;
private string _permissions = null; private List<Permission> _permissions = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
@ -290,7 +292,7 @@
_containertype = PageState.Site.DefaultContainerType; _containertype = PageState.Site.DefaultContainerType;
} }
_icon = page.Icon; _icon = page.Icon;
_permissions = page.Permissions; _permissions = page.PermissionList;
_createdby = page.CreatedBy; _createdby = page.CreatedBy;
_createdon = page.CreatedOn; _createdon = page.CreatedOn;
_modifiedby = page.ModifiedBy; _modifiedby = page.ModifiedBy;
@ -337,7 +339,7 @@
{ {
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null)) foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
_children.Add(p); _children.Add(p);
} }
@ -347,7 +349,7 @@
{ {
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid))) foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
_children.Add(p); _children.Add(p);
} }
@ -507,7 +509,7 @@
page.DefaultContainerType = string.Empty; page.DefaultContainerType = string.Empty;
} }
page.Icon = _icon ?? string.Empty; page.Icon = _icon ?? string.Empty;
page.Permissions = _permissionGrid.GetPermissions(); page.PermissionList = _permissionGrid.GetPermissionList();
page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable)); page.IsPersonalizable = (_ispersonalizable != null && Boolean.Parse(_ispersonalizable));
page.UserId = null; page.UserId = null;
page.Meta = _meta; page.Meta = _meta;

View File

@ -104,7 +104,7 @@
private string modifiedby; private string modifiedby;
private DateTime modifiedon; private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
public override string Actions => "Add,Edit"; public override string Actions => "Add,Edit";

View File

@ -10,7 +10,7 @@
} }
else else
{ {
<ActionLink Action="Add" Security="SecurityAccessLevel.Admin" Text="Add Profile" ResourceKey="AddProfile" /> <ActionLink Action="Add" Text="Add Profile" Security="SecurityAccessLevel.Edit" ResourceKey="AddProfile" />
<Pager Items="@_profiles"> <Pager Items="@_profiles">
<Header> <Header>
@ -19,8 +19,8 @@ else
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" ResourceKey="EditProfile" /></td> <td><ActionLink Action="Edit" Parameters="@($"id=" + context.ProfileId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditProfile" /></td>
<td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td> <td><ActionDialog Header="Delete Profile" Message="@string.Format(Localizer["Confirm.Profile.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td>
<td>@context.Name</td> <td>@context.Name</td>
</Row> </Row>
</Pager> </Pager>
@ -29,7 +29,7 @@ else
@code { @code {
private List<Profile> _profiles; private List<Profile> _profiles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {

View File

@ -0,0 +1,20 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Profiles
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Profiles",
Description = "Manage Profiles",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit}," +
$"{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -22,7 +22,7 @@ else
} }
else else
{ {
<Pager Items="@_pages.Where(item => item.IsDeleted)"> <Pager Items="@_pages.Where(item => item.IsDeleted)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
@ -50,7 +50,7 @@ else
} }
else else
{ {
<Pager Items="@_modules.Where(item => item.IsDeleted)"> <Pager Items="@_modules.Where(item => item.IsDeleted)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
@ -78,7 +78,8 @@ else
@code { @code {
private List<Page> _pages; private List<Page> _pages;
private List<Module> _modules; private List<Module> _modules;
private int _pagePage = 1;
private int _pageModule = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@ -185,7 +186,7 @@ else
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
// check if there are any remaining module instances in the site // check if there are any remaining module instances in the site
if (!_modules.Exists(item => item.ModuleId == module.ModuleId)) if (!_modules.Exists (item => item.ModuleId == module.ModuleId && item.PageModuleId != module.PageModuleId))
{ {
await ModuleService.DeleteModuleAsync(module.ModuleId); await ModuleService.DeleteModuleAsync(module.ModuleId);
} }
@ -206,12 +207,14 @@ else
try try
{ {
ModuleInstance.ShowProgressIndicator(); ModuleInstance.ShowProgressIndicator();
foreach (Module module in _modules.Where(item => item.IsDeleted)) foreach (Module module in _modules.Where(item => item.IsDeleted).ToList())
{ {
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
// DeletePageModuleAsync does not update _modules so remove it.
_modules.Remove(module);
// check if there are any remaining module instances in the site // check if there are any remaining module instances in the site
if (!_modules.Exists(item => item.ModuleId == module.ModuleId)) if (!_modules.Exists(item => item.ModuleId == module.ModuleId && item.PageModuleId != module.PageModuleId))
{ {
await ModuleService.DeleteModuleAsync(module.ModuleId); await ModuleService.DeleteModuleAsync(module.ModuleId);
} }
@ -229,4 +232,12 @@ else
ModuleInstance.HideProgressIndicator(); ModuleInstance.HideProgressIndicator();
} }
} }
private void OnPageChangePage(int page)
{
_pagePage = page;
}
private void OnPageChangeModule(int page)
{
_pageModule = page;
}
} }

View File

@ -4,6 +4,7 @@
@inject IUserService UserService @inject IUserService UserService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@if (PageState.Site.AllowRegistration) @if (PageState.Site.AllowRegistration)
{ {
@ -15,7 +16,7 @@
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" /> <ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
</Authorized> </Authorized>
<NotAuthorized> <NotAuthorized>
<ModuleMessage Message="@Localizer["Info.Registration.InvalidEmail"]" Type="MessageType.Info" /> <ModuleMessage Message="@_passwordconstruction" Type="MessageType.Info" />
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -29,7 +30,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -38,7 +39,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required /> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -78,11 +79,44 @@ else
private string _email = string.Empty; private string _email = string.Empty;
private string _displayname = string.Empty; private string _displayname = string.Empty;
//Password construction
private string _minimumlength;
private string _uniquecharacters;
private bool _requiredigit;
private bool _requireupper;
private bool _requirelower;
private bool _requirepunctuation;
private string _passwordconstruction;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override async Task OnInitializedAsync()
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
_uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
_requiredigit = bool.Parse(SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true"));
_requireupper = bool.Parse(SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true"));
_requirelower = bool.Parse(SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true"));
_requirepunctuation = bool.Parse(SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true"));
// Replace the placeholders with the actual values of the variables
string digitRequirement = _requiredigit ? Localizer["Password.DigitRequirement"] + ", " : "";
string uppercaseRequirement = _requireupper ? Localizer["Password.UppercaseRequirement"] + ", " : "";
string lowercaseRequirement = _requirelower ? Localizer["Password.LowercaseRequirement"] + ", " : "";
string punctuationRequirement = _requirepunctuation ? Localizer["Password.PunctuationRequirement"] + ", " : "";
// Replace the placeholders with the actual values of the variables
string passwordValidationCriteriaTemplate = Localizer["Password.ValidationCriteria"];
_passwordconstruction = Localizer["Info.Registration.InvalidEmail"] + ". " + string.Format(passwordValidationCriteriaTemplate,
_minimumlength, _uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement);
}
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
} }
private async Task Register() private async Task Register()

View File

@ -18,7 +18,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -27,7 +27,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required /> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -42,7 +42,7 @@
private string _description = string.Empty; private string _description = string.Empty;
private string _isautoassigned = "False"; private string _isautoassigned = "False";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
private async Task SaveRole() private async Task SaveRole()
{ {

View File

@ -49,7 +49,7 @@
private string _modifiedby; private string _modifiedby;
private DateTime _modifiedon; private DateTime _modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {

View File

@ -10,7 +10,7 @@
} }
else else
{ {
<ActionLink Action="Add" Text="Add Role" ResourceKey="AddRole" /> <ActionLink Action="Add" Text="Add Role" Security="SecurityAccessLevel.Edit" ResourceKey="AddRole" />
<Pager Items="@_roles"> <Pager Items="@_roles">
<Header> <Header>
@ -20,9 +20,9 @@ else
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Name"]</th>
</Header> </Header>
<Row> <Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td> <td><ActionLink Action="Edit" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" Disabled="@(context.IsSystem)" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td> <td><ActionDialog Header="Delete Role" Message="@string.Format(Localizer["Confirm.DeleteUser"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteRole(context))" Disabled="@(context.IsSystem)" ResourceKey="DeleteRole" /></td>
<td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" ResourceKey="Users" /></td> <td><ActionLink Action="Users" Parameters="@($"id=" + context.RoleId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Users" /></td>
<td>@context.Name</td> <td>@context.Name</td>
</Row> </Row>
</Pager> </Pager>
@ -31,7 +31,7 @@ else
@code { @code {
private List<Role> _roles; private List<Role> _roles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {

View File

@ -0,0 +1,21 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Roles
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Roles",
Description = "Manage Roles",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit}," +
$"{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin}," +
$"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -23,13 +23,7 @@ else
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="user" HelpText="Select a user" ResourceKey="User">User: </Label> <Label Class="col-sm-3" For="user" HelpText="Select a user" ResourceKey="User">User: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="user" class="form-select" @bind="@userid" required> <AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["User.Select"]" @ref="user" />
<option value="-1">&lt;@Localizer["User.Select"]&gt;</option>
@foreach (UserRole userrole in users)
{
<option value="@(userrole.UserId)">@userrole.User.DisplayName</option>
}
</select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -64,7 +58,7 @@ else
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || PageState.User.Username == UserNames.Host)" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -80,13 +74,12 @@ else
private int roleid; private int roleid;
private string name = string.Empty; private string name = string.Empty;
private List<UserRole> users; private AutoComplete user;
private int userid = -1;
private DateTime? effectivedate = null; private DateTime? effectivedate = null;
private DateTime? expirydate = null; private DateTime? expirydate = null;
private List<UserRole> userroles; private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -95,7 +88,6 @@ else
roleid = Int32.Parse(PageState.QueryString["id"]); roleid = Int32.Parse(PageState.QueryString["id"]);
Role role = await RoleService.GetRoleAsync(roleid); Role role = await RoleService.GetRoleAsync(roleid);
name = role.Name; name = role.Name;
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
await GetUserRoles(); await GetUserRoles();
} }
catch (Exception ex) catch (Exception ex)
@ -105,6 +97,22 @@ else
} }
} }
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
try
{
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Users {filter} {Error}", filter, ex.Message);
AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error);
}
return new Dictionary<string, string>();
}
private async Task GetUserRoles() private async Task GetUserRoles()
{ {
try try
@ -126,7 +134,7 @@ else
{ {
try try
{ {
if (userid != -1) if (!string.IsNullOrEmpty(user.Key) && int.TryParse(user.Key, out int userid))
{ {
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault(); var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null) if (userrole != null)
@ -149,6 +157,7 @@ else
await logger.LogInformation("User Assigned To Role {UserRole}", userrole); await logger.LogInformation("User Assigned To Role {UserRole}", userrole);
AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success); AddModuleMessage(Localizer["Success.User.AssignedRole"], MessageType.Success);
await GetUserRoles(); await GetUserRoles();
user.Clear();
StateHasChanged(); StateHasChanged();
} }
else else

View File

@ -78,7 +78,7 @@
<option value="-">&lt;@Localizer["Not Specified"]&gt;</option> <option value="-">&lt;@Localizer["Not Specified"]&gt;</option>
@foreach (Page page in PageState.Pages) @foreach (Page page in PageState.Pages)
{ {
if (UserSecurity.ContainsRole(page.Permissions, PermissionNames.View, RoleNames.Everyone)) if (UserSecurity.ContainsRole(page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{ {
<option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option> <option value="@(page.PageId)">@(new string('-', page.Level * 2))@(page.Name)</option>
} }
@ -95,6 +95,12 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sitemap" HelpText="The site map url for this site which can be submitted to search engines for indexing" ResourceKey="SiteMap">Site Map: </Label>
<div class="col-sm-9">
<input id="sitemap" class="form-control" @bind="@_sitemap" required disabled />
</div>
</div>
</div> </div>
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings"> <Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container"> <div class="container">
@ -127,7 +133,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmptUsername">Username: </Label> <Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" /> <input id="username" class="form-control" @bind="@_smtpusername" />
</div> </div>
@ -137,15 +143,24 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" /> <input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword">@_togglesmtppassword</button> <button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmptSender">Email Sender: </Label> <Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmtpSender">Email Sender: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" /> <input id="sender" class="form-control" @bind="@_smtpsender" />
</div> </div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div> </div>
<div class="row mb-1 align-items-center"> <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> <Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
@ -258,16 +273,16 @@
</div> </div>
</div> </div>
</Section> </Section>
<Section Name="TenantInformation" Heading="Tenant Information" ResourceKey="TenantInformation"> <Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The tenant for the site" ResourceKey="Tenant">Tenant: </Label> <Label Class="col-sm-3" For="tenant" HelpText="The name of the database used for the site" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="tenant" class="form-control" @bind="@_tenant" readonly /> <input id="tenant" class="form-control" @bind="@_tenant" readonly />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="The database for the tenant" ResourceKey="Database">Database: </Label> <Label Class="col-sm-3" For="database" HelpText="The type of database" ResourceKey="Database">Type: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="database" class="form-control" @bind="@_database" readonly /> <input id="database" class="form-control" @bind="@_database" readonly />
</div> </div>
@ -275,7 +290,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label> <Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionstring" rows="2" readonly></textarea> <input id="connectionstring" class="form-control" @bind="@_connectionstring" readonly />
</div> </div>
</div> </div>
</div> </div>
@ -312,6 +327,7 @@
private string _containertype = "-"; private string _containertype = "-";
private string _admincontainertype = "-"; private string _admincontainertype = "-";
private string _homepageid = "-"; private string _homepageid = "-";
private string _sitemap = "";
private string _smtphost = string.Empty; private string _smtphost = string.Empty;
private string _smtpport = string.Empty; private string _smtpport = string.Empty;
private string _smtpssl = "False"; private string _smtpssl = "False";
@ -320,6 +336,7 @@
private string _smtppasswordtype = "password"; private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty; private string _togglesmtppassword = string.Empty;
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private string _retention = string.Empty; private string _retention = string.Empty;
private string _pwaisenabled; private string _pwaisenabled;
private int _pwaappiconfileid = -1; private int _pwaappiconfileid = -1;
@ -351,6 +368,7 @@
_runtime = site.Runtime; _runtime = site.Runtime;
_prerender = site.RenderMode.Replace(_runtime, ""); _prerender = site.RenderMode.Replace(_runtime, "");
_isdeleted = site.IsDeleted.ToString(); _isdeleted = site.IsDeleted.ToString();
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/pages/sitemap.xml";
await GetAliases(); await GetAliases();
@ -393,6 +411,7 @@
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"]; _togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30"); _retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -444,7 +463,7 @@
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Pane Layouts For Theme {ThemeType} {Error}", _themetype, ex.Message); await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message);
AddModuleMessage(Localizer["Error.Theme.LoadPane"], MessageType.Error); AddModuleMessage(Localizer["Error.Theme.LoadPane"], MessageType.Error);
} }
} }
@ -532,6 +551,7 @@
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
@ -602,12 +622,12 @@
try try
{ {
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true); settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true); settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true); settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true); settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");

View File

@ -103,7 +103,7 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the tenant for the site" ResourceKey="Tenant">Tenant: </Label> <Label Class="col-sm-3" For="tenant" HelpText="Select the database for the site" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="tenant" class="form-select" @onchange="(e => TenantChanged(e))" required> <select id="tenant" class="form-select" @onchange="(e => TenantChanged(e))" required>
<option value="-">&lt;@Localizer["Tenant.Select"]&gt;</option> <option value="-">&lt;@Localizer["Tenant.Select"]&gt;</option>
@ -121,13 +121,13 @@ else
<hr class="app-rule" /> <hr class="app-rule" />
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the name for the tenant" ResourceKey="TenantName">Tenant Name: </Label> <Label Class="col-sm-3" For="name" HelpText="Enter the name for the database" ResourceKey="TenantName">Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="name" class="form-control" @bind="@_tenantName" maxlength="100" required /> <input id="name" class="form-control" @bind="@_tenantName" maxlength="100" required />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databaseType" HelpText="Select the database type for the tenant" ResourceKey="DatabaseType">Database Type: </Label> <Label Class="col-sm-3" For="databaseType" HelpText="Select the database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@if (_databases != null) @if (_databases != null)
{ {
@ -160,7 +160,7 @@ else
else else
{ {
<div class="row mb-1 align-items-center"> <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> <Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">Settings:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea> <textarea id="connectionstring" class="form-control" @bind="@_connectionString" rows="3"></textarea>
</div> </div>
@ -315,7 +315,7 @@ else
_urls = Regex.Replace(_urls, @"\r\n?|\n", ","); _urls = Regex.Replace(_urls, @"\r\n?|\n", ",");
var duplicates = new List<string>(); var duplicates = new List<string>();
var aliases = await AliasService.GetAliasesAsync(); var aliases = await AliasService.GetAliasesAsync();
foreach (string name in _urls.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string name in _urls.Split(',', StringSplitOptions.RemoveEmptyEntries))
{ {
if (aliases.Exists(item => item.Name == name)) if (aliases.Exists(item => item.Name == name))
{ {
@ -329,7 +329,7 @@ else
if (_tenantid == "+") if (_tenantid == "+")
{ {
if (!string.IsNullOrEmpty(_tenantName) && _tenants.FirstOrDefault(item => item.Name == _tenantName) == null) if (!string.IsNullOrEmpty(_tenantName) && !_tenants.Exists(item => item.Name == _tenantName))
{ {
// validate host credentials // validate host credentials
var user = new User(); var user = new User();

View File

@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.Sql @namespace Oqtane.Modules.Admin.Sql
@inherits ModuleBase @inherits ModuleBase
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ISystemService SystemService
@inject ITenantService TenantService @inject ITenantService TenantService
@inject IDatabaseService DatabaseService @inject IDatabaseService DatabaseService
@inject ISqlService SqlService @inject ISqlService SqlService
@ -14,40 +15,112 @@
else else
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="Select the tenant for the SQL server" ResourceKey="Tenant">Tenant: </Label> <Label Class="col-sm-3" For="connection" HelpText="Select a database connection (from appsettings.json)" ResourceKey="Connection">Connection: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="tenant" class="form-select" value="@_tenantid" @onchange="(e => TenantChanged(e))"> <select id="tenant" class="form-select" value="@_connection" @onchange="(e => ConnectionChanged(e))">
<option value="-1">&lt;@Localizer["Tenant.Select"]&gt;</option> <option value="-">&lt;@Localizer["Connection.Select"]&gt;</option>
@foreach (Tenant tenant in _tenants) <option value="+">&lt;@Localizer["Connection.Add"]&gt;</option>
@foreach (var connection in _connections)
{ {
<option value="@tenant.TenantId">@tenant.Name</option> <option value="@connection.Key">@connection.Key</option>
} }
</select> </select>
</div> </div>
</div> </div>
@if (_tenantid != "-1") @if (_connection == "+")
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="database" HelpText="The database for the tenant" ResourceKey="Database">Database: </Label> <Label Class="col-sm-3" For="name" HelpText="Enter the name of the connection" ResourceKey="Name">Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="database" class="form-control" @bind="@_database" readonly /> <input id="name" class="form-control" @bind="@_name" maxlength="100" required />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label> <Label Class="col-sm-3" For="databasetype" HelpText="Select the database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="connectionstring" class="form-control" @bind="@_connectionstring" rows="2" readonly></textarea> @if (_databases != null)
{
<div class="input-group">
<select id="databasetype" class="form-select" value="@_databasetype" @onchange="(e => DatabaseTypeChanged(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="ShowConnectionString">@Localizer["EnterConnectionString"]</button>
}
else
{
<button type="button" class="btn btn-secondary" @onclick="ShowConnectionString">@Localizer["EnterConnectionParameters"]</button>
}
</div>
}
</div> </div>
</div> </div>
@if (!_showConnectionString)
{
if (_databaseConfigType != null)
{
@DatabaseConfigComponent
}
}
else
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sqlQeury" HelpText="Enter the query for the SQL server" ResourceKey="SqlQuery">SQL Query: </Label> <Label Class="col-sm-3" For="connectionstring" HelpText="Enter a complete connection string including all parameters and delimiters" ResourceKey="ConnectionString">Settings:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="sqlQeury" class="form-control" @bind="@_sql" rows="3"></textarea> <textarea id="connectionstring" class="form-control" @bind="@_connectionstring" rows="3"></textarea>
</div> </div>
</div> </div>
} }
<br />
<button type="button" class="btn btn-success" @onclick="Add">@Localizer["Add"]</button>
}
else
{
@if (_connection != "-")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
@if (_databases != null)
{
<select id="databasetype" class="form-select" @bind="@_databasetype" required>
<option value="-">&lt;@Localizer["Type.Select"]&gt;</option>
@foreach (var database in _databases)
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
</select>
}
</div>
</div>
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9">
<input id="tenant" class="form-control" @bind="@_tenant" readonly />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="The connection string" ResourceKey="ConnectionString">Settings: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="connectionstring" type="@_connectionstringtype" class="form-control" @bind="@_connectionstring" readonly />
<button type="button" class="btn btn-secondary" @onclick="@ToggleConnectionString">@_connectionstringtoggle</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sqlQuery" HelpText="Enter a valid SQL query for the database" ResourceKey="SqlQuery">SQL Query: </Label>
<div class="col-sm-9">
<textarea id="sqlQuery" class="form-control" @bind="@_sql" rows="3"></textarea>
</div>
</div> </div>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="Execute">@Localizer["Execute"]</button> <button type="button" class="btn btn-success" @onclick="Execute">@Localizer["Execute"]</button>
@ -77,15 +150,30 @@ else
@Localizer["Return.NoResult"] @Localizer["Return.NoResult"]
} }
<br /> <br />
<br /> <br />
} }
}
}
</div>
} }
@code { @code {
private string _connection = "-";
private Dictionary<string, object> _connections;
private List<Tenant> _tenants; private List<Tenant> _tenants;
private string _tenantid = "-1"; private List<Database> _databases;
private string _database = string.Empty;
private string _name = string.Empty;
private string _databasetype = string.Empty;
private Type _databaseConfigType;
private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false;
private string _tenant = string.Empty;
private string _connectionstring = string.Empty; private string _connectionstring = string.Empty;
private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty; private string _sql = string.Empty;
private List<Dictionary<string, string>> _results; private List<Dictionary<string, string>> _results;
@ -95,7 +183,10 @@ else
{ {
try try
{ {
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync(); _tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -104,35 +195,127 @@ else
} }
} }
private async void TenantChanged(ChangeEventArgs e) private async void ConnectionChanged(ChangeEventArgs e)
{ {
try try
{ {
_tenantid = (string)e.Value; _connection = (string)e.Value;
var tenants = await TenantService.GetTenantsAsync(); if (_connection != "-" && _connection != "+")
var _databases = await DatabaseService.GetDatabasesAsync(); {
var tenant = tenants.Find(item => item.TenantId == int.Parse(_tenantid)); _connectionstring = _connections[_connection].ToString();
_tenant = "";
_databasetype = "-";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null) if (tenant != null)
{ {
_database = _databases.Find(item => item.DBType == tenant.DBType)?.Name; _tenant = tenant.Name;
_connectionstring = tenant.DBConnectionString; _databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType).Name;
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = "LocalDB";
}
_showConnectionString = false;
LoadDatabaseConfigComponent();
} }
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Tenant {TenantId} {Error}", _tenantid, ex.Message); await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
} }
} }
private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{
try
{
_databasetype = (string)eventArgs.Value;
_showConnectionString = false;
LoadDatabaseConfigComponent();
}
catch
{
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
}
}
private void LoadDatabaseConfigComponent()
{
var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
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 ShowConnectionString()
{
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
_connectionstring = databaseConfigControl.GetConnectionString();
}
_showConnectionString = !_showConnectionString;
}
private async Task Add()
{
var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{
connectionstring = databaseConfigControl.GetConnectionString();
}
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{
var settings = new Dictionary<string, object>();
settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring);
await SystemService.UpdateSystemInfoAsync(settings);
_connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_connection = "-";
AddModuleMessage(Localizer["Message.Connection.Added"], MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Required.Connection"], MessageType.Warning);
}
}
private void ToggleConnectionString()
{
if (_connectionstringtype == "password")
{
_connectionstringtype = "text";
_connectionstringtoggle = SharedLocalizer["HidePassword"];
}
else
{
_connectionstringtype = "password";
_connectionstringtoggle = SharedLocalizer["ShowPassword"];
}
}
private async Task Execute() private async Task Execute()
{ {
try try
{ {
if (_tenantid != "-1" && !string.IsNullOrEmpty(_sql)) if (_databasetype != "-" && !string.IsNullOrEmpty(_sql))
{ {
var sqlquery = new SqlQuery { TenantId = int.Parse(_tenantid), Query = _sql }; var dbtype = _databases.FirstOrDefault(item => item.Name == _databasetype).DBType;
var sqlquery = new SqlQuery { DBConnectionString = _connection, DBType = dbtype, Query = _sql };
sqlquery = await SqlService.ExecuteQueryAsync(sqlquery); sqlquery = await SqlService.ExecuteQueryAsync(sqlquery);
_results = sqlquery.Results; _results = sqlquery.Results;
AddModuleMessage(Localizer["Success.QueryExecuted"], MessageType.Success); AddModuleMessage(Localizer["Success.QueryExecuted"], MessageType.Success);

View File

@ -147,6 +147,18 @@
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp; <a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" /> <ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel> </TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="log" HelpText="System log information for current day" ResourceKey="Log">Log: </Label>
<div class="col-sm-9">
<textarea id="log" class="form-control" rows="10" @bind="@_log" readonly />
</div>
</div>
</div>
<br /><br />
<button type="button" class="btn btn-danger" @onclick="ClearLog">@Localizer["Clear"]</button>
</TabPanel>
</TabStrip> </TabStrip>
<br /><br /> <br /><br />
@ -172,11 +184,13 @@
private string _swagger = string.Empty; private string _swagger = string.Empty;
private string _packageservice = string.Empty; private string _packageservice = string.Empty;
private string _log = string.Empty;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_version = Constants.Version; _version = Constants.Version;
Dictionary<string, object> systeminfo = await SystemService.GetSystemInfoAsync("environment"); var systeminfo = await SystemService.GetSystemInfoAsync("environment");
if (systeminfo != null) if (systeminfo != null)
{ {
_clrversion = systeminfo["CLRVersion"].ToString(); _clrversion = systeminfo["CLRVersion"].ToString();
@ -191,7 +205,7 @@
_workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB"; _workingset = (Convert.ToInt64(systeminfo["WorkingSet"].ToString()) / 1000000).ToString() + " MB";
} }
systeminfo = await SystemService.GetSystemInfoAsync(); systeminfo = await SystemService.GetSystemInfoAsync("configuration");
if (systeminfo != null) if (systeminfo != null)
{ {
_installationid = systeminfo["InstallationId"].ToString(); _installationid = systeminfo["InstallationId"].ToString();
@ -201,6 +215,12 @@
_swagger = systeminfo["UseSwagger"].ToString(); _swagger = systeminfo["UseSwagger"].ToString();
_packageservice = systeminfo["PackageService"].ToString(); _packageservice = systeminfo["PackageService"].ToString();
} }
systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null)
{
_log = systeminfo["Log"].ToString();
}
} }
private async Task SaveConfig() private async Task SaveConfig()
@ -223,6 +243,23 @@
} }
} }
private async Task ClearLog()
{
try
{
var settings = new Dictionary<string, object>();
settings.Add("clearlog", "true");
await SystemService.UpdateSystemInfoAsync(settings);
_log = string.Empty;
AddModuleMessage(Localizer["Success.ClearLog"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Clearing Log");
AddModuleMessage(Localizer["Error.ClearLog"], MessageType.Error);
}
}
private async Task RestartApplication() private async Task RestartApplication()
{ {
try try

View File

@ -65,13 +65,15 @@
</div> </div>
} }
} }
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
</TabPanel> </TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload"> <TabPanel Name="Upload" ResourceKey="Upload">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload one or more theme packages. Once they are uploaded click Install to complete the installation." ResourceKey="Theme">Theme: </Label> <Label Class="col-sm-3" HelpText="Upload one or more theme packages. Once they are uploaded click Install to complete the installation." ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" /> <FileManager Folder="@Constants.PackagesFolder" UploadMultiple="true" OnUpload="OnUpload" />
</div> </div>
</div> </div>
</div> </div>
@ -111,13 +113,8 @@
</div> </div>
} }
<button type="button" class="btn btn-success" @onclick="InstallThemes">@SharedLocalizer["Install"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<ModuleMessage Type="MessageType.Info" Message="@SharedLocalizer["Oqtane.Marketplace"]" />
@code { @code {
private List<Package> _packages; private List<Package> _packages;
private string _price = "free"; private string _price = "free";
@ -236,7 +233,7 @@
{ {
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version); await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(Localizer["Success.Theme.Download"], MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Theme.Download"], NavigateUrl("admin/system")), MessageType.Success);
_productname = ""; _productname = "";
_license = ""; _license = "";
StateHasChanged(); StateHasChanged();
@ -248,16 +245,8 @@
} }
} }
private async Task InstallThemes() private void OnUpload()
{ {
try AddModuleMessage(string.Format(Localizer["Success.Theme.Download"], NavigateUrl("admin/system")), MessageType.Success);
{
await ThemeService.InstallThemesAsync();
AddModuleMessage(string.Format(Localizer["Success.Theme.Install"], NavigateUrl("admin/system")), MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Installing Theme");
}
} }
} }

View File

@ -121,7 +121,7 @@
private bool IsValid(string name) private bool IsValid(string name)
{ {
// must contain letters, underscores and digits and first character must be letter or underscore // must contain letters, underscores and digits and first character must be letter or underscore
return !string.IsNullOrEmpty(name) && name.ToLower() != "theme" && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$"); return !string.IsNullOrEmpty(name) && name.ToLower() != "theme" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
} }
private void TemplateChanged(ChangeEventArgs e) private void TemplateChanged(ChangeEventArgs e)

View File

@ -116,7 +116,6 @@ else
{ {
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder); await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await logger.LogInformation("Theme Downloaded {ThemeName} {Version}", packagename, version); await logger.LogInformation("Theme Downloaded {ThemeName} {Version}", packagename, version);
await ThemeService.InstallThemesAsync();
AddModuleMessage(string.Format(Localizer["Success.Theme.Install"], NavigateUrl("admin/system")), MessageType.Success); AddModuleMessage(string.Format(Localizer["Success.Theme.Install"], NavigateUrl("admin/system")), MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -34,7 +34,7 @@ else
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -43,7 +43,7 @@ else
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" /> <input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -211,8 +211,11 @@ else
</Detail> </Detail>
</Pager> </Pager>
} }
@if (notifications.Any())
{
<br /> <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" /> <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 /> <br /><hr />
<select class="form-select" @onchange="(e => FilterChanged(e))"> <select class="form-select" @onchange="(e => FilterChanged(e))">
<option value="to">@Localizer["Inbox"]</option> <option value="to">@Localizer["Inbox"]</option>
@ -305,7 +308,14 @@ else
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task Save() private async Task Save()
{ {
@ -337,7 +347,9 @@ else
photo = null; photo = null;
} }
await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved"); await logger.LogInformation("User Profile Saved");
@ -345,6 +357,11 @@ else
StateHasChanged(); StateHasChanged();
} }
else else
{
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
}
}
else
{ {
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning); AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);
} }

View File

@ -23,7 +23,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -32,7 +32,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" required /> <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> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -104,7 +104,7 @@
private Dictionary<string, string> settings; private Dictionary<string, string> settings;
private string category = string.Empty; private string category = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -122,7 +122,14 @@
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task SaveUser() private async Task SaveUser()
{ {

View File

@ -32,7 +32,7 @@ else
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" /> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -41,7 +41,7 @@ else
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" /> <input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
</div> </div>
@ -174,7 +174,7 @@ else
private string deletedby; private string deletedby;
private DateTime? deletedon; private DateTime? deletedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
@ -223,7 +223,14 @@ else
} }
private string GetProfileValue(string SettingName, string DefaultValue) private string GetProfileValue(string SettingName, string DefaultValue)
=> SettingService.GetSetting(settings, SettingName, DefaultValue); {
string value = SettingService.GetSetting(settings, SettingName, DefaultValue);
if (value.Contains("]"))
{
value = value.Substring(value.IndexOf("]") + 1);
}
return value;
}
private async Task SaveUser() private async Task SaveUser()
{ {
@ -249,12 +256,18 @@ else
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null)
{
await SettingService.UpdateUserSettingsAsync(settings, user.UserId); await SettingService.UpdateUserSettingsAsync(settings, user.UserId);
await logger.LogInformation("User Saved {User}", user); await logger.LogInformation("User Saved {User}", user);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
else else
{
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
}
}
else
{ {
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning); AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
} }

View File

@ -20,7 +20,7 @@ else
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col-sm-4"> <div class="col-sm-4">
<ActionLink Action="Add" Text="Add User" ResourceKey="AddUser" /> <ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<input class="form-control" @bind="@_search" /> <input class="form-control" @bind="@_search" />
@ -41,21 +41,21 @@ else
</Header> </Header>
<Row> <Row>
<td> <td>
<ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="EditUser" /> <ActionLink Action="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td> </td>
<td> <td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" />
</td> </td>
<td> <td>
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" ResourceKey="Roles" /> <ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td> </td>
<td>@context.User.Username</td> <td>@context.User.Username</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</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> <td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row> </Row>
</Pager> </Pager>
</TabPanel> </TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings"> <TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container"> <div class="container">
<Section Name="User" Heading="User Settings" ResourceKey="UserSettings"> <Section Name="User" Heading="User Settings" ResourceKey="UserSettings">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -294,6 +294,12 @@ else
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" /> <input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</div>
</div>
} }
<div class="row mb-1 align-items-center"> <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> <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>
@ -395,6 +401,7 @@ else
private string _identifierclaimtype; private string _identifierclaimtype;
private string _emailclaimtype; private string _emailclaimtype;
private string _roleclaimtype; private string _roleclaimtype;
private string _profileclaimtypes;
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
@ -406,7 +413,7 @@ else
private string _lifetime; private string _lifetime;
private string _token; private string _token;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -449,6 +456,7 @@ else
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub"); _identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email"); _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", ""); _roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
@ -456,7 +464,8 @@ else
_togglesecret = SharedLocalizer["ShowPassword"]; _togglesecret = SharedLocalizer["ShowPassword"];
_issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name); _issuer = SettingService.GetSetting(settings, "JwtOptions:Issuer", PageState.Uri.Scheme + "://" + PageState.Alias.Name);
_audience = SettingService.GetSetting(settings, "JwtOptions:Audience", ""); _audience = SettingService.GetSetting(settings, "JwtOptions:Audience", "");
_lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20"); } _lifetime = SettingService.GetSetting(settings, "JwtOptions:Lifetime", "20");
}
} }
private async Task LoadUsersAsync(bool load) private async Task LoadUsersAsync(bool load)
@ -522,7 +531,7 @@ else
private async Task UpdateUserSettingsAsync() private async Task UpdateUserSettingsAsync()
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingSearch, _search); settings = SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
} }
@ -567,6 +576,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);

View File

@ -0,0 +1,21 @@
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Modules.Admin.Users
{
[PrivateApi("Mark this as private, since it's not very useful in the public docs")]
public class ModuleInfo : IModule
{
public ModuleDefinition ModuleDefinition => new ModuleDefinition
{
Name = "Users",
Description = "Manage Users",
Categories = "Admin",
Version = Constants.Version,
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit}," +
$"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}," +
$"{EntityNames.UserRole}:{PermissionNames.Write}:{RoleNames.Admin}"
};
}
}

View File

@ -63,7 +63,7 @@ else
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -79,7 +79,7 @@ else
private string expirydate = string.Empty; private string expirydate = string.Empty;
private List<UserRole> userroles; private List<UserRole> userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@using System.Text.Json
@inherits LocalizableComponent @inherits LocalizableComponent
@if (_visible) @if (_visible)
@ -40,7 +41,7 @@
@code { @code {
private bool _visible = false; private bool _visible = false;
private string _permissions = string.Empty; private List<Permission> _permissions;
private bool _editmode = false; private bool _editmode = false;
private bool _authorized = false; private bool _authorized = false;
private string _iconSpan = string.Empty; private string _iconSpan = string.Empty;
@ -61,7 +62,10 @@
public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel
[Parameter] [Parameter]
public string Permissions { get; set; } // optional - can be used to specify a permission string public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public List<Permission> PermissionList { get; set; } // optional - can be used to specify permissions
[Parameter] [Parameter]
public string Class { get; set; } // optional public string Class { get; set; } // optional
@ -78,6 +82,14 @@
[Parameter] [Parameter]
public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon
protected override void OnInitialized()
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
}
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
base.OnParametersSet(); base.OnParametersSet();
@ -109,7 +121,7 @@
Header = Localize(nameof(Header), Header); Header = Localize(nameof(Header), Header);
Message = Localize(nameof(Message), Message); Message = Localize(nameof(Message), Message);
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions; _permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized(); _authorized = IsAuthorized();
} }

View File

@ -1,5 +1,6 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@using System.Net @using System.Net
@using System.Text.Json
@inherits LocalizableComponent @inherits LocalizableComponent
@inject IUserService UserService @inject IUserService UserService
@ -26,7 +27,7 @@
private string _text = string.Empty; private string _text = string.Empty;
private string _parameters = string.Empty; private string _parameters = string.Empty;
private string _url = string.Empty; private string _url = string.Empty;
private string _permissions = string.Empty; private List<Permission> _permissions;
private bool _editmode = false; private bool _editmode = false;
private bool _authorized = false; private bool _authorized = false;
private string _classname = "btn btn-primary"; private string _classname = "btn btn-primary";
@ -52,7 +53,10 @@
public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel
[Parameter] [Parameter]
public string Permissions { get; set; } // optional - can be used to specify a permission string public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public List<Permission> PermissionList { get; set; } // optional - can be used to specify permissions
[Parameter] [Parameter]
public bool Disabled { get; set; } // optional public bool Disabled { get; set; } // optional
@ -75,6 +79,14 @@
[Parameter] [Parameter]
public string ReturnUrl { get; set; } // optional - used to set a url to redirect to public string ReturnUrl { get; set; } // optional - used to set a url to redirect to
protected override void OnInitialized()
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
}
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
base.OnParametersSet(); base.OnParametersSet();
@ -119,7 +131,7 @@
_iconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}"; _iconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
} }
_permissions = (string.IsNullOrEmpty(Permissions)) ? ModuleState.Permissions : Permissions; _permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_text = Localize(nameof(Text), _text); _text = Localize(nameof(Text), _text);
_url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters); _url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters);
if (!string.IsNullOrEmpty(ReturnUrl)) if (!string.IsNullOrEmpty(ReturnUrl))

View File

@ -0,0 +1,153 @@
@namespace Oqtane.Modules.Controls
@inherits LocalizableComponent
<div class="app-autocomplete">
<input class="form-control" value="@Value" @oninput="OnInput" @onkeyup="OnKeyUp" placeholder="@Placeholder" autocomplete="off" />
@if (_results != null)
{
<select class="form-select" style="position: relative;" value="@Value" size="@Rows" @onkeyup="OnKeyUp" @onchange="(e => OnChange(e))">
@if (_results.Any())
{
@foreach (var result in _results)
{
if (result.Value == Value)
{
<option selected>@result.Value</option>
}
else
{
<option>@result.Value</option>
}
}
}
else
{
<option disabled>No Results</option>
}
</select>
}
</div>
@code {
Dictionary<string, string> _results;
[Parameter]
public Func<string, Task<Dictionary<string, string>>> OnSearch { get; set; } // required - an async delegate method which accepts a filter string parameter and returns a dictionary
[Parameter]
public int Characters { get; set; } = 3; // optional - number of characters before search is initiated
[Parameter]
public int Rows { get; set; } = 3; // optional - number of result rows to display
[Parameter]
public string Placeholder { get; set; } // optional - placeholder input text
[Parameter]
public string Value { get; set; } // value of item selected
[Parameter]
public string Key { get; set; } // key of item selected
private async Task OnInput(ChangeEventArgs e)
{
Value = e.Value?.ToString();
if (Value?.Length >= Characters)
{
_results = await OnSearch?.Invoke(Value);
}
else
{
_results = null;
}
SetKey();
}
private async Task OnKeyUp(KeyboardEventArgs e)
{
var index = -1;
switch (e.Key)
{
case "ArrowDown":
if (_results == null)
{
if (Value?.Length >= Characters)
{
_results = await OnSearch?.Invoke(Value);
}
}
else
{
index = GetIndex();
if (index < _results.Count - 1)
{
Value = _results.ElementAt(index + 1).Value;
Key = _results.ElementAt(index + 1).Key;
}
}
break;
case "ArrowUp":
index = GetIndex();
if (index > 0)
{
Value = _results.ElementAt(index - 1).Value;
Key = _results.ElementAt(index - 1).Key;
}
break;
case "ArrowRight":
case "Tab":
_results = null;
break;
case "Enter": // note within a form the enter key submits the entire form
case "NumpadEnter":
_results = null;
break;
case "Escape":
Value = "";
_results = null;
break;
}
}
private void OnChange(ChangeEventArgs e)
{
Value = (string)e.Value;
SetKey();
_results = null;
}
private int GetIndex()
{
if (_results != null)
{
for (int index = 0; index < _results.Count; index++)
{
if (_results.ElementAt(index).Value == Value)
{
return index;
}
}
}
return -1;
}
private void SetKey()
{
var index = GetIndex();
if (index != -1)
{
Key = _results.ElementAt(index).Key;
}
else
{
Key = "";
}
}
public void Clear()
{
Value = "";
Key = "";
_results = null;
}
}

View File

@ -1,11 +1,12 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@using System.Threading
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IFolderService FolderService @inject IFolderService FolderService
@inject IFileService FileService @inject IFileService FileService
@inject IStringLocalizer<FileManager> Localizer @inject IStringLocalizer<FileManager> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_folders != null) @if (_initialized)
{ {
<div id="@Id" class="container-fluid px-0"> <div id="@Id" class="container-fluid px-0">
<div class="row"> <div class="row">
@ -53,7 +54,7 @@
} }
</div> </div>
<div class="col mt-2 text-end"> <div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button> <button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1) @if (ShowFiles && GetFileId() != -1)
{ {
<button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button> <button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button>
@ -86,7 +87,7 @@
} }
@code { @code {
private string _id; private bool _initialized = false;
private List<Folder> _folders; private List<Folder> _folders;
private List<File> _files = new List<File>(); private List<File> _files = new List<File>();
private string _fileinputid = string.Empty; private string _fileinputid = string.Empty;
@ -144,11 +145,6 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (!string.IsNullOrEmpty(Id))
{
_id = Id;
}
// packages folder is a framework folder for uploading installable nuget packages // packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder) if (Folder == Constants.PackagesFolder)
{ {
@ -207,9 +203,11 @@
// create unique id for component // create unique id for component
_guid = Guid.NewGuid().ToString("N"); _guid = Guid.NewGuid().ToString("N");
_fileinputid = _guid + "FileInput"; _fileinputid = "FileInput_" + _guid;
_progressinfoid = _guid + "ProgressInfo"; _progressinfoid = "ProgressInfo_" + _guid;
_progressbarid = _guid + "ProgressBar"; _progressbarid = "ProgressBar_" + _guid;
_initialized = true;
} }
private async Task GetFiles() private async Task GetFiles()
@ -225,7 +223,7 @@
Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId); Folder folder = _folders.FirstOrDefault(item => item.FolderId == FolderId);
if (folder != null) if (folder != null)
{ {
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.Permissions); _haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.PermissionList);
_files = await FileService.GetFilesAsync(FolderId); _files = await FileService.GetFilesAsync(FolderId);
} }
else else
@ -233,7 +231,6 @@
_haseditpermission = false; _haseditpermission = false;
_files = new List<File>(); _files = new List<File>();
} }
}
if (_filter != "*") if (_filter != "*")
{ {
List<File> filtered = new List<File>(); List<File> filtered = new List<File>();
@ -247,6 +244,7 @@
_files = filtered; _files = filtered;
} }
} }
}
private async Task FolderChanged(ChangeEventArgs e) private async Task FolderChanged(ChangeEventArgs e)
{ {
@ -304,17 +302,17 @@
} }
} }
private async Task UploadFile() private async Task UploadFiles()
{ {
_message = string.Empty; _message = string.Empty;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var upload = await interop.GetFiles(_fileinputid); var uploads = await interop.GetFiles(_fileinputid);
if (upload.Length > 0) if (uploads.Length > 0)
{ {
string restricted = ""; string restricted = "";
foreach (var file in upload) foreach (var upload in uploads)
{ {
var extension = (file.LastIndexOf(".") != -1) ? file.Substring(file.LastIndexOf(".") + 1) : ""; var extension = (upload.LastIndexOf(".") != -1) ? upload.Substring(upload.LastIndexOf(".") + 1) : "";
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower())) if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
{ {
restricted += (restricted == "" ? "" : ",") + extension; restricted += (restricted == "" ? "" : ",") + extension;
@ -324,28 +322,63 @@
{ {
try try
{ {
string result; // upload the files
if (Folder == Constants.PackagesFolder) var posturl = Utilities.TenantUrl(PageState.Alias, "/api/file/upload");
var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString();
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken);
// uploading is asynchronous so we need to wait for the uploads to complete
// note that this will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
bool success = false;
int attempts = 0;
while (attempts < 5 && !success)
{ {
result = await FileService.UploadFilesAsync(Folder, upload, _guid); attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
success = true;
List<File> files = await FileService.GetFilesAsync(folder);
if (files.Count > 0)
{
foreach (string upload in uploads)
{
if (!files.Exists(item => item.Name == upload))
{
success = false;
}
}
} }
else
{
result = await FileService.UploadFilesAsync(FolderId, upload, _guid);
} }
if (result == string.Empty) // reset progress indicators
await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
if (success)
{ {
await logger.LogInformation("File Upload Succeeded {Files}", upload); await logger.LogInformation("File Upload Succeeded {Files}", uploads);
if (ShowSuccess) if (ShowSuccess)
{ {
_message = Localizer["Success.File.Upload"]; _message = Localizer["Success.File.Upload"];
_messagetype = MessageType.Success; _messagetype = MessageType.Success;
} }
}
else
{
await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
if (Folder == Constants.PackagesFolder)
{
await OnUpload.InvokeAsync(-1);
}
else
{
// set FileId to first file in upload collection // set FileId to first file in upload collection
await GetFiles(); await GetFiles();
var file = _files.Where(item => item.Name == upload[0]).FirstOrDefault(); var file = _files.Where(item => item.Name == uploads[0]).FirstOrDefault();
if (file != null) if (file != null)
{ {
FileId = file.FileId; FileId = file.FileId;
@ -354,18 +387,10 @@
} }
StateHasChanged(); StateHasChanged();
} }
else
{
await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", "));
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "File Upload Failed {Error}", ex.Message); await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }

View File

@ -8,16 +8,16 @@
@if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems) @if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? "" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
@ -25,27 +25,27 @@
var pager = i; var pager = i;
if (pager == _page) if (pager == _page)
{ {
<li class="page-item active"> <li class="page-item app-pager-pointer active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item"> <li class="page-item app-pager-pointer">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? "" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
@ -119,16 +119,16 @@
@if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems) @if ((Toolbar == "Bottom" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{ {
<ul class="pagination justify-content-center my-2"> <ul class="pagination justify-content-center my-2">
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_page > _displayPages) ? "" : " disabled")"> <li class="page-item@((_page > _displayPages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page > 1) ? "" : " disabled")"> <li class="page-item@((_page > 1) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("previous"))><span class="oi oi-chevron-left" title="previous" aria-hidden="true"></span></a>
</li> </li>
@for (int i = _startPage; i <= _endPage; i++) @for (int i = _startPage; i <= _endPage; i++)
@ -136,27 +136,27 @@
var pager = i; var pager = i;
if (pager == _page) if (pager == _page)
{ {
<li class="page-item active"> <li class="page-item app-pager-pointer active">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
else else
{ {
<li class="page-item"> <li class="page-item app-pager-pointer">
<a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a> <a class="page-link" @onclick=@(async () => UpdateList(pager))>@pager</a>
</li> </li>
} }
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li> </li>
@if (_pages > _displayPages && _displayPages > 1) @if (_pages > _displayPages && _displayPages > 1)
{ {
<li class="page-item@((_endPage < _pages) ? "" : " disabled")"> <li class="page-item@((_endPage < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
</li> </li>
} }
<li class="page-item@((_page < _pages) ? "" : " disabled")"> <li class="page-item@((_page < _pages) ? " app-pager-pointer" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a> <a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li> </li>
<li class="page-item disabled"> <li class="page-item disabled">
@ -311,7 +311,6 @@
{ {
_page = _pages; _page = _pages;
} }
ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
SetPagerSize(); SetPagerSize();
} }
} }
@ -324,13 +323,13 @@
{ {
_endPage = _pages; _endPage = _pages;
} }
OnPageChange?.Invoke(_page); ItemList = Items.Skip((_page - 1) * _maxItems).Take(_maxItems);
StateHasChanged(); StateHasChanged();
OnPageChange?.Invoke(_page);
} }
public void UpdateList(int page) public void UpdateList(int page)
{ {
ItemList = Items.Skip((page - 1) * _maxItems).Take(_maxItems);
_page = page; _page = page;
SetPagerSize(); SetPagerSize();
} }

View File

@ -1,7 +1,9 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@using System.Text.Json
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IRoleService RoleService @inject IRoleService RoleService
@inject IUserService UserService @inject IUserService UserService
@inject IUserRoleService UserRoleService
@inject IStringLocalizer<PermissionGrid> Localizer @inject IStringLocalizer<PermissionGrid> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -14,20 +16,19 @@
<tbody> <tbody>
<tr> <tr>
<th scope="col">@Localizer["Role"]</th> <th scope="col">@Localizer["Role"]</th>
@foreach (PermissionString permission in _permissions) @foreach (var permissionname in _permissionnames)
{ {
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th> <th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
} }
</tr> </tr>
@foreach (Role role in _roles) @foreach (Role role in _roles)
{ {
<tr> <tr>
<td>@role.Name</td> <td>@role.Name</td>
@foreach (PermissionString permission in _permissions) @foreach (var permissionname in _permissionnames)
{ {
var p = permission;
<td style="text-align: center;"> <td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, role.Name) Disabled=@GetPermissionDisabled(role.Name) OnChange="@(e => PermissionChanged(e, p.PermissionName, role.Name))" /> <TriStateCheckBox Value=@GetPermissionValue(permissionname, role.Name, -1) Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td> </td>
} }
</tr> </tr>
@ -49,23 +50,21 @@
<thead> <thead>
<tr> <tr>
<th scope="col">@Localizer["User"]</th> <th scope="col">@Localizer["User"]</th>
@foreach (PermissionString permission in _permissions) @foreach (var permissionname in _permissionnames)
{ {
<th style="text-align: center; width: 1px;">@Localizer[permission.PermissionName]</th> <th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
} }
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (User user in _users) @foreach (User user in _users)
{ {
string userid = "[" + user.UserId.ToString() + "]";
<tr> <tr>
<td>@user.DisplayName</td> <td>@user.DisplayName</td>
@foreach (PermissionString permission in _permissions) @foreach (var permissionname in _permissionnames)
{ {
var p = permission;
<td style="text-align: center; width: 1px;"> <td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(p.Permissions, userid) Disabled=false OnChange="@(e => PermissionChanged(e, p.PermissionName, userid))" /> <TriStateCheckBox Value=@GetPermissionValue(permissionname, "", user.UserId) Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
</td> </td>
} }
</tr> </tr>
@ -77,34 +76,27 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col-11">
<table class="table table-borderless"> <AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
<tbody> </div>
<tr> <div class="col-1">
<td class="input-group">
<input type="text" name="Username" class="form-control" placeholder="@Localizer["Username.Enter"]" @bind="@_username" />
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button> <button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</td>
</tr>
</tbody>
</table>
<br />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<ModuleMessage Type="MessageType.Error" Message="@_message" /> <ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div> </div>
</div> </div>
</div> </div>
} }
@code { @code {
private string _permissionnames = string.Empty; private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles; private List<Role> _roles;
private List<PermissionString> _permissions;
private List<User> _users = new List<User>(); private List<User> _users = new List<User>();
private string _username = string.Empty; private AutoComplete _user;
private string _message = string.Empty; private string _message = string.Empty;
[Parameter] [Parameter]
@ -114,17 +106,16 @@
public string PermissionNames { get; set; } public string PermissionNames { get; set; }
[Parameter] [Parameter]
public string Permissions { get; set; } public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public List<Permission> PermissionList { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (string.IsNullOrEmpty(PermissionNames)) if (!string.IsNullOrEmpty(Permissions))
{ {
_permissionnames = Shared.PermissionNames.View + "," + Shared.PermissionNames.Edit; PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
else
{
_permissionnames = PermissionNames;
} }
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true); _roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
@ -133,140 +124,212 @@
_roles.RemoveAll(item => item.Name == RoleNames.Host); _roles.RemoveAll(item => item.Name == RoleNames.Host);
} }
_permissions = new List<PermissionString>(); // get permission names
if (string.IsNullOrEmpty(PermissionNames))
foreach (string permissionname in _permissionnames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{ {
// initialize with admin role _permissionnames = new List<string>();
_permissions.Add(new PermissionString { PermissionName = permissionname, Permissions = RoleNames.Admin }); _permissionnames.Add(Shared.PermissionNames.View);
} _permissionnames.Add(Shared.PermissionNames.Edit);
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));
}
}
}
}
}
}
}
private bool? GetPermissionValue(string permissions, string securityKey)
{
if ((";" + permissions + ";").Contains(";" + "!" + securityKey + ";"))
{
return false; // deny permission
} }
else else
{ {
if ((";" + permissions + ";").Contains(";" + securityKey + ";")) _permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{ {
return true; // grant permission foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
} }
else else
{ {
return null; // not specified foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
} }
} }
} }
private bool GetPermissionDisabled(string roleName) private string GetPermissionName(string permissionName)
=> (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) ? true : false; {
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
return true;
}
else
{
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private void PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
}
}
}
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName);
}
private async Task AddUser() private async Task AddUser()
{ {
if (_users.Where(item => item.Username == _username).FirstOrDefault() == null) if (!string.IsNullOrEmpty(_user.Key))
{ {
try var user = await UserService.GetUserAsync(int.Parse(_user.Key), ModuleState.SiteId);
{ if (user != null && !_users.Any(item => item.UserId == user.UserId))
var user = await UserService.GetUserAsync(_username, ModuleState.SiteId);
if (user != null)
{ {
_users.Add(user); _users.Add(user);
} }
} }
catch else
{ {
_message = Localizer["Message.Username.DontExist"]; _message = Localizer["Message.Username.DontExist"];
} }
} _user.Clear();
_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();
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
}
_permissions[_permissions.FindIndex(item => item.PermissionName == permissionName)].Permissions = string.Join(";", ids.ToArray());
}
} }
public string GetPermissions() public string GetPermissions()
{ {
ValidatePermissions(); ValidatePermissions();
return UserSecurity.SetPermissionStrings(_permissions); return JsonSerializer.Serialize(_permissions);
}
public List<Permission> GetPermissionList()
{
ValidatePermissions();
return _permissions;
} }
private void ValidatePermissions() private void ValidatePermissions()
{ {
PermissionString permission; // remove deny all users, unauthenticated, and registered users
for (int i = 0; i < _permissions.Count; i++) var permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Everyone || item.RoleName == RoleNames.Unauthenticated || item.RoleName == RoleNames.Registered)).ToList();
foreach (var permission in permissions)
{ {
permission = _permissions[i]; _permissions.Remove(permission);
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)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
ids.Remove("!" + RoleNames.Admin); // remove deny administrators // remove deny administrators and host users
ids.Remove("!" + RoleNames.Host); // remove deny host users permissions = _permissions.Where(item => !item.IsAuthorized &&
if (!ids.Contains(RoleNames.Host) && !ids.Contains(RoleNames.Admin)) (item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList();
foreach (var permission in permissions)
{ {
// add administrators role if host user role is not assigned _permissions.Remove(permission);
ids.Add(RoleNames.Admin); }
foreach (var permissionname in _permissionnames)
{
// add administrators role if neither host or administrator is assigned
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)))
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Admin, null, true));
} }
} }
permission.Permissions = string.Join(";", ids.ToArray());
_permissions[i] = permission;
} }
} }
} }

View File

@ -3,13 +3,13 @@
@if (Name == Parent.ActiveTab) @if (Name == Parent.ActiveTab)
{ {
<div id="@Name" class="tab-pane fade show active" role="tabpanel"> <div id="@(Parent.Id + Name)" class="tab-pane fade show active" role="tabpanel">
@ChildContent @ChildContent
</div> </div>
} }
else else
{ {
<div id="@Name" class="tab-pane fade" role="tabpanel"> <div id="@(Parent.Id + Name)" class="tab-pane fade" role="tabpanel">
@ChildContent @ChildContent
</div> </div>
} }

View File

@ -10,13 +10,13 @@
<li class="nav-item" @key="tabPanel.Name"> <li class="nav-item" @key="tabPanel.Name">
@if (tabPanel.Name == ActiveTab) @if (tabPanel.Name == ActiveTab)
{ {
<a class="nav-link active" data-bs-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true"> <a class="nav-link active" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading() @tabPanel.DisplayHeading()
</a> </a>
} }
else else
{ {
<a class="nav-link" data-bs-toggle="tab" href="#@tabPanel.Name" role="tab" @onclick:preventDefault="true"> <a class="nav-link" data-bs-toggle="tab" href="#@(Id + tabPanel.Name)" role="tab" @onclick:preventDefault="true">
@tabPanel.DisplayHeading() @tabPanel.DisplayHeading()
</a> </a>
} }
@ -33,6 +33,7 @@
@code { @code {
private List<TabPanel> _tabPanels; private List<TabPanel> _tabPanels;
private string _tabpanelid = string.Empty;
[Parameter] [Parameter]
public RenderFragment ChildContent { get; set; } // contains the TabPanels public RenderFragment ChildContent { get; set; } // contains the TabPanels
@ -43,6 +44,18 @@
[Parameter] [Parameter]
public bool Refresh { get; set; } // optional - used in scenarios where TabPanels are added/removed dynamically within a parent form. ActiveTab may need to be reset as well when this property is used. public bool Refresh { get; set; } // optional - used in scenarios where TabPanels are added/removed dynamically within a parent form. ActiveTab may need to be reset as well when this property is used.
[Parameter]
public string Id { get; set; } // optional - used to uniquely identify an instance of a tab strip component (will be set automatically if no value provided)
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(Id))
{
// create unique id for component
Id = "TabStrip_" + Guid.NewGuid().ToString("N") + "_" ;
}
}
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
if (PageState.QueryString.ContainsKey("tab")) if (PageState.QueryString.ContainsKey("tab"))
@ -80,10 +93,10 @@
authorized = true; authorized = true;
break; break;
case SecurityAccessLevel.View: case SecurityAccessLevel.View:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, ModuleState.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, ModuleState.PermissionList);
break; break;
case SecurityAccessLevel.Edit: case SecurityAccessLevel.Edit:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList);
break; break;
case SecurityAccessLevel.Admin: case SecurityAccessLevel.Admin:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin); authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin);

View File

@ -72,15 +72,24 @@ namespace Oqtane.Modules
{ {
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script)) if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{ {
var interop = new Interop(JSRuntime);
var scripts = new List<object>(); var scripts = new List<object>();
var inline = 0;
foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script)) foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script))
{
if (!string.IsNullOrEmpty(resource.Url))
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url; 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 }); scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
} }
else
{
inline += 1;
await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower());
}
}
if (scripts.Any()) if (scripts.Any())
{ {
var interop = new Interop(JSRuntime);
await interop.IncludeScripts(scripts.ToArray()); await interop.IncludeScripts(scripts.ToArray());
} }
} }
@ -181,12 +190,7 @@ namespace Oqtane.Modules
public string AddUrlParameters(params object[] parameters) public string AddUrlParameters(params object[] parameters)
{ {
var url = ""; return Utilities.AddUrlParameters(parameters);
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 // template is in the form of a standard route template ie. "/{id}/{name}" and produces dictionary of key/value pairs
@ -306,15 +310,10 @@ namespace Oqtane.Modules
{ {
int pageId = ModuleState.PageId; int pageId = ModuleState.PageId;
int moduleId = ModuleState.ModuleId; int moduleId = ModuleState.ModuleId;
int? userId = null;
if (PageState.User != null)
{
userId = PageState.User.UserId;
}
string category = GetType().AssemblyQualifiedName; string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1); string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(alias, pageId, moduleId, userId, category, feature, function, level, exception, message, args); await LoggingService.Log(alias, pageId, moduleId, PageState.User?.UserId, category, feature, function, level, exception, message, args);
} }
public class Logger public class Logger

View File

@ -5,7 +5,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RazorLangVersion>3.0</RazorLangVersion> <RazorLangVersion>3.0</RazorLangVersion>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>3.2.1</Version> <Version>3.4.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -13,7 +13,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>

View File

@ -16,6 +16,7 @@ using Microsoft.JSInterop;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Modules; using Oqtane.Modules;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI; using Oqtane.UI;
namespace Oqtane.Client namespace Oqtane.Client
@ -56,7 +57,11 @@ namespace Oqtane.Client
RegisterClientStartups(assembly, builder.Services); RegisterClientStartups(assembly, builder.Services);
} }
await builder.Build().RunAsync(); var host = builder.Build();
await SetCultureFromLocalizationCookie(host.Services);
await host.RunAsync();
} }
private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider) private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider)
@ -193,6 +198,8 @@ namespace Oqtane.Client
private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) private static void RegisterModuleServices(Assembly assembly, IServiceCollection services)
{ {
// dynamically register module scoped services // dynamically register module scoped services
try
{
var implementationTypes = assembly.GetInterfaces<IService>(); var implementationTypes = assembly.GetInterfaces<IService>();
foreach (var implementationType in implementationTypes) foreach (var implementationType in implementationTypes)
{ {
@ -203,8 +210,15 @@ namespace Oqtane.Client
} }
} }
} }
catch
{
// could not interrogate assembly - likely missing dependencies
}
}
private static void RegisterClientStartups(Assembly assembly, IServiceCollection services) private static void RegisterClientStartups(Assembly assembly, IServiceCollection services)
{
try
{ {
var startUps = assembly.GetInstances<IClientStartup>(); var startUps = assembly.GetInstances<IClientStartup>();
foreach (var startup in startUps) foreach (var startup in startUps)
@ -212,6 +226,11 @@ namespace Oqtane.Client
startup.ConfigureServices(services); startup.ConfigureServices(services);
} }
} }
catch
{
// could not interrogate assembly - likely missing dependencies
}
}
private static async Task SetCultureFromLocalizationCookie(IServiceProvider serviceProvider) private static async Task SetCultureFromLocalizationCookie(IServiceProvider serviceProvider)
{ {
@ -220,7 +239,7 @@ namespace Oqtane.Client
var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName); var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName);
var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value; var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value;
var localizationService = serviceProvider.GetRequiredService<ILocalizationService>(); var localizationService = serviceProvider.GetRequiredService<ILocalizationService>();
var cultures = await localizationService.GetCulturesAsync(); var cultures = await localizationService.GetCulturesAsync(false);
if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase))) if (culture == null || !cultures.Any(c => c.Name.Equals(culture, StringComparison.OrdinalIgnoreCase)))
{ {

View File

@ -136,7 +136,7 @@
<value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value> <value>Please Enter All Required Fields. Ensure Passwords Match And Email Address Provided Is Valid.</value>
</data> </data>
<data name="Message.Password.Invalid" xml:space="preserve"> <data name="Message.Password.Invalid" xml:space="preserve">
<value>The Password Provided Does Not Meet The Password Policy. Please Verify The Minimum Password Length And Complexity Requirements.</value> <value>The Password Provided Does Not Meet The Complexity Policy. Passwords Must Be At Least 6 Characters In Length And Contain Uppercase, Lowercase, Numeric, And Punctuation Characters.</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value> <value>Please Register Me For Major Product Updates And Security Bulletins</value>
@ -172,7 +172,7 @@
<value>Enter a complete connection string including all parameters and delimiters</value> <value>Enter a complete connection string including all parameters and delimiters</value>
</data> </data>
<data name="ConnectionString.Text" xml:space="preserve"> <data name="ConnectionString.Text" xml:space="preserve">
<value>String:</value> <value>Settings:</value>
</data> </data>
<data name="EnterConnectionParameters" xml:space="preserve"> <data name="EnterConnectionParameters" xml:space="preserve">
<value>Enter Connection Parameters</value> <value>Enter Connection Parameters</value>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -193,6 +193,6 @@
<value>Execute Once</value> <value>Execute Once</value>
</data> </data>
<data name="Message.NoJobs" xml:space="preserve"> <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> <value>Please Note That After An Initial Installation You Must &lt;a href={0}&gt;Restart&lt;/a&gt; The Application In Order To Activate The Default Scheduled Jobs.</value>
</data> </data>
</root> </root>

View File

@ -117,9 +117,6 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="The Only Supported Culture That Has Been Defined Is English" xml:space="preserve">
<value>The Only Supported Culture That Has Been Defined Is English</value>
</data>
<data name="Error.Language.Add" xml:space="preserve"> <data name="Error.Language.Add" xml:space="preserve">
<value>Error Adding Language</value> <value>Error Adding Language</value>
</data> </data>
@ -135,29 +132,11 @@
<data name="IsDefault.Text" xml:space="preserve"> <data name="IsDefault.Text" xml:space="preserve">
<value>Default?</value> <value>Default?</value>
</data> </data>
<data name="AllLanguages" xml:space="preserve">
<value>All The Installed Languages Have Been Added.</value>
</data>
<data name="Error.Language.Download" xml:space="preserve">
<value>Error Downloading Translation</value>
</data>
<data name="OnlyEnglish" xml:space="preserve">
<value>The Only Installed Language Is English</value>
</data>
<data name="Success.Language.Download" xml:space="preserve"> <data name="Success.Language.Download" xml:space="preserve">
<value>Translation Downloaded Successfully. Click Install To Complete Installation.</value> <value>Translation Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; To Complete The Installation.</value>
</data>
<data name="Success.Language.Install" xml:space="preserve">
<value>Translations Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
</data>
<data name="Search.NoResults" xml:space="preserve">
<value>No Translations Match The Criteria Provided Or Package Service Is Disabled</value>
</data>
<data name="Download.Heading" xml:space="preserve">
<value>Translations</value>
</data> </data>
<data name="LanguageUpload.HelpText" xml:space="preserve"> <data name="LanguageUpload.HelpText" xml:space="preserve">
<value>Upload one or more translation packages. Once they are uploaded click Install to complete the installation.</value> <value>Upload one or more translation packages.</value>
</data> </data>
<data name="LanguageUpload.Text" xml:space="preserve"> <data name="LanguageUpload.Text" xml:space="preserve">
<value>Translation</value> <value>Translation</value>

View File

@ -132,12 +132,12 @@
<data name="DeleteLanguage.Header" xml:space="preserve"> <data name="DeleteLanguage.Header" xml:space="preserve">
<value>Delete Language</value> <value>Delete Language</value>
</data> </data>
<data name="Success.Language.Download" xml:space="preserve">
<value>Translation Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data>
<data name="Error.Language.Download" xml:space="preserve"> <data name="Error.Language.Download" xml:space="preserve">
<value>Error Downloading Translation</value> <value>Error Downloading Translation</value>
</data> </data>
<data name="Success.Language.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="Default" xml:space="preserve"> <data name="Default" xml:space="preserve">
<value>Default</value> <value>Default</value>
</data> </data>

View File

@ -123,17 +123,14 @@
<data name="Error.Package.Load" xml:space="preserve"> <data name="Error.Package.Load" xml:space="preserve">
<value>Error Loading Packages</value> <value>Error Loading Packages</value>
</data> </data>
<data name="Success.Module.Install" xml:space="preserve">
<value>Module Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
</data>
<data name="Success.Module.Download" xml:space="preserve"> <data name="Success.Module.Download" xml:space="preserve">
<value>Module Downloaded Successfully. Click Install To Complete Installation.</value> <value>Module Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data> </data>
<data name="Error.Module.Download" xml:space="preserve"> <data name="Error.Module.Download" xml:space="preserve">
<value>Error Downloading Module</value> <value>Error Downloading Module</value>
</data> </data>
<data name="Module.HelpText" xml:space="preserve"> <data name="Module.HelpText" xml:space="preserve">
<value>Upload one or more module packages. Once they are uploaded click Install to complete the installation.</value> <value>Upload one or more module packages.</value>
</data> </data>
<data name="Search.NoResults" xml:space="preserve"> <data name="Search.NoResults" xml:space="preserve">
<value>No Modules Match The Criteria Provided Or Package Service Is Disabled</value> <value>No Modules Match The Criteria Provided Or Package Service Is Disabled</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -204,16 +204,19 @@
<data name="Error.Translation.Download" xml:space="preserve"> <data name="Error.Translation.Download" xml:space="preserve">
<value>Error Downloading Translation</value> <value>Error Downloading Translation</value>
</data> </data>
<data name="Search.PackageNameMissing" xml:space="preserve">
<value>A Package Name Was Not Provided For The Module</value>
</data>
<data name="Search.NoResults" xml:space="preserve"> <data name="Search.NoResults" xml:space="preserve">
<value>No Translations Exist For This Module Or Package Service Is Disabled</value> <value>No Translations Exist For This Module Or Package Service Is Disabled</value>
</data> </data>
<data name="Success.Translation.Download" xml:space="preserve"> <data name="Success.Translation.Download" xml:space="preserve">
<value>Translation Downloaded Successfully. Click Install To Complete Installation.</value> <value>Translation Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The 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>
<data name="Translations.Heading" xml:space="preserve"> <data name="Translations.Heading" xml:space="preserve">
<value>Translations</value> <value>Translations</value>
</data> </data>
<data name="Message.DuplicateName" xml:space="preserve">
<value>A Module With The Name Specified Already Exists</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -166,7 +166,7 @@
<value>Email:</value> <value>Email:</value>
</data> </data>
<data name="Password.HelpText" xml:space="preserve"> <data name="Password.HelpText" xml:space="preserve">
<value>Please choose a sufficiently secure password and enter it here</value> <value>Please enter a sufficiently secure password which meets the password complexity requirements</value>
</data> </data>
<data name="Password.Text" xml:space="preserve"> <data name="Password.Text" xml:space="preserve">
<value>Password:</value> <value>Password:</value>
@ -177,4 +177,19 @@
<data name="Username.Text" xml:space="preserve"> <data name="Username.Text" xml:space="preserve">
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="Password.ValidationCriteria" xml:space="preserve">
<value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site.</value>
</data>
<data name="Password.DigitRequirement" xml:space="preserve">
<value>At Least One Digit</value>
</data>
<data name="Password.LowercaseRequirement" xml:space="preserve">
<value>At Least One Lowercase Letter</value>
</data>
<data name="Password.PunctuationRequirement" xml:space="preserve">
<value>At Least One Punctuation Mark</value>
</data>
<data name="Password.UppercaseRequirement" xml:space="preserve">
<value>At Least One Uppercase Letter</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -121,7 +121,7 @@
<value>User: </value> <value>User: </value>
</data> </data>
<data name="User.Select" xml:space="preserve"> <data name="User.Select" xml:space="preserve">
<value>Select User</value> <value>Enter User's Name</value>
</data> </data>
<data name="Users" xml:space="preserve"> <data name="Users" xml:space="preserve">
<value>Users</value> <value>Users</value>
@ -129,9 +129,6 @@
<data name="Error.User.Load" xml:space="preserve"> <data name="Error.User.Load" xml:space="preserve">
<value>Error Loading Users</value> <value>Error Loading Users</value>
</data> </data>
<data name="Error.User.LoadRole" xml:space="preserve">
<value>Error Loading User Roles</value>
</data>
<data name="Success.User.AssignedRole" xml:space="preserve"> <data name="Success.User.AssignedRole" xml:space="preserve">
<value>User Assigned To Role</value> <value>User Assigned To Role</value>
</data> </data>
@ -151,7 +148,7 @@
<value>The role you are assigning users to</value> <value>The role you are assigning users to</value>
</data> </data>
<data name="User.HelpText" xml:space="preserve"> <data name="User.HelpText" xml:space="preserve">
<value>Select a user</value> <value>Enter the name of a user</value>
</data> </data>
<data name="EffectiveDate.HelpText" xml:space="preserve"> <data name="EffectiveDate.HelpText" xml:space="preserve">
<value>The date that this role assignment is active</value> <value>The date that this role assignment is active</value>

View File

@ -163,7 +163,7 @@
<value>Enter the site name</value> <value>Enter the site name</value>
</data> </data>
<data name="Tenant.HelpText" xml:space="preserve"> <data name="Tenant.HelpText" xml:space="preserve">
<value>Enter the tenant for the site</value> <value>The name of the database used for the site</value>
</data> </data>
<data name="Aliases.HelpText" xml:space="preserve"> <data name="Aliases.HelpText" xml:space="preserve">
<value>The aliases for the site. An alias can be a domain name (www.site.com) or a virtual folder (ie. www.site.com/folder).</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>
@ -195,13 +195,13 @@
<data name="UseSsl.HelpText" xml:space="preserve"> <data name="UseSsl.HelpText" xml:space="preserve">
<value>Specify if SSL is required for your SMTP server</value> <value>Specify if SSL is required for your SMTP server</value>
</data> </data>
<data name="SmptUsername.HelpText" xml:space="preserve"> <data name="SmtpUsername.HelpText" xml:space="preserve">
<value>Enter the username for your SMTP account</value> <value>Enter the username for your SMTP account</value>
</data> </data>
<data name="SmtpPassword.HelpText" xml:space="preserve"> <data name="SmtpPassword.HelpText" xml:space="preserve">
<value>Enter the password for your SMTP account</value> <value>Enter the password for your SMTP account</value>
</data> </data>
<data name="SmptSender.HelpText" xml:space="preserve"> <data name="SmtpSender.HelpText" xml:space="preserve">
<value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value> <value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value>
</data> </data>
<data name="EnablePWA.HelpText" xml:space="preserve"> <data name="EnablePWA.HelpText" xml:space="preserve">
@ -214,7 +214,7 @@
<value>Include a splash icon for your PWA. It should be a PNG which is 512 X 512 pixels in dimension.</value> <value>Include a splash icon for your PWA. It should be a PNG which is 512 X 512 pixels in dimension.</value>
</data> </data>
<data name="Tenant.Text" xml:space="preserve"> <data name="Tenant.Text" xml:space="preserve">
<value>Tenant: </value> <value>Database: </value>
</data> </data>
<data name="Aliases.Text" xml:space="preserve"> <data name="Aliases.Text" xml:space="preserve">
<value>Aliases: </value> <value>Aliases: </value>
@ -243,13 +243,13 @@
<data name="UseSsl.Text" xml:space="preserve"> <data name="UseSsl.Text" xml:space="preserve">
<value>SSL Enabled: </value> <value>SSL Enabled: </value>
</data> </data>
<data name="SmptUsername.Text" xml:space="preserve"> <data name="SmtpUsername.Text" xml:space="preserve">
<value>Username: </value> <value>Username: </value>
</data> </data>
<data name="SmtpPassword.Text" xml:space="preserve"> <data name="SmtpPassword.Text" xml:space="preserve">
<value>Password: </value> <value>Password: </value>
</data> </data>
<data name="SmptSender.Text" xml:space="preserve"> <data name="SmtpSender.Text" xml:space="preserve">
<value>Email Sender: </value> <value>Email Sender: </value>
</data> </data>
<data name="EnablePWA.Text" xml:space="preserve"> <data name="EnablePWA.Text" xml:space="preserve">
@ -292,7 +292,7 @@
<value>Browse</value> <value>Browse</value>
</data> </data>
<data name="TenantInformation.Heading" xml:space="preserve"> <data name="TenantInformation.Heading" xml:space="preserve">
<value>Tenant Information</value> <value>Database</value>
</data> </data>
<data name="PWASettings.Heading" xml:space="preserve"> <data name="PWASettings.Heading" xml:space="preserve">
<value>PWA Settings</value> <value>PWA Settings</value>
@ -304,13 +304,13 @@
<value>Connection:</value> <value>Connection:</value>
</data> </data>
<data name="Database.Text" xml:space="preserve"> <data name="Database.Text" xml:space="preserve">
<value>Database:</value> <value>Type:</value>
</data> </data>
<data name="ConnectionString.HelpText" xml:space="preserve"> <data name="ConnectionString.HelpText" xml:space="preserve">
<value>The connection information for the database</value> <value>The connection information for the database</value>
</data> </data>
<data name="Database.HelpText" xml:space="preserve"> <data name="Database.HelpText" xml:space="preserve">
<value>The database for the tenant</value> <value>The type of database</value>
</data> </data>
<data name="DeleteSite.Text" xml:space="preserve"> <data name="DeleteSite.Text" xml:space="preserve">
<value>Delete Site</value> <value>Delete Site</value>
@ -339,4 +339,16 @@
<data name="HomePage.Text" xml:space="preserve"> <data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value> <value>Home Page:</value>
</data> </data>
<data name="SmtpRelay.HelpText" xml:space="preserve">
<value>Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above.</value>
</data>
<data name="SmtpRelay.Text" xml:space="preserve">
<value>Relay Configured?</value>
</data>
<data name="SiteMap.HelpText" xml:space="preserve">
<value>The site map url for this site which can be submitted to search engines for indexing</value>
</data>
<data name="SiteMap.Text" xml:space="preserve">
<value>Site Map:</value>
</data>
</root> </root>

View File

@ -123,9 +123,6 @@
<data name="SqlServer" xml:space="preserve"> <data name="SqlServer" xml:space="preserve">
<value>SQL Server</value> <value>SQL Server</value>
</data> </data>
<data name="Server.Text" xml:space="preserve">
<value>Server: </value>
</data>
<data name="Container.Select" xml:space="preserve"> <data name="Container.Select" xml:space="preserve">
<value>Select Container</value> <value>Select Container</value>
</data> </data>
@ -145,7 +142,7 @@
<value>Select the default container for the site</value> <value>Select the default container for the site</value>
</data> </data>
<data name="Tenant.Text" xml:space="preserve"> <data name="Tenant.Text" xml:space="preserve">
<value>Tenant: </value> <value>Database: </value>
</data> </data>
<data name="Aliases.Text" xml:space="preserve"> <data name="Aliases.Text" xml:space="preserve">
<value>Aliases: </value> <value>Aliases: </value>
@ -157,10 +154,10 @@
<value>Select Site Template</value> <value>Select Site Template</value>
</data> </data>
<data name="Tenant.Select" xml:space="preserve"> <data name="Tenant.Select" xml:space="preserve">
<value>Select Tenant</value> <value>Select Database</value>
</data> </data>
<data name="Tenant.Add" xml:space="preserve"> <data name="Tenant.Add" xml:space="preserve">
<value>Create New Tenant</value> <value>Create Database</value>
</data> </data>
<data name="Error.Theme.LoadContainers" xml:space="preserve"> <data name="Error.Theme.LoadContainers" xml:space="preserve">
<value>Error Loading Containers For Theme</value> <value>Error Loading Containers For Theme</value>
@ -172,19 +169,19 @@
<value>Invalid Host Password</value> <value>Invalid Host Password</value>
</data> </data>
<data name="Error.TenantName.Exists" xml:space="preserve"> <data name="Error.TenantName.Exists" xml:space="preserve">
<value>Tenant Name Is Missing Or Already Exists</value> <value>Database Name Is Missing Or Already Exists</value>
</data> </data>
<data name="Message.SiteName.InUse" xml:space="preserve"> <data name="Message.SiteName.InUse" xml:space="preserve">
<value>{0} Already Used For Another Site</value> <value>{0} Already Used For Another Site</value>
</data> </data>
<data name="Message.Required.Tenant" xml:space="preserve"> <data name="Message.Required.Tenant" xml:space="preserve">
<value>You Must Provide A Tenant, Site Name, Alias, Default Theme/Container, And Site Template</value> <value>You Must Provide A Database, Site Name, Alias, Default Theme/Container, And Site Template</value>
</data> </data>
<data name="Name.HelpText" xml:space="preserve"> <data name="Name.HelpText" xml:space="preserve">
<value>Enter the name of the site</value> <value>Enter the name of the site</value>
</data> </data>
<data name="DefaultTheme.HelpText" xml:space="preserve"> <data name="DefaultTheme.HelpText" xml:space="preserve">
<value>Select the default theme for the website</value> <value>Select the default theme for the site</value>
</data> </data>
<data name="AdminContainer.HelpText" xml:space="preserve"> <data name="AdminContainer.HelpText" xml:space="preserve">
<value>Select the admin container for the site</value> <value>Select the admin container for the site</value>
@ -193,28 +190,13 @@
<value>Select the site template</value> <value>Select the site template</value>
</data> </data>
<data name="Tenant.HelpText" xml:space="preserve"> <data name="Tenant.HelpText" xml:space="preserve">
<value>Select the tenant for the site</value> <value>Select the database for the site</value>
</data> </data>
<data name="TenantName.HelpText" xml:space="preserve"> <data name="TenantName.HelpText" xml:space="preserve">
<value>Enter the name for the tenant</value> <value>Enter the name for the database</value>
</data> </data>
<data name="DatabaseType.HelpText" xml:space="preserve"> <data name="DatabaseType.HelpText" xml:space="preserve">
<value>Select the database type for the tenant</value> <value>Select the database type</value>
</data>
<data name="DatabaseServer.HelpText" xml:space="preserve">
<value>Enter the server for the tenant</value>
</data>
<data name="Database.HelpText" xml:space="preserve">
<value>Enter the database for the tenant</value>
</data>
<data name="IntegratedSecurity.HelpText" xml:space="preserve">
<value>Select if you want integrated security or not</value>
</data>
<data name="DatabaseUsername.HelpText" xml:space="preserve">
<value>Enter the username for the integrated security</value>
</data>
<data name="DatabasePassword.HelpText" xml:space="preserve">
<value>Enter the password for the integrated security</value>
</data> </data>
<data name="HostUsername.HelpText" xml:space="preserve"> <data name="HostUsername.HelpText" xml:space="preserve">
<value>Enter the username of an existing host user</value> <value>Enter the username of an existing host user</value>
@ -232,23 +214,14 @@
<value>Site Template: </value> <value>Site Template: </value>
</data> </data>
<data name="TenantName.Text" xml:space="preserve"> <data name="TenantName.Text" xml:space="preserve">
<value>Tenant Name: </value> <value>Name: </value>
</data> </data>
<data name="DatabaseType.Text" xml:space="preserve"> <data name="DatabaseType.Text" xml:space="preserve">
<value>Database Type: </value> <value>Type: </value>
</data> </data>
<data name="Database.Text" xml:space="preserve"> <data name="Database.Text" xml:space="preserve">
<value>Database: </value> <value>Database: </value>
</data> </data>
<data name="IntegratedSecurity.Text" xml:space="preserve">
<value>Integrated Security: </value>
</data>
<data name="DatabaseUsername.Text" xml:space="preserve">
<value>Database Username: </value>
</data>
<data name="DatabasePassword.Text" xml:space="preserve">
<value>Database Password: </value>
</data>
<data name="HostUsername.Text" xml:space="preserve"> <data name="HostUsername.Text" xml:space="preserve">
<value>Host Username:</value> <value>Host Username:</value>
</data> </data>
@ -274,7 +247,7 @@
<value>Enter a complete connection string including all parameters and delimiters</value> <value>Enter a complete connection string including all parameters and delimiters</value>
</data> </data>
<data name="ConnectionString.Text" xml:space="preserve"> <data name="ConnectionString.Text" xml:space="preserve">
<value>String:</value> <value>Settings:</value>
</data> </data>
<data name="EnterConnectionParameters" xml:space="preserve"> <data name="EnterConnectionParameters" xml:space="preserve">
<value>Enter Connection Parameters</value> <value>Enter Connection Parameters</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -117,30 +117,75 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Tenant.Text" xml:space="preserve"> <data name="Connection.Text" xml:space="preserve">
<value>Tenant: </value> <value>Connection: </value>
</data> </data>
<data name="Tenant.Select" xml:space="preserve"> <data name="Connection.HelpText" xml:space="preserve">
<value>Select Tenant</value> <value>Select a database connection (from appsettings.json)</value>
</data>
<data name="Connection.Select" xml:space="preserve">
<value>Select Connection</value>
</data>
<data name="Connection.Add" xml:space="preserve">
<value>Add Connection</value>
</data>
<data name="Name.Text" xml:space="preserve">
<value>Name: </value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Enter the name of the connection</value>
</data>
<data name="DatabaseType.Text" xml:space="preserve">
<value>Type: </value>
</data>
<data name="DatabaseType.HelpText" xml:space="preserve">
<value>Select the database type</value>
</data>
<data name="Type.Select" xml:space="preserve">
<value>Select Type</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>
<data name="ConnectionString.Text" xml:space="preserve">
<value>Settings: </value>
</data>
<data name="ConnectionString.HelpText" xml:space="preserve">
<value>A complete connection string including all parameters and delimiters</value>
</data>
<data name="Add" xml:space="preserve">
<value>Add</value>
</data>
<data name="Tenant.Text" xml:space="preserve">
<value>Database: </value>
</data>
<data name="Tenant.HelpText" xml:space="preserve">
<value>The database using this connection</value>
</data>
<data name="SqlQuery.Text" xml:space="preserve">
<value>SQL Query: </value>
</data>
<data name="SqlQuery.HelpText" xml:space="preserve">
<value>Enter a valid SQL query for the database</value>
</data> </data>
<data name="Execute" xml:space="preserve"> <data name="Execute" xml:space="preserve">
<value>Execute</value> <value>Execute</value>
</data> </data>
<data name="Message.Required.Tenant" xml:space="preserve"> <data name="Message.Required.Tenant" xml:space="preserve">
<value>You Must Select A Tenant And Provide A Valid SQL Query</value> <value>You Must Select A Database Type And Provide A Valid SQL Query</value>
</data>
<data name="Message.Required.Connection" xml:space="preserve">
<value>You Must Provide A Connection Name And Settings</value>
</data>
<data name="Message.Connection.Added" xml:space="preserve">
<value>Connection Added Successfully</value>
</data> </data>
<data name="Return.NoResult" xml:space="preserve"> <data name="Return.NoResult" xml:space="preserve">
<value>No Results Returned</value> <value>No Results Returned</value>
</data> </data>
<data name="Tenant.HelpText" xml:space="preserve">
<value>Select the tenant for the SQL server</value>
</data>
<data name="SqlQuery.HelpText" xml:space="preserve">
<value>Enter the query for the SQL server</value>
</data>
<data name="SqlQuery.Text" xml:space="preserve">
<value>SQL Query: </value>
</data>
<data name="Success.QueryExecuted" xml:space="preserve"> <data name="Success.QueryExecuted" xml:space="preserve">
<value>SQL Query Executed</value> <value>SQL Query Executed</value>
</data> </data>

View File

@ -209,6 +209,9 @@
</data> </data>
<data name="Options.Heading" xml:space="preserve"> <data name="Options.Heading" xml:space="preserve">
<value>Options</value> <value>Options</value>
</data>
<data name="Log.Heading" xml:space="preserve">
<value>Log</value>
</data> </data>
<data name="Register" xml:space="preserve"> <data name="Register" xml:space="preserve">
<value>Please Register Me For Major Product Updates And Security Bulletins</value> <value>Please Register Me For Major Product Updates And Security Bulletins</value>
@ -276,4 +279,19 @@
<data name="Environment.Text" xml:space="preserve"> <data name="Environment.Text" xml:space="preserve">
<value>Environment:</value> <value>Environment:</value>
</data> </data>
<data name="Log.Text" xml:space="preserve">
<value>Log:</value>
</data>
<data name="Log.HelpText" xml:space="preserve">
<value>System log information for current day</value>
</data>
<data name="Clear" xml:space="preserve">
<value>Clear</value>
</data>
<data name="Success.ClearLog" xml:space="preserve">
<value>System Log Has Been Successfully Cleared</value>
</data>
<data name="Error.ClearLog" xml:space="preserve">
<value>Ann Error Occurred Clearing The System Log</value>
</data>
</root> </root>

View File

@ -123,17 +123,14 @@
<data name="Theme.Text" xml:space="preserve"> <data name="Theme.Text" xml:space="preserve">
<value>Theme: </value> <value>Theme: </value>
</data> </data>
<data name="Success.Theme.Install" xml:space="preserve">
<value>Theme Installed Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
</data>
<data name="Success.Theme.Download" xml:space="preserve"> <data name="Success.Theme.Download" xml:space="preserve">
<value>Theme Downloaded Successfully. Click Install To Complete Installation.</value> <value>Theme Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data> </data>
<data name="Error.Theme.Download" xml:space="preserve"> <data name="Error.Theme.Download" xml:space="preserve">
<value>Error Downloading Theme</value> <value>Error Downloading Theme</value>
</data> </data>
<data name="Theme.HelpText" xml:space="preserve"> <data name="Theme.HelpText" xml:space="preserve">
<value>Upload one or more theme packages. Once they are uploaded click Install to complete the installation.</value> <value>Upload one or more theme packages.</value>
</data> </data>
<data name="Search.NoResults" xml:space="preserve"> <data name="Search.NoResults" xml:space="preserve">
<value>No Themes Match The Criteria Provided Or Package Service Is Disabled</value> <value>No Themes Match The Criteria Provided Or Package Service Is Disabled</value>

View File

@ -120,6 +120,9 @@
<data name="Message.Password.Invalid" xml:space="preserve"> <data name="Message.Password.Invalid" xml:space="preserve">
<value>Passwords Entered Do Not Match</value> <value>Passwords Entered Do Not Match</value>
</data> </data>
<data name="Message.Password.Complexity" xml:space="preserve">
<value>Password Provided Does Not Meet The Complexity Policy</value>
</data>
<data name="From" xml:space="preserve"> <data name="From" xml:space="preserve">
<value>From</value> <value>From</value>
</data> </data>

View File

@ -120,6 +120,9 @@
<data name="Message.Password.NoMatch" xml:space="preserve"> <data name="Message.Password.NoMatch" xml:space="preserve">
<value>Passwords Entered Do Not Match</value> <value>Passwords Entered Do Not Match</value>
</data> </data>
<data name="Message.Password.Complexity" xml:space="preserve">
<value>Password Provided Does Not Meet The Complexity Policy</value>
</data>
<data name="Identity.Name" xml:space="preserve"> <data name="Identity.Name" xml:space="preserve">
<value>Identity</value> <value>Identity</value>
</data> </data>

View File

@ -388,6 +388,12 @@
<value>Optionally provide the name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value> <value>Optionally provide the name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value>
</data> </data>
<data name="RoleClaimType.Text" xml:space="preserve"> <data name="RoleClaimType.Text" xml:space="preserve">
<value>Role Claim Type:</value> <value>Role Claim:</value>
</data>
<data name="ProfileClaimTypes.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of user profile claims provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value>
</data>
<data name="ProfileClaimTypes.Text" xml:space="preserve">
<value>User Profile Claims:</value>
</data> </data>
</root> </root>

View File

@ -127,7 +127,7 @@
<value>Error Loading Files</value> <value>Error Loading Files</value>
</data> </data>
<data name="Error.File.Upload" xml:space="preserve"> <data name="Error.File.Upload" xml:space="preserve">
<value>File Upload Failed</value> <value>File Upload Failed Or Is Still In Progress</value>
</data> </data>
<data name="Message.File.NotSelected" xml:space="preserve"> <data name="Message.File.NotSelected" xml:space="preserve">
<value>You Have Not Selected A File To Upload</value> <value>You Have Not Selected A File To Upload</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -124,9 +124,9 @@
<value>User</value> <value>User</value>
</data> </data>
<data name="Username.Enter" xml:space="preserve"> <data name="Username.Enter" xml:space="preserve">
<value>Enter Username</value> <value>Enter User's Name</value>
</data> </data>
<data name="Message.Username.DontExist" xml:space="preserve"> <data name="Message.Username.DontExist" xml:space="preserve">
<value>Username Does Not Exist</value> <value>User Does Not Exist With Name Specified</value>
</data> </data>
</root> </root>

View File

@ -340,6 +340,48 @@
<value>Visitor Management</value> <value>Visitor Management</value>
</data> </data>
<data name="Oqtane.Marketplace" xml:space="preserve"> <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> <value>Please note that third party extensions are 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>
<data name="Home" xml:space="preserve">
<value>Home</value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Apply" xml:space="preserve">
<value>Apply</value>
</data>
<data name="Select" xml:space="preserve">
<value>Select</value>
</data>
<data name="Next" xml:space="preserve">
<value>Next</value>
</data>
<data name="Previous" xml:space="preserve">
<value>Previous</value>
</data>
<data name="Submit" xml:space="preserve">
<value>Submit</value>
</data>
<data name="Refresh" xml:space="preserve">
<value>Refresh</value>
</data>
<data name="Back" xml:space="preserve">
<value>Back</value>
</data>
<data name="Return" xml:space="preserve">
<value>Return</value>
</data>
<data name="New" xml:space="preserve">
<value>New</value>
</data>
<data name="View" xml:space="preserve">
<value>View</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Confirm</value>
</data> </data>
</root> </root>

View File

@ -2,27 +2,17 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Services namespace Oqtane.Services
{ {
[PrivateApi("Don't show in the documentation, as everything should use the Interface")] [PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class FileService : ServiceBase, IFileService public class FileService : ServiceBase, IFileService
{ {
private readonly SiteState _siteState; public FileService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly IJSRuntime _jsRuntime;
public FileService(HttpClient http, SiteState siteState, IJSRuntime jsRuntime) : base(http, siteState)
{
_siteState = siteState;
_jsRuntime = jsRuntime;
}
private string Apiurl => CreateApiUrl("File"); private string Apiurl => CreateApiUrl("File");
@ -75,54 +65,6 @@ namespace Oqtane.Services
return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}&name={name}"); return await GetJsonAsync<File>($"{Apiurl}/upload?url={WebUtility.UrlEncode(url)}&folderid={folderId}&name={name}");
} }
public async Task<string> UploadFilesAsync(int folderId, string[] files, string id)
{
return await UploadFilesAsync(folderId.ToString(), files, id);
}
public async Task<string> UploadFilesAsync(string folder, string[] files, string id)
{
string result = "";
var interop = new Interop(_jsRuntime);
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;
int attempts = 0;
while (attempts < 5 && success == false)
{
Thread.Sleep(2000); // wait 2 seconds
result = "";
List<File> fileList = await GetFilesAsync(folder);
if (fileList.Count > 0)
{
success = true;
foreach (string file in files)
{
if (!fileList.Exists(item => item.Name == file))
{
success = false;
result += file + ",";
}
}
}
attempts += 1;
}
await interop.SetElementAttribute(id + "ProgressInfo", "style", "display: none;");
await interop.SetElementAttribute(id + "ProgressBar", "style", "display: none;");
if (!success)
{
result = result.Substring(0, result.Length - 1);
}
return result;
}
public async Task<byte[]> DownloadFileAsync(int fileId) public async Task<byte[]> DownloadFileAsync(int fileId)
{ {
return await GetByteArrayAsync($"{Apiurl}/download/{fileId}"); return await GetByteArrayAsync($"{Apiurl}/download/{fileId}");

View File

@ -66,27 +66,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<File> UploadFileAsync(string url, int folderId, string name); Task<File> UploadFileAsync(string url, int folderId, string name);
/// <summary>
/// Upload one or more files.
/// </summary>
/// <param name="folderId">Target <see cref="Folder"/></param>
/// <param name="files">The files to upload, serialized as a string.</param>
/// <param name="fileUploadName">A task-identifier, to ensure communication about this upload.</param>
/// <returns></returns>
Task<string> UploadFilesAsync(int folderId, string[] files, string fileUploadName);
/// <summary>
/// Upload one or more files.
/// </summary>
/// <param name="folder">Target <see cref="Folder"/>
/// TODO: todoc verify exactly from where the folder path must start
/// </param>
/// <param name="files">The files to upload, serialized as a string.</param>
/// <param name="fileUploadName">A task-identifier, to ensure communication about this upload.</param>
/// <returns></returns>
Task<string> UploadFilesAsync(string folder, string[] files, string fileUploadName);
/// <summary> /// <summary>
/// Get / download a file (the body). /// Get / download a file (the body).
/// </summary> /// </summary>

View File

@ -13,6 +13,6 @@ namespace Oqtane.Services
/// Returns a collection of supported cultures /// Returns a collection of supported cultures
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<Culture>> GetCulturesAsync(); Task<IEnumerable<Culture>> GetCulturesAsync(bool installed);
} }
} }

View File

@ -33,13 +33,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateModuleDefinitionAsync(ModuleDefinition moduleDefinition); Task UpdateModuleDefinitionAsync(ModuleDefinition moduleDefinition);
/// <summary>
/// Installs all module definitions located in //TODO: 2dm where?
/// </summary>
/// <returns></returns>
Task InstallModuleDefinitionsAsync();
/// <summary> /// <summary>
/// Deletes a module definition /// Deletes a module definition
/// </summary> /// </summary>

View File

@ -62,7 +62,7 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Returns a key-value dictionary of all page module settings for the given page module /// Returns a key-value dictionary of all page module settings for the given page module
/// </summary> /// </summary>
/// <param name="pageId"></param> /// <param name="pageModuleId"></param>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetPageModuleSettingsAsync(int pageModuleId); Task<Dictionary<string, string>> GetPageModuleSettingsAsync(int pageModuleId);
@ -107,7 +107,7 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Returns a key-value dictionary of all user settings for the given user /// Returns a key-value dictionary of all user settings for the given user
/// </summary> /// </summary>
/// <param name="pageId"></param> /// <param name="userId"></param>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetUserSettingsAsync(int userId); Task<Dictionary<string, string>> GetUserSettingsAsync(int userId);
@ -122,7 +122,7 @@ namespace Oqtane.Services
/// <summary> /// <summary>
/// Returns a key-value dictionary of all folder settings for the given folder /// Returns a key-value dictionary of all folder settings for the given folder
/// </summary> /// </summary>
/// <param name="pageId"></param> /// <param name="folderId"></param>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<string, string>> GetFolderSettingsAsync(int folderId); Task<Dictionary<string, string>> GetFolderSettingsAsync(int folderId);
@ -148,6 +148,21 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateHostSettingsAsync(Dictionary<string, string> hostSettings); Task UpdateHostSettingsAsync(Dictionary<string, string> hostSettings);
/// <summary>
/// Returns a key-value dictionary of all settings for the given visitor
/// </summary>
/// <param name="visitorId"></param>
/// <returns></returns>
Task<Dictionary<string, string>> GetVisitorSettingsAsync(int visitorId);
/// <summary>
/// Updates a visitor setting
/// </summary>
/// <param name="visitorSettings"></param>
/// <param name="visitorId"></param>
/// <returns></returns>
Task UpdateVisitorSettingsAsync(Dictionary<string, string> visitorSettings, int visitorId);
/// <summary> /// <summary>
/// Returns a key-value dictionary of all settings for the given entityName /// Returns a key-value dictionary of all settings for the given entityName
/// </summary> /// </summary>
@ -164,6 +179,24 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId); Task UpdateSettingsAsync(Dictionary<string, string> settings, string entityName, int entityId);
/// <summary>
/// Returns a specific setting
/// </summary>
/// <param name="entityName"></param>
/// <param name="entityId"></param>
/// <param name="settingName"></param>
/// <returns></returns>
Task DeleteSettingAsync(string entityName, int entityId, string settingName);
/// <summary>
/// Returns a specific setting
/// </summary>
/// <param name="entityName"></param>
/// <param name="entityId"></param>
/// <param name="settingName"></param>
/// <returns></returns>
Task<List<Setting>> GetSettingsAsync(string entityName, int entityId, string settingName);
/// <summary> /// <summary>
/// Returns a specific setting /// Returns a specific setting
/// </summary> /// </summary>

View File

@ -32,11 +32,5 @@ namespace Oqtane.Services
/// <param name="settings"></param> /// <param name="settings"></param>
/// <returns></returns> /// <returns></returns>
Task UpdateSystemInfoAsync(Dictionary<string, object> settings); Task UpdateSystemInfoAsync(Dictionary<string, object> settings);
/// <summary>
/// updates a config value
/// </summary>
/// <returns></returns>
Task UpdateSystemInfoAsync(string settingKey, object settingValue);
} }
} }

View File

@ -39,12 +39,6 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
List<ThemeControl> GetContainerControls(List<Theme> themes, string themeName); List<ThemeControl> GetContainerControls(List<Theme> themes, string themeName);
/// <summary>
/// Installs all themes located in //TODO: 2dm where?
/// </summary>
/// <returns></returns>
Task InstallThemesAsync();
/// <summary> /// <summary>
/// Deletes a theme /// Deletes a theme
/// </summary> /// </summary>

View File

@ -14,6 +14,6 @@ namespace Oqtane.Services
private string Apiurl => CreateApiUrl("Localization"); private string Apiurl => CreateApiUrl("Localization");
public async Task<IEnumerable<Culture>> GetCulturesAsync() => await GetJsonAsync<IEnumerable<Culture>>(Apiurl); public async Task<IEnumerable<Culture>> GetCulturesAsync(bool installed) => await GetJsonAsync<IEnumerable<Culture>>($"{Apiurl}?installed={installed}");
} }
} }

View File

@ -34,11 +34,6 @@ namespace Oqtane.Services
await PutJsonAsync($"{Apiurl}/{moduleDefinition.ModuleDefinitionId}", moduleDefinition); await PutJsonAsync($"{Apiurl}/{moduleDefinition.ModuleDefinitionId}", moduleDefinition);
} }
public async Task InstallModuleDefinitionsAsync()
{
await GetJsonAsync<List<string>>($"{Apiurl}/install");
}
public async Task DeleteModuleDefinitionAsync(int moduleDefinitionId, int siteId) public async Task DeleteModuleDefinitionAsync(int moduleDefinitionId, int siteId)
{ {
await DeleteAsync($"{Apiurl}/{moduleDefinitionId}?siteid={siteId}"); await DeleteAsync($"{Apiurl}/{moduleDefinitionId}?siteid={siteId}");

View File

@ -18,11 +18,7 @@ namespace Oqtane.Services
public async Task<List<Module>> GetModulesAsync(int siteId) public async Task<List<Module>> GetModulesAsync(int siteId)
{ {
List<Module> modules = await GetJsonAsync<List<Module>>($"{Apiurl}?siteid={siteId}"); return await GetJsonAsync<List<Module>>($"{Apiurl}?siteid={siteId}");
modules = modules
.OrderBy(item => item.Order)
.ToList();
return modules;
} }
public async Task<Module> GetModuleAsync(int moduleId) public async Task<Module> GetModuleAsync(int moduleId)

View File

@ -111,15 +111,37 @@ namespace Oqtane.Services
await UpdateSettingsAsync(hostSettings, EntityNames.Host, -1); await UpdateSettingsAsync(hostSettings, EntityNames.Host, -1);
} }
public async Task<Dictionary<string, string>> GetVisitorSettingsAsync(int visitorId)
{
if (visitorId != -1)
{
return await GetSettingsAsync(EntityNames.Visitor, visitorId);
}
else
{
return new Dictionary<string, string>();
}
}
public async Task UpdateVisitorSettingsAsync(Dictionary<string, string> visitorSettings, int visitorId)
{
if (visitorId != -1)
{
await UpdateSettingsAsync(visitorSettings, EntityNames.Visitor, visitorId);
}
}
public async Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId) public async Task<Dictionary<string, string>> GetSettingsAsync(string entityName, int entityId)
{ {
var dictionary = new Dictionary<string, string>(); var dictionary = new Dictionary<string, string>();
var settings = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}"); var settings = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
if (settings != null)
foreach(Setting setting in settings.OrderBy(item => item.SettingName).ToList()) {
foreach (Setting setting in settings.OrderBy(item => item.SettingName).ToList())
{ {
dictionary.Add(setting.SettingName, setting.SettingValue); dictionary.Add(setting.SettingName, setting.SettingValue);
} }
}
return dictionary; return dictionary;
} }
@ -170,6 +192,26 @@ namespace Oqtane.Services
} }
} }
public async Task DeleteSettingAsync(string entityName, int entityId, string settingName)
{
var settings = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
var setting = settings.FirstOrDefault(item => item.SettingName == settingName);
if (setting != null)
{
await DeleteAsync($"{Apiurl}/{setting.SettingId}/{entityName}");
}
}
public async Task<List<Setting>> GetSettingsAsync(string entityName, int entityId, string settingName)
{
var settings = await GetJsonAsync<List<Setting>>($"{Apiurl}?entityname={entityName}&entityid={entityId}");
if (!string.IsNullOrEmpty(settingName))
{
settings = settings.Where(item => item.SettingName == settingName).ToList();
}
return settings;
}
public async Task<Setting> GetSettingAsync(string entityName, int settingId) public async Task<Setting> GetSettingAsync(string entityName, int settingId)
{ {
return await GetJsonAsync<Setting>($"{Apiurl}/{settingId}/{entityName}"); return await GetJsonAsync<Setting>($"{Apiurl}/{settingId}/{entityName}");

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Shared; using Oqtane.Shared;
using System.Net;
namespace Oqtane.Services namespace Oqtane.Services
{ {
@ -32,9 +33,5 @@ namespace Oqtane.Services
{ {
await PostJsonAsync(Apiurl, settings); await PostJsonAsync(Apiurl, settings);
} }
public async Task UpdateSystemInfoAsync(string settingKey, object settingValue)
{
await PutJsonAsync($"{Apiurl}/{settingKey}/{settingValue}", "");
}
} }
} }

View File

@ -38,11 +38,6 @@ namespace Oqtane.Services
.SelectMany(item => item.Containers).ToList(); .SelectMany(item => item.Containers).ToList();
} }
public async Task InstallThemesAsync()
{
await GetJsonAsync<List<string>>($"{ApiUrl}/install");
}
public async Task DeleteThemeAsync(string themeName) public async Task DeleteThemeAsync(string themeName)
{ {
await DeleteAsync($"{ApiUrl}/{themeName}"); await DeleteAsync($"{ApiUrl}/{themeName}");

View File

@ -2,7 +2,7 @@
@inherits ModuleActionsBase @inherits ModuleActionsBase
@attribute [OqtaneIgnore] @attribute [OqtaneIgnore]
@if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions) && PageState.Action == Constants.DefaultAction) @if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction)
{ {
<div class="app-moduleactions py-2 px-3"> <div class="app-moduleactions py-2 px-3">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"></a> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"></a>

View File

@ -30,11 +30,11 @@ namespace Oqtane.Themes.Controls
{ {
var actionList = new List<ActionViewModel>(); var actionList = new List<ActionViewModel>();
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions)) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
actionList.Add(new ActionViewModel { Icon = Icons.Cog, Name = "Manage Settings", Action = async (u, m) => await Settings(u, m) }); actionList.Add(new ActionViewModel { Icon = Icons.Cog, Name = "Manage Settings", Action = async (u, m) => await Settings(u, m) });
if (UserSecurity.ContainsRole(ModuleState.Permissions, PermissionNames.View, RoleNames.Everyone)) if (UserSecurity.ContainsRole(ModuleState.PermissionList, PermissionNames.View, RoleNames.Everyone))
{ {
actionList.Add(new ActionViewModel { Icon = Icons.CircleX, Name = "Unpublish Module", Action = async (s, m) => await Unpublish(s, m) }); actionList.Add(new ActionViewModel { Icon = Icons.CircleX, Name = "Unpublish Module", Action = async (s, m) => await Unpublish(s, m) });
} }
@ -44,7 +44,7 @@ namespace Oqtane.Themes.Controls
} }
actionList.Add(new ActionViewModel { Icon = Icons.Trash, Name = "Delete Module", Action = async (u, m) => await DeleteModule(u, m) }); actionList.Add(new ActionViewModel { Icon = Icons.Trash, Name = "Delete Module", Action = async (u, m) => await DeleteModule(u, m) });
if (ModuleState.ModuleDefinition != null && ModuleState.ModuleDefinition.ServerManagerType != "") if (ModuleState.ModuleDefinition != null && ModuleState.ModuleDefinition.IsPortable)
{ {
actionList.Add(new ActionViewModel { Name = "" }); actionList.Add(new ActionViewModel { Name = "" });
actionList.Add(new ActionViewModel { Icon = Icons.CloudUpload, Name = "Import Content", Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Import") }); actionList.Add(new ActionViewModel { Icon = Icons.CloudUpload, Name = "Import Content", Action = async (u, m) => await EditUrlAsync(u, m.ModuleId, "Import") });
@ -93,7 +93,7 @@ namespace Oqtane.Themes.Controls
protected async Task ModuleAction(ActionViewModel action) protected async Task ModuleAction(ActionViewModel action)
{ {
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.Permissions)) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList))
{ {
PageModule pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); PageModule pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
@ -136,36 +136,32 @@ namespace Oqtane.Themes.Controls
private async Task<string> Publish(string url, PageModule pagemodule) private async Task<string> Publish(string url, PageModule pagemodule)
{ {
var permissions = UserSecurity.GetPermissionStrings(pagemodule.Module.Permissions); var permissions = pagemodule.Module.PermissionList;
foreach (var permissionstring in permissions) if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
{ {
if (permissionstring.PermissionName == PermissionNames.View) permissions.Add(new Permission(ModuleState.SiteId, EntityNames.Page, pagemodule.PageId, PermissionNames.View, RoleNames.Everyone, null, true));
}
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{ {
List<string> ids = permissionstring.Permissions.Split(';').ToList(); permissions.Add(new Permission(ModuleState.SiteId, EntityNames.Page, pagemodule.PageId, PermissionNames.View, RoleNames.Registered, null, true));
if (!ids.Contains(RoleNames.Everyone)) ids.Add(RoleNames.Everyone);
if (!ids.Contains(RoleNames.Registered)) ids.Add(RoleNames.Registered);
permissionstring.Permissions = string.Join(";", ids.ToArray());
} }
} pagemodule.Module.PermissionList = permissions;
pagemodule.Module.Permissions = UserSecurity.SetPermissionStrings(permissions);
await ModuleService.UpdateModuleAsync(pagemodule.Module); await ModuleService.UpdateModuleAsync(pagemodule.Module);
return url; return url;
} }
private async Task<string> Unpublish(string url, PageModule pagemodule) private async Task<string> Unpublish(string url, PageModule pagemodule)
{ {
var permissions = UserSecurity.GetPermissionStrings(pagemodule.Module.Permissions); var permissions = pagemodule.Module.PermissionList;
foreach (var permissionstring in permissions) if (permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
{ {
if (permissionstring.PermissionName == PermissionNames.View) permissions.Remove(permissions.First(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone));
}
if (permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{ {
List<string> ids = permissionstring.Permissions.Split(';').ToList(); permissions.Remove(permissions.First(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered));
ids.Remove(RoleNames.Everyone);
ids.Remove(RoleNames.Registered);
permissionstring.Permissions = string.Join(";", ids.ToArray());
} }
} pagemodule.Module.PermissionList = permissions;
pagemodule.Module.Permissions = UserSecurity.SetPermissionStrings(permissions);
await ModuleService.UpdateModuleAsync(pagemodule.Module); await ModuleService.UpdateModuleAsync(pagemodule.Module);
return url; return url;
} }

View File

@ -33,7 +33,7 @@
} }
} }
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions)) @if (_canViewAdminDashboard || UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
<button type="button" class="btn @ButtonClass" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel"> <button type="button" class="btn @ButtonClass" data-bs-toggle="offcanvas" data-bs-target="#offcanvasControlPanel" aria-controls="offcanvasControlPanel">
<span class="oi oi-cog"></span> <span class="oi oi-cog"></span>
@ -46,16 +46,17 @@
</div> </div>
<div class="@BodyClass"> <div class="@BodyClass">
<div class="container-fluid"> <div class="container-fluid">
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) @if (_canViewAdminDashboard)
{ {
<div class="row d-flex"> <div class="row d-flex">
<div class="col"> <div class="col">
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button> <button type="button" data-bs-dismiss="offcanvas" class="btn btn-primary col-12" @onclick=@(async () => Navigate("Admin"))>@Localizer["AdminDash"]</button>
</div> </div>
</div> </div>
<hr class="app-rule" /> <hr class="app-rule" />
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<label class="control-label">@Localizer["Page.Manage"] </label> <label class="control-label">@Localizer["Page.Manage"] </label>
@ -70,7 +71,7 @@
</div> </div>
<div class="row d-flex"> <div class="row d-flex">
<div class="col"> <div class="col">
@if (UserSecurity.ContainsRole(PageState.Page.Permissions, PermissionNames.View, RoleNames.Everyone)) @if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone))
{ {
<button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button> <button type="button" class="btn btn-secondary col-12" @onclick=@(async () => Publish("unpublish"))>@Localizer["Page.Unpublish"]</button>
} }
@ -80,7 +81,7 @@
} }
</div> </div>
</div> </div>
} <hr class="app-rule" />
@if (_deleteConfirmation) @if (_deleteConfirmation)
{ {
@ -104,7 +105,10 @@
</div> </div>
</div> </div>
} }
<hr class="app-rule" /> }
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{
<div class="row"> <div class="row">
<div class="col text-center"> <div class="col text-center">
<label for="Module" class="control-label">@Localizer["Module.Manage"] </label> <label for="Module" class="control-label">@Localizer["Module.Manage"] </label>
@ -140,7 +144,7 @@
} }
@foreach (var moduledefinition in _moduleDefinitions) @foreach (var moduledefinition in _moduleDefinitions)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, moduledefinition.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Utilize, moduledefinition.PermissionList))
{ {
if (moduledefinition.Runtimes == "" || moduledefinition.Runtimes.Contains(PageState.Runtime.ToString())) if (moduledefinition.Runtimes == "" || moduledefinition.Runtimes.Contains(PageState.Runtime.ToString()))
{ {
@ -205,19 +209,21 @@
<div class="col text-center"> <div class="col text-center">
<label for="visibility" class="control-label">@Localizer["Visibility"]</label> <label for="visibility" class="control-label">@Localizer["Visibility"]</label>
<select class="form-select" @bind="@Visibility"> <select class="form-select" @bind="@Visibility">
<option value="edit" selected>@Localizer["VisibilityEdit"]</option>
<option value="view">@Localizer["VisibilityView"]</option> <option value="view">@Localizer["VisibilityView"]</option>
<option value="edit">@Localizer["VisibilityEdit"]</option>
</select> </select>
</div> </div>
</div> </div>
<button type="button" class="btn btn-primary col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button> <button type="button" class="btn btn-primary col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button>
@((MarkupString) Message) @((MarkupString)Message)
}
</div> </div>
</div> </div>
</div> </div>
} }
@code{ @code{
private bool _canViewAdminDashboard = false;
private bool _showEditMode = false; private bool _showEditMode = false;
private bool _deleteConfirmation = false; private bool _deleteConfirmation = false;
private List<string> _categories = new List<string>(); private List<string> _categories = new List<string>();
@ -265,7 +271,7 @@
protected string Title { get; private set; } = ""; protected string Title { get; private set; } = "";
protected string ContainerType { get; private set; } = ""; protected string ContainerType { get; private set; } = "";
protected string Visibility { get; private set; } = "edit"; protected string Visibility { get; private set; } = "view";
protected string Message { get; private set; } = ""; protected string Message { get; private set; } = "";
[Parameter] [Parameter]
@ -286,15 +292,16 @@
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
_canViewAdminDashboard = CanViewAdminDashboard();
_showEditMode = false; _showEditMode = false;
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
_showEditMode = true; _showEditMode = true;
_pages?.Clear(); _pages?.Clear();
foreach (Page p in PageState.Pages) foreach (Page p in PageState.Pages)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
_pages.Add(p); _pages.Add(p);
} }
@ -312,7 +319,7 @@
{ {
foreach (var module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId)) foreach (var module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId))
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.PermissionList))
{ {
_showEditMode = true; _showEditMode = true;
break; break;
@ -321,6 +328,22 @@
} }
} }
private bool CanViewAdminDashboard()
{
var admin = PageState.Pages.FirstOrDefault(item => item.Path == "admin");
if (admin != null)
{
foreach (var page in PageState.Pages.Where(item => item.ParentId == admin?.PageId))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList))
{
return true;
}
}
}
return false;
}
private void CategoryChanged(ChangeEventArgs e) private void CategoryChanged(ChangeEventArgs e)
{ {
Category = (string)e.Value; Category = (string)e.Value;
@ -348,7 +371,7 @@
{ {
_modules = PageState.Modules _modules = PageState.Modules
.Where(module => module.PageId == int.Parse(PageId) && .Where(module => module.PageId == int.Parse(PageId) &&
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions)) UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList))
.ToList(); .ToList();
} }
ModuleId = "-"; ModuleId = "-";
@ -357,7 +380,7 @@
private async Task AddModule() private async Task AddModule()
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
if ((ModuleType == "new" && ModuleDefinitionName != "-") || (ModuleType != "new" && ModuleId != "-")) if ((ModuleType == "new" && ModuleDefinitionName != "-") || (ModuleType != "new" && ModuleId != "-"))
{ {
@ -369,18 +392,20 @@
module.ModuleDefinitionName = ModuleDefinitionName; module.ModuleDefinitionName = ModuleDefinitionName;
module.AllPages = false; module.AllPages = false;
List<PermissionString> permissions = UserSecurity.GetPermissionStrings(PageState.Page.Permissions); var permissions = new List<Permission>();
if (Visibility == "view") if (Visibility == "view")
{ {
// set module view permissions to page view permissions // set module view permissions to page view permissions
permissions.Find(p => p.PermissionName == PermissionNames.View).Permissions = permissions.Find(p => p.PermissionName == PermissionNames.View).Permissions; permissions = SetPermissions(permissions, module.SiteId, PermissionNames.View, PermissionNames.View);
} }
else else
{ {
// set module view permissions to page edit permissions // set module view permissions to page edit permissions
permissions.Find(p => p.PermissionName == PermissionNames.View).Permissions = permissions.Find(p => p.PermissionName == PermissionNames.Edit).Permissions; permissions = SetPermissions(permissions, module.SiteId, PermissionNames.View, PermissionNames.Edit);
} }
module.Permissions = UserSecurity.SetPermissionStrings(permissions); // set module edit permissions to page edit permissions
permissions = SetPermissions(permissions, module.SiteId, PermissionNames.Edit, PermissionNames.Edit);
module.PermissionList = permissions;
module = await ModuleService.AddModuleAsync(module); module = await ModuleService.AddModuleAsync(module);
ModuleId = module.ModuleId.ToString(); ModuleId = module.ModuleId.ToString();
@ -431,6 +456,15 @@
} }
} }
private List<Permission> SetPermissions(List<Permission> permissions, int siteId, string modulePermission, string pagePermission)
{
foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission))
{
permissions.Add(new Permission { SiteId = siteId, EntityName = EntityNames.Module, PermissionName = modulePermission, RoleId = permission.RoleId, UserId = permission.UserId, IsAuthorized = permission.IsAuthorized });
}
return permissions;
}
private async Task ToggleEditMode(bool EditMode) private async Task ToggleEditMode(bool EditMode)
{ {
if (_showEditMode) if (_showEditMode)
@ -444,7 +478,7 @@
PageState.EditMode = true; PageState.EditMode = true;
} }
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, "edit=" + ((PageState.EditMode) ? "1" : "0"))); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, "edit=" + ((PageState.EditMode) ? "true" : "false")));
} }
else else
{ {
@ -452,7 +486,7 @@
{ {
await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId); await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
PageState.EditMode = true; PageState.EditMode = true;
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, "edit=" + ((PageState.EditMode) ? "1" : "0"))); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, "edit=" + ((PageState.EditMode) ? "true" : "false")));
} }
} }
} }
@ -465,12 +499,10 @@
case "Admin": case "Admin":
// get admin dashboard moduleid // get admin dashboard moduleid
module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule); module = PageState.Modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule);
if (module != null) if (module != null)
{ {
NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, module.ModuleId, "Index", "")); NavigationManager.NavigateTo(EditUrl(PageState.Page.Path, module.ModuleId, "Index", ""));
} }
break; break;
case "Add": case "Add":
case "Edit": case "Edit":
@ -502,34 +534,19 @@
private async void Publish(string action) private async void Publish(string action)
{ {
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{ {
List<PermissionString> permissions; var permissions = PageState.Page.PermissionList;
if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Everyone))
// publish/unpublish page
var page = PageState.Page;
permissions = UserSecurity.GetPermissionStrings(page.Permissions);
foreach (var permissionstring in permissions)
{ {
if (permissionstring.PermissionName == PermissionNames.View) permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Everyone, null, true));
{
List<string> ids = permissionstring.Permissions.Split(';').ToList();
switch (action)
{
case "publish":
if (!ids.Contains(RoleNames.Everyone)) ids.Add(RoleNames.Everyone);
if (!ids.Contains(RoleNames.Registered)) ids.Add(RoleNames.Registered);
break;
case "unpublish":
ids.Remove(RoleNames.Everyone);
ids.Remove(RoleNames.Registered);
break;
} }
permissionstring.Permissions = string.Join(";", ids.ToArray()); if (!permissions.Any(item => item.PermissionName == PermissionNames.View && item.RoleName == RoleNames.Registered))
{
permissions.Add(new Permission(PageState.Site.SiteId, EntityNames.Page, PageState.Page.PageId, PermissionNames.View, RoleNames.Registered, null, true));
} }
} PageState.Page.PermissionList = permissions;
page.Permissions = UserSecurity.SetPermissionStrings(permissions); await PageService.UpdatePageAsync(PageState.Page);
await PageService.UpdatePageAsync(page);
NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true));
} }
} }
@ -551,19 +568,19 @@
{ {
page.IsDeleted = true; page.IsDeleted = true;
await PageService.UpdatePageAsync(page); await PageService.UpdatePageAsync(page);
await logger.Log(page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page); await logger.Log(page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page);
NavigationManager.NavigateTo(NavigateUrl("")); NavigationManager.NavigateTo(NavigateUrl(""));
} }
else // personalized page else // personalized page
{ {
await PageService.DeletePageAsync(page.PageId); await PageService.DeletePageAsync(page.PageId);
await logger.Log(page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page); await logger.Log(page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, null, "Page Deleted {Page}", page);
NavigationManager.NavigateTo(NavigateUrl()); NavigationManager.NavigateTo(NavigateUrl());
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.Log(page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, ex, "Page Deleted {Page} {Error}", page, ex.Message); await logger.Log(page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "ControlPanel", LogFunction.Delete, LogLevel.Information, ex, "Page Deleted {Page} {Error}", page, ex.Message);
} }
} }
@ -596,8 +613,8 @@
private async Task UpdateSettingsAsync() private async Task UpdateSettingsAsync()
{ {
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId); Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
SettingService.SetSetting(settings, settingCategory, _category); settings = SettingService.SetSetting(settings, settingCategory, _category);
SettingService.SetSetting(settings, settingPane, _pane); settings = SettingService.SetSetting(settings, settingPane, _pane);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId); await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
} }

View File

@ -1,8 +1,10 @@
using System; using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Models;
using Oqtane.Providers; using Oqtane.Providers;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services; using Oqtane.Services;
@ -22,21 +24,19 @@ namespace Oqtane.Themes.Controls
protected void LoginUser() protected void LoginUser()
{ {
var returnurl = PageState.Alias.Path; Route route = new Route(PageState.Uri.AbsoluteUri, PageState.Alias.Path);
if (PageState.Page.Path != "/") NavigationManager.NavigateTo(NavigateUrl("login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery)));
{
returnurl += "/" + PageState.Page.Path;
}
NavigationManager.NavigateTo(NavigateUrl("login", "?returnurl=" + returnurl));
} }
protected async Task LogoutUser() protected async Task LogoutUser()
{ {
await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User.Username); await LoggingService.Log(PageState.Alias, PageState.Page.PageId, null, PageState.User?.UserId, GetType().AssemblyQualifiedName, "Logout", LogFunction.Security, LogLevel.Information, null, "User Logout For Username {Username}", PageState.User?.Username);
// check if anonymous user can access page Route route = new Route(PageState.Uri.AbsoluteUri, PageState.Alias.Path);
var url = PageState.Alias.Path + "/" + PageState.Page.Path; var url = route.PathAndQuery;
if (!UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.Permissions))
// verify if anonymous users can access page
if (!UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList))
{ {
url = PageState.Alias.Path; url = PageState.Alias.Path;
} }

View File

@ -32,7 +32,7 @@ namespace Oqtane.Themes.Controls
var securityLevel = int.MaxValue; var securityLevel = int.MaxValue;
foreach (Page p in PageState.Pages.Where(item => item.IsNavigation)) foreach (Page p in PageState.Pages.Where(item => item.IsNavigation))
{ {
if (p.Level <= securityLevel && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.Permissions)) if (p.Level <= securityLevel && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{ {
securityLevel = int.MaxValue; securityLevel = int.MaxValue;
yield return p; yield return p;

View File

@ -10,7 +10,7 @@
</button> </button>
</span> </span>
<div class="app-menu navbar-expand-md"> <div class="app-menu navbar-expand-md">
<div class="collapse navbar-collapse" id="Menu"> <div class="collapse navbar-collapse navbar-nav-scroll" id="Menu">
<MenuItemsHorizontal ParentPage="null" Pages="MenuPages" /> <MenuItemsHorizontal ParentPage="null" Pages="MenuPages" />
</div> </div>
</div> </div>

View File

@ -32,15 +32,24 @@ namespace Oqtane.Themes
{ {
if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script)) if (Resources != null && Resources.Exists(item => item.ResourceType == ResourceType.Script))
{ {
var interop = new Interop(JSRuntime);
var scripts = new List<object>(); var scripts = new List<object>();
var inline = 0;
foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script)) foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script))
{
if (!string.IsNullOrEmpty(resource.Url))
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url; 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 }); scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module });
} }
else
{
inline += 1;
await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower());
}
}
if (scripts.Any()) if (scripts.Any())
{ {
var interop = new Interop(JSRuntime);
await interop.IncludeScripts(scripts.ToArray()); await interop.IncludeScripts(scripts.ToArray());
} }
} }

View File

@ -43,7 +43,7 @@
container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer; container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer;
} }
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions) && PageState.Action == Constants.DefaultAction) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction)
{ {
_useadminborder = true; _useadminborder = true;
} }

View File

@ -60,13 +60,13 @@ namespace Oqtane.UI
} }
} }
public Task IncludeMeta(string id, string attribute, string name, string content, string key) public Task IncludeMeta(string id, string attribute, string name, string content)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( _jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.includeMeta", "Oqtane.Interop.includeMeta",
id, attribute, name, content, key); id, attribute, name, content);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch
@ -105,6 +105,7 @@ namespace Oqtane.UI
} }
} }
// external scripts need to specify src, inline scripts need to specify id and content
public Task IncludeScript(string id, string src, string integrity, string crossorigin, string content, string location) public Task IncludeScript(string id, string src, string integrity, string crossorigin, string content, string location)
{ {
try try

View File

@ -87,10 +87,9 @@ else
// retrieve friendly localized error // retrieve friendly localized error
_error = Localizer["Error.Module.Exception"]; _error = Localizer["Error.Module.Exception"];
// log error // log error
int? userId = (PageState.User != null) ? PageState.User.UserId : null;
string category = GetType().AssemblyQualifiedName; string category = GetType().AssemblyQualifiedName;
string feature = Utilities.GetTypeNameLastSegment(category, 1); string feature = Utilities.GetTypeNameLastSegment(category, 1);
await LoggingService.Log(null, ModuleState.PageId, ModuleState.ModuleId, userId, category, feature, LogFunction.Other, LogLevel.Error, exception, "An Unexpected Error Has Occurred In {ModuleDefinitionName}: {Error}", ModuleState.ModuleDefinitionName, exception.Message); await LoggingService.Log(null, ModuleState.PageId, ModuleState.ModuleId, PageState.User?.UserId, category, feature, LogFunction.Other, LogLevel.Error, exception, "An Unexpected Error Has Occurred In {ModuleDefinitionName}: {Error}", ModuleState.ModuleDefinitionName, exception.Message);
await base.OnErrorAsync(exception); await base.OnErrorAsync(exception);
return; return;
} }

View File

@ -30,7 +30,7 @@ else
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions) && PageState.Action == Constants.DefaultAction) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction)
{ {
_useadminborder = true; _useadminborder = true;
_panetitle = "<div class=\"app-pane-admin-title\">" + Name + " Pane</div>"; _panetitle = "<div class=\"app-pane-admin-title\">" + Name + " Pane</div>";
@ -58,7 +58,11 @@ else
} }
if (Name.ToLower() == pane.ToLower()) if (Name.ToLower() == pane.ToLower())
{ {
Module module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId); Module module = PageState.Modules.FirstOrDefault(item => item.PageId == PageState.Page.PageId && item.ModuleId == PageState.ModuleId);
if (module == null)
{
module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId);
}
if (module != null) if (module != null)
{ {
var moduleType = Type.GetType(module.ModuleType); var moduleType = Type.GetType(module.ModuleType);
@ -67,7 +71,7 @@ else
bool authorized = false; bool authorized = false;
if (Constants.DefaultModuleActions.Contains(PageState.Action)) if (Constants.DefaultModuleActions.Contains(PageState.Action))
{ {
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList);
} }
else else
{ {
@ -77,10 +81,10 @@ else
authorized = true; authorized = true;
break; break;
case SecurityAccessLevel.View: case SecurityAccessLevel.View:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList);
break; break;
case SecurityAccessLevel.Edit: case SecurityAccessLevel.Edit:
authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.Permissions); authorized = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, module.PermissionList);
break; break;
case SecurityAccessLevel.Admin: case SecurityAccessLevel.Admin:
authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin); authorized = UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin);
@ -107,11 +111,15 @@ else
{ {
if (PageState.ModuleId != -1) if (PageState.ModuleId != -1)
{ {
Module module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId); Module module = PageState.Modules.FirstOrDefault(item => item.PageId == PageState.Page.PageId && item.ModuleId == PageState.ModuleId);
if (module == null)
{
module = PageState.Modules.FirstOrDefault(item => item.ModuleId == PageState.ModuleId);
}
if (module != null && module.Pane.ToLower() == Name.ToLower()) if (module != null && module.Pane.ToLower() == Name.ToLower())
{ {
// check if user is authorized to view module // check if user is authorized to view module
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList))
{ {
CreateComponent(builder, module); CreateComponent(builder, module);
} }
@ -119,10 +127,10 @@ else
} }
else else
{ {
foreach (Module module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId && item.Pane.ToLower() == Name.ToLower()).OrderBy(x => x.Order).ToArray()) foreach (Module module in PageState.Modules.Where(item => item.PageId == PageState.Page.PageId && item.Pane.ToLower() == Name.ToLower()))
{ {
// check if user is authorized to view module // check if user is authorized to view module
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.Permissions)) if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList))
{ {
CreateComponent(builder, module); CreateComponent(builder, module);
} }

View File

@ -97,7 +97,7 @@
// reload the client application from the server if there is a forced reload or the user navigated to a site with a different alias // reload the client application from the server if there is a forced reload or the user navigated to a site with a different alias
if (querystring.ContainsKey("reload") || (!NavigationManager.ToBaseRelativePath(_absoluteUri).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path))) if (querystring.ContainsKey("reload") || (!NavigationManager.ToBaseRelativePath(_absoluteUri).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
{ {
if (querystring["reload"] == "post") if (querystring.ContainsKey("reload") && querystring["reload"] == "post")
{ {
// post back so that the cookies are set correctly - required on any change to the principal // post back so that the cookies are set correctly - required on any change to the principal
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
@ -124,6 +124,10 @@
editmode = PageState.EditMode; editmode = PageState.EditMode;
lastsyncdate = PageState.LastSyncDate; lastsyncdate = PageState.LastSyncDate;
} }
if (PageState?.Page.Path != route.PagePath)
{
editmode = false; // reset edit mode when navigating to different page
}
// get user // get user
if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId) if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
@ -181,7 +185,6 @@
if (PageState == null || refresh || PageState.Page.Path != route.PagePath) if (PageState == null || refresh || PageState.Page.Path != route.PagePath)
{ {
page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
editmode = false;
} }
else else
{ {
@ -204,7 +207,7 @@
if (page != null) if (page != null)
{ {
// check if user is authorized to view page // check if user is authorized to view page
if (UserSecurity.IsAuthorized(user, PermissionNames.View, page.Permissions)) if (UserSecurity.IsAuthorized(user, PermissionNames.View, page.PermissionList))
{ {
// load additional metadata for current page // load additional metadata for current page
page = await ProcessPage(page, site, user); page = await ProcessPage(page, site, user);
@ -250,7 +253,7 @@
if (user == null) if (user == null)
{ {
// redirect to login page if user not logged in as they may need to be authenticated // redirect to login page if user not logged in as they may need to be authenticated
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + route.AbsolutePath)); NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(route.PathAndQuery)));
} }
else else
{ {
@ -299,22 +302,28 @@
{ {
query = query.Substring(1); // ignore "?" query = query.Substring(1); // ignore "?"
} }
foreach (string kvp in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string kvp in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
{ {
if (kvp != "") if (kvp != "")
{ {
if (kvp.Contains("=")) if (kvp.Contains("="))
{ {
string[] pair = kvp.Split('='); string[] pair = kvp.Split('=');
if (!querystring.ContainsKey(pair[0]))
{
querystring.Add(pair[0], pair[1]); querystring.Add(pair[0], pair[1]);
} }
}
else else
{
if (!querystring.ContainsKey(kvp))
{ {
querystring.Add(kvp, "true"); // default parameter when no value is provided querystring.Add(kvp, "true"); // default parameter when no value is provided
} }
} }
} }
} }
}
return querystring; return querystring;
} }
@ -358,7 +367,7 @@
} }
if (!string.IsNullOrEmpty(panes)) if (!string.IsNullOrEmpty(panes))
{ {
page.Panes = panes.Replace(";", ",").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); page.Panes = panes.Replace(";", ",").Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
if (!page.Panes.Contains(PaneNames.Default) && !page.Panes.Contains(PaneNames.Admin)) if (!page.Panes.Contains(PaneNames.Default) && !page.Panes.Contains(PaneNames.Admin))
{ {
_error = "The Current Theme Does Not Contain A Default Or Admin Pane"; _error = "The Current Theme Does Not Contain A Default Or Admin Pane";
@ -407,7 +416,7 @@
// check if the module defines custom action routes // check if the module defines custom action routes
if (module.ModuleDefinition.ControlTypeRoutes != "") if (module.ModuleDefinition.ControlTypeRoutes != "")
{ {
foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(';', StringSplitOptions.RemoveEmptyEntries))
{ {
if (route.StartsWith(action + "=")) if (route.StartsWith(action + "="))
{ {

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.2.1</Version> <Version>3.4.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Database.MySQL</id> <id>Oqtane.Database.MySQL</id>
<version>3.2.1</version> <version>3.4.3</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane MySQL Provider</title> <title>Oqtane MySQL Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.1</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3</releaseNotes>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Version>3.2.1</Version> <Version>3.4.3</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

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