Compare commits

..

167 Commits

Author SHA1 Message Date
8b80f72313 Merge pull request #3386 from oqtane/master
4.0.6 Release
2023-10-16 15:00:27 -04:00
06ebce31ca Merge pull request #3385 from oqtane/dev
4.0.6 Release
2023-10-16 14:59:59 -04:00
59db8ba997 Merge pull request #3383 from sbwalker/dev
4.0.6 data provider packages
2023-10-16 09:08:17 -04:00
df37d4aa7f 4.0.6 data provider packages 2023-10-16 09:08:03 -04:00
fd9935c800 Merge pull request #3382 from sbwalker/dev
prepare for 4.0.6 release
2023-10-16 08:43:42 -04:00
c057430488 prepare for 4.0.6 release 2023-10-16 08:43:31 -04:00
767fa78e22 Merge pull request #3380 from sbwalker/dev
resolve issue in making Pager search work with sorting
2023-10-16 08:03:42 -04:00
a0e289dcd6 resolve issue in making Pager search work with sorting 2023-10-16 08:03:31 -04:00
c62d147254 Merge pull request #3375 from leigh-pointer/AllowPunctuation
Module Creator allow punctuation in Description field
2023-10-13 08:46:15 -04:00
f739bee353 Merge pull request #3376 from leigh-pointer/BootswatchThemeIssue
Fix for #3374 Backward compatability
2023-10-13 08:46:03 -04:00
db4dfb69fb Merge pull request #3377 from sbwalker/dev
improve Pager search to support properties on child objects
2023-10-13 08:39:23 -04:00
9729a5ef16 improve Pager search to support properties on child objects 2023-10-13 08:39:02 -04:00
6f12818352 Fix for #3374 Backward compatability
Fixes the issue in the Language selector.
2023-10-13 14:06:21 +02:00
75fc318da0 Module Creator all punctuation in description
Modified the Regex to all punctuation in the description.
2023-10-13 11:50:00 +02:00
acf71cc2c2 Merge pull request #3370 from leigh-pointer/LanguageAlignment
Language Selector Alignment fix  #3368
2023-10-12 11:10:47 -04:00
42efe10473 Merge pull request #3372 from sbwalker/dev
optimize Pager search to remove redundant AllowSearch parameter and avoid consuming memory when not in use
2023-10-12 11:10:35 -04:00
b77e72880b optimize Pager search to remove redundant AllowSearch parameter and avoid consuming memory when not in use 2023-10-12 11:10:14 -04:00
297c1524b4 Language Selector Alignment fix #3368
Themes fixed also
2023-10-11 15:48:05 +02:00
6140743769 Merge pull request #3352 from Rodien/dev
Introduce a dropdown menu for authorization response types
2023-10-11 09:08:48 -04:00
d5ca700828 Merge pull request #3366 from sbwalker/dev
change name of RESX key and value to reflect purpose
2023-10-10 10:33:39 -04:00
a7e1fe76c3 change name of RESX key and value to reflect purpose 2023-10-10 10:33:11 -04:00
f6c55279d1 Merge pull request #3364 from leigh-pointer/PagerSearch
Pager Search Localization
2023-10-10 10:25:47 -04:00
826f4835bb Merge branch 'dev' into PagerSearch 2023-10-10 10:25:37 -04:00
119a28def1 Pager Search Localization
Localization is now referencing the Shared Resources
2023-10-10 14:33:16 +02:00
c1ffb8bc33 Merge pull request #3365 from sbwalker/dev
use SharedLocalizer in Pager
2023-10-10 08:07:21 -04:00
60cc9d14af use SharedLocalizer in Pager 2023-10-10 08:07:07 -04:00
7cf4f8fdaa Pager Search Localization
Localized the new Search Buttons
Added a Localized Placeholder that will display what columns are being searched.
2023-10-10 11:28:57 +02:00
1b84e83061 Merge pull request #3363 from sbwalker/dev
add Search capability to Pager and include in management UIs
2023-10-09 16:59:54 -04:00
b0d2ee8760 add Search capability to Pager and include in management UIs 2023-10-09 16:59:36 -04:00
2022432d35 Merge pull request #3362 from sbwalker/dev
Add Rows option to Profile Management. Improve Profile validation feedback. Fix Add User so that profile Options are supported.
2023-10-09 14:27:24 -04:00
c0ed335d84 Add Rows option to Profile Management. Improve Profile validation feedback. Fix Add User so that profile Options are supported. 2023-10-09 14:26:56 -04:00
4964562866 Merge pull request #3361 from sbwalker/dev
include Pane in Module Settings
2023-10-09 13:19:00 -04:00
f78dc443ad include Pane in Module Settings 2023-10-09 13:18:43 -04:00
56b47db778 Merge pull request #3360 from sbwalker/dev
allow module to be inserted at top of pane
2023-10-09 12:58:15 -04:00
2d4bf17b28 allow module to be inserted at top of pane 2023-10-09 12:58:01 -04:00
3b1819c68d Merge pull request #3359 from sbwalker/dev
optimize UrlCombine method
2023-10-09 12:00:51 -04:00
575bbdb53b optimize UrlCombine method 2023-10-09 12:00:34 -04:00
2fa7482028 Introduce a dropdown menu in the 'External Login' settings area for authentication flow response types. 2023-10-03 09:00:53 +02:00
e5062317e2 Merge pull request #3347 from sbwalker/dev
do not show uploaded file name or delete button when multiple uploads are enabled
2023-09-29 11:49:05 -04:00
543f4fa3c2 do not show uploaded file name or delete button when multiple uploads are enabled 2023-09-29 11:48:45 -04:00
49cdf69815 Merge pull request #3344 from Mostafa-Moafi/dev
Fixed issue #3282
2023-09-28 15:36:00 -04:00
04196a1a19 Merge pull request #3346 from sbwalker/dev
display uploaded file name in FileManager when ShowFiles = false
2023-09-28 15:35:26 -04:00
b8622b8708 display uploaded file name in FileManager when ShowFiles = false 2023-09-28 15:35:13 -04:00
a2feca0ba3 Fixed Bug #3282 2023-09-28 19:23:31 +03:30
ae6c6a75b2 Merge pull request #3342 from sbwalker/dev
style page names in Admin Dashboard so that they are always visible in Bootstrap themes
2023-09-28 07:51:05 -04:00
bd987e9531 style page names in Admin Dashboard so that they are always visible in Bootstrap themes 2023-09-27 16:26:59 -04:00
24d69fd820 Merge pull request #3341 from sbwalker/dev
include CopyLocalLockFileAssemblies property in Client project in Default Module Template
2023-09-27 16:11:46 -04:00
9e2fa8d7f1 include CopyLocalLockFileAssemblies property in Client project in Default Module Template 2023-09-27 16:11:29 -04:00
74ba2f7283 Merge pull request #3340 from sbwalker/dev
add hyperlinks to product logos
2023-09-27 16:09:30 -04:00
7603b5c63f add hyperlinks to product logos 2023-09-27 16:09:19 -04:00
03566afe66 Merge pull request #3339 from sbwalker/dev
fix UrlCombine so that it handles leading and trailing path delimiters and returns proper paths
2023-09-27 16:07:32 -04:00
3c2e314e2d fix UrlCombine so that it handles leading and trailing path delimiters and returns proper paths 2023-09-27 16:07:13 -04:00
1fcf08b5cd Update README.md 2023-09-26 14:06:18 -04:00
c13db89ed4 Merge pull request #3330 from sbwalker/dev
4.0.5 data provider packages
2023-09-26 14:03:19 -04:00
e68ae9e8a6 4.0.5 data provider packages 2023-09-26 14:03:06 -04:00
fb252d6db3 Merge pull request #3329 from oqtane/master
4.0.5 release
2023-09-26 13:59:55 -04:00
df959353d6 Merge pull request #3328 from oqtane/dev
4.0.5 release
2023-09-26 13:59:29 -04:00
fbc443483d Merge pull request #3327 from sbwalker/dev
prepare for 4.0.5
2023-09-26 11:35:05 -04:00
00f1dbc3dd prepare for 4.0.5 2023-09-26 11:34:52 -04:00
c6021ff012 Merge pull request #3326 from ijaz-saeed/dev
Empty _themetype check to avoid page crash
2023-09-26 10:23:02 -04:00
7a8cfcee35 Empty _themetype check to avoid page crash
without this check, admin dashboard -> Page Management -> Edit page
is crashing
2023-09-26 18:23:29 +05:00
c15586a1c4 Update README.md 2023-09-25 16:00:23 -04:00
be1d124e78 Merge pull request #3322 from oqtane/master
Merge pull request #3321 from oqtane/dev
2023-09-25 15:55:54 -04:00
6fabf84d05 Merge pull request #3321 from oqtane/dev
4.0.4 release
2023-09-25 15:55:37 -04:00
3299275da2 Merge pull request #3320 from sbwalker/dev
resolve cookie configuration
2023-09-25 15:10:50 -04:00
5539243bf3 resolve cookie configuration 2023-09-25 15:10:36 -04:00
c3d330f500 Merge pull request #3319 from sbwalker/dev
refactor logic to redirect to external login provider when local site login is disabled
2023-09-25 13:53:26 -04:00
8c9e886136 refactor logic to redirect to external login provider when local site login is disabled 2023-09-25 13:53:05 -04:00
cfde9944f8 Merge pull request #3318 from sbwalker/dev
include Logout link in Control Panel for scenarios where a theme does not include a Login/Logout component
2023-09-25 12:42:59 -04:00
b35d778abe include Logout link in Control Panel for scenarios where a theme does not include a Login/Logout component 2023-09-25 12:42:39 -04:00
1e9ee81df3 Merge pull request #3317 from sbwalker/dev
updated database provider packages for 4.0.4
2023-09-25 10:01:59 -04:00
545d5730b6 database provider packages for 4.0.4 2023-09-25 09:59:07 -04:00
28e645f9ca Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2023-09-25 09:54:38 -04:00
5b5c8a4beb database provider packages for 4.0.4 2023-09-25 09:54:27 -04:00
6178be974a Merge pull request #3310 from leigh-pointer/LangSwitchPadding
upated LanguageSwitcher with end padding
2023-09-25 07:42:24 -04:00
7771c3159b upated LanguageSwitcher with end padding
When the LanguageSwitcher is dsiplayed the wsa no end padding on the button so it appeared flush with the following button.  Adding "pe-1" to the class corrects the issue
2023-09-24 18:49:50 +02:00
fc1e9c5d71 Merge pull request #3308 from sbwalker/dev
revert remaining logic which forced page themes to be a member of a site theme (4.0.0)
2023-09-23 11:47:08 -04:00
359de5b85e revert remaining logic which forced page themes to be a member of a site theme 2023-09-23 11:46:35 -04:00
68dc4904cf Merge pull request #3307 from sbwalker/dev
improve user import API
2023-09-23 11:37:43 -04:00
057fd02e26 improve user import API 2023-09-23 11:37:29 -04:00
5c86ef6682 Merge pull request #3303 from leigh-pointer/Retention-resx
Fix for  Site 'SMTP retention' is not in resource file (Issue #3301) and #3304 Users 'Import Users' resource
2023-09-23 09:12:33 -04:00
7c14bd799f Merge pull request #3305 from sbwalker/dev
improvements based on user import testing
2023-09-23 09:04:35 -04:00
edac046fcd improvements based on user import testing 2023-09-23 09:04:18 -04:00
8b23b386f7 ImportUsers Added to Resx and Razor resource key 2023-09-23 10:56:17 +02:00
e9bed59032 Fix for Site 'SMTP retention' is not in resource file (Issue #3301)
Fix for  Site 'SMTP retention' is not in resource file (Issue #3301)
2023-09-23 10:42:39 +02:00
54077be32c Merge pull request #3299 from sbwalker/dev
fix reference to DisplayName
2023-09-22 15:03:57 -04:00
30ad442dd1 fix reference to DisplayName 2023-09-22 15:03:45 -04:00
b0c677d5a7 Merge pull request #3298 from sbwalker/dev
removing href link from DisplayName as Email is now included
2023-09-22 14:44:36 -04:00
abe1b93e3c removing href link from DisplayName as Email is now included 2023-09-22 14:44:25 -04:00
fd699cbb73 Merge pull request #3293 from W6HBR/dev
Add Email column to User Manager screen
2023-09-22 14:31:56 -04:00
6f93ca11de Merge pull request #3297 from sbwalker/dev
fix issue where module migrations were not being executed on upgrade due to version not being overridden
2023-09-22 14:12:36 -04:00
916663b493 fix issue where module migrations were not being executed on upgrade due to version not being overridden 2023-09-22 14:12:18 -04:00
dbf2cddd87 Update Index.razor to include Email column. 2023-09-21 16:58:48 -07:00
d3c248cf5c Update Index.resx for Email text. 2023-09-21 16:57:01 -07:00
bd93a94bb7 Merge pull request #3292 from thabaum/patch-6
Fix typo in app.css
2023-09-21 19:42:02 -04:00
a46836e2a6 Fix typo in app.css 2023-09-21 16:34:57 -07:00
a1d88b4faa Merge pull request #3290 from sbwalker/dev
add named site option support to factory
2023-09-21 17:17:59 -04:00
29741c0ec8 add named site option support to factory 2023-09-21 17:17:46 -04:00
0b5fb92452 Merge pull request #3289 from sbwalker/dev
add support for named site options
2023-09-21 16:54:04 -04:00
65782e87c1 add support for named site options 2023-09-21 16:53:52 -04:00
6403df3330 Merge pull request #3288 from sbwalker/dev
set DefaultScheme for authentication
2023-09-21 14:45:09 -04:00
c6a8f5305a set DefaultScheme for authentication 2023-09-21 14:44:57 -04:00
d354e63267 Merge pull request #3285 from sbwalker/dev
fix localization in Profile Management
2023-09-21 10:02:36 -04:00
836b174d15 fix localization in Profile Management 2023-09-21 10:02:21 -04:00
4435f25ee6 Merge pull request #3275 from W6HBR/dev
Update to Profiles field list to include Title, Category and ViewOrder
2023-09-21 09:57:55 -04:00
4ab4bf4a3a Merge pull request #3279 from thabaum/patch-5
Updates Child Page Link Color in Theme Creator Template - Theme.css - Fixes #3278
2023-09-21 09:57:30 -04:00
d2f1d3c86c Merge pull request #3283 from sbwalker/dev
user import improvements
2023-09-21 09:37:44 -04:00
98257de005 user import improvements 2023-09-21 09:37:29 -04:00
5ea5c6ff7d Updates Child Page Link Color in Theme.css 2023-09-20 17:12:04 -07:00
441324d354 Update to Profiles field list to include Title, Category and ViewOrder
When adding additional profile fields, this enhancement makes it easier to see details of the profile fields for the purpose of field organization.
2023-09-20 14:59:44 -07:00
44f093b2fa Merge pull request #3266 from W6HBR/W6HBR-patch-1
Add password complexity requirements message to password Reset module.
2023-09-20 15:19:03 -04:00
38a16d1ae2 Merge pull request #3273 from sbwalker/dev
prepare for 4.0.4 release
2023-09-20 14:59:10 -04:00
6b9de40442 prepare for 4.0.4 release 2023-09-20 14:58:54 -04:00
d9c9eb9799 Merge pull request #3258 from leigh-pointer/ViewNotification
Removed the ReadOnly attribute in the Notifications Reply TextArea
2023-09-20 14:32:41 -04:00
e88c8eee0f Merge pull request #3271 from sbwalker/dev
fix #3231 - validate module description
2023-09-20 14:29:21 -04:00
a6ca843ced fix #3231 - validate module description 2023-09-20 14:29:06 -04:00
6967a5cc14 Merge pull request #3270 from sbwalker/dev
if site login is disabled redirect Login to external identity provider
2023-09-20 13:45:34 -04:00
e0ebf70907 if site login is disabled redirect Login to external identity provider 2023-09-20 13:45:19 -04:00
53d0202f3b Merge pull request #3268 from sbwalker/dev
prevent System Update in development environment
2023-09-20 12:34:20 -04:00
8daf38654d prevent System Update in development environment 2023-09-20 12:33:43 -04:00
17337440f2 Merge pull request #3267 from sbwalker/dev
add ability to import users
2023-09-20 10:44:05 -04:00
8e5e79a799 add ability to import users 2023-09-20 10:43:49 -04:00
5c1cf14303 Add password complexity requirements message to password Reset module.
This adds the same functionality that already exists in the UserProfile module.
2023-09-19 18:59:38 -07:00
f2ad796104 Merge pull request #3263 from sbwalker/dev
adjust error message text when adding users with duplicate email addresses
2023-09-19 09:00:11 -04:00
886df69058 adjust error message text when adding users with duplicate email addresses 2023-09-19 08:59:53 -04:00
703aa95e60 Merge pull request #3262 from sbwalker/dev
fix error when Allow User Login is set to false (ElementReference has not been configured correctly)
2023-09-19 08:49:21 -04:00
7919e14d90 fix error when Allow User Login is set to false (ElementReference has not been configured correctly) 2023-09-19 08:49:03 -04:00
c8ef191660 Merge pull request #3261 from sbwalker/dev
remove using statements added by Visual Studio
2023-09-18 09:47:52 -04:00
840b656d1c remove using statements added by Visual Studio 2023-09-18 09:47:38 -04:00
d28516b6b1 Merge pull request #3260 from sbwalker/dev
fix issue with module order in panes caused by transition from Admin to Default pane naming
2023-09-18 09:46:40 -04:00
5797bf549c fix issue with module order in panes caused by transition from Admin to Default pane naming 2023-09-18 09:46:16 -04:00
e1ea721ea9 Removed the ReadOnly attribute
The replay textarea can now be used to enter a reply to the notification.
2023-09-16 18:35:22 +02:00
5ed1284baf Merge pull request #3257 from sbwalker/dev
fix #3255 - behavior when moving pages to other parents
2023-09-15 16:23:28 -04:00
e507023a03 fix #3255 - behavior when moving pages to other parents 2023-09-15 16:23:14 -04:00
2b328b9bb2 Merge pull request #3254 from sbwalker/dev
fix #3253 - login needs to validate User.IsDeleted property
2023-09-13 10:02:27 -04:00
d155e13399 fix #3253 - login needs to validate User.IsDeleted property 2023-09-13 10:02:11 -04:00
6993d4782d Merge pull request #3244 from sbwalker/dev
improve polling in file upload to use actual file sizes to calculate duration
2023-09-08 17:16:28 -04:00
9267efce01 improve polling in file upload to use actual file sizes to calculate duration 2023-09-08 17:16:09 -04:00
8c88cec863 Merge pull request #3242 from sbwalker/dev
retain querystring parameters on url mapping redirect
2023-09-08 12:05:35 -04:00
b4b9976567 retain querystring parameters on url mapping redirect 2023-09-08 12:05:20 -04:00
3cbd820d9c Merge pull request #3240 from sbwalker/dev
fix #3236 - script injection issue
2023-09-08 11:25:40 -04:00
edd77b0222 fix #3236 - script injection issue 2023-09-08 11:25:27 -04:00
c0991df9a2 Merge pull request #3239 from sbwalker/dev
fix #3235 - </script> not being removed from Head Content
2023-09-08 11:09:46 -04:00
26921c899e fix #3235 - </script> not being removed from Head Content 2023-09-08 11:09:07 -04:00
a1e5912d37 Merge pull request #3238 from sbwalker/dev
fix #3229 - changing page path in Edot Page when invoked from Control Panel results in 404 when redirecting
2023-09-08 10:35:52 -04:00
037f1ec887 fix #3229 - changing page path in Edot Page when invoked from Control Panel results in 404 when redirecting 2023-09-08 10:35:22 -04:00
89ab44590b Merge pull request #3224 from sbwalker/dev
capitalize
2023-09-05 11:28:52 -04:00
4a20be8b34 capitalize 2023-09-05 11:28:36 -04:00
32a401ba67 Merge pull request #3223 from sbwalker/dev
display Site Guid in Site Settings
2023-09-05 11:27:30 -04:00
138c01aef8 display Site Guid in Site Settings 2023-09-05 11:27:17 -04:00
3e54bc9c77 Merge pull request #3221 from sbwalker/dev
add ability to validate and download packages
2023-09-02 14:08:34 -04:00
9966fc4651 add ability to validate and download packages 2023-09-02 14:08:21 -04:00
c9ed5ec98f Merge pull request #3220 from sbwalker/dev
improve help text for paclage name
2023-09-01 14:04:38 -04:00
1f4ae5dbfb improve help text for paclage name 2023-09-01 14:04:27 -04:00
d0d3cc8faa Merge pull request #3219 from sbwalker/dev
fix paths in Edit Page / Modules tab / Edit option
2023-09-01 13:57:26 -04:00
7513ae28f0 fix paths in Edit Page / Modules tab / Edit option 2023-09-01 13:57:14 -04:00
c151c6f55a Merge pull request #3218 from sbwalker/dev
include User Settings when calling UserService
2023-09-01 13:16:51 -04:00
d491aeeba6 include User Settings when calling UserService 2023-09-01 13:16:39 -04:00
ab93d0acea Merge pull request #3217 from sbwalker/dev
Fix #3215 - module creator creates incorrect ServerManagerType value
2023-09-01 13:03:01 -04:00
f40f3f934f Fix #3215 - module creator creates incorrect ServerManagerType value 2023-09-01 13:02:44 -04:00
4d57adb393 Merge pull request #3216 from sbwalker/dev
allow an administratot to browse to the SiteMap, handle default module panes and ordering for PageTemplates, allow trial products to be purchased
2023-09-01 12:14:24 -04:00
e91db18677 allow an administratot to browse to the SiteMap, handle default module panes and ordering for PageTemplates, allow trial products to be purchased 2023-09-01 12:14:04 -04:00
9a88d65ff7 Merge pull request #3212 from sbwalker/dev
need to use context rather than filter
2023-08-31 14:18:31 -04:00
211d8c7f19 need to use context rather than filter 2023-08-31 14:18:19 -04:00
763cdca114 Merge pull request #3211 from sbwalker/dev
Marketplace UI consistency
2023-08-31 13:55:08 -04:00
0d56d35646 Marketplace UI consistency 2023-08-31 13:54:56 -04:00
d1e80cb86a Update README.md 2023-08-29 14:01:00 -04:00
122 changed files with 1835 additions and 807 deletions

View File

@ -12,11 +12,12 @@
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
string url = NavigateUrl(p.Path);
<div class="col-md-2 mx-auto text-center mb-3">
<p class="col-md-2 mx-auto text-center mb-3">
<NavLink class="nav-link text-primary" href="@url" Match="NavLinkMatch.All">
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>@SharedLocalizer[p.Name]
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>
<p class="lead">@((MarkupString)SharedLocalizer[p.Name].ToString().Replace(" ", "<br />"))</p>
</NavLink>
</div>
</p>
}
}
</div>

View File

@ -67,6 +67,7 @@
</div>
</div>
</form>
<br /><br />
@if (!_isSystem)
{
@ -79,8 +80,7 @@
@((MarkupString)"&nbsp;")
<ActionDialog Header="Delete Folder" Message="Are You Sure You Wish To Delete This Folder?" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFolder())" ResourceKey="DeleteFolder" />
}
<br />
<br />
<br /><br />
@if (PageState.QueryString.ContainsKey("id"))
{
<AuditInfo CreatedBy="@_createdBy" CreatedOn="@_createdOn" ModifiedBy="@_modifiedBy" ModifiedOn="@_modifiedOn"></AuditInfo>

View File

@ -8,27 +8,28 @@
@if (_files != null)
{
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-2">
<label class="control-label">@Localizer["Folder"] </label>
<div class="row">
<div class="col-md mb-1">
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />
</div>
<div class="col-sm-6">
<div class="col-md-8 mb-1">
<div class="input-group">
<span class="input-group-text">@Localizer["Folder"]:</span>
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
</div>
<div class="col-sm-4">
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />&nbsp;
</div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div>
</div>
<Pager Items="@_files">
<Pager Items="@_files" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>

View File

@ -15,7 +15,7 @@ else
<br />
<br />
<Pager Items="@_jobs">
<Pager Items="@_jobs" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>

View File

@ -14,7 +14,7 @@ else
{
<ActionLink Action="Add" Text="Add Language" ResourceKey="AddLanguage" />
<Pager Items="@_languages">
<Pager Items="@_languages" SearchProperties="Name,Code">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
@ -146,7 +146,7 @@ else
{
try
{
_package = await PackageService.GetPackageAsync(Constants.PackageId + "." + code, version);
_package = await PackageService.GetPackageAsync(Constants.PackageId + "." + code, version, false);
if (_package != null)
{
StateHasChanged();
@ -168,7 +168,7 @@ else
{
try
{
await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version);
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;

View File

@ -3,6 +3,7 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject ISettingService SettingService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -92,23 +93,23 @@
{
try
{
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]))
{
_allowsitelogin = bool.Parse(PageState.Site.Settings["LoginOptions:AllowSiteLogin"]);
}
if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"]))
{
_allowexternallogin = true;
}
if (PageState.QueryString.ContainsKey("returnurl"))
{
_returnUrl = PageState.QueryString["returnurl"];
}
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
if (_allowexternallogin && !_allowsitelogin)
{
// redirect to external login
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + _returnUrl), true);
return;
}
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name"))
{
_username = PageState.QueryString["name"];
@ -167,7 +168,7 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && PageState.User == null)
if (firstRender && PageState.User == null && _allowsitelogin)
{
await username.FocusAsync();
}
@ -206,8 +207,7 @@
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider
.GetService(typeof(IdentityAuthenticationStateProvider));
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(WebUtility.UrlDecode(_returnUrl), true));
}
@ -221,7 +221,7 @@
}
else
{
if ((PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && PageState.Site.Settings["LoginOptions:TwoFactor"] == "required") || user.TwoFactorRequired)
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired)
{
twofactor = true;
validated = false;

View File

@ -48,7 +48,10 @@
<select class="form-select" value="@_sort" @onchange="(e => SortChanged(e))">
<option value="popularity">@SharedLocalizer["Search.Popularity"]</option>
<option value="alphabetical">@SharedLocalizer["Search.Alphabetical"]</option>
@if (_price == "free")
{
<option value="downloads">@SharedLocalizer["Search.Downloads"]</option>
}
<option value="recent">@SharedLocalizer["Search.RecentlyReleased"]</option>
@if (_price == "paid")
{
@ -63,6 +66,7 @@
<div class="container-fluid px-0">
<div class="row g-0 mb-2">
<div class="col-4">
<a href="@context.ProductUrl" target="_blank">
@if (context.LogoUrl != null)
{
<img src="@context.LogoUrl" class="img-fluid" alt="@context.Name" />
@ -71,15 +75,24 @@
{
<img src="/package.png" class="img-fluid" alt="@context.Name" />
}
</a>
</div>
<div class="col-8 text-end">
<small>@SharedLocalizer["Search.Version"]:</small> <strong>@context.Version</strong>
<br /><small>@SharedLocalizer["Search.Downloads"]:</small> <strong>@(String.Format("{0:n0}", context.Downloads))</strong>
<br /><small>@SharedLocalizer["Search.Released"]:</small> <strong>@context.ReleaseDate.ToString("MM/dd/yyyy")</strong>
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<br /><small>@SharedLocalizer["Search.Source"]:</small> <strong>@(new Uri(context.PackageUrl).Host)</strong>
}
@if (context.Price == null)
{
<br /><small>@SharedLocalizer["Search.Downloads"]:</small> <strong>@(String.Format("{0:n0}", context.Downloads))</strong>
}
else
{
<br /><small>@SharedLocalizer["From"]:</small> <strong>@context.Price.Value.ToString("$#,##0.00")</strong>
@((MarkupString)(context.TrialPeriod > 0 ? " <strong>(" + context.TrialPeriod + " Day Trial)</strong>" : ""))
}
</div>
</div>
<div class="row g-0">
@ -87,17 +100,12 @@
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_blank">@context.Name</a></h3><br />
<small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<small>@SharedLocalizer["From"]:</small> <strong>@context.Price.Value.ToString("$#,##0.00")</strong>
@((MarkupString)(context.TrialPeriod > 0 ? " <strong>(" + context.TrialPeriod + " Day Trial)</strong>" : ""))
}
<br />
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl) && string.IsNullOrEmpty(context.PackageUrl))
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
}
@ -213,6 +221,7 @@
private async void PriceChanged(string price)
{
_price = price;
_sort = "popularity";
await LoadModuleDefinitions();
StateHasChanged();
}
@ -256,7 +265,7 @@
{
try
{
var package = await PackageService.GetPackageAsync(packageid, version);
var package = await PackageService.GetPackageAsync(packageid, version, false);
if (package != null)
{
_productname = package.Name;
@ -285,7 +294,7 @@
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _packageversion, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(_packageid, _packageversion);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _packageversion);
AddModuleMessage(string.Format(Localizer["Success.Module.Download"], NavigateUrl("admin/system")), MessageType.Success);
_productname = "";

View File

@ -88,9 +88,12 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override void OnInitialized()
{
if (!NavigationManager.BaseUri.Contains("localhost:"))
{
AddModuleMessage(Localizer["Info.Module.Development"], MessageType.Info);
}
}
protected override async Task OnParametersSetAsync()
{
@ -114,6 +117,8 @@
try
{
if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
{
if (IsValidXML(_description))
{
var template = _templates.FirstOrDefault(item => item.Name == _template);
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference, ModuleDefinitionName = template.Namespace };
@ -122,6 +127,11 @@
AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Require.ValidDescription"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Require.ValidName"], MessageType.Warning);
}
@ -143,6 +153,12 @@
return !string.IsNullOrEmpty(name) && name.ToLower() != "module" && !name.ToLower().Contains("oqtane") && Regex.IsMatch(name, "^[A-Za-z_][A-Za-z0-9_]*$");
}
private bool IsValidXML(string description)
{
// must contain letters, digits, or spaces
return Regex.IsMatch(description, "^[A-Za-z0-9 .,!?]+$");
}
private void TemplateChanged(ChangeEventArgs e)
{
_template = (string)e.Value;

View File

@ -59,9 +59,26 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed" ResourceKey="PackageName">Package Name: </Label>
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed. This value must be specified within the module's IModule interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
</div>
</div>
<div class="row mb-1 align-items-center">
@ -191,7 +208,7 @@
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" @onclick="DownloadPackage">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-success" @onclick="DownloadTranslation">@SharedLocalizer["Accept"]</button>
<button type="button" class="btn btn-secondary" @onclick="HideModal">@SharedLocalizer["Cancel"]</button>
</div>
</div>
@ -213,6 +230,7 @@
private string _moduledefinitionname = "";
private string _version;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -364,7 +382,7 @@
var version = _packages.Where(item => item.PackageId == packagename).FirstOrDefault().Version;
try
{
_package = await PackageService.GetPackageAsync(packagename, version);
_package = await PackageService.GetPackageAsync(packagename, version, false);
if (_package != null)
{
StateHasChanged();
@ -382,11 +400,11 @@
}
}
private async Task DownloadPackage()
private async Task DownloadTranslation()
{
try
{
await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(_package.PackageId, _package.Version);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _package.PackageId, _package.Version);
AddModuleMessage(string.Format(Localizer["Success.Translation.Download"], NavigateUrl("admin/system")), MessageType.Success);
_package = null;
@ -398,4 +416,28 @@
AddModuleMessage(Localizer["Error.Translation.Download"], MessageType.Error);
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
}

View File

@ -128,8 +128,7 @@ else
private async Task LoadModuleDefinitions()
{
_moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList();
var list = _moduleDefinitions.Where(item => !string.IsNullOrEmpty(item.PackageName)).Select(item => item.PackageName).Distinct().ToList();
_packages = await PackageService.GetPackagesAsync(list);
_packages = await PackageService.GetPackageUpdatesAsync("module");
}
private string PurchaseLink(string packagename)
@ -187,7 +186,7 @@ else
{
try
{
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(packagename, version);
await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version);
AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success);
}

View File

@ -26,6 +26,17 @@
<input id="title" type="text" class="form-control" @bind="@_title" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pane" HelpText="The pane where the module will be displayed" ResourceKey="Pane">Pane: </Label>
<div class="col-sm-9">
<select class="form-select" @bind="@_pane">
@foreach (string pane in PageState.Page.Panes)
{
<option value="@pane">@pane Pane</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="container" HelpText="Select the module's container" ResourceKey="Container">Container: </Label>
<div class="col-sm-9">
@ -112,6 +123,7 @@
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _module;
private string _title;
private string _pane;
private string _containerType;
private string _allPages = "false";
private string _permissionNames = "";
@ -134,6 +146,7 @@
{
_module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title;
_pane = ModuleState.Pane;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType);
_containerType = ModuleState.ContainerType;
_allPages = ModuleState.AllPages.ToString();
@ -206,6 +219,7 @@
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
pagemodule.PageId = int.Parse(_pageId);
pagemodule.Title = _title;
pagemodule.Pane = _pane;
pagemodule.ContainerType = (_containerType != "-") ? _containerType : string.Empty;
if (!string.IsNullOrEmpty(pagemodule.ContainerType) && pagemodule.ContainerType == PageState.Page.DefaultContainerType)
{

View File

@ -40,9 +40,9 @@
<Label Class="col-sm-3" For="insert" HelpText="Select the location where you would like the page to be inserted in relation to other pages" ResourceKey="Insert">Insert: </Label>
<div class="col-sm-9">
<select id="insert" class="form-select" @bind="@_insert" required>
<option value="<<">@Localizer["AtBeginning"]</option>
@if (_children != null && _children.Count > 0)
{
<option value="<<">@Localizer["AtBeginning"]</option>
<option value="<">@Localizer["Before"]</option>
<option value=">">@Localizer["After"]</option>
}
@ -257,7 +257,14 @@
_themes = ThemeService.GetThemeControls(PageState.Site.Themes);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = PageState.Site.DefaultContainerType;
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_children = new List<Page>();
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
}
ThemeSettings();
_initialized = true;
}
@ -280,26 +287,13 @@
{
_parentid = (string)e.Value;
_children = new List<Page>();
if (_parentid == "-1")
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
}
}
else
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
}
}
StateHasChanged();
}
catch (Exception ex)

View File

@ -48,13 +48,16 @@
{
<option value="=">&lt;@Localizer["ThisLocation.Keep"]&gt;</option>
}
<option value="<<">@Localizer["ToBeginning"]</option>
@if (_children != null && _children.Count > 0)
{
<option value="<<">@Localizer["ToBeginning"]</option>
<option value="<">@Localizer["Before"]</option>
<option value=">">@Localizer["After"]</option>
}
@if (_parentid != _currentparentid)
{
<option value=">>">@Localizer["ToEnd"]</option>
}
</select>
@if (_children != null && _children.Count > 0 && (_insert == "<" || _insert == ">"))
{
@ -209,7 +212,7 @@
<th>@Localizer["ModuleDefinition"]</th>
</Header>
<Row>
<td><ActionLink Action="Settings" Text="Edit" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<td><ActionLink Action="Settings" Text="Edit" Path="@_actualpath" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" PermissionList="@context.PermissionList" ResourceKey="ModuleSettings" /></td>
<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.ModuleDefinition?.Name</td>
@ -292,6 +295,7 @@
private int _childid = -1;
private string _isnavigation;
private string _isclickable;
private string _actualpath;
private string _path;
private string _url;
private string _ispersonalizable;
@ -323,7 +327,6 @@
{
try
{
_children = PageState.Pages.Where(item => item.ParentId == null).ToList();
_pageId = Int32.Parse(PageState.QueryString["id"]);
_page = await PageService.GetPageAsync(_pageId);
_icons = await SystemService.GetIconsAsync();
@ -341,10 +344,19 @@
_parentid = _page.ParentId.ToString();
_parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId);
}
_children = new List<Page>();
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{
if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
}
_currentparentid = _parentid;
_isnavigation = _page.IsNavigation.ToString();
_isclickable = _page.IsClickable.ToString();
_path = _page.Path;
_actualpath = _page.Path;
_path = _actualpath;
if (string.IsNullOrEmpty(_path))
{
_path = "/";
@ -363,7 +375,7 @@
// appearance
_title = _page.Title;
_themetype = _page.ThemeType;
if (string.IsNullOrEmpty(_themetype) || ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
if (string.IsNullOrEmpty(_themetype))
{
_themetype = PageState.Site.DefaultThemeType;
}
@ -415,34 +427,14 @@
{
_parentid = (string)e.Value;
_children = new List<Page>();
if (_parentid == "-1")
foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid))))
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == null))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
}
}
else
{
foreach (Page p in PageState.Pages.Where(item => item.ParentId == int.Parse(_parentid)))
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
{
_children.Add(p);
}
}
}
if (_parentid == _currentparentid)
{
_insert = "=";
}
else
{
_insert = ">>";
}
_insert = (_parentid == _currentparentid) ? "=" : ">>";
StateHasChanged();
}
catch (Exception ex)
@ -672,5 +664,4 @@
{
_icon = NewIcon;
}
}

View File

@ -9,7 +9,7 @@
{
<ActionLink Action="Add" Text="Add Page" ResourceKey="AddPage" />
<Pager Items="@PageState.Pages.Where(item => !item.IsDeleted)">
<Pager Items="@PageState.Pages.Where(item => !item.IsDeleted)" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>

View File

@ -22,7 +22,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="The help text displayed to the user for this profile item" ResourceKey="Description">Description: </Label>
<div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="5" maxlength="256" required ></textarea>
<textarea id="description" class="form-control" @bind="@_description" rows="3" maxlength="256" required ></textarea>
</div>
</div>
<div class="row mb-1 align-items-center">
@ -43,6 +43,12 @@
<input id="length" class="form-control" @bind="@_maxlength" maxlength="4" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="rows" HelpText="The number of rows for text entry (one is the default)" ResourceKey="Rows">Rows: </Label>
<div class="col-sm-9">
<input id="rows" class="form-control" @bind="@_rows" maxlength="2" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="defaultVal" HelpText="The default value for this profile item" ResourceKey="DefaultValue">Default Value: </Label>
<div class="col-sm-9">
@ -101,6 +107,7 @@
private string _category = string.Empty;
private string _vieworder = "0";
private string _maxlength = "0";
private string _rows = "1";
private string _defaultvalue = string.Empty;
private string _options = string.Empty;
private string _validation = string.Empty;
@ -131,6 +138,7 @@
_category = profile.Category;
_vieworder = profile.ViewOrder.ToString();
_maxlength = profile.MaxLength.ToString();
_rows = profile.Rows.ToString();
_defaultvalue = profile.DefaultValue;
_options = profile.Options;
_validation = profile.Validation;
@ -175,6 +183,7 @@
profile.Category = _category;
profile.ViewOrder = int.Parse(_vieworder);
profile.MaxLength = int.Parse(_maxlength);
profile.Rows = int.Parse(_rows);
profile.DefaultValue = _defaultvalue;
profile.Options = _options;
profile.Validation = _validation;

View File

@ -12,16 +12,22 @@ else
{
<ActionLink Action="Add" Text="Add Profile" Security="SecurityAccessLevel.Edit" ResourceKey="AddProfile" />
<Pager Items="@_profiles">
<Pager Items="@_profiles" SearchProperties="Title,Category">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Title"]</th>
<th>@Localizer["Category"]</th>
<th>@Localizer["Order"]</th>
</Header>
<Row>
<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" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteProfile(context.ProfileId))" ResourceKey="DeleteProfile" /></td>
<td>@context.Name</td>
<td>@context.Title</td>
<td>@context.Category</td>
<td>@context.ViewOrder</td>
</Row>
</Pager>
}

View File

@ -6,6 +6,7 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username will be populated from the link you received in the password reset notification" ResourceKey="Username">Username: </Label>
@ -45,12 +46,14 @@
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _passwordrequirements;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
protected override async Task OnInitializedAsync()
{
_togglepassword = SharedLocalizer["ShowPassword"];
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
if (PageState.QueryString.ContainsKey("name") && PageState.QueryString.ContainsKey("token"))
{

View File

@ -12,7 +12,7 @@ else
{
<ActionLink Action="Add" Text="Add Role" Security="SecurityAccessLevel.Edit" ResourceKey="AddRole" />
<Pager Items="@_roles">
<Pager Items="@_roles" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>

View File

@ -49,7 +49,16 @@
<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 class="input-group">
<input id="sitemap" class="form-control" @bind="@_sitemap" disabled />
<a href="@_sitemap" class="btn btn-secondary" target="_new">@Localizer["Browse"]</a>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="siteguid" HelpText="The Unique Identifier For The Site" ResourceKey="SiteGuid">ID: </Label>
<div class="col-sm-9">
<input id="siteguid" class="form-control" @bind="@_siteguid" required disabled />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -348,6 +357,7 @@
private string _homepageid = "-";
private string _isdeleted;
private string _sitemap = "";
private string _siteguid = "";
private string _version = "";
private int _logofileid = -1;
private FileManager _logofilemanager;
@ -406,6 +416,7 @@
}
_isdeleted = site.IsDeleted.ToString();
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/pages/sitemap.xml";
_siteguid = site.SiteGuid;
_version = site.Version;
// appearance
@ -610,6 +621,7 @@
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);

View File

@ -14,7 +14,7 @@ else
{
<ActionLink Action="Add" Text="Add Site" ResourceKey="AddSite" />
<Pager Items="@_sites">
<Pager Items="@_sites" SearchProperties="Name">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>

View File

@ -48,7 +48,10 @@
<select class="form-select" value="@_sort" @onchange="(e => SortChanged(e))">
<option value="popularity">@SharedLocalizer["Search.Popularity"]</option>
<option value="alphabetical">@SharedLocalizer["Search.Alphabetical"]</option>
@if (_price == "free")
{
<option value="downloads">@SharedLocalizer["Search.Downloads"]</option>
}
<option value="recent">@SharedLocalizer["Search.RecentlyReleased"]</option>
@if (_price == "paid")
{
@ -63,6 +66,7 @@
<div class="container-fluid px-0">
<div class="row g-0 mb-2">
<div class="col-4">
<a href="@context.ProductUrl" target="_blank">
@if (context.LogoUrl != null)
{
<img src="@context.LogoUrl" class="img-fluid" alt="@context.Name" />
@ -71,16 +75,23 @@
{
<img src="/package.png" class="img-fluid" alt="@context.Name" />
}
</a>
</div>
<div class="col-8 text-end">
<small>@SharedLocalizer["Search.Version"]:</small> <strong>@context.Version</strong>
<br /><small>@SharedLocalizer["Search.Downloads"]:</small> <strong>@(String.Format("{0:n0}", context.Downloads))</strong>
<br /><small>@SharedLocalizer["Search.Released"]:</small> <strong>@context.ReleaseDate.ToString("MM/dd/yyyy")</strong>
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<br />
<small>@SharedLocalizer["Search.Source"]:</small> <strong>@(new Uri(context.PackageUrl).Host)</strong>
<br /><small>@SharedLocalizer["Search.Source"]:</small> <strong>@(new Uri(context.PackageUrl).Host)</strong>
}
@if (context.Price == null)
{
<br /><small>@SharedLocalizer["Search.Downloads"]:</small> <strong>@(String.Format("{0:n0}", context.Downloads))</strong>
}
else
{
<br /><small>@SharedLocalizer["From"]:</small> <strong>@context.Price.Value.ToString("$#,##0.00")</strong>
@((MarkupString)(context.TrialPeriod > 0 ? " <strong>(" + context.TrialPeriod + " Day Trial)</strong>" : ""))
}
</div>
</div>
@ -89,17 +100,12 @@
<h3 style="display: inline;"><a href="@context.ProductUrl" target="_blank">@context.Name</a></h3><br />
<small>@SharedLocalizer["Search.By"]:</small> <strong><a href="@context.OwnerUrl" target="new">@context.Owner</a></strong><br />
@(context.Description.Length > 400 ? (context.Description.Substring(0, 400) + "...") : context.Description)<br />
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<small>@SharedLocalizer["From"]:</small> <strong>@context.Price.Value.ToString("$#,##0.00")</strong>
@((MarkupString)(context.TrialPeriod > 0 ? " <strong>(" + context.TrialPeriod + " Day Trial)</strong>" : ""))
}
<br />
@if (!string.IsNullOrEmpty(context.PackageUrl))
{
<button type="button" class="btn btn-primary" @onclick=@(async () => await GetPackage(context.PackageId, context.Version))>@SharedLocalizer["Download"]</button>
}
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl) && string.IsNullOrEmpty(context.PackageUrl))
@if (context.Price != null && !string.IsNullOrEmpty(context.PaymentUrl))
{
<a class="btn btn-success ms-2" style="text-decoration: none !important" href="@context.PaymentUrl" target="_new">@SharedLocalizer["Buy"]</a>
}
@ -215,6 +221,7 @@
private async void PriceChanged(string price)
{
_price = price;
_sort = "popularity";
await LoadThemes();
StateHasChanged();
}
@ -258,7 +265,7 @@
{
try
{
var package = await PackageService.GetPackageAsync(packageid, version);
var package = await PackageService.GetPackageAsync(packageid, version, false);
if (package != null)
{
_productname = package.Name;
@ -287,7 +294,7 @@
{
try
{
await PackageService.DownloadPackageAsync(_packageid, _version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(_packageid, _version);
await logger.LogInformation("Package {PackageId} {Version} Downloaded Successfully", _packageid, _version);
AddModuleMessage(string.Format(Localizer["Success.Theme.Download"], NavigateUrl("admin/system")), MessageType.Success);
_productname = "";

View File

@ -79,9 +79,12 @@
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override void OnInitialized()
{
if (!NavigationManager.BaseUri.Contains("localhost:"))
{
AddModuleMessage(Localizer["Info.Theme.CreatorIntent"], MessageType.Info);
}
}
protected override async Task OnParametersSetAsync()
{

View File

@ -2,6 +2,7 @@
@using System.Net
@inherits ModuleBase
@inject IThemeService ThemeService
@inject IPackageService PackageService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -42,9 +43,26 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this module was installed" ResourceKey="PackageName">Package Name: </Label>
<Label Class="col-sm-3" For="packagename" HelpText="The unique name of the package from which this theme was installed. This value must be specified within the theme's ITheme interface specification." ResourceKey="PackageName">Package Name: </Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(_packagename))
{
<div class="input-group">
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
@if (string.IsNullOrEmpty(_packageurl))
{
<button type="button" class="btn btn-secondary" @onclick="ValidatePackage">@Localizer["Validate"]</button>
}
else
{
<a href="@_packageurl" target="_blank" class="btn btn-primary">@SharedLocalizer["Download"]</a>
}
</div>
}
else
{
<input id="packagename" class="form-control" @bind="@_packagename" disabled />
}
</div>
</div>
<div class="row mb-1 align-items-center">
@ -97,7 +115,8 @@
private string _isenabled;
private string _name;
private string _version;
private string _packagename;
private string _packagename = "";
private string _packageurl = "";
private string _owner = "";
private string _url = "";
private string _contact = "";
@ -166,4 +185,27 @@
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
private async Task ValidatePackage()
{
try
{
var package = await PackageService.GetPackageAsync(_packagename, _version, true);
if (package == null || string.IsNullOrEmpty(package.PackageUrl))
{
AddModuleMessage(Localizer["Message.Validate"], MessageType.Warning);
}
else
{
_packageurl = package.PackageUrl;
AddModuleMessage(Localizer["Message.Download"], MessageType.Info);
}
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Downloading Package {PackageId} {Version}", _packagename, _version);
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
}

View File

@ -79,8 +79,7 @@ else
try
{
_themes = await ThemeService.GetThemesAsync();
var list = _themes.Where(item => !string.IsNullOrEmpty(item.PackageName)).Select(item => item.PackageName).Distinct().ToList();
_packages = await PackageService.GetPackagesAsync(list);
_packages = await PackageService.GetPackageUpdatesAsync("theme");
}
catch (Exception ex)
{
@ -147,7 +146,7 @@ else
{
try
{
await PackageService.DownloadPackageAsync(packagename, version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(packagename, version);
await logger.LogInformation("Theme Downloaded {ThemeName} {Version}", packagename, version);
AddModuleMessage(string.Format(Localizer["Success.Theme.Install"], NavigateUrl("admin/system")), MessageType.Success);
}

View File

@ -7,6 +7,8 @@
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_initialized)
{
<TabStrip>
<TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable)
@ -21,7 +23,7 @@
}
</TabPanel>
<TabPanel Name="Upload" ResourceKey="Upload">
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessgeUpgrade.Text"]></ModuleMessage>
<ModuleMessage Type="MessageType.Info" Message=@Localizer["MessageUpgrade.Text"]></ModuleMessage>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Upload A Framework Package And Then Select Upgrade" ResourceKey="Framework">Framework: </Label>
@ -33,8 +35,10 @@
<button type="button" class="btn btn-success" @onclick="Upgrade">@SharedLocalizer["Upgrade"]</button>
</TabPanel>
</TabStrip>
}
@code {
private bool _initialized = false;
private Package _package;
private bool _upgradeavailable = false;
@ -43,6 +47,12 @@
protected override async Task OnInitializedAsync()
{
try
{
if (NavigationManager.BaseUri.Contains("localhost:"))
{
AddModuleMessage(Localizer["Localhost.Text"], MessageType.Info);
}
else
{
List<Package> packages = await PackageService.GetPackagesAsync("framework", "", "", "");
if (packages != null)
@ -57,6 +67,8 @@
_package = new Package { Name = Constants.PackageId, Version = Constants.Version };
}
}
_initialized = true;
}
}
catch
{
@ -85,8 +97,8 @@
{
try
{
await PackageService.DownloadPackageAsync(packageid, version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version, Constants.PackagesFolder);
await PackageService.DownloadPackageAsync(packageid, version);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version);
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
}
catch (Exception ex)

View File

@ -28,7 +28,7 @@ else
</div>
</div>
<br/>
<Pager Items="@_urlMappings">
<Pager Items="@_urlMappings" SearchProperties="Url">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>

View File

@ -122,6 +122,8 @@ else
</select>
}
else
{
@if (p.Rows == 1)
{
@if (p.IsRequired)
{
@ -132,6 +134,18 @@ else
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
}
else
{
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
}
}
}
</div>
</div>
}
@ -282,9 +296,11 @@ else
private int folderid = -1;
private int photofileid = -1;
private File photo = null;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
private string filter = "to";
private List<Notification> notifications;
private string notificationSummary = string.Empty;
@ -299,10 +315,7 @@ else
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.Site.Settings.ContainsKey("LoginOptions:TwoFactor") && !string.IsNullOrEmpty(PageState.Site.Settings["LoginOptions:TwoFactor"]))
{
allowtwofactor = (PageState.Site.Settings["LoginOptions:TwoFactor"] == "true");
}
allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
if (PageState.User != null)
{
@ -366,9 +379,11 @@ else
{
try
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
if (username != string.Empty && email != string.Empty)
{
if (_password == confirm)
{
if (ValidateProfiles())
{
var user = PageState.User;
user.Username = username;
@ -406,6 +421,7 @@ else
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Message.Password.Invalid"], MessageType.Warning);
@ -427,7 +443,6 @@ else
private bool ValidateProfiles()
{
bool valid = true;
foreach (Profile profile in profiles)
{
if (string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)) && !string.IsNullOrEmpty(profile.DefaultValue))
@ -436,18 +451,24 @@ else
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (valid == true && profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
if (profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
{
valid = false;
AddModuleMessage(string.Format(SharedLocalizer["ProfileRequired"], profile.Title), MessageType.Warning);
return false;
}
if (valid == true && !string.IsNullOrEmpty(profile.Validation))
if (!string.IsNullOrEmpty(profile.Validation))
{
Regex regex = new Regex(profile.Validation);
valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
bool valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
if (!valid)
{
AddModuleMessage(string.Format(SharedLocalizer["ProfileInvalid"], profile.Title), MessageType.Warning);
return false;
}
}
}
return valid;
}
return true;
}
private void Cancel()

View File

@ -55,7 +55,7 @@
<div class="row mb-1 align-items-center">
<label class="col-sm-3">@Localizer["Message"] </label>
<div class="col-sm-9">
<textarea class="form-control" @bind="@body" rows="5" readonly />
<textarea id="txtFrom" class="form-control" @bind="@body" rows="5" readonly />
</div>
</div>
@ -66,7 +66,7 @@
<div class="row mb-1 align-items-center">
<label class="col-sm-3">@Localizer["Message"] </label>
<div class="col-sm-9">
<textarea class="form-control" @bind="@body" rows="5" readonly />
<textarea id="txtTo" class="form-control" @bind="@body" rows="5" />
</div>
</div>
@ -93,7 +93,7 @@
{
<div class="control-group">
<label class="control-label">@Localizer["OriginalMessage"] </label>
<textarea class="form-control" @bind="@reply" rows="5" readonly />
<textarea id="txtReply" class="form-control" @bind="@reply" rows="5" readonly />
</div>
}
}

View File

@ -17,7 +17,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="A unique username for a user. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@username" />
<input id="username" class="form-control" @bind="@_username" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -33,7 +33,7 @@
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@confirm" autocomplete="new-password" required />
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
@ -41,17 +41,25 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@email" />
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@displayname" />
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9">
<select id="notify" class="form-select" @bind="@_notify" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
}
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
@ -72,6 +80,26 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@p.Name" HelpText="@p.Description">@p.Title</Label>
<div class="col-sm-9">
@if (!string.IsNullOrEmpty(p.Options))
{
<select id="@p.Name" class="form-select" @onchange="@(e => ProfileChanged(e, p.Name))">
@foreach (var option in p.Options.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
@if (GetProfileValue(p.Name, "") == option || (GetProfileValue(p.Name, "") == "" && p.DefaultValue == option))
{
<option value="@option" selected>@option</option>
}
else
{
<option value="@option">@option</option>
}
}
</select>
}
else
{
@if (p.Rows == 1)
{
@if (p.IsRequired)
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))" />
@ -80,10 +108,22 @@
{
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
}
else
{
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
}
}
}
</div>
</div>
}
</div>
</div>
}
@ -96,13 +136,14 @@
@code {
private string _passwordrequirements;
private string username = string.Empty;
private string _username = string.Empty;
private string _password = string.Empty;
private string _passwordtype = "password";
private string _togglepassword = string.Empty;
private string confirm = string.Empty;
private string email = string.Empty;
private string displayname = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _displayname = string.Empty;
private string _notify = "True";
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
@ -139,17 +180,20 @@
{
try
{
if (username != string.Empty && _password != string.Empty && confirm != string.Empty && email != string.Empty && ValidateProfiles())
if (_username != string.Empty && _password != string.Empty && _confirm != string.Empty && _email != string.Empty)
{
if (_password == confirm)
if (_password == _confirm)
{
if (ValidateProfiles())
{
var user = new User();
user.SiteId = PageState.Site.SiteId;
user.Username = username;
user.Username = _username;
user.Password = _password;
user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
user.Email = _email;
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.PhotoFileId = null;
user.SuppressNotification = !bool.Parse(_notify);
user = await UserService.AddUserAsync(user);
@ -161,10 +205,11 @@
}
else
{
await logger.LogError("Error Adding User {Username} {Email}", username, email);
await logger.LogError("Error Adding User {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Error.User.AddCheckPass"], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
@ -177,14 +222,13 @@
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Adding User {Username} {Email} {Error}", username, email, ex.Message);
await logger.LogError(ex, "Error Adding User {Username} {Email} {Error}", _username, _email, ex.Message);
AddModuleMessage(Localizer["Error.User.Add"], MessageType.Error);
}
}
private bool ValidateProfiles()
{
bool valid = true;
foreach (Profile profile in profiles)
{
if (string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)) && !string.IsNullOrEmpty(profile.DefaultValue))
@ -193,18 +237,24 @@
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (valid == true && profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
if (profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
{
valid = false;
AddModuleMessage(string.Format(SharedLocalizer["ProfileRequired"], profile.Title), MessageType.Warning);
return false;
}
if (valid == true && !string.IsNullOrEmpty(profile.Validation))
if (!string.IsNullOrEmpty(profile.Validation))
{
Regex regex = new Regex(profile.Validation);
valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
bool valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
if (!valid)
{
AddModuleMessage(string.Format(SharedLocalizer["ProfileInvalid"], profile.Title), MessageType.Warning);
return false;
}
}
}
return valid;
}
return true;
}
private void ProfileChanged(ChangeEventArgs e, string SettingName)

View File

@ -124,6 +124,8 @@ else
</select>
}
else
{
@if (p.Rows == 1)
{
@if (p.IsRequired)
{
@ -134,6 +136,18 @@ else
<input id="@p.Name" class="form-control" maxlength="@p.MaxLength" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))" />
}
}
else
{
@if (p.IsRequired)
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" required @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
}
else
{
<textarea id="@p.Name" class="form-control" maxlength="@p.MaxLength" rows="@p.Rows" value="@GetProfileValue(p.Name, p.DefaultValue)" @onchange="@(e => ProfileChanged(e, p.Name))"></textarea>
}
}
}
</div>
</div>
}
@ -240,9 +254,11 @@ else
{
try
{
if (username != string.Empty && email != string.Empty && ValidateProfiles())
if (username != string.Empty && email != string.Empty)
{
if (_password == confirm)
{
if (ValidateProfiles())
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
user.SiteId = PageState.Site.SiteId;
@ -271,6 +287,7 @@ else
AddModuleMessage(Localizer["Message.Password.Complexity"], MessageType.Error);
}
}
}
else
{
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
@ -290,7 +307,6 @@ else
private bool ValidateProfiles()
{
bool valid = true;
foreach (Profile profile in profiles)
{
if (string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)) && !string.IsNullOrEmpty(profile.DefaultValue))
@ -299,18 +315,24 @@ else
}
if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
if (valid == true && profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
if (profile.IsRequired && string.IsNullOrEmpty(SettingService.GetSetting(settings, profile.Name, string.Empty)))
{
valid = false;
AddModuleMessage(string.Format(SharedLocalizer["ProfileRequired"], profile.Title), MessageType.Warning);
return false;
}
if (valid == true && !string.IsNullOrEmpty(profile.Validation))
if (!string.IsNullOrEmpty(profile.Validation))
{
Regex regex = new Regex(profile.Validation);
valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
bool valid = regex.Match(SettingService.GetSetting(settings, profile.Name, string.Empty)).Success;
if (!valid)
{
AddModuleMessage(string.Format(SharedLocalizer["ProfileInvalid"], profile.Title), MessageType.Warning);
return false;
}
}
}
return valid;
}
return true;
}
private void ProfileChanged(ChangeEventArgs e, string SettingName)

View File

@ -17,26 +17,17 @@ else
{
<TabStrip>
<TabPanel Name="Users" Heading="Users" ResourceKey="Users">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-4">
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />
</div>
<div class="col-sm-4">
<input class="form-control" @bind="@_search" />
</div>
<div class="col-sm-4">
<button type="button" class="btn btn-secondary" @onclick="OnSearch">@SharedLocalizer["Search"]</button>
</div>
</div>
</div>
<Pager Items="@users" RowClass="align-middle">
<ActionLink Action="Add" Text="Add User" Security="SecurityAccessLevel.Edit" ResourceKey="AddUser" />&nbsp;
<ActionLink Text="Import Users" Class="btn btn-secondary ms-2" Action="Users" Security="SecurityAccessLevel.Admin" ResourceKey="ImportUsers"/>
<Pager Items="@users" RowClass="align-middle" SearchProperties="User.Username,User.Email,User.DisplayName">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("Username"))">@Localizer["Username"]<i class="@(SetSortIcon("Username"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("DisplayName"))">@Localizer["Name"]<i class="@(SetSortIcon("DisplayName"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("Email"))">@Localizer["Email"]<i class="@(SetSortIcon("Email"))"></i></th>
<th class="app-sort-th link-primary text-decoration-underline" @onclick="@(() => SortTable("LastLoginOn"))">@Localizer["LastLoginOn"]<i class="@(SetSortIcon("LastLoginOn"))"></i></th>
</Header>
<Row>
@ -50,7 +41,8 @@ else
<ActionLink Action="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
</td>
<td>@context.User.Username</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.DisplayName))</td>
<td>@context.User.DisplayName</td>
<td>@((MarkupString)string.Format("<a href=\"mailto:{0}\">{1}</a>", @context.User.Email, @context.User.Email))</td>
<td>@((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "")</td>
</Row>
</Pager>
@ -259,6 +251,21 @@ else
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authresponsetype" HelpText="Specify the authorization response type" ResourceKey="AuthResponseType">Authorization Response Type</Label>
<div class="col-sm-9">
<select id="authresponsetype" class="form-select" @bind="@_authresponsetype" required>
<option value="code">@Localizer["AuthFlow.Code"]</option>
<option value="code id_token">@Localizer["AuthFlow.CodeIdToken"]</option>
<option value="code id_token token">@Localizer["AuthFlow.CodeIdTokenToken"]</option>
<option value="code token">@Localizer["AuthFlow.CodeToken"]</option>
<option value="id_token">@Localizer["AuthFlow.IdToken"]</option>
<option value="id_token token">@Localizer["AuthFlow.IdTokenToken"]</option>
<option value="token">@Localizer["AuthFlow.Token"]</option>
<option value="none">@Localizer["AuthFlow.None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
@ -365,9 +372,7 @@ else
}
@code {
private List<UserRole> allusers;
private List<UserRole> users;
private string _search = "";
private string _allowregistration;
private string _allowsitelogin;
@ -397,6 +402,7 @@ else
private string _scopes;
private string _parameters;
private string _pkce;
private string _authresponsetype;
private string _redirecturl;
private string _identifierclaimtype;
private string _emailclaimtype;
@ -420,7 +426,6 @@ else
protected override async Task OnInitializedAsync()
{
await LoadUserSettingsAsync();
await LoadUsersAsync(true);
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
@ -455,6 +460,7 @@ else
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
@ -475,32 +481,14 @@ else
{
if (load)
{
allusers = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
var hosts = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Host);
allusers.AddRange(hosts);
allusers = allusers.OrderBy(u => u.User.DisplayName).ToList();
users.AddRange(hosts);
users = users.OrderBy(u => u.User.DisplayName).ToList();
}
}
users = allusers;
if (!string.IsNullOrEmpty(_search))
{
users = users.Where(item =>
(
item.User.Username.Contains(_search, StringComparison.OrdinalIgnoreCase) ||
item.User.Email.Contains(_search, StringComparison.OrdinalIgnoreCase) ||
item.User.DisplayName.Contains(_search, StringComparison.OrdinalIgnoreCase)
)
).ToList();
}
}
private async Task OnSearch()
{
await UpdateUserSettingsAsync();
await LoadUsersAsync(false);
}
private async Task DeleteUser(UserRole UserRole)
@ -523,21 +511,6 @@ else
}
}
private string settingSearch = "AU-search";
private async Task LoadUserSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_search = SettingService.GetSetting(settings, settingSearch, "");
}
private async Task UpdateUserSettingsAsync()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
settings = SettingService.SetSetting(settings, settingSearch, _search);
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
}
private async Task SaveSiteSettings()
{
try
@ -576,6 +549,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);

View File

@ -0,0 +1,69 @@
@namespace Oqtane.Modules.Admin.Users
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject IStringLocalizer<Users> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="importfile" HelpText="Upload or select a tab delimited text file containing user information. The file must be in the Template format specified (Roles can be specified as a comma delimited list)." ResourceKey="ImportFile">Import File:</Label>
<div class="col-sm-9">
<FileManager Id="importfile" @ref="_filemanager" Filter="txt" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="notify" HelpText="Indicate if new users should receive an email notification" ResourceKey="Notify">Notify? </Label>
<div class="col-sm-9">
<select id="notify" class="form-select" @bind="@_notify" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="ImportUsers">@Localizer["Import"]</button>&nbsp;
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>&nbsp;
<a class="btn btn-info" href="/users.txt" target="_new">@Localizer["Template"]</a>
@code {
private FileManager _filemanager;
public override string Title => "Import Users";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
private string _notify = "True";
private async Task ImportUsers()
{
try
{
var fileid = _filemanager.GetFileId();
if (fileid != -1)
{
ShowProgressIndicator();
var results = await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid, bool.Parse(_notify));
if (bool.Parse(results["Success"]))
{
AddModuleMessage(string.Format(Localizer["Message.Import.Success"], results["Users"]), MessageType.Success);
}
else
{
AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error);
}
HideProgressIndicator();
}
else
{
AddModuleMessage(Localizer["Message.Import.Validation"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Importing Users {Error}", ex.Message);
AddModuleMessage(Localizer["Error.Import"], MessageType.Error);
}
}
}

View File

@ -25,6 +25,8 @@
@code {
private string _text = string.Empty;
private int _moduleId = -1;
private string _path = string.Empty;
private string _parameters = string.Empty;
private string _url = string.Empty;
private List<Permission> _permissions;
@ -41,10 +43,13 @@
public string Text { get; set; } // optional - defaults to Action if not specified
[Parameter]
public string Parameters { get; set; } // optional - querystring parameters should be in the form of "id=x&name=y"
public int ModuleId { get; set; } = -1; // optional - allows the link to target a specific moduleid
[Parameter]
public int ModuleId { get; set; } = -1; // optional - allows the link to target a specific moduleid
public string Path { get; set; } = null; // optional - allows the link to target a specific page
[Parameter]
public string Parameters { get; set; } // optional - querystring parameters should be in the form of "id=x&name=y"
[Parameter]
public Action OnClick { get; set; } = null; // optional - executes a method in the calling component
@ -79,6 +84,7 @@
[Parameter]
public string ReturnUrl { get; set; } // optional - used to set a url to redirect to
protected override void OnInitialized()
{
if (!string.IsNullOrEmpty(Permissions))
@ -102,6 +108,18 @@
_text = string.Empty;
}
_moduleId = ModuleState.ModuleId;
if (ModuleId != -1)
{
_moduleId = ModuleId;
}
_path = PageState.Page.Path;
if (Path != null)
{
_path = Path;
}
if (!string.IsNullOrEmpty(Parameters))
{
_parameters = Parameters;
@ -133,7 +151,8 @@
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_text = Localize(nameof(Text), _text);
_url = (ModuleId == -1) ? EditUrl(Action, _parameters) : EditUrl(ModuleId, Action, _parameters);
_url = EditUrl(_path, _moduleId, Action, _parameters);
if (!string.IsNullOrEmpty(ReturnUrl))
{
_url += ((_url.Contains("?")) ? "&" : "?") + $"returnurl={WebUtility.UrlEncode(ReturnUrl)}";

View File

@ -40,6 +40,13 @@
</div>
</div>
}
else
{
if (GetFileId() != -1 && !UploadMultiple)
{
<input class="form-control" @bind="@_file.Name" disabled />
}
}
@if (ShowUpload && _haseditpermission)
{
<div class="row">
@ -55,7 +62,7 @@
</div>
<div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFiles">@SharedLocalizer["Upload"]</button>
@if (GetFileId() != -1)
@if (GetFileId() != -1 && !UploadMultiple)
{
<button type="button" class="btn btn-danger mx-1" @onclick="DeleteFile">@SharedLocalizer["Delete"]</button>
}
@ -341,7 +348,8 @@
string restricted = "";
foreach (var upload in uploads)
{
var extension = (upload.LastIndexOf(".") != -1) ? upload.Substring(upload.LastIndexOf(".") + 1) : "";
var filename = upload.Split(':')[0];
var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : "";
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
{
restricted += (restricted == "" ? "" : ",") + extension;
@ -368,24 +376,27 @@
while (upload < uploads.Length && success)
{
success = false;
// note that progressive retry will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
var filename = uploads[upload].Split(':')[0];
var size = Int64.Parse(uploads[upload].Split(':')[1]);
var maxattempts = (int)Math.Ceiling(size / 500000.0) + 4; // 30 MB takes 1 minute at 5 Mbps (min 5 attempts)
int attempts = 0;
while (attempts < 5 && !success)
while (attempts < maxattempts && !success)
{
attempts += 1;
Thread.Sleep(1000 * attempts); // progressive retry
Thread.Sleep(1000);
if (Folder == Constants.PackagesFolder)
{
var files = await FileService.GetFilesAsync(folder);
if (files != null && files.Any(item => item.Name == uploads[upload]))
if (files != null && files.Any(item => item.Name == filename))
{
success = true;
}
}
else
{
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[upload]);
var file = await FileService.GetFileAsync(int.Parse(folder), filename);
if (file != null)
{
success = true;
@ -433,7 +444,7 @@
else
{
// set FileId to first file in upload collection
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0]);
var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0].Split(":")[0]);
if (file != null)
{
FileId = file.FileId;

View File

@ -1,10 +1,20 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@inject IStringLocalizerFactory LocalizerFactory
@inject IStringLocalizer<SharedResources> SharedLocalizer
@typeparam TableItem
@if (ItemList != null)
{
@if (!string.IsNullOrEmpty(SearchProperties))
{
<div class="input-group my-3">
<input id="search" class="form-control" placeholder=@string.Format(Localizer["SearchPlaceholder"], FormatSearchProperties()) @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>
}
@if ((Toolbar == "Top" || Toolbar == "Both") && _pages > 0 && Items.Count() > _maxItems)
{
<ul class="pagination justify-content-center my-2">
@ -175,6 +185,9 @@
private int _startPage = 0;
private int _endPage = 0;
private int _columns = 0;
private string _search = "";
private IEnumerable<TableItem> AllItems;
[Parameter]
public string Format { get; set; } // Table or Grid
@ -221,6 +234,9 @@
[Parameter]
public Action<int> OnPageChange { get; set; } // a method to be executed in the calling component when the page changes
[Parameter]
public string SearchProperties { get; set; } // comma delimited list of property names to include in search
private IEnumerable<TableItem> ItemList { get; set; }
protected override void OnInitialized()
@ -276,6 +292,15 @@
}
}
if (!string.IsNullOrEmpty(SearchProperties))
{
AllItems = Items; // only used in search
if (!string.IsNullOrEmpty(_search))
{
Search();
}
}
if (!string.IsNullOrEmpty(PageSize))
{
_maxItems = int.Parse(PageSize);
@ -369,4 +394,75 @@
UpdateList(_page);
}
public void Search()
{
if (!string.IsNullOrEmpty(_search))
{
Items = AllItems.Where(item =>
{
var values = SearchProperties.Split(',')
.Select(itemType => GetPropertyValue(item, itemType))
.Where(value => value != null)
.Select(value => value.ToString().ToLower());
return values.Any(value => value.Contains(_search.ToLower()));
}).ToList();
}
else
{
Items = AllItems;
}
_pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems);
UpdateList(1);
}
private object GetPropertyValue(object obj, string propertyName)
{
var index = propertyName.IndexOf(".");
if (index != -1)
{
var propertyInfo = obj.GetType().GetProperty(propertyName.Substring(0, index));
if (propertyInfo != null)
{
return GetPropertyValue(propertyInfo.GetValue(obj), propertyName.Substring(index + 1));
}
return null;
}
else
{
var propertyInfo = obj.GetType().GetProperty(propertyName);
if (propertyInfo != null)
{
return propertyInfo.GetValue(obj);
}
return null;
}
}
public void Reset()
{
_search = "";
Items = AllItems;
_pages = (int)Math.Ceiling(Items.Count() / (decimal)_maxItems);
UpdateList(1);
}
private string FormatSearchProperties()
{
var properties = new List<string>();
foreach (var property in SearchProperties.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
var index = property.LastIndexOf(".");
if (index != -1)
{
properties.Add(property.Substring(index + 1));
}
else
{
properties.Add(property);
}
}
return string.Join(",", properties);
}
}

View File

@ -4,7 +4,7 @@
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -124,7 +124,7 @@
<value>Error Loading Packages</value>
</data>
<data name="Success.Module.Download" xml:space="preserve">
<value>Module Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
<value>Module Package Downloaded Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data>
<data name="Error.Module.Download" xml:space="preserve">
<value>Error Downloading Module</value>

View File

@ -132,6 +132,9 @@
<data name="Message.Require.ValidName" xml:space="preserve">
<value>You Must Provide A Valid Owner Name And Module Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template</value>
</data>
<data name="Message.Require.ValidDescription" xml:space="preserve">
<value>You Must Provide A Valid Description (ie. No Punctuation)</value>
</data>
<data name="OwnerName.HelpText" xml:space="preserve">
<value>Enter the name of the organization who is developing this module. It should not contain spaces or punctuation.</value>
</data>

View File

@ -196,7 +196,7 @@
<value>Information</value>
</data>
<data name="PackageName.HelpText" xml:space="preserve">
<value>The unique name of the package from which this module was installed</value>
<value>The unique name of the package from which this module was installed. This value must be specified within the module's IModule interface specification.</value>
</data>
<data name="PackageName.Text" xml:space="preserve">
<value>Package Name:</value>
@ -228,4 +228,16 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
</root>

View File

@ -159,4 +159,10 @@
<data name="Module Settings" xml:space="preserve">
<value>Module Settings</value>
</data>
<data name="Pane.HelpText" xml:space="preserve">
<value>The pane where the module will be displayed</value>
</data>
<data name="Pane.Text" xml:space="preserve">
<value>Pane:</value>
</data>
</root>

View File

@ -138,4 +138,13 @@
<data name="EditProfile.Text" xml:space="preserve">
<value>Edit</value>
</data>
<data name="Category" xml:space="preserve">
<value>Category</value>
</data>
<data name="Order" xml:space="preserve">
<value>Order</value>
</data>
<data name="Title" xml:space="preserve">
<value>Title</value>
</data>
</root>

View File

@ -136,7 +136,7 @@
<value>User Account Created. Please Check Your Email For Verification Instructions.</value>
</data>
<data name="Error.User.AddInfo" xml:space="preserve">
<value>Error Adding User. Please Ensure Password Meets Complexity Requirements And Username Is Not Already In Use.</value>
<value>Error Adding User. Please Ensure Password Meets Complexity Requirements And Username And Email Is Not Already In Use.</value>
</data>
<data name="Message.Password.NoMatch" xml:space="preserve">
<value>Passwords Entered Do Not Match</value>

View File

@ -390,4 +390,16 @@
<data name="DeleteAlias.Header" xml:space="preserve">
<value>Delete Alias</value>
</data>
<data name="SiteGuid.HelpText" xml:space="preserve">
<value>The Unique Identifier For The Site</value>
</data>
<data name="SiteGuid.Text" xml:space="preserve">
<value>ID:</value>
</data>
<data name="Retention.HelpText" xml:space="preserve">
<value>Number of days of notifications to retain</value>
</data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
</root>

View File

@ -124,7 +124,7 @@
<value>Theme: </value>
</data>
<data name="Success.Theme.Download" xml:space="preserve">
<value>Theme Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
<value>Theme Package Downloaded Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Complete The Installation.</value>
</data>
<data name="Error.Theme.Download" xml:space="preserve">
<value>Error Downloading Theme</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -163,7 +163,7 @@
<value>The license of the theme</value>
</data>
<data name="PackageName.HelpText" xml:space="preserve">
<value>The unique name of the package from which this module was installed</value>
<value>The unique name of the package from which this theme was installed. This value must be specified within the themee's ITheme interface specification.</value>
</data>
<data name="PackageName.Text" xml:space="preserve">
<value>Package Name:</value>
@ -180,4 +180,16 @@
<data name="View License" xml:space="preserve">
<value>View License</value>
</data>
<data name="Error.Validate" xml:space="preserve">
<value>Error Validating Package</value>
</data>
<data name="Message.Download" xml:space="preserve">
<value>Package Version Has Been Verified. Please Select The Download Button To Obtain The Package.</value>
</data>
<data name="Message.Validate" xml:space="preserve">
<value>This Package Version Has Not Been Registered In The Oqtane Marketplace Or You Do Not Have The Right To Use It From This Installation</value>
</data>
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
</root>

View File

@ -144,7 +144,10 @@
<data name="Message.Text" xml:space="preserve">
<value>Framework Is Already Up To Date</value>
</data>
<data name="MessgeUpgrade.Text" xml:space="preserve">
<data name="MessageUpgrade.Text" xml:space="preserve">
<value>Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade</value>
</data>
<data name="Localhost.Text" xml:space="preserve">
<value>You Cannot Perform A System Update In A Development Environment</value>
</data>
</root>

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Error.User.AddCheckPass" xml:space="preserve">
<value>Error Adding User. Please Ensure Password Meets Complexity Requirements And Username Is Not Already In Use.</value>
<value>Error Adding User. Please Ensure Password Meets Complexity Requirements And Username And Email Is Not Already In Use.</value>
</data>
<data name="Message.Password.NoMatch" xml:space="preserve">
<value>Passwords Entered Do Not Match</value>
@ -171,4 +171,10 @@
<data name="Password.Placeholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="Notify.HelpText" xml:space="preserve">
<value>Indicate if new users should receive an email notification</value>
</data>
<data name="Notify.Text" xml:space="preserve">
<value>Notify?</value>
</data>
</root>

View File

@ -402,4 +402,37 @@
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Email" xml:space="preserve">
<value>Email</value>
</data>
<data name="ImportUsers.Text" xml:space="preserve">
<value>Import Users</value>
</data>
<data name="AuthFlow.Code" xml:space="preserve">
<value>code</value>
</data>
<data name="AuthFlow.CodeIdToken" xml:space="preserve">
<value>code id_token</value>
</data>
<data name="AuthFlow.CodeIdTokenToken" xml:space="preserve">
<value>code id_token token</value>
</data>
<data name="AuthFlow.CodeToken" xml:space="preserve">
<value>code token</value>
</data>
<data name="AuthFlow.IdToken" xml:space="preserve">
<value>id_token</value>
</data>
<data name="AuthFlow.IdTokenToken" xml:space="preserve">
<value>id_token token</value>
</data>
<data name="AuthFlow.None" xml:space="preserve">
<value>none</value>
</data>
<data name="AuthFlow.Token" xml:space="preserve">
<value>token</value>
</data>
<data name="AuthResponseType" xml:space="preserve">
<value>Authorization Response Type</value>
</data>
</root>

View File

@ -117,61 +117,34 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Template.Text" xml:space="preserve">
<value>Template: </value>
<data name="ImportFile.HelpText" xml:space="preserve">
<value>Upload or select a tab delimited text file containing user information. The file must be in the Template format specified (Roles can be specified as a comma delimited list).</value>
</data>
<data name="Template.Select" xml:space="preserve">
<value>Select Template</value>
<data name="ImportFile.Text" xml:space="preserve">
<value>Import File:</value>
</data>
<data name="Module.Create" xml:space="preserve">
<value>Create Module</value>
<data name="Error.Import" xml:space="preserve">
<value>Error Importing Users</value>
</data>
<data name="Module.Activate" xml:space="preserve">
<value>Activate Module</value>
<data name="Import" xml:space="preserve">
<value>Import</value>
</data>
<data name="Info.Module.Creator" xml:space="preserve">
<value>Please Note That The Module Creator Is Only Intended To Be Used In A Development Environment</value>
<data name="Message.Import.Failure" xml:space="preserve">
<value>User Import Failed. Please Review Your Event Log For More Detailed Information.</value>
</data>
<data name="Info.Module.Activate" xml:space="preserve">
<value>Once You Have Compiled The Module And Restarted The Application You Can Activate The Module Below</value>
<data name="Message.Import.Success" xml:space="preserve">
<value>User Import Successful. {0} Users Imported.</value>
</data>
<data name="Success.Module.Create" xml:space="preserve">
<value>The Source Code For Your Module Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must &lt;a href={0}&gt;Restart&lt;/a&gt; Your Application To Apply These Changes.</value>
<data name="Message.Import.Validation" xml:space="preserve">
<value>You Must Specify A User File For Import</value>
</data>
<data name="Message.Require.ValidName" xml:space="preserve">
<value>You Must Provide A Valid Owner Name And Module Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template</value>
<data name="Template" xml:space="preserve">
<value>Template</value>
</data>
<data name="OwnerName.HelpText" xml:space="preserve">
<value>Enter the name of the organization who is developing this module. It should not contain spaces or punctuation.</value>
<data name="Notify.HelpText" xml:space="preserve">
<value>Indicate if new users should receive an email notification</value>
</data>
<data name="ModuleName.HelpText" xml:space="preserve">
<value>Enter a name for this module. It should not contain spaces or punctuation.</value>
</data>
<data name="Description.HelpText" xml:space="preserve">
<value>Enter a short description for the module</value>
</data>
<data name="Template.HelpText" xml:space="preserve">
<value>Select a module template. Templates are located in the wwwroot/Modules/Templates folder on the server.</value>
</data>
<data name="FrameworkReference.HelpText" xml:space="preserve">
<value>Select a framework reference version</value>
</data>
<data name="Location.HelpText" xml:space="preserve">
<value>Location where the module will be created</value>
</data>
<data name="OwnerName.Text" xml:space="preserve">
<value>Owner Name: </value>
</data>
<data name="ModuleName.Text" xml:space="preserve">
<value>Module Name: </value>
</data>
<data name="Description.Text" xml:space="preserve">
<value>Description: </value>
</data>
<data name="FrameworkReference.Text" xml:space="preserve">
<value>Framework Reference: </value>
</data>
<data name="Location.Text" xml:space="preserve">
<value>Location: </value>
<data name="Notify.Text" xml:space="preserve">
<value>Notify?</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -120,4 +120,7 @@
<data name="PageOfPages" xml:space="preserve">
<value>Page {0} of {1}</value>
</data>
<data name="SearchPlaceholder" xml:space="preserve">
<value>Search: {0}</value>
</data>
</root>

View File

@ -426,4 +426,10 @@
<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="ProfileInvalid" xml:space="preserve">
<value>{0} Is Not Valid</value>
</data>
<data name="ProfileRequired" xml:space="preserve">
<value>{0} Is Required</value>
</data>
</root>

View File

@ -189,4 +189,13 @@
<data name="Confirm.Page.Delete" xml:space="preserve">
<value>Are You Sure You Want To Delete This Page?</value>
</data>
<data name="Location" xml:space="preserve">
<value>Location:</value>
</data>
<data name="LocationBottom" xml:space="preserve">
<value>Bottom</value>
</data>
<data name="LocationTop" xml:space="preserve">
<value>Top</value>
</data>
</root>

View File

@ -39,11 +39,10 @@ namespace Oqtane.Services
Task<List<Package>> GetPackagesAsync(string type, string search, string price, string package, string sort);
/// <summary>
/// Returns a list of packages matching the list of package names
/// Returns a list of packages based on installationid
/// </summary>
/// <param name="names"></param>
/// <returns></returns>
Task<List<Package>> GetPackagesAsync(List<string> packagenames);
Task<List<Package>> GetPackageUpdatesAsync(string type);
/// <summary>
/// Returns a specific package
@ -51,7 +50,7 @@ namespace Oqtane.Services
/// <param name="packageId"></param>
/// <param name="version"></param>
/// <returns></returns>
Task<Package> GetPackageAsync(string packageId, string version);
Task<Package> GetPackageAsync(string packageId, string version, bool download);
/// <summary>
/// Downloads a specific package as .nupkg file
@ -60,7 +59,7 @@ namespace Oqtane.Services
/// <param name="version"></param>
/// <param name="folder"></param>
/// <returns></returns>
Task DownloadPackageAsync(string packageId, string version, string folder);
Task DownloadPackageAsync(string packageId, string version);
/// <summary>
/// Installs all packages located in //TODO: 2dm where?

View File

@ -142,5 +142,14 @@ namespace Oqtane.Services
/// <param name="siteId">ID of a <see cref="Site"/></param>
/// <returns></returns>
Task<string> GetPasswordRequirementsAsync(int siteId);
/// <summary>
/// Bulk import of users
/// </summary>
/// <param name="siteId">ID of a <see cref="Site"/></param>
/// <param name="fileId">ID of a <see cref="File"/></param>
/// <param name="notify">Indicates if new users should be notified by email</param>
/// <returns></returns>
Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify);
}
}

View File

@ -31,19 +31,19 @@ namespace Oqtane.Services
return await GetJsonAsync<List<Package>>($"{Apiurl}?type={type}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}");
}
public async Task<List<Package>> GetPackagesAsync(List<string> packagenames)
public async Task<List<Package>> GetPackageUpdatesAsync(string type)
{
return await GetJsonAsync<List<Package>>($"{Apiurl}/list/?names={string.Join(",", packagenames)}");
return await GetJsonAsync<List<Package>>($"{Apiurl}/updates/?type={type}");
}
public async Task<Package> GetPackageAsync(string packageId, string version)
public async Task<Package> GetPackageAsync(string packageId, string version, bool download)
{
return await PostJsonAsync<Package>($"{Apiurl}?packageid={packageId}&version={version}", null);
return await PostJsonAsync<Package>($"{Apiurl}?packageid={packageId}&version={version}&download={download}&install=false", null);
}
public async Task DownloadPackageAsync(string packageId, string version, string folder)
public async Task DownloadPackageAsync(string packageId, string version)
{
await PostAsync($"{Apiurl}?packageid={packageId}&version={version}&folder={folder}");
await PostAsync($"{Apiurl}?packageid={packageId}&version={version}&download=true&install=true");
}
public async Task InstallPackagesAsync()

View File

@ -4,6 +4,7 @@ using System.Net.Http;
using System;
using Oqtane.Documentation;
using Oqtane.Shared;
using System.Globalization;
namespace Oqtane.Services
{
@ -18,7 +19,7 @@ namespace Oqtane.Services
/// <inheritdoc />
public async Task<Sync> GetSyncEventsAsync(DateTime lastSyncDate)
{
return await GetJsonAsync<Sync>($"{ApiUrl}/{lastSyncDate.ToString("yyyyMMddHHmmssfff")}");
return await GetJsonAsync<Sync>($"{ApiUrl}/{lastSyncDate.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture)}");
}
}
}

View File

@ -6,6 +6,9 @@ using Oqtane.Documentation;
using System.Net;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Oqtane.Modules.Admin.Roles;
using System.Xml.Linq;
namespace Oqtane.Services
{
@ -123,5 +126,10 @@ namespace Oqtane.Services
// format requirements
return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement);
}
public async Task<Dictionary<string, string>> ImportUsersAsync(int siteId, int fileId, bool notify)
{
return await PostJsonAsync<Dictionary<string, string>>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}&notify={notify}", null);
}
}
}

View File

@ -15,7 +15,7 @@
<div class="main g-0">
<div class="top-row px-4">
<div class="ms-auto"><UserProfile /> <Login /> <ControlPanel /></div>
<div class="ms-auto"><UserProfile /> <Login /> <ControlPanel LanguageDropdownAlignment="right" /></div>
</div>
<div class="container">
<div class="row px-4">

View File

@ -10,12 +10,14 @@
@inject IPageModuleService PageModuleService
@inject ILogService logger
@inject ISettingService SettingService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<ControlPanel> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (ShowLanguageSwitcher)
{
<LanguageSwitcher />
<LanguageSwitcher DropdownAlignment="@LanguageDropdownAlignment" />
}
@if (_showEditMode || (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered)))
@ -109,10 +111,7 @@
</div>
</div>
}
}
@if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{
<div class="row">
<div class="col text-center">
<label for="Module" class="control-label">@Localizer["Module.Manage"]</label>
@ -184,8 +183,6 @@
<input type="text" name="Title" class="form-control" @bind="@Title" />
</div>
</div>
@if (_pane.Length > 1)
{
<div class="row">
<div class="col text-center">
<label for="Pane" class="control-label">@Localizer["Pane"]</label>
@ -197,7 +194,15 @@
</select>
</div>
</div>
}
<div class="row">
<div class="col text-center">
<label for="Insert" class="control-label">@Localizer["Location"]</label>
<select class="form-select" @bind="@Location">
<option value="@int.MinValue">@Localizer["LocationTop"]</option>
<option value="@int.MaxValue">@Localizer["LocationBottom"]</option>
</select>
</div>
</div>
<div class="row">
<div class="col text-center">
<label for="Container" class="control-label">@Localizer["Container"]</label>
@ -220,13 +225,38 @@
</div>
<button type="button" class="btn btn-primary col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button>
@((MarkupString)Message)
<hr class="app-rule" />
}
<div class="row d-flex">
<div class="col">
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-secondary col-12" @onclick=@(async () => await LogoutUser())>@Localizer["Logout"]</button>
</div>
</div>
</div>
</div>
</div>
}
@code{
[Parameter]
public string ButtonClass { get; set; } = "btn-outline-secondary";
[Parameter]
public string ContainerClass { get; set; } = "offcanvas offcanvas-end";
[Parameter]
public string HeaderClass { get; set; } = "offcanvas-header";
[Parameter]
public string BodyClass { get; set; } = "offcanvas-body overflow-auto";
[Parameter]
public bool ShowLanguageSwitcher { get; set; } = true;
[Parameter]
public string LanguageDropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right
private bool _canViewAdminDashboard = false;
private bool _showEditMode = false;
private bool _deleteConfirmation = false;
@ -237,12 +267,22 @@
private List<Module> _modules = new List<Module>();
private List<ThemeControl> _containers = new List<ThemeControl>();
private string _category = "Common";
private string _pane = "";
protected string PageId { get; private set; } = "-";
protected string ModuleId { get; private set; } = "-";
protected string ModuleType { get; private set; } = "new";
protected string ModuleDefinitionName { get; private set; } = "-";
protected string Title { get; private set; } = "";
protected string ContainerType { get; private set; } = "";
protected int Location { get; private set; } = int.MaxValue;
protected string Visibility { get; private set; } = "view";
protected string Message { get; private set; } = "";
private string settingCategory = "CP-category";
private string settingPane = "CP-pane";
protected string Category
{
get => _category;
@ -273,27 +313,6 @@
}
}
protected string Title { get; private set; } = "";
protected string ContainerType { get; private set; } = "";
protected string Visibility { get; private set; } = "view";
protected string Message { get; private set; } = "";
[Parameter]
public string ButtonClass { get; set; } = "btn-outline-secondary";
[Parameter]
public string ContainerClass { get; set; } = "offcanvas offcanvas-end";
[Parameter]
public string HeaderClass { get; set; } = "offcanvas-header";
[Parameter]
public string BodyClass { get; set; } = "offcanvas-body overflow-auto";
[Parameter]
public bool ShowLanguageSwitcher { get; set; } = true;
protected override async Task OnParametersSetAsync()
{
_canViewAdminDashboard = CanViewAdminDashboard();
@ -301,8 +320,9 @@
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList))
{
_showEditMode = true;
_pages?.Clear();
LoadSettingsAsync();
_pages?.Clear();
foreach (Page p in PageState.Pages)
{
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList))
@ -310,7 +330,6 @@
_pages.Add(p);
}
}
await LoadSettingsAsync();
var themes = await ThemeService.GetThemesAsync();
_containers = ThemeService.GetContainerControls(themes, PageState.Page.ThemeType);
@ -434,7 +453,7 @@
}
pageModule.Pane = Pane;
pageModule.Order = int.MaxValue;
pageModule.Order = Location;
pageModule.ContainerType = ContainerType;
if (pageModule.ContainerType == PageState.Site.DefaultContainerType)
@ -613,15 +632,41 @@
}
}
private string settingCategory = "CP-category";
private string settingPane = "CP-pane";
private string _pane = "";
private async Task LoadSettingsAsync()
// the following code is duplicated from LoginBase
private async Task LogoutUser()
{
Dictionary<string, string> settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
_category = SettingService.GetSetting(settings, settingCategory, "Common");
var pane = SettingService.GetSetting(settings, settingPane, "");
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);
Route route = new Route(PageState.Uri.AbsoluteUri, PageState.Alias.Path);
var url = route.PathAndQuery;
// verify if anonymous users can access page
if (!UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.PermissionList))
{
url = PageState.Alias.Path;
}
if (PageState.Runtime == Shared.Runtime.Hybrid)
{
// hybrid apps utilize an interactive logout
await UserService.LogoutUserAsync(PageState.User);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(url, true);
}
else
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}
}
private void LoadSettingsAsync()
{
_category = SettingService.GetSetting(PageState.User.Settings, settingCategory, "Common");
var pane = SettingService.GetSetting(PageState.User.Settings, settingPane, "");
if (PageState.Page.Panes.Contains(pane))
{
_pane = pane;

View File

@ -8,11 +8,11 @@
@if (_supportedCultures?.Count() > 1)
{
<div class="btn-group" role="group">
<div class="btn-group pe-1" role="group">
<button id="btnCultures" type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="oi oi-globe"></span>
</button>
<div class="dropdown-menu" aria-labelledby="btnCultures">
<div class="dropdown-menu @MenuAlignment" aria-labelledby="btnCultures">
@foreach (var culture in _supportedCultures)
{
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="#" @onclick="@(async e => await SetCultureAsync(culture.Name))">@culture.DisplayName</a>
@ -23,9 +23,14 @@
@code{
private IEnumerable<Culture> _supportedCultures;
[Parameter]
public string DropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right
private string MenuAlignment = string.Empty;
protected override void OnParametersSet()
{
MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty;
var languages = PageState.Languages;
_supportedCultures = languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name });
}

View File

@ -17,6 +17,7 @@ namespace Oqtane.Themes.Controls
{
[Inject] public NavigationManager NavigationManager { get; set; }
[Inject] public IUserService UserService { get; set; }
[Inject] public ISettingService SettingService { get; set; }
[Inject] public IJSRuntime jsRuntime { get; set; }
[Inject] public IServiceProvider ServiceProvider { get; set; }

View File

@ -6,7 +6,7 @@
<nav class="navbar navbar-dark bg-primary fixed-top">
<Logo /><Menu Orientation="Horizontal" />
<div class="controls ms-auto">
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel /></div>
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel LanguageDropdownAlignment="right" /></div>
</div>
</nav>
<div class="content">

View File

@ -146,6 +146,10 @@
{
editmode = false; // reset edit mode when navigating to different page
}
if (querystring.ContainsKey("edit") && querystring["edit"] == "true")
{
editmode = true; // querystring can set edit mode
}
// get user
if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
@ -290,7 +294,7 @@
var urlMapping = await UrlMappingService.GetUrlMappingAsync(site.SiteId, route.PagePath);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl;
var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl + route.Query;
NavigationManager.NavigateTo(url, false);
}
else // not mapped

View File

@ -72,7 +72,7 @@
while (index >= 0)
{
var element = content.Substring(index, content.IndexOf(">", index) - index + 1);
if (!string.IsNullOrEmpty(element) && !element.ToLower().StartsWith("<script"))
if (!string.IsNullOrEmpty(element) && !element.ToLower().StartsWith("<script") && !element.ToLower().StartsWith("</script"))
{
if (!headcontent.Contains(element))
{
@ -166,7 +166,7 @@
if (!string.IsNullOrEmpty(src))
{
src = (src.Contains("://")) ? src : PageState.Alias.BaseUrl + src;
scripts.Add(new { href = src, bundle = "", integrity = integrity, crossorigin = crossorigin, es6module = (type == "module"), location = location });
scripts.Add(new { href = src, bundle = "", integrity = integrity, crossorigin = crossorigin, es6module = (type == "module"), location = location.ToString().ToLower() });
}
else
{

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

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

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

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

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.SqlServer</id>
<version>4.0.3</version>
<version>4.0.6</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQL Server Provider</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

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

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>4.0.3</ApplicationDisplayVersion>
<ApplicationDisplayVersion>4.0.6</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Client</id>
<version>4.0.3</version>
<version>4.0.6</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>4.0.3</version>
<version>4.0.6</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v4.0.3/Oqtane.Framework.4.0.3.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v4.0.6/Oqtane.Framework.4.0.6.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane framework</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Server</id>
<version>4.0.3</version>
<version>4.0.6</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Shared</id>
<version>4.0.3</version>
<version>4.0.6</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Updater</id>
<version>4.0.3</version>
<version>4.0.6</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.3.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.6.Install.zip" -Force

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.3.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.6.Upgrade.zip" -Force

View File

@ -139,14 +139,14 @@ namespace Oqtane.Controllers
if (moduleDefinition.Template.ToLower().Contains("internal"))
{
rootPath = Utilities.PathCombine(rootFolder.FullName, Path.DirectorySeparatorChar.ToString());
moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", Oqtane.Client";
moduleDefinition.ServerManagerType = moduleDefinition.ModuleDefinitionName + ".Manager." + moduleDefinition.Name + "Manager, Oqtane.Server";
moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", Oqtane.Client";
}
else
{
rootPath = Utilities.PathCombine(rootFolder.Parent.FullName, moduleDefinition.Owner + ".Module." + moduleDefinition.Name, Path.DirectorySeparatorChar.ToString());
moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", " + moduleDefinition.ModuleDefinitionName + ".Client.Oqtane";
moduleDefinition.ServerManagerType = moduleDefinition.ModuleDefinitionName + ".Manager." + moduleDefinition.Name + "Manager, " + moduleDefinition.ModuleDefinitionName + ".Server.Oqtane";
moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", " + moduleDefinition.ModuleDefinitionName + ".Client.Oqtane";
}
ProcessTemplatesRecursively(new DirectoryInfo(templatePath), rootPath, rootFolder.Name, templatePath, moduleDefinition);

View File

@ -51,20 +51,20 @@ namespace Oqtane.Controllers
return packages;
}
// GET: api/<controller>/list/?names=x,y,z
[HttpGet("list")]
public async Task<IEnumerable<Package>> GetPackages(string names)
// GET: api/<controller>/updates/?type=x
[HttpGet("updates")]
public async Task<IEnumerable<Package>> GetPackageUpdates(string type)
{
// get packages
List<Package> packages = new List<Package>();
var url = _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl);
if (!string.IsNullOrEmpty(url) && !string.IsNullOrEmpty(names))
if (!string.IsNullOrEmpty(url))
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, url + $"/api/registry/list/?id={_configManager.GetInstallationId()}&version={Constants.Version}&list={names}");
packages = await GetJson<List<Package>>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}");
}
}
return packages;
@ -72,14 +72,13 @@ namespace Oqtane.Controllers
[HttpPost]
[Authorize(Roles = RoleNames.Host)]
public async Task<Package> Post(string packageid, string version, string folder)
public async Task<Package> Post(string packageid, string version, string download, string install)
{
// get package info
Package package = null;
var url = _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl);
if (!string.IsNullOrEmpty(url))
{
var download = (string.IsNullOrEmpty(folder)) ? "false" : "true";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
@ -89,16 +88,16 @@ namespace Oqtane.Controllers
if (package != null)
{
if (bool.Parse(download))
if (bool.Parse(install))
{
using (var httpClient = new HttpClient())
{
folder = Path.Combine(_environment.ContentRootPath, folder);
var folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
var response = await httpClient.GetAsync(package.PackageUrl).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
string filename = packageid + "." + version + ".nupkg";
using (var fileStream = new FileStream(Path.Combine(folder, filename), FileMode.Create, FileAccess.Write, FileShare.None))
using (var fileStream = new FileStream(Path.Combine(Constants.PackagesFolder, filename), FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
}
@ -112,17 +111,19 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "Package {PackageId}.{Version} Is Not Registered", packageid, version);
_logger.Log(LogLevel.Error, this, LogFunction.Create, "Package {PackageId}.{Version} Is Not Registered In The Marketplace", packageid, version);
}
}
return package;
}
private async Task<T> GetJson<T>(HttpClient httpClient, string url)
{
try
{
Uri uri = new Uri(url);
var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode && ValidateJsonContent(response.Content))
{
var stream = await response.Content.ReadAsStreamAsync();
using (var streamReader = new StreamReader(stream))
@ -132,6 +133,18 @@ namespace Oqtane.Controllers
}
return default(T);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, ex, "Error Accessing Marketplace API {Url}", url);
return default(T);
}
}
private static bool ValidateJsonContent(HttpContent content)
{
var mediaType = content?.Headers.ContentType?.MediaType;
return mediaType != null && mediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase);
}
[HttpGet("install")]
[Authorize(Roles = RoleNames.Host)]

View File

@ -280,16 +280,10 @@ namespace Oqtane.Controllers
if (currentPage.Path != page.Path)
{
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path);
if (urlMapping == null)
if (urlMapping != null)
{
urlMapping = new UrlMapping();
urlMapping.SiteId = page.SiteId;
urlMapping.Url = currentPage.Path;
urlMapping.MappedUrl = page.Path;
urlMapping.Requests = 0;
urlMapping.CreatedOn = System.DateTime.UtcNow;
urlMapping.RequestedOn = System.DateTime.UtcNow;
_urlMappings.AddUrlMapping(urlMapping);
_urlMappings.UpdateUrlMapping(urlMapping);
}
}

View File

@ -9,7 +9,7 @@ using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using System.Net;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace Oqtane.Controllers
{
@ -133,14 +133,22 @@ namespace Oqtane.Controllers
var page = _pages.GetPage(pageid);
if (page != null && page.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageid, PermissionNames.Edit))
{
var panes = pane;
if (pane == PaneNames.Default || pane == PaneNames.Admin)
{
// treat default and admin panes as a single pane
panes = PaneNames.Default + "," + PaneNames.Admin;
pane = PaneNames.Default;
}
int order = 1;
List<PageModule> pagemodules = _pageModules.GetPageModules(page.SiteId)
.Where(item => item.PageId == pageid && item.Pane == pane).OrderBy(item => item.Order).ToList();
.Where(item => item.PageId == pageid && panes.Split(',').Contains(item.Pane)).OrderBy(item => item.Order).ToList();
foreach (PageModule pagemodule in pagemodules)
{
if (pagemodule.Order != order)
if (pagemodule.Order != order || pagemodule.Pane != pane)
{
pagemodule.Order = order;
pagemodule.Pane = pane;
_pageModules.UpdatePageModule(pagemodule);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.PageModule, pagemodule.PageModuleId, SyncEventActions.Update);
}

View File

@ -26,17 +26,21 @@ namespace Oqtane.Controllers
private readonly IUserManager _userManager;
private readonly ISiteRepository _sites;
private readonly IUserPermissions _userPermissions;
private readonly ISettingRepository _settings;
private readonly IJwtManager _jwtManager;
private readonly IFileRepository _files;
private readonly ILogManager _logger;
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ILogManager logger)
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, ISettingRepository settings, IJwtManager jwtManager, IFileRepository files, ILogManager logger)
{
_users = users;
_tenantManager = tenantManager;
_userManager = userManager;
_sites = sites;
_userPermissions = userPermissions;
_settings = settings;
_jwtManager = jwtManager;
_files = files;
_logger = logger;
}
@ -52,6 +56,12 @@ namespace Oqtane.Controllers
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
else
{
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
user.Settings = settings.Where(item => !item.IsPrivate || _userPermissions.GetUser(User).UserId == user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
return Filter(user);
}
else
@ -75,6 +85,12 @@ namespace Oqtane.Controllers
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
else
{
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
user.Settings = settings.Where(item => !item.IsPrivate || _userPermissions.GetUser(User).UserId == user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
return Filter(user);
}
else
@ -355,5 +371,41 @@ namespace Oqtane.Controllers
return requirements;
}
// POST api/<controller>/import?siteid=x&fileid=y&notify=z
[HttpPost("import")]
[Authorize(Roles = RoleNames.Admin)]
public async Task<Dictionary<string, string>> Import(string siteid, string fileid, string notify)
{
if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId) && bool.TryParse(notify, out bool Notify))
{
var file = _files.GetFile(FileId);
if (file != null)
{
if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
return await _userManager.ImportUsers(SiteId, _files.GetFilePath(file), Notify);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Import File Does Not Exist {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
}
}

View File

@ -121,6 +121,7 @@ namespace Microsoft.Extensions.DependencyInjection
public static IServiceCollection ConfigureOqtaneCookieOptions(this IServiceCollection services)
{
// note that ConfigureApplicationCookie internally uses an ApplicationScheme of "Identity.Application"
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = false;

View File

@ -28,7 +28,7 @@ namespace Oqtane.Extensions
public static OqtaneSiteOptionsBuilder WithSiteAuthentication(this OqtaneSiteOptionsBuilder builder)
{
// site cookie authentication options
builder.AddSiteOptions<CookieAuthenticationOptions>((options, alias, sitesettings) =>
builder.AddSiteNamedOptions<CookieAuthenticationOptions>(Constants.AuthenticationScheme, (options, alias, sitesettings) =>
{
options.Cookie.Name = sitesettings.GetValue("LoginOptions:CookieName", ".AspNetCore.Identity.Application");
});
@ -44,7 +44,7 @@ namespace Oqtane.Extensions
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // authorization code flow
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
// cookie config is required to avoid Correlation Failed errors

View File

@ -17,10 +17,22 @@ namespace Microsoft.Extensions.DependencyInjection
}
public OqtaneSiteOptionsBuilder AddSiteOptions<TOptions>(
Action<TOptions, Alias, Dictionary<string, string>> action) where TOptions : class, new()
Action<TOptions, Alias, Dictionary<string, string>> configureOptions) where TOptions : class, new()
{
Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, SiteOptionsCache<TOptions>>();
Services.AddSingleton<ISiteOptions<TOptions>, SiteOptions<TOptions>> (sp => new SiteOptions<TOptions>(action));
Services.AddSingleton<ISiteOptions<TOptions>, SiteOptions<TOptions>> (_ => new SiteOptions<TOptions>(configureOptions));
Services.TryAddTransient<IOptionsFactory<TOptions>, SiteOptionsFactory<TOptions>>();
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
return this;
}
public OqtaneSiteOptionsBuilder AddSiteNamedOptions<TOptions>(string name,
Action<TOptions, Alias, Dictionary<string, string>> configureOptions) where TOptions : class, new()
{
Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, SiteOptionsCache<TOptions>>();
Services.AddSingleton<ISiteNamedOptions<TOptions>, SiteNamedOptions<TOptions>>(_ => new SiteNamedOptions<TOptions>(name, configureOptions));
Services.TryAddTransient<IOptionsFactory<TOptions>, SiteOptionsFactory<TOptions>>();
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public interface ISiteNamedOptions<TOptions>
where TOptions : class, new()
{
void Configure(string name, TOptions options, Alias alias, Dictionary<string, string> sitesettings);
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using Oqtane.Models;
namespace Oqtane.Infrastructure
{
public class SiteNamedOptions<TOptions> : ISiteNamedOptions<TOptions>
where TOptions : class, new()
{
public string Name { get; }
private readonly Action<TOptions, Alias, Dictionary<string, string>> configureOptions;
public SiteNamedOptions(string name, Action<TOptions, Alias, Dictionary<string, string>> configureOptions)
{
Name = name;
this.configureOptions = configureOptions;
}
public void Configure(string name, TOptions options, Alias alias, Dictionary<string, string> sitesettings)
{
if (name == Name)
{
configureOptions(options, alias, sitesettings);
}
}
}
}

View File

@ -11,13 +11,15 @@ namespace Oqtane.Infrastructure
private readonly IConfigureOptions<TOptions>[] _configureOptions;
private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions;
private readonly ISiteOptions<TOptions>[] _siteOptions;
private readonly ISiteNamedOptions<TOptions>[] _siteNamedOptions;
private readonly IHttpContextAccessor _accessor;
public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<ISiteOptions<TOptions>> siteOptions, IHttpContextAccessor accessor)
public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<ISiteOptions<TOptions>> siteOptions, IEnumerable<ISiteNamedOptions<TOptions>> siteNamedOptions, IHttpContextAccessor accessor)
{
_configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray();
_siteOptions = siteOptions as ISiteOptions<TOptions>[] ?? new List<ISiteOptions<TOptions>>(siteOptions).ToArray();
_siteNamedOptions = siteNamedOptions as ISiteNamedOptions<TOptions>[] ?? new List<ISiteNamedOptions<TOptions>>(siteNamedOptions).ToArray();
_accessor = accessor;
}
@ -44,6 +46,11 @@ namespace Oqtane.Infrastructure
{
siteOption.Configure(options, _accessor.HttpContext.GetAlias(), _accessor.HttpContext.GetSiteSettings());
}
foreach (var siteNamedOption in _siteNamedOptions)
{
siteNamedOption.Configure(name, options, _accessor.HttpContext.GetAlias(), _accessor.HttpContext.GetSiteSettings());
}
}
// post configuration

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Models;
@ -18,5 +19,6 @@ namespace Oqtane.Managers
User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
Task<bool> ValidatePassword(string password);
Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify);
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -16,24 +17,32 @@ namespace Oqtane.Managers
public class UserManager : IUserManager
{
private readonly IUserRepository _users;
private readonly IRoleRepository _roles;
private readonly IUserRoleRepository _userRoles;
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
private readonly IFolderRepository _folders;
private readonly IFileRepository _files;
private readonly IProfileRepository _profiles;
private readonly ISettingRepository _settings;
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
public UserManager(IUserRepository users, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ILogManager logger)
public UserManager(IUserRepository users, IRoleRepository roles, IUserRoleRepository userRoles, UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, IFileRepository files, IProfileRepository profiles, ISettingRepository settings, ISyncManager syncManager, ILogManager logger)
{
_users = users;
_roles = roles;
_userRoles = userRoles;
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_notifications = notifications;
_folders = folders;
_files = files;
_profiles = profiles;
_settings = settings;
_syncManager = syncManager;
_logger = logger;
}
@ -95,6 +104,13 @@ namespace Oqtane.Managers
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser == null)
{
if (string.IsNullOrEmpty(user.Password))
{
// create random interal password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
}
identityuser = new IdentityUser();
identityuser.UserName = user.Username;
identityuser.Email = user.Email;
@ -141,12 +157,15 @@ namespace Oqtane.Managers
_notifications.AddNotification(notification);
}
else
{
if (!user.SuppressNotification)
{
string url = alias.Protocol + alias.Name;
string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully Created For You With The Username " + user.Username + ". Please Visit " + url + " And Use The Login Option To Sign In. If You Do Not Know Your Password, Use The Forgot Password Option On The Login Page To Reset Your Account.\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Notification", body);
_notifications.AddNotification(notification);
}
}
User.Password = ""; // remove sensitive information
_logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", User);
@ -167,7 +186,7 @@ namespace Oqtane.Managers
{
identityuser.Email = user.Email;
var valid = true;
if (user.Password != "")
if (!string.IsNullOrEmpty(user.Password))
{
var validator = new PasswordValidator<IdentityUser>();
var result = await validator.ValidateAsync(_identityUserManager, null, user.Password);
@ -179,7 +198,10 @@ namespace Oqtane.Managers
}
if (valid)
{
await _identityUserManager.UpdateAsync(identityuser);
if (!string.IsNullOrEmpty(user.Password))
{
await _identityUserManager.UpdateAsync(identityuser); // requires password to be provided
}
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Update);
@ -257,6 +279,8 @@ namespace Oqtane.Managers
var LastIPAddress = user.LastIPAddress ?? "";
user = _users.GetUser(user.Username);
if (!user.IsDeleted)
{
if (user.TwoFactorRequired)
{
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
@ -299,6 +323,11 @@ namespace Oqtane.Managers
}
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed - Account Deleted {Username}", user.Username);
}
}
else
{
if (result.IsLockedOut)
{
@ -436,5 +465,189 @@ namespace Oqtane.Managers
var result = await validator.ValidateAsync(_identityUserManager, null, password);
return result.Succeeded;
}
public async Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify)
{
var success = true;
int rows = 0;
int users = 0;
if (System.IO.File.Exists(filePath))
{
var roles = _roles.GetRoles(siteId).ToList();
var profiles = _profiles.GetProfiles(siteId).ToList();
try
{
string row = "";
using (var reader = new StreamReader(filePath))
{
// header row
if (reader.Peek() > -1)
{
row = reader.ReadLine();
}
if (!string.IsNullOrEmpty(row.Trim()))
{
var header = row.Replace("\"", "").Split('\t');
if (header[0].Trim() == "Email")
{
for (int index = 4; index < header.Length - 1; index++)
{
if (!string.IsNullOrEmpty(header[index].Trim()) && !profiles.Any(item => item.Name == header[index].Trim()))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Import Contains Profile Name {Profile} Which Does Not Exist", header[index]);
success = false;
}
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Import File Is Not In Correct Format. Please Use Template Provided.");
success = false;
}
if (success)
{
// detail rows
while (reader.Peek() > -1)
{
row = reader.ReadLine();
rows++;
if (!string.IsNullOrEmpty(row.Trim()))
{
var values = row.Replace("\"", "").Split('\t');
// user
var email = (values.Length > 0) ? values[0].Trim() : "";
var username = (values.Length > 1) ? values[1].Trim() : "";
var displayname = (values.Length > 2) ? values[2].Trim() : "";
var user = _users.GetUser(username, email);
if (user == null)
{
user = new User();
user.SiteId = siteId;
user.Email = values[0];
user.Username = (!string.IsNullOrEmpty(username)) ? username : user.Email;
user.DisplayName = (!string.IsNullOrEmpty(displayname)) ? displayname : user.Username;
user.EmailConfirmed = true;
user.SuppressNotification = !notify;
user = await AddUser(user);
if (user == null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Import Error Importing User {Email} {Username} {DisplayName}", email, username, displayname);
success = false;
}
}
else
{
if (!string.IsNullOrEmpty(displayname))
{
user.DisplayName = displayname;
user.Password = "";
user = await UpdateUser(user);
}
}
var rolenames = (values.Length > 3) ? values[3].Trim() : "";
if (user != null && !string.IsNullOrEmpty(rolenames))
{
// roles (comma delimited)
foreach (var rolename in rolenames.Split(','))
{
var role = roles.FirstOrDefault(item => item.Name == rolename.Trim());
if (role == null)
{
role = new Role();
role.SiteId = siteId;
role.Name = rolename.Trim();
role.Description = rolename.Trim();
role = _roles.AddRole(role);
roles.Add(role);
}
if (role != null)
{
var userrole = _userRoles.GetUserRole(user.UserId, role.RoleId, false);
if (userrole == null)
{
userrole = new UserRole();
userrole.UserId = user.UserId;
userrole.RoleId = role.RoleId;
_userRoles.AddUserRole(userrole);
}
}
}
}
if (user != null && values.Length > 4)
{
// profiles
var settings = _settings.GetSettings(EntityNames.User, user.UserId);
for (int index = 4; index < values.Length - 1; index++)
{
if (header.Length > index && !string.IsNullOrEmpty(values[index].Trim()))
{
var profile = profiles.FirstOrDefault(item => item.Name == header[index].Trim());
if (profile != null)
{
var setting = settings.FirstOrDefault(item => item.SettingName == profile.Name);
if (setting == null)
{
setting = new Setting();
setting.EntityName = EntityNames.User;
setting.EntityId = user.UserId;
setting.SettingName = profile.Name;
setting.SettingValue = values[index].Trim();
_settings.AddSetting(setting);
}
else
{
if (setting.SettingValue != values[index].Trim())
{
setting.SettingValue = values[index].Trim();
_settings.UpdateSetting(setting);
}
}
}
}
}
}
users++;
}
}
}
}
else
{
success = false;
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Import File Contains No Header Row");
}
}
_logger.Log(LogLevel.Information, this, LogFunction.Create, "User Import: {Rows} Rows Processed, {Users} Users Imported", rows, users);
}
catch (Exception ex)
{
success = false;
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "Error Importing User Import File {SiteId} {FilePath} {Notify}", siteId, filePath, notify);
}
}
else
{
success = false;
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Import File Does Not Exist {FilePath}", filePath);
}
// return results
var result = new Dictionary<string, string>();
result.Add("Success", success.ToString());
result.Add("Users", users.ToString());
return result;
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.04.00.06.01")]
public class AddProfileRows : MultiDatabaseMigration
{
public AddProfileRows(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var profileEntityBuilder = new ProfileEntityBuilder(migrationBuilder, ActiveDatabase);
profileEntityBuilder.AddIntegerColumn("Rows", true);
profileEntityBuilder.UpdateColumn("Rows", "1");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>4.0.3</Version>
<Version>4.0.6</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -4,6 +4,9 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Extensions;
using Oqtane.Managers;
using Oqtane.Shared;
namespace Oqtane.Pages
{
@ -12,11 +15,13 @@ namespace Oqtane.Pages
{
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly IUserManager _userManager;
public LoginModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager)
public LoginModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager)
{
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_userManager = userManager;
}
public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl)
@ -29,13 +34,19 @@ namespace Oqtane.Pages
{
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, password, true);
if (result.Succeeded)
{
var alias = HttpContext.GetAlias();
var user = _userManager.GetUser(identityuser.UserName, alias.SiteId);
if (user != null && !user.IsDeleted)
{
validuser = true;
}
}
}
if (validuser)
{
// note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync
await _identitySignInManager.SignInAsync(identityuser, remember);
}
}

View File

@ -437,17 +437,17 @@ namespace Oqtane.Pages
"</script>";
}
private string ParseScripts(string headcontent)
private string ParseScripts(string content)
{
// iterate scripts
var scripts = "";
if (!string.IsNullOrEmpty(headcontent))
if (!string.IsNullOrEmpty(content))
{
var index = headcontent.IndexOf("<script");
var index = content.IndexOf("<script");
while (index >= 0)
{
scripts += headcontent.Substring(index, headcontent.IndexOf("</script>", index) + 9 - index);
index = headcontent.IndexOf("<script", index + 1);
scripts += content.Substring(index, content.IndexOf("</script>", index) + 9 - index);
index = content.IndexOf("<script", index + 1);
}
}
return scripts;

View File

@ -32,8 +32,9 @@ namespace Oqtane.Repository
public IEnumerable<File> GetFiles(int folderId, bool tracking)
{
var alias = _tenants.GetAlias();
IEnumerable<Permission> permissions = _permissions.GetPermissions(alias.SiteId, EntityNames.Folder, folderId).ToList();
var folder = _folderRepository.GetFolder(folderId, false);
IEnumerable<Permission> permissions = _permissions.GetPermissions(folder.SiteId, EntityNames.Folder, folderId).ToList();
IEnumerable<File> files;
if (tracking)
{
@ -46,6 +47,7 @@ namespace Oqtane.Repository
foreach (File file in files)
{
file.Folder.PermissionList = permissions.ToList();
var alias = _tenants.GetAlias();
file.Url = GetFileUrl(file, alias);
}
return files;

View File

@ -11,6 +11,8 @@ namespace Oqtane.Repository
UserRole UpdateUserRole(UserRole userRole);
UserRole GetUserRole(int userRoleId);
UserRole GetUserRole(int userRoleId, bool tracking);
UserRole GetUserRole(int userId, int roleId);
UserRole GetUserRole(int userId, int roleId, bool tracking);
void DeleteUserRole(int userRoleId);
void DeleteUserRoles(int userId);
}

View File

@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
@ -148,10 +147,11 @@ namespace Oqtane.Repository
if (moduledefinition == null)
{
// new module definition (version is explicitly not set because it is updated as part of module migrations at startup)
// new module definition
moduledefinition = new ModuleDefinition { ModuleDefinitionName = ModuleDefinition.ModuleDefinitionName };
_db.ModuleDefinition.Add(moduledefinition);
_db.SaveChanges();
// version is explicitly not set because it is updated as part of module migrations at startup
ModuleDefinition.Version = "";
}
else
@ -160,6 +160,8 @@ namespace Oqtane.Repository
ModuleDefinition.Name = (!string.IsNullOrEmpty(moduledefinition.Name)) ? moduledefinition.Name : ModuleDefinition.Name;
ModuleDefinition.Description = (!string.IsNullOrEmpty(moduledefinition.Description)) ? moduledefinition.Description : ModuleDefinition.Description;
ModuleDefinition.Categories = (!string.IsNullOrEmpty(moduledefinition.Categories)) ? moduledefinition.Categories : ModuleDefinition.Categories;
// get current version
ModuleDefinition.Version = moduledefinition.Version;
// remove module definition from list as it is already synced
moduledefinitions.Remove(moduledefinition);

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