Compare commits

...

109 Commits

Author SHA1 Message Date
Shaun Walker
6c58ab4554 Merge pull request #5439 from oqtane/master
6.1.4 Release
2025-07-30 15:11:01 -04:00
Shaun Walker
085187cfac 6.1.4 Release
6.1.4 Release
2025-07-30 15:10:42 -04:00
Shaun Walker
3d0f0a5adc Merge pull request #5437 from sbwalker/dev
synchronize app.css with .NET MAUI
2025-07-30 13:40:41 -04:00
sbwalker
eae8b431ee synchronize app.css with .NET MAUI 2025-07-30 13:40:25 -04:00
Shaun Walker
e3a34446c0 Merge pull request #5436 from sbwalker/dev
synchronize interop,js with .NET MAUI
2025-07-30 13:35:56 -04:00
sbwalker
bfe57c3ac7 synchronize interop,js with .NET MAUI 2025-07-30 13:35:39 -04:00
Shaun Walker
d4001be716 Merge pull request #5435 from sbwalker/dev
fix #5364 - add ability to specify preferred Container per Pane
2025-07-30 10:43:51 -04:00
sbwalker
662a1817f2 fix #5364 - add ability to specify preferred Container per Pane 2025-07-30 10:43:36 -04:00
Shaun Walker
2c99ef412d Merge pull request #5434 from sbwalker/dev
use consistent terminology
2025-07-30 10:01:11 -04:00
sbwalker
f53ed5b13b use consistent terminology 2025-07-30 10:00:57 -04:00
Shaun Walker
b5d51838c6 Merge pull request #5433 from sbwalker/dev
allow specific time zones to be excluded
2025-07-30 09:29:56 -04:00
sbwalker
92fd70198a allow specific time zones to be excluded 2025-07-30 09:29:43 -04:00
Shaun Walker
7f1990f851 Merge pull request #5432 from sbwalker/dev
fix incorrect resource reference
2025-07-30 08:48:04 -04:00
sbwalker
797d7afc3e fix incorrect resource reference 2025-07-30 08:47:50 -04:00
Shaun Walker
c5a23cdfa0 Merge pull request #5431 from sbwalker/dev
update Oqtane theme to Bootstrap 5.3.7
2025-07-30 08:30:54 -04:00
sbwalker
906358f1f8 update Oqtane theme to Bootstrap 5.3.7 2025-07-30 08:30:40 -04:00
Shaun Walker
638f2a59c5 Merge pull request #5430 from sbwalker/dev
use margin rather than padding
2025-07-30 08:16:20 -04:00
sbwalker
cf9b4b869c use margin rather than padding 2025-07-30 08:16:07 -04:00
Shaun Walker
671c52fbbb Merge pull request #5429 from leigh-pointer/CDN-Bootstrap
Discussion #5426 updated and returned to https://cdnjs.com/
2025-07-30 08:07:52 -04:00
Leigh Pointer
6c0e2a62e7 Discussion #5426 updated and returned to https://cdnjs.com/
Updated and styles tested - reload.js needs still testing?
2025-07-30 12:53:59 +02:00
Shaun Walker
1b78c9ad81 Merge pull request #5428 from sbwalker/dev
use consistent naming
2025-07-29 16:36:43 -04:00
sbwalker
7a4b98aec9 use consistent naming 2025-07-29 16:36:28 -04:00
Shaun Walker
9ef6c15014 Merge pull request #5427 from sbwalker/dev
fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site
2025-07-29 16:20:37 -04:00
sbwalker
f4cea3fe03 fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site 2025-07-29 16:20:07 -04:00
Shaun Walker
5dd9b1ec91 Merge pull request #5425 from sbwalker/dev
fix #5346 - deleting role should remove associated useroles
2025-07-29 09:05:54 -04:00
sbwalker
658059806b fix #5346 - deleting role should remove associated useroles 2025-07-29 09:05:37 -04:00
Shaun Walker
4f8a18451c Merge pull request #5424 from sbwalker/dev
fix #5346 - deleting role should remove associated permissions
2025-07-29 08:40:54 -04:00
sbwalker
b1770ebb76 fix #5346 - deleting role should remove associated permissions 2025-07-29 08:40:38 -04:00
Shaun Walker
6923065d86 Merge pull request #5423 from sbwalker/dev
fix #5348 - ensure time zones work consistently on all platforms
2025-07-29 08:11:56 -04:00
sbwalker
9f097521f6 fix #5348 - ensure time zones work consistently on all platforms 2025-07-29 08:11:42 -04:00
Shaun Walker
235e5c1d3a Merge pull request #5421 from sbwalker/dev
improve TimeZoneService
2025-07-28 17:00:47 -04:00
sbwalker
e179976fe8 improve TimeZoneService 2025-07-28 17:00:27 -04:00
Shaun Walker
082726b405 Merge pull request #5420 from sbwalker/dev
fix #5372 - add support for sending SMTP emails using OAuth
2025-07-28 10:26:34 -04:00
sbwalker
91c5309855 fix #5372 - add support for sending SMTP emails using OAuth 2025-07-28 10:26:18 -04:00
Shaun Walker
92be1e7a5c Merge pull request #5419 from sbwalker/dev
add OAuth support to Notification Job (#5372)
2025-07-28 09:06:55 -04:00
sbwalker
cceda1db1e add OAuth support to Notification Job (#5372) 2025-07-28 09:06:36 -04:00
Shaun Walker
a59191cea7 Merge pull request #5416 from sbwalker/dev
fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations
2025-07-25 15:22:54 -04:00
sbwalker
b0dee4a60c fix #5414 - add DelimitName database provider method to better support MigrationBuilder.Sql() operations 2025-07-25 15:22:26 -04:00
Shaun Walker
3f33f2b9df Merge pull request #5412 from sbwalker/dev
fix #5410 - allow duplicate email addresses
2025-07-23 16:40:27 -04:00
sbwalker
97116b4e0c fix #5410 - allow duplicate email addresses 2025-07-23 16:40:12 -04:00
Shaun Walker
a5f51ff9a1 Merge pull request #5411 from sbwalker/dev
localize time zone names
2025-07-23 14:52:34 -04:00
sbwalker
962488fd34 localize time zone names 2025-07-23 14:52:18 -04:00
Shaun Walker
190d973b77 Merge pull request #5406 from leigh-pointer/Refs
Solutions References update
2025-07-22 16:14:13 -04:00
Shaun Walker
397e0b3f71 Merge pull request #5408 from sbwalker/dev
improve user experience of permissions grid
2025-07-22 16:12:48 -04:00
sbwalker
83ba9ca73e improve user experience of permissions grid 2025-07-22 16:07:52 -04:00
Shaun Walker
3d08138686 Merge pull request #5407 from sbwalker/dev
improve documentation
2025-07-22 09:23:42 -04:00
sbwalker
262fa6b99b improve documentation 2025-07-22 09:23:26 -04:00
Leigh Pointer
372db9dcfa Solutions References update
MySql.Data 9.4.0
HtmlAgilityPack 1.12.2
2025-07-22 07:45:03 +02:00
Shaun Walker
e9dc52919c Merge pull request #5405 from sbwalker/dev
fix Control Panel to initialize extended module permissions when module is added or copied
2025-07-21 16:34:56 -04:00
sbwalker
a981dd0e97 fix Control Panel to initialize extended module permissions when module is added or copied 2025-07-21 16:34:34 -04:00
Shaun Walker
7c2775119b Merge pull request #5404 from sbwalker/dev
add new option to FileManager component to anonymize filenames during upload
2025-07-21 09:14:30 -04:00
sbwalker
0be7f1bdb5 add new option to FileManager component to anonymize filenames during upload 2025-07-21 09:14:07 -04:00
Shaun Walker
8446b9e8d5 Merge pull request #5392 from thabaum/patch-15 2025-07-15 14:28:50 -04:00
Shaun Walker
ce404668d3 Merge pull request #5391 from thabaum/6.1.4-dependencies 2025-07-15 14:28:28 -04:00
Cody
948fab50ee [FIX] #5164 – Raise z‑index for .app‑moduleactions .dropdown‑menu to 9999 2025-07-14 17:10:07 -07:00
Cody
9690f1df48 [FIX] oqtane#5164 – Raise z‑index for .app‑moduleactions .dropdown‑menu to 9999 2025-07-14 17:09:24 -07:00
Cody
5a24f87293 [FIX] #5164 ‑ Set z‑index for .dropdown‑menu in .app‑moduleactions 2025-07-14 17:07:39 -07:00
Cody
d2ff49fe73 [FIX] #5164 - Set z-index for .dropdown-menu in .app-moduleactions 2025-07-14 16:10:50 -07:00
Cody
e9035df9d2 Update Package Dependencies 2025-07-14 13:40:52 -07:00
Shaun Walker
1ddf21f4fc Merge pull request #5387 from mdmontesinos/feat-nodatime 2025-07-11 07:18:10 -04:00
David Montesinos
63d2ded038 Merge branch 'dev' into feat-nodatime 2025-07-11 09:07:12 +02:00
Shaun Walker
7b8e0e48c0 Merge pull request #5385 from leigh-pointer/907 2025-07-11 02:28:38 -04:00
David Montesinos
bb52402a17 feat: handle timezones and conversions with NodaTime 2025-07-09 12:09:00 +02:00
Leigh Pointer
13d9cb461b Update Oqtane Maui project to 9.0.7 2025-07-09 03:42:26 +02:00
Leigh Pointer
0a994afd67 Update References .NetCore 9.0.7 2025-07-09 02:52:30 +02:00
Shaun Walker
57a1257750 Merge pull request #5384 from sbwalker/dev
update External Login default values for Facebook OAuth2
2025-07-08 16:27:58 -04:00
sbwalker
b0c1d36bab update External Login default values for Facebook OAuth2 2025-07-08 16:27:35 -04:00
Shaun Walker
0621751968 Merge pull request #5383 from sbwalker/dev
resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user
2025-07-08 16:04:37 -04:00
sbwalker
461330773a resolve issue where IDP fails to provide email claim resulting in External Login Remote Failure due to dbo.AspNetUsers requiring a unique email value for each user 2025-07-08 16:04:19 -04:00
Shaun Walker
818a97cc2c Merge pull request #5382 from sbwalker/dev
bump version to 6.1.4
2025-07-08 13:20:43 -04:00
sbwalker
17045073c8 bump version to 6.1.4 2025-07-08 13:20:28 -04:00
Shaun Walker
7a818ee698 Merge pull request #5381 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:15:10 -04:00
sbwalker
668e0cb4eb update to .NET SDK 9.0.6 2025-07-08 13:14:53 -04:00
Shaun Walker
741b16ca4e Merge pull request #5380 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:12:06 -04:00
sbwalker
85a376b17d update to .NET SDK 9.0.6 2025-07-08 13:11:52 -04:00
Shaun Walker
df86cd909c Merge pull request #5379 from sbwalker/dev
update to .NET SDK 9.0.6
2025-07-08 13:09:24 -04:00
sbwalker
ac236607f5 update to .NET SDK 9.0.6 2025-07-08 13:09:10 -04:00
Shaun Walker
19813b7eb6 Merge pull request #5378 from sbwalker/dev
remove unused variable
2025-07-07 12:42:51 -04:00
sbwalker
cb5e4e076f remove unused variable 2025-07-07 12:42:35 -04:00
Shaun Walker
48fca77f59 Merge pull request #5276 from leigh-pointer/Bootstrap
Updated to Bootstrap 5.3.5
2025-07-07 12:40:39 -04:00
Shaun Walker
76372451aa Merge pull request #5370 from zyhfish/task/fix-5363
Fix #5363: update SettingService.MergeSettings.
2025-07-07 12:40:14 -04:00
Shaun Walker
34cd197122 Merge pull request #5376 from mdmontesinos/feat-mailkit
feat: replace System.Net.Mail with MailKit (#5372)
2025-07-07 12:40:05 -04:00
David Montesinos
6b567364f9 feat: use appropriate UseSSL equivalent in MailKit 2025-07-04 14:55:02 +02:00
David Montesinos
711de49571 feat: replace System.Net.Mail with MailKit (#5372) 2025-07-04 12:55:40 +02:00
Shaun Walker
9a0f7ad83f Merge pull request #5375 from sbwalker/dev
fix #5374 Visitor Settings not returned due to change in Visitor cookie format
2025-07-03 16:45:19 -04:00
sbwalker
0d3d693799 fix #5374 Visitor Settings not returned due to change in Visitor cookie format 2025-07-03 16:44:59 -04:00
Ben
b63590d6c7 Fix #5363: update SettingService.MergeSettings. 2025-07-03 15:42:11 +08:00
Leigh Pointer
5f3a3d4d54 Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-06-13 19:58:18 +02:00
Shaun Walker
b1a8c28283 Merge pull request #5356 from leigh-pointer/Schedular
Fix for Scheduled Jobs UI #5354
2025-06-13 08:36:47 -04:00
Leigh Pointer
1412737036 Date / Time validations
This PR ensures time fields are required when dates are set, using Oqtane validation and dynamically toggles the required attribute on time inputs when their corresponding date fields have values. Benefits:
- Uses Oqtane's validation for a polished UX.
- Reduces custom validation code.
- Aligns with our internal form logic.

- Tested across all date/time scenarios—works flawlessly!
**Testing Confirmed:**
- Date + Time Provided → Saves successfully.
- No Date + No Time → Optional (no validation).
- Date + No Time → Browser blocks submission with icon error.
2025-06-10 12:27:55 +02:00
Shaun Walker
ffb3f4fa50 Merge pull request #5353 from mdmontesinos/fix-cookieconsent
fix #5352: remove requests to cookie consent service when not enabled
2025-06-09 15:57:59 -04:00
Leigh Pointer
ff450ca43a Fix for Scheduled Jobs UI #5354
This PR addresses an issue where null date/time values could cause exceptions when processing job scheduling.
Changes Made:
- Added proper null checks for _startDate, _startTime, _endDate, _endTime, _nextDate, and _nextTime
- Improved parsing safety for _retentionHistory using int.TryParse()
- Added validation to fail early with meaningful error messages

Impact:

Prevents NullReferenceException and InvalidOperationException when date/time fields are missing
2025-06-09 10:29:43 +02:00
David Montesinos
d4f0805108 fix #5352: remove requests to cookie consent service when not enabled 2025-06-06 10:05:40 +02:00
Shaun Walker
64ce69d1c7 Merge pull request #5351 from sbwalker/dev
stop gap fix to mitigate date conversion exceptions on WebAssembly
2025-06-05 10:38:51 -04:00
sbwalker
ca3cb48091 Merge branch 'dev' of https://github.com/sbwalker/oqtane.framework into dev 2025-06-05 10:37:31 -04:00
sbwalker
85085bf4c7 stop gap fix to mitigate date conversion exceptions on WebAssembly 2025-06-05 10:37:25 -04:00
Shaun Walker
873af6b598 Merge pull request #5349 from leigh-pointer/References
Server References Updated
2025-06-05 09:32:34 -04:00
Shaun Walker
c423895f31 Merge pull request #5350 from sbwalker/dev
rendering optimizations
2025-06-05 09:32:12 -04:00
sbwalker
4418e27c29 rendering optimizations 2025-06-05 09:31:54 -04:00
Leigh Pointer
f776977af8 Server References Updated
update SixLabors.ImageSharp
update Swashbuckle.AspNetCore
2025-06-04 13:28:49 +02:00
Leigh Pointer
c13ce3d0f1 Update Index.razor
Deprecated .text-muted will be replaced by .text-body-secondary in v6.
2025-06-03 15:24:43 +02:00
Leigh Pointer
2c4c669ea2 Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-30 16:06:19 +02:00
Shaun Walker
29fe3dfd0b Merge pull request #5344 from sbwalker/dev
update Azure ARM template to 6.1.3
2025-05-29 17:05:04 -04:00
sbwalker
985e50d415 update Azure ARM template to 6.1.3 2025-05-29 17:04:45 -04:00
Shaun Walker
11150b6a10 Update README.md 2025-05-29 17:03:20 -04:00
Leigh Pointer
018737c42a Merge remote-tracking branch 'upstream/dev' into Bootstrap 2025-05-15 11:52:51 +02:00
Leigh Pointer
3811b8f0c0 Theme Template updated 2025-05-07 11:46:07 +02:00
Leigh Pointer
d81514e9be Update for Blazor Theme 2025-05-02 12:19:58 +02:00
Leigh Pointer
14b0d7abf0 Updated to Bootstrap 5
Updated to Bootstrap 5.3.5
Update bootswatch Cyborg to 5.3.5 using https://cdn.jsdelivr.net because it is not available at https://cdnjs.com/libraries
2025-05-02 12:16:55 +02:00
106 changed files with 1551 additions and 834 deletions

View File

@@ -53,8 +53,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
services.AddScoped<ICookieConsentService, CookieConsentService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
services.AddScoped<ITimeZoneService, TimeZoneService>();
services.AddScoped<IOutputCacheService, OutputCacheService>();
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@@ -56,7 +56,7 @@
<input id="starting" type="date" class="form-control" @bind="@_startDate" />
</div>
<div class="col">
<input id="starting" type="time" class="form-control" placeholder="hh:mm" @bind="@_startTime" />
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" />
</div>
</div>
</div>
@@ -69,7 +69,7 @@
<input id="ending" type="date" class="form-control" @bind="@_endDate" />
</div>
<div class="col">
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" />
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" />
</div>
</div>
</div>
@@ -82,7 +82,7 @@
<input id="next" type="date" class="form-control" @bind="@_nextDate" />
</div>
<div class="col">
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" />
<input id="next" type="time" class="form-control" placeholder="hh:mm" @bind="@_nextTime" required="@(_nextDate.HasValue)" />
</div>
</div>
</div>
@@ -176,10 +176,18 @@
{
job.Interval = int.Parse(_interval);
}
job.StartDate = LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay));
job.EndDate = LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay));
job.RetentionHistory = int.Parse(_retentionHistory);
job.NextExecution = LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay));
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
: null;
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay))
: null;
job.RetentionHistory = int.Parse(_retentionHistory);
try
{
@@ -198,5 +206,4 @@
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
}

View File

@@ -21,9 +21,7 @@ else
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br />
<br />
<br /><br />
}
@if (_allowsitelogin)
{
@@ -49,15 +47,11 @@ else
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
{
<br />
<br />
<br /><br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
}
}

View File

@@ -17,8 +17,8 @@ else
<div class="row mb-3 align-items-center">
<div class="col-sm-6">
<ActionLink Action="Add" Text="Install Module" ResourceKey="InstallModule" />
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ps-2" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<ActionLink Action="Create" Text="Create Module" ResourceKey="CreateModule" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
</div>
<div class="col-sm-6">
<select class="form-select" @onchange="(e => CategoryChanged(e))">

View File

@@ -3,10 +3,10 @@
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@if (_initialized)
{
@@ -115,7 +115,7 @@
{
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
_timezoneid = PageState.Site.TimeZoneId;
_initialized = true;
}

View File

@@ -46,7 +46,7 @@
<Row>
<div class="search-item mb-2">
<h4 class="mb-1"><a href="@context.Url">@context.Title</a></h4>
<p class="mb-0 text-muted">@((MarkupString)context.Snippet)</p>
<p class="mb-0 text-body-secondary">@((MarkupString)context.Snippet)</p>
</div>
</Row>
</Pager>

View File

@@ -194,80 +194,125 @@
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container">
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SmtpEnabled">Enabled? </Label>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
<input id="host" class="form-control" @bind="@_smtphost" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port number for the SMTP server. Please note this field is required if you provide a host name." ResourceKey="Port">Port: </Label>
<div class="col-sm-9">
<input id="port" class="form-control" @bind="@_smtpport" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="UseSsl">SSL Enabled: </Label>
<div class="col-sm-9">
<select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<select id="smtpenabled" class="form-select" value="@_smtpenabled" @onchange="(e => SMTPEnabledChanged(e))">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off"/>
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
@if (_smtpenabled == "True")
{
<div class="row mb-1 align-items-center">
<div class="col-sm-3">
</div>
<div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmtpSender">Email Sender: </Label>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9">
<input id="host" class="form-control" @bind="@_smtphost" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified above." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="port" HelpText="Enter the port number for the SMTP server. Please note this field is required if you provide a host name." ResourceKey="Port">Port: </Label>
<div class="col-sm-9">
<input id="port" class="form-control" @bind="@_smtpport" />
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SMTPEnabled">Enabled? </Label>
<div class="col-sm-9">
<select id="smtpenabled" class="form-select" @bind="@_smtpenabled">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="SmtpSSL">SSL Required: </Label>
<div class="col-sm-9">
<select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpauthentication" HelpText="Specify the SMTP authentication type" ResourceKey="SMTPAuthentication">Authentication: </Label>
<div class="col-sm-9">
<select id="smtpauthentication" class="form-select" value="@_smtpauthentication" @onchange="(e => SMTPAuthenticationChanged(e))">
<option value="Basic">@Localizer["Basic"]</option>
<option value="OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
@if (_smtpauthentication == "Basic")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="relay" HelpText="Only specify this option if you have properly configured an SMTP Relay Service to route your outgoing mail. This option will send notifications from the user's email rather than from the Email Sender specified below." ResourceKey="SmtpRelay">Relay Configured? </Label>
<div class="col-sm-9">
<select id="relay" class="form-select" @bind="@_smtprelay" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
else
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpauthority" HelpText="The Authority Url for the SMTP provider" ResourceKey="SmtpAuthority">Authority Url:</Label>
<div class="col-sm-9">
<input id="smtpauthority" class="form-control" @bind="@_smtpauthority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpclientid" HelpText="The Client ID for the SMTP provider" ResourceKey="SmtpClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="smtpclientid" class="form-control" @bind="@_smtpclientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpclientsecret" HelpText="The Client Secret for the SMTP provider" ResourceKey="SmtpClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_smtpclientsecrettype" id="smtpclientsecret" class="form-control" @bind="@_smtpclientsecret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSmtpClientSecret">@_togglesmtpclientsecret</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="smtpscopes" HelpText="A list of Scopes for the SMTP provider (separated by commas)" ResourceKey="SmtpScopes">Scopes:</Label>
<div class="col-sm-9">
<input id="smtpscopes" class="form-control" @bind="@_smtpscopes" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="sender" HelpText="Enter the email address which emails will be sent from. Please note that this email address usually needs to be authorized with the SMTP provider." ResourceKey="SmtpSender">Email Sender: </Label>
<div class="col-sm-9">
<input id="sender" class="form-control" @bind="@_smtpsender" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
<br /><br />
}
</div>
</Section>
<Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings">
@@ -455,16 +500,23 @@
private string _headcontent = string.Empty;
private string _bodycontent = string.Empty;
private string _smtpenabled = "False";
private string _smtpauthentication = "Basic";
private string _smtphost = string.Empty;
private string _smtpport = string.Empty;
private string _smtpssl = "False";
private string _smtpssl = "True";
private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty;
private string _smtpauthority = string.Empty;
private string _smtpclientid = string.Empty;
private string _smtpclientsecret = string.Empty;
private string _smtpclientsecrettype = "password";
private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private string _smtpenabled = "True";
private int _retention = 30;
private string _pwaisenabled;
@@ -508,7 +560,7 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null)
{
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
@@ -556,15 +608,21 @@
_bodycontent = site.BodyContent;
// SMTP
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False");
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpauthentication = SettingService.GetSetting(settings, "SMTPAuthentication", "Basic");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"];
_smtpauthority = SettingService.GetSetting(settings, "SMTPAuthority", string.Empty);
_smtpclientid = SettingService.GetSetting(settings, "SMTPClientId", string.Empty);
_smtpclientsecret = SettingService.GetSetting(settings, "SMTPClientSecret", string.Empty);
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty);
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
// PWA
@@ -745,8 +803,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true);
settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true);
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
@@ -813,6 +876,46 @@
}
}
private void SMTPAuthenticationChanged(ChangeEventArgs e)
{
_smtpauthentication = (string)e.Value;
StateHasChanged();
}
private void SMTPEnabledChanged(ChangeEventArgs e)
{
_smtpenabled = (string)e.Value;
StateHasChanged();
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private void ToggleSmtpClientSecret()
{
if (_smtpclientsecrettype == "password")
{
_smtpclientsecrettype = "text";
_togglesmtpclientsecret = SharedLocalizer["HidePassword"];
}
else
{
_smtpclientsecrettype = "password";
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
}
}
private async Task SendEmail()
{
if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
@@ -823,8 +926,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, true);
settings = SettingService.SetSetting(settings, "SMTPAuthentication", _smtpauthentication, true);
settings = SettingService.SetSetting(settings, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, true);
settings = SettingService.SetSetting(settings, "SMTPAuthority", _smtpauthority, true);
settings = SettingService.SetSetting(settings, "SMTPClientId", _smtpclientid, true);
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, true);
settings = SettingService.SetSetting(settings, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved");
@@ -845,20 +953,6 @@
}
}
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases()
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@@ -15,8 +15,8 @@
else
{
<ActionLink Action="Add" Text="Install Theme" ResourceKey="InstallTheme" />
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ps-2" />
<button type="button" class="btn btn-secondary pw-2" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<ActionLink Action="Create" Text="Create Theme" ResourceKey="CreateTheme" Class="btn btn-secondary ms-1" />
<button type="button" class="btn btn-secondary ms-1" @onclick="@Synchronize">@Localizer["Synchronize"]</button>
<Pager Items="@_themes">
<Header>

View File

@@ -367,7 +367,6 @@
}
@code {
private List<Models.TimeZone> _timezones;
private bool _initialized = false;
private string _passwordrequirements;
private string _username = string.Empty;
@@ -381,6 +380,7 @@
private string _displayname = string.Empty;
private FileManager _filemanager;
private int _folderid = -1;
private List<Models.TimeZone> _timezones;
private string _timezoneid = string.Empty;
private int _photofileid = -1;
private File _photo = null;
@@ -404,7 +404,7 @@
_togglepassword = SharedLocalizer["ShowPassword"];
_allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true");
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
if (PageState.User != null)
{
@@ -414,11 +414,6 @@
_displayname = PageState.User.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
if (string.IsNullOrEmpty(_email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
// get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)

View File

@@ -28,6 +28,15 @@
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Verified?</Label>
<div class="col-sm-9">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
@@ -120,6 +129,7 @@
private bool _initialized = false;
private string _username = string.Empty;
private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _notify = "True";
@@ -133,7 +143,7 @@
{
try
{
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId;
@@ -169,6 +179,7 @@
user.Username = _username;
user.Password = ""; // will be auto generated
user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
user.PhotoFileId = null;

View File

@@ -48,7 +48,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Confirmed?</Label>
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Verified?</Label>
<div class="col-sm-9">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option>
@@ -159,7 +159,7 @@
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True")
{
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger ms-1" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
<br /><br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
@@ -204,7 +204,7 @@
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"];
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_timezones = await TimeZoneService.GetTimeZonesAsync();
_timezones = TimeZoneService.GetTimeZones();
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId))
{

View File

@@ -74,10 +74,19 @@ else
<input id="profileurl" class="form-control" @bind="@_profileurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="requireconfirmedemail" HelpText="Do you want to require registered users to verify their email address before they are allowed to log in?" ResourceKey="RequireConfirmedEmail">Require Verified Email?</Label>
<div class="col-sm-9">
<select id="requireconfirmedemail" class="form-select" @bind="@_requireconfirmedemail">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor?</Label>
<Label Class="col-sm-3" For="twofactor" HelpText="Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out." ResourceKey="TwoFactor">Two Factor Authentication?</Label>
<div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option>
@@ -490,6 +499,7 @@ else
private string _allowregistration;
private string _registerurl;
private string _profileurl;
private string _requireconfirmedemail;
private string _twofactor;
private string _cookiename;
private string _cookieexpiration;
@@ -560,6 +570,7 @@ else
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
@@ -685,6 +696,7 @@ else
{
settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false);
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);

View File

@@ -157,6 +157,9 @@
[Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public bool AnonymizeUploadFilenames { get; set; } = false; // optional - indicate if file names should be anonymized on upload - default false
[Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
@@ -408,7 +411,7 @@
}
// upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token);
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, AnonymizeUploadFilenames, tokenSource.Token);
// reset progress indicators
if (ShowProgress)

View File

@@ -9,62 +9,26 @@
@if (_permissions != null)
{
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
@foreach (Role role in _roles)
{
<div class="container">
<div class="row">
<div class="col">
<table class="table table-borderless">
<tbody>
<tr>
<td>@role.Name</td>
<th scope="col">@Localizer["Role"]</th>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, role.Name, -1) Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td>
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
}
</tbody>
</table>
<br />
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
@foreach (Role role in _roles)
{
<tr>
<td>@user.DisplayName (@user.Username)</td>
<td>@role.Name</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value=@GetPermissionValue(permissionname, "", user.UserId) Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
<td style="text-align: center;">
<TriStateCheckBox Value="@GetPermissionValue(permissionname, role.Name, -1)" Disabled="@GetPermissionDisabled(permissionname, role.Name)" OnChange="@(e => PermissionChanged(e, permissionname, role.Name, -1))" />
</td>
}
</tr>
@@ -72,200 +36,242 @@
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col">
@if (_users.Count != 0)
{
<div class="row">
<div class="col">
</div>
</div>
<table class="table table-borderless">
<thead>
<tr>
<th scope="col">@Localizer["User"]</th>
@foreach (var permissionname in _permissionnames)
{
<th style="text-align: center; width: 1px;">@((MarkupString)DisplayPermissionName(permissionname).Replace(" ", "<br />"))</th>
}
</tr>
</thead>
<tbody>
@foreach (User user in _users)
{
<tr>
<td>@user.DisplayName (@user.Username)</td>
@foreach (var permissionname in _permissionnames)
{
<td style="text-align: center; width: 1px;">
<TriStateCheckBox Value="@GetPermissionValue(permissionname, "", user.UserId)" Disabled="@GetPermissionDisabled(permissionname, "")" OnChange="@(e => PermissionChanged(e, permissionname, "", user.UserId))" />
</td>
}
</tr>
}
</tbody>
</table>
<br />
}
</div>
</div>
<div class="row">
<div class="col-11">
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div>
</div>
</div>
<div class="row">
<div class="col-11">
<AutoComplete OnSearch="GetUsers" Placeholder="@Localizer["Username.Enter"]" @ref="_user" />
</div>
<div class="col-1">
<button type="button" class="btn btn-primary" @onclick="AddUser">@SharedLocalizer["Add"]</button>
</div>
</div>
<div class="row">
<div class="col">
<ModuleMessage Type="MessageType.Warning" Message="@_message" />
</div>
</div>
</div>
}
@code {
private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles;
private List<User> _users = new List<User>();
private AutoComplete _user;
private string _message = string.Empty;
private List<string> _permissionnames;
private List<Permission> _permissions;
private List<Role> _roles;
private List<User> _users = new List<User>();
private AutoComplete _user;
private string _message = string.Empty;
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string EntityName { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string PermissionNames { get; set; }
[Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public string Permissions { get; set; } // deprecated - use PermissionList instead
[Parameter]
public List<Permission> PermissionList { get; set; }
[Parameter]
public List<Permission> PermissionList { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
{
if (!string.IsNullOrEmpty(Permissions))
{
PermissionList = JsonSerializer.Deserialize<List<Permission>>(Permissions);
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
if (!UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
_roles.RemoveAll(item => item.Name == RoleNames.Host);
}
_roles = await RoleService.GetRolesAsync(ModuleState.SiteId, true);
_roles.RemoveAll(item => item.Name == RoleNames.Host); // remove host role
// get permission names
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit);
}
else
{
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// get permission names
if (string.IsNullOrEmpty(PermissionNames))
{
_permissionnames = new List<string>();
_permissionnames.Add(Shared.PermissionNames.View);
_permissionnames.Add(Shared.PermissionNames.Edit);
}
else
{
_permissionnames = PermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{
foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
}
else
{
foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
}
}
}
// initialize permissions
_permissions = new List<Permission>();
if (PermissionList != null && PermissionList.Any())
{
foreach (var permission in PermissionList)
{
_permissions.Add(permission);
if (permission.UserId != null)
{
if (!_users.Any(item => item.UserId == permission.UserId.Value))
{
_users.Add(await UserService.GetUserAsync(permission.UserId.Value, ModuleState.SiteId));
}
}
}
}
else
{
foreach (string permissionname in _permissionnames)
{
// permission names can be in the form of "EntityName:PermissionName:Roles"
if (permissionname.Contains(":"))
{
var segments = permissionname.Split(':');
if (segments.Length == 3)
{
foreach (var role in segments[2].Split(';'))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], role, null, true));
}
// ensure admin access
if (!_permissions.Any(item => item.EntityName == segments[0] && item.PermissionName == segments[1] && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, segments[0], segments[1], RoleNames.Admin, null, true));
}
}
}
else
{
_permissions.Add(new Permission(ModuleState.SiteId, EntityName, permissionname, RoleNames.Admin, null, true));
}
}
}
}
private string GetPermissionName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetPermissionName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[1] : permissionName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string GetEntityName(string permissionName)
{
return (permissionName.Contains(":")) ? permissionName.Split(':')[0] : EntityName;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private string DisplayPermissionName(string permissionName)
{
var name = Localizer[GetPermissionName(permissionName)].ToString();
name += " " + Localizer[GetEntityName(permissionName)].ToString();
return name;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool? GetPermissionValue(string permissionName, string roleName, int userId)
{
bool? isauthorized = null;
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
isauthorized = permission.IsAuthorized;
}
}
return isauthorized;
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
return true;
}
else
{
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
return true;
}
else
{
return false;
}
}
}
private bool GetPermissionDisabled(string permissionName, string roleName)
{
var disabled = false;
private void PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
}
}
}
// administrator role permissions can only be changed by a host
if (roleName == RoleNames.Admin && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
disabled = true;
}
// API permissions can only be changed by an administrator
if (GetEntityName(permissionName) != EntityName && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
disabled = true;
}
return disabled;
}
private bool? PermissionChanged(bool? value, string permissionName, string roleName, int userId)
{
if (roleName != "")
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.RoleName == roleName);
if (permission != null)
{
_permissions.Remove(permission);
}
// system roles cannot be denied - only custom roles can be denied
var role = _roles.FirstOrDefault(item => item.Name == roleName);
if (value != null && !value.Value && role.IsSystem)
{
value = null;
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), roleName, null, value.Value));
}
}
else
{
var permission = _permissions.FirstOrDefault(item => item.EntityName == GetEntityName(permissionName) && item.PermissionName == GetPermissionName(permissionName) && item.UserId == userId);
if (permission != null)
{
_permissions.Remove(permission);
}
if (value != null)
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionName), GetPermissionName(permissionName), null, userId, value.Value));
}
}
return value;
}
private async Task<Dictionary<string, string>> GetUsers(string filter)
{
@@ -305,29 +311,20 @@
private void ValidatePermissions()
{
// remove deny all users, unauthenticated, and registered users
var permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Everyone || item.RoleName == RoleNames.Unauthenticated || item.RoleName == RoleNames.Registered)).ToList();
foreach (var permission in permissions)
{
_permissions.Remove(permission);
}
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
// remove deny administrators and host users
permissions = _permissions.Where(item => !item.IsAuthorized &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)).ToList();
foreach (var permission in permissions)
// remove host role permissions
var permissions = _permissions.Where(item => item.RoleName == RoleNames.Host).ToList();
foreach (var permission in permissions)
{
_permissions.Remove(permission);
}
// add host role permissions if administrator role is not assigned (to prevent lockout)
foreach (var permissionname in _permissionnames)
{
_permissions.Remove(permission);
}
foreach (var permissionname in _permissionnames)
{
// add administrators role if neither host or administrator is assigned
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) &&
(item.RoleName == RoleNames.Admin || item.RoleName == RoleNames.Host)))
if (!_permissions.Any(item => item.EntityName == GetEntityName(permissionname) && item.PermissionName == GetPermissionName(permissionname) && item.RoleName == RoleNames.Admin))
{
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Admin, null, true));
_permissions.Add(new Permission(ModuleState.SiteId, GetEntityName(permissionname), GetPermissionName(permissionname), RoleNames.Host, null, true));
}
}
}

View File

@@ -16,7 +16,7 @@
public bool Disabled { get; set; }
[Parameter]
public Action<bool?> OnChange { get; set; }
public Func<bool?, bool?> OnChange { get; set; }
protected override void OnInitialized()
{
@@ -41,27 +41,35 @@
break;
}
_value = OnChange(_value);
SetImage();
OnChange(_value);
}
}
private void SetImage()
{
switch (_value)
if (!Disabled)
{
case true:
_src = "images/checked.png";
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";
_title = string.Empty;
break;
switch (_value)
{
case true:
_src = "images/checked.png";
_title = Localizer["PermissionGranted"];
break;
case false:
_src = "images/unchecked.png";
_title = Localizer["PermissionDenied"];
break;
case null:
_src = "images/null.png";
_title = string.Empty;
break;
}
}
else
{
_src = "images/disabled.png";
_title = Localizer["PermissionDisabled"];
}
StateHasChanged();

View File

@@ -500,33 +500,45 @@ namespace Oqtane.Modules
};
}
// date methods
// date conversion methods
public DateTime? UtcToLocal(DateTime? datetime)
{
TimeZoneInfo timezone = null;
// Early return if input is null
if (datetime == null)
return null;
string timezoneId = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
timezoneId = PageState.User.TimeZoneId;
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
timezoneId = PageState.Site.TimeZoneId;
}
return Utilities.UtcAsLocalDateTime(datetime, timezone);
return Utilities.UtcAsLocalDateTime(datetime, timezoneId);
}
public DateTime? LocalToUtc(DateTime? datetime)
{
TimeZoneInfo timezone = null;
// Early return if input is null
if (datetime == null)
return null;
string timezoneId = null;
if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId);
timezoneId = PageState.User.TimeZoneId;
}
else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId))
{
timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId);
timezoneId = PageState.Site.TimeZoneId;
}
return Utilities.LocalDateAndTimeAsUtc(datetime, timezone);
return Utilities.LocalDateAndTimeAsUtc(datetime, timezoneId);
}
// logging methods

View File

@@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@@ -22,10 +22,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -133,7 +133,7 @@
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification.</value>
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That New User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification Containing Further Instructions.</value>
</data>
<data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value>

View File

@@ -192,7 +192,7 @@
<data name="Port.HelpText" xml:space="preserve">
<value>Enter the port number for the SMTP server. Please note this field is required if you provide a host name.</value>
</data>
<data name="UseSsl.HelpText" xml:space="preserve">
<data name="SmtpSSL.HelpText" xml:space="preserve">
<value>Specify if SSL is required for your SMTP server</value>
</data>
<data name="SmtpUsername.HelpText" xml:space="preserve">
@@ -202,7 +202,7 @@
<value>Enter the password for your SMTP account</value>
</data>
<data name="SmtpSender.HelpText" xml:space="preserve">
<value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value>
<value>Enter the email address which emails will be sent from. Please note that this email address usually needs to be authorized with the SMTP server.</value>
</data>
<data name="EnablePWA.HelpText" xml:space="preserve">
<value>Select whether you would like this site to be available as a Progressive Web Application (PWA)</value>
@@ -240,8 +240,8 @@
<data name="Port.Text" xml:space="preserve">
<value>Port: </value>
</data>
<data name="UseSsl.Text" xml:space="preserve">
<value>SSL Enabled: </value>
<data name="SmtpSSL.Text" xml:space="preserve">
<value>SSL Required: </value>
</data>
<data name="SmtpUsername.Text" xml:space="preserve">
<value>Username: </value>
@@ -372,10 +372,10 @@
<data name="PageContent.Heading" xml:space="preserve">
<value>Page Content</value>
</data>
<data name="SMTPEnabled.HelpText" xml:space="preserve">
<data name="SmtpEnabled.HelpText" xml:space="preserve">
<value>Specify if SMTP is enabled for this site</value>
</data>
<data name="SMTPEnabled.Text" xml:space="preserve">
<data name="SmtpEnabled.Text" xml:space="preserve">
<value>Enabled?</value>
</data>
<data name="Version.HelpText" xml:space="preserve">
@@ -453,4 +453,40 @@
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The default time zone for the site</value>
</data>
<data name="Basic" xml:space="preserve">
<value>Basic</value>
</data>
<data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0 (OAuth2)</value>
</data>
<data name="SmtpAuthentication.Text" xml:space="preserve">
<value>Authentication:</value>
</data>
<data name="SmtpAuthentication.HelpText" xml:space="preserve">
<value>Specify the SMTP authentication type</value>
</data>
<data name="SmtpClientID.Text" xml:space="preserve">
<value>Client ID:</value>
</data>
<data name="SmtpClientID.HelpText" xml:space="preserve">
<value>The Client ID for the SMTP provider</value>
</data>
<data name="SmtpClientSecret.Text" xml:space="preserve">
<value>Client Secret:</value>
</data>
<data name="SmtpClientSecret.HelpText" xml:space="preserve">
<value>The Client Secret for the SMTP provider</value>
</data>
<data name="SmtpScopes.Text" xml:space="preserve">
<value>Scopes:</value>
</data>
<data name="SmtpScopes.HelpText" xml:space="preserve">
<value>A list of Scopes for the SMTP provider (separated by commas)</value>
</data>
<data name="SmtpAuthority.Text" xml:space="preserve">
<value>Authority Url:</value>
</data>
<data name="SmtpAuthority.HelpText" xml:space="preserve">
<value>The Authority Url for the SMTP provider</value>
</data>
</root>

View File

@@ -147,9 +147,6 @@
<data name="Message.User.NoLogIn" xml:space="preserve">
<value>Current User Is Not Logged In</value>
</data>
<data name="Message.User.NoEmail" xml:space="preserve">
<value>You Must Provide An Email Address For Your User Account</value>
</data>
<data name="Error.Profile.Load" xml:space="preserve">
<value>Error Loading User Profile</value>
</data>

View File

@@ -162,4 +162,10 @@
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
<data name="Confirmed.Text" xml:space="preserve">
<value>Verified?</value>
</data>
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
</root>

View File

@@ -217,7 +217,7 @@
<value>The user's time zone</value>
</data>
<data name="Confirmed.Text" xml:space="preserve">
<value>Confirmed?</value>
<value>Verified?</value>
</data>
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>

View File

@@ -370,7 +370,13 @@
<value>Do you want users to use two factor authentication? Note that you should use the Disabled option until you have successfully verified that the Notification Job in Scheduled Jobs is enabled and your SMTP options in Site Settings are configured or else you will lock yourself out.</value>
</data>
<data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor?</value>
<value>Two Factor Authentication?</value>
</data>
<data name="RequireConfirmedEmail.HelpText" xml:space="preserve">
<value>Do you want to require registered users to verify their email address before they are allowed to log in?</value>
</data>
<data name="RequireConfirmedEmail.Text" xml:space="preserve">
<value>Require Verified Email?</value>
</data>
<data name="Disabled" xml:space="preserve">
<value>Disabled</value>
@@ -502,7 +508,7 @@
<value>Info</value>
</data>
<data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0</value>
<value>OAuth 2.0 (OAuth2)</value>
</data>
<data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value>

View File

@@ -123,4 +123,7 @@
<data name="PermissionDenied" xml:space="preserve">
<value>Permission Denied</value>
</data>
<data name="PermissionDisabled" xml:space="preserve">
<value>Permission Disabled</value>
</data>
</root>

View File

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

View File

@@ -256,7 +256,7 @@ namespace Oqtane.Services
Dictionary<string, string> SetSetting(Dictionary<string, string> settings, string settingName, string settingValue, bool isPrivate);
Dictionary<string, string> MergeSettings(Dictionary<string, string> settings1, Dictionary<string, string> settings2);
Dictionary<string, string> MergeSettings(Dictionary<string, string> baseSettings, Dictionary<string, string> overwriteSettings);
[Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)]

View File

@@ -1,11 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Services
{
/// <summary>
/// Service to store and retrieve <see cref="TimeZone"/> entries
/// Service to retrieve <see cref="TimeZone"/> entries
/// </summary>
public interface ITimeZoneService
{
@@ -13,6 +12,6 @@ namespace Oqtane.Services
/// Get the list of time zones
/// </summary>
/// <returns></returns>
Task<List<TimeZone>> GetTimeZonesAsync();
List<TimeZone> GetTimeZones();
}
}

View File

@@ -266,27 +266,25 @@ namespace Oqtane.Services
return settings;
}
public Dictionary<string, string> MergeSettings(Dictionary<string, string> settings1, Dictionary<string, string> settings2)
public Dictionary<string, string> MergeSettings(Dictionary<string, string> baseSettings, Dictionary<string, string> overwriteSettings)
{
if (settings1 == null)
var settings = baseSettings != null ? new Dictionary<string, string>(baseSettings) : new Dictionary<string, string>();
if (overwriteSettings != null)
{
settings1 = new Dictionary<string, string>();
}
if (settings2 != null)
{
foreach (var setting in settings2)
foreach (var setting in overwriteSettings)
{
if (settings1.ContainsKey(setting.Key))
if (settings.ContainsKey(setting.Key))
{
settings1[setting.Key] = setting.Value;
settings[setting.Key] = setting.Value;
}
else
{
settings1.Add(setting.Key, setting.Value);
settings.Add(setting.Key, setting.Value);
}
}
}
return settings1;
return settings;
}
[Obsolete("GetSettingAsync(int settingId) is deprecated. Use GetSettingAsync(string entityName, int settingId) instead.", false)]

View File

@@ -1,22 +1,58 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Localization;
using NodaTime.TimeZones;
using NodaTime;
using Oqtane.Documentation;
using Oqtane.Models;
using Oqtane.Shared;
using NodaTime.Extensions;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class TimeZoneService : ServiceBase, ITimeZoneService
public class TimeZoneService : ITimeZoneService
{
public TimeZoneService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private readonly IStringLocalizer<TimeZoneResources> _TimeZoneLocalizer;
private string Apiurl => CreateApiUrl("TimeZone");
public async Task<List<TimeZone>> GetTimeZonesAsync()
public TimeZoneService(IStringLocalizer<TimeZoneResources> TimeZoneLocalizer)
{
return await GetJsonAsync<List<TimeZone>>($"{Apiurl}");
_TimeZoneLocalizer = TimeZoneLocalizer;
}
public List<Models.TimeZone> GetTimeZones()
{
var timezones = new List<Models.TimeZone>();
foreach (var tz in DateTimeZoneProviders.Tzdb.GetAllZones()
// only include timezones which have a country code defined or are US timezones
.Where(item => !string.IsNullOrEmpty(TzdbDateTimeZoneSource.Default.ZoneLocations.FirstOrDefault(l => l.ZoneId == item.Id)?.CountryCode) || item.Id.ToLower().Contains("us/"))
// order by UTC offset (ie. -11:00 to +14:00)
.OrderBy(item => item.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks))
{
// get localized display name
var displayname = _TimeZoneLocalizer[tz.Id].Value;
if (displayname == tz.Id)
{
// use default "friendly" display format
displayname = displayname.Replace("_", " ").Replace("/", " / ");
}
// time zones can be excluded from the list by providing an empty translation in the localization file
if (!string.IsNullOrEmpty(displayname))
{
// include offset prefix
var offset = tz.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks;
displayname = "(UTC" + (offset >= 0 ? "+" : "-") + new DateTime(Math.Abs(offset)).ToString("HH:mm") + ") " + displayname;
timezones.Add(new Models.TimeZone()
{
Id = tz.Id,
DisplayName = displayname
});
}
}
return timezones;
}
}
}

View File

@@ -37,7 +37,7 @@
public override List<Resource> Resources => new List<Resource>()
{
// obtained from https://cdnjs.com/libraries
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", "anonymous"),
new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"),
new Stylesheet(ThemePath() + "Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
};

View File

@@ -20,6 +20,7 @@
protected override void OnParametersSet()
{
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// only include properties required by the ModuleActionsInteractive component
_pageState = new PageState
{
Alias = PageState.Alias,

View File

@@ -91,6 +91,7 @@
}
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// only include properties required by the ControlPanelInteractive component
_pageState = new PageState
{
Alias = PageState.Alias,

View File

@@ -353,7 +353,7 @@
module.PageId = PageState.Page.PageId;
module.ModuleDefinitionName = _moduleDefinitionName;
module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId);
module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName);
module = await ModuleService.AddModuleAsync(module);
newModuleId = module.ModuleId;
@@ -365,7 +365,7 @@
module.SiteId = PageState.Page.SiteId;
module.PageId = PageState.Page.PageId;
module.AllPages = false;
module.PermissionList = GenerateDefaultPermissions(module.SiteId);
module.PermissionList = GenerateDefaultPermissions(module.SiteId, module.ModuleDefinitionName);
module = await ModuleService.AddModuleAsync(module);
var moduleContent = await ModuleService.ExportModuleAsync(int.Parse(_moduleId), PageState.Page.PageId);
@@ -430,9 +430,11 @@
}
}
private List<Permission> GenerateDefaultPermissions(int siteId)
private List<Permission> GenerateDefaultPermissions(int siteId, string moduleDefinitionName)
{
var permissions = new List<Permission>();
// set module view permissions
if (_visibility == "view")
{
// set module view permissions to page view permissions
@@ -443,8 +445,22 @@
// set module view permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit);
}
// set module edit permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, PermissionNames.Edit, PermissionNames.Edit);
// set remaining module permissions
var permissionNames = PermissionNames.Edit;
var moduleDefinition = _allModuleDefinitions.FirstOrDefault(item => item.ModuleDefinitionName == moduleDefinitionName);
if (moduleDefinition != null && !string.IsNullOrEmpty(moduleDefinition.PermissionNames))
{
permissionNames = moduleDefinition.PermissionNames; // custom module permissions
}
foreach (var permission in permissionNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (permission != PermissionNames.View)
{
// set module permissions to page edit permissions
permissions = SetPermissions(permissions, siteId, permission, PermissionNames.Edit);
}
}
return permissions;
}

View File

@@ -103,6 +103,9 @@
{
var cookieConsentSetting = SettingService.GetSetting(PageState.Site.Settings, "CookieConsent", string.Empty);
_enabled = !string.IsNullOrEmpty(cookieConsentSetting);
if (!_enabled) return;
_optout = cookieConsentSetting == "optout";
_actioned = await CookieConsentService.IsActionedAsync();
@@ -164,4 +167,4 @@
StateHasChanged();
}
}
}
}

View File

@@ -16,8 +16,8 @@ namespace Oqtane.Themes.OqtaneTheme
ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client",
Resources = new List<Resource>()
{
// obtained from https://cdnjs.com/libraries
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", "anonymous"),
// obtained from https://cdnjs.com/libraries/bootswatch
new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.7/cyborg/bootstrap.min.css", "sha512-/LQFzDeXqysGQ/POl5YOEjgVZH1BmqDHvshhnFIChf50bMGQ470qhUrsecD9MRCUwzwqRoshwAbmA2oTW4I6Yg==", "anonymous"),
new Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}

View File

@@ -0,0 +1,14 @@
namespace Oqtane
{
/// <summary>
/// Dummy class used to collect shared resource strings for this application
/// </summary>
/// <remarks>
/// This class is mostly used with IStringLocalizer and IHtmlLocalizer interfaces.
/// The class must reside at the project root.
/// </remarks>
public class TimeZoneResources
{
}
}

View File

@@ -6,7 +6,7 @@
@if (ComponentType != null && _visible)
{
<a id="@ModuleState.PageModuleId.ToString()"></a>
<CascadingValue Value="@ModuleState">
<CascadingValue Value="@ModuleState" IsFixed="true">
@if (_useadminborder)
{
<div class="app-pane-admin-border">
@@ -31,6 +31,9 @@
[Parameter]
public Module ModuleState { get; set; }
[Parameter]
public string ContainerType { get; set; }
protected override bool ShouldRender()
{
return PageState?.RenderId == ModuleState?.RenderId;
@@ -44,6 +47,10 @@
protected override void OnParametersSet()
{
string container = ModuleState.ContainerType;
if (!string.IsNullOrEmpty(ContainerType))
{
container = ContainerType;
}
if (PageState.ModuleId != -1 && PageState.Route.Action != "" && ModuleState.UseAdminContainer)
{
container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer;

View File

@@ -224,17 +224,17 @@ namespace Oqtane.UI
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
{
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1);
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1, false);
return Task.CompletedTask;
}
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default)
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, bool anonymizeuploadfilenames, CancellationToken cancellationToken = default)
{
try
{
return _jsRuntime.InvokeAsync<bool>(
"Oqtane.Interop.uploadFiles", cancellationToken,
posturl, folder, id, antiforgerytoken, jwt, chunksize);
posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames);
}
catch
{

View File

@@ -10,11 +10,11 @@
@((MarkupString)_comment)
@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static)
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" />
<RenderModeBoundary ModuleState="@ModuleState" PageState="@_pageState" SiteState="@SiteState" />
}
else
{
<RenderModeBoundary ModuleState="@ModuleState" PageState="@PageState" SiteState="@SiteState" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, _prerender)" />
<RenderModeBoundary ModuleState="@ModuleState" PageState="@_pageState" SiteState="@SiteState" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(PageState.Site.Runtime, _prerender)" />
}
}
@if (PageState.ModuleId == -1)
@@ -32,6 +32,7 @@
private bool _prerender;
private string _comment;
private PageState _pageState;
protected override void OnParametersSet()
{
@@ -48,11 +49,12 @@
}
_comment += " -->";
_pageState = PageState.Clone();
if (PageState.RenderMode == RenderModes.Static && ModuleState.RenderMode == RenderModes.Interactive)
{
// trim PageState to mitigate page bloat caused by Blazor serializing/encrypting state when crossing render mode boundaries
// please note that this performance optimization results in the PageState.Pages property not being available for use in Interactive components
PageState.Site.Pages = new List<Page>();
// please note that this performance optimization results in the PageState.Pages property not being available for use in downstream Interactive components
_pageState.Site.Pages = new List<Page>();
}
}

View File

@@ -37,5 +37,34 @@ namespace Oqtane.UI
{
get { return Site?.Languages; }
}
public PageState Clone()
{
return new PageState
{
Alias = Alias,
Site = Site,
Page = Page,
Modules = Modules,
User = User,
Uri = Uri,
Route = Route,
QueryString = QueryString,
UrlParameters = UrlParameters,
ModuleId = ModuleId,
Action = Action,
EditMode = EditMode,
LastSyncDate = LastSyncDate,
RenderMode = RenderMode,
Runtime = Runtime,
VisitorId = VisitorId,
RemoteIPAddress = RemoteIPAddress,
ReturnUrl = ReturnUrl,
IsInternalNavigation = IsInternalNavigation,
RenderId = RenderId,
Refresh = Refresh,
AllowCookies = AllowCookies
};
}
}
}

View File

@@ -26,10 +26,13 @@ else
[Parameter]
public string Name { get; set; }
[Parameter]
public string ContainerType { get; set; }
RenderFragment DynamicComponent { get; set; }
protected override void OnParametersSet()
{
{
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList) && PageState.Action == Constants.DefaultAction)
{
_useadminborder = true;
@@ -45,12 +48,6 @@ else
{
foreach (Module module in PageState.Modules)
{
// set renderid - this allows the framework to determine which components should be rendered when PageState changes
if (module.RenderId != PageState.RenderId)
{
module.RenderId = PageState.RenderId;
}
var pane = module.Pane;
if (module.ModuleId == PageState.ModuleId && PageState.Action != Constants.DefaultAction)
{
@@ -101,7 +98,7 @@ else
if (authorized)
{
CreateComponent(builder, module, module.PageModuleId);
CreateComponent(builder, module);
}
}
}
@@ -112,7 +109,7 @@ else
// check if user is authorized to view module
if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList))
{
CreateComponent(builder, module, -1);
CreateComponent(builder, module);
}
}
}
@@ -121,14 +118,12 @@ else
};
}
private void CreateComponent(RenderTreeBuilder builder, Module module, int key)
private void CreateComponent(RenderTreeBuilder builder, Module module)
{
builder.OpenComponent(0, typeof(ContainerBuilder));
builder.AddAttribute(1, "ModuleState", module);
if (key != -1)
{
builder.SetKey(module.PageModuleId);
}
builder.AddAttribute(2, "ContainerType", ContainerType);
builder.SetKey(module.PageModuleId);
builder.CloseComponent();
}
}

View File

@@ -4,8 +4,8 @@
@inject ILogService LoggingService
@inherits ErrorBoundary
<CascadingValue Value="@PageState">
<CascadingValue Value="@ModuleState">
<CascadingValue Value="@PageState" IsFixed="true">
<CascadingValue Value="@ModuleState" IsFixed="true">
@if (CurrentException is null)
{
@if (ModuleType != null)

View File

@@ -48,12 +48,18 @@
private bool _initialized = false;
private bool _installed = false;
private string _display = "display: none;";
private string _display = "";
private PageState _pageState { get; set; }
protected override async Task OnParametersSetAsync()
{
if (PageState != null && PageState.RenderMode == RenderModes.Interactive && PageState.Site.Prerender)
{
// prevents flash on initial interactive page load when using prerendering
_display = "display: none;";
}
SiteState.AntiForgeryToken = AntiForgeryToken;
SiteState.AuthorizationToken = AuthorizationToken;
SiteState.Platform = Platform;
@@ -61,7 +67,7 @@
if (Runtime == Runtimes.Hybrid)
{
var installation = await InstallationService.IsInstalled();
var installation = await InstallationService.IsInstalled();
_installed = installation.Success;
if (installation.Alias != null)
{
@@ -73,8 +79,8 @@
if (PageState != null)
{
_pageState = PageState;
SiteState.Alias = PageState.Alias;
SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : "";
SiteState.Alias = _pageState.Alias;
SiteState.RemoteIPAddress = _pageState.RemoteIPAddress;
_installed = true;
}
}
@@ -85,9 +91,7 @@
{
if (firstRender)
{
// prevents flash on initial interactive page load
_display = "";
StateHasChanged();
}
}

View File

@@ -71,7 +71,7 @@
{
if (PageState == null || PageState.Refresh)
{
await Refresh();
await Refresh(false);
}
}
@@ -79,7 +79,7 @@
{
_absoluteUri = args.Location;
_isInternalNavigation = true;
await Refresh();
await Refresh(true);
}
Task IHandleAfterRender.OnAfterRenderAsync()
@@ -93,7 +93,7 @@
}
[SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
private async Task Refresh()
private async Task Refresh(bool locationChanged)
{
Site site = null;
Page page = null;
@@ -103,6 +103,7 @@
var refresh = false;
var lastsyncdate = DateTime.MinValue;
var visitorId = -1;
var renderid = Guid.Empty;
_error = "";
Route route = new Route(_absoluteUri, SiteState.Alias.Path);
@@ -288,11 +289,21 @@
modules = PageState.Modules;
}
// renderid allows the framework to determine which module components should be rendered on a page
if (PageState == null || locationChanged)
{
renderid = Guid.NewGuid();
}
else
{
renderid = PageState.RenderId;
}
// load additional metadata for current page
page = ProcessPage(page, site, user, SiteState.Alias, action);
// load additional metadata for modules
(page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
(page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias, renderid);
//cookie consent
var _allowCookies = PageState?.AllowCookies;
@@ -324,7 +335,7 @@
RemoteIPAddress = SiteState.RemoteIPAddress,
ReturnUrl = returnurl,
IsInternalNavigation = _isInternalNavigation,
RenderId = Guid.NewGuid(),
RenderId = renderid,
Refresh = false,
AllowCookies = _allowCookies.GetValueOrDefault(true)
};
@@ -447,7 +458,7 @@
return page;
}
private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias)
private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias, Guid renderid)
{
var paneindex = new Dictionary<string, int>();
@@ -592,6 +603,8 @@
{
module.ContainerType = defaultcontainertype;
}
module.RenderId = renderid;
}
foreach (Module module in modules.Where(item => item.PageId == page.PageId))

View File

@@ -20,13 +20,6 @@
return;
}
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
// set page title
if (!string.IsNullOrEmpty(PageState.Page.Title))
{

View File

@@ -75,13 +75,9 @@ namespace Oqtane.Database.MySQL
return dr;
}
public override string RewriteName(string name, bool isQuery)
public override string DelimitName(string name)
{
if (name.ToLower() == "rows" && isQuery)
{
name = $"`{name}`"; // escape reserved word in SQL query
}
return name;
return $"`{name}`";
}
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySql.Data" Version="9.3.0" />
<PackageReference Include="MySql.Data" Version="9.4.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>

View File

@@ -87,9 +87,9 @@ namespace Oqtane.Database.PostgreSQL
return _rewriter.RewriteName(name);
}
public override string RewriteName(string name, bool isQuery)
public override string DelimitName(string name)
{
return _rewriter.RewriteName(name);
return $"\"{name}\"";
}
public override string RewriteValue(string value, string type)

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -46,6 +46,11 @@ namespace Oqtane.Database.SqlServer
}
}
public override string DelimitName(string name)
{
return $"[{name}]";
}
public override int ExecuteNonQuery(string connectionString, string query)
{
var conn = new SqlConnection(FormatConnectionString(connectionString));

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@@ -33,7 +33,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -84,6 +84,11 @@ namespace Oqtane.Database.Sqlite
return dr;
}
public override string DelimitName(string name)
{
return $"\"{name}\"";
}
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{
return optionsBuilder.UseSqlite(connectionString)

View File

@@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@@ -30,7 +30,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>6.1.3</ApplicationDisplayVersion>
<ApplicationDisplayVersion>6.1.4</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@@ -67,14 +67,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.5" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.61" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.61" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.61" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.7" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.90" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.90" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.90" />
</ItemGroup>
<ItemGroup>

View File

@@ -75,6 +75,10 @@ app {
color: gray;
}
.app-moduleactions .dropdown-menu {
z-index: 9999;
}
.app-moduleactions .dropdown-submenu {
position: relative;
}
@@ -235,18 +239,19 @@ app {
.app-form-inline {
display: inline;
}
.app-search{
.app-search {
display: inline-block;
position: relative;
}
.app-search input + button{
.app-search input + button {
background: none;
border: none;
position: absolute;
right: 0;
top: 0;
}
.app-search input + button .oi{
.app-search input + button .oi {
top: 0;
}
.app-search-noinput {
@@ -270,4 +275,14 @@ app {
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}
}
/* cookie consent */
.gdpr-consent-bar .btn-show {
bottom: -3px;
left: 5px;
}
.gdpr-consent-bar .btn-hide {
top: 0;
right: 5px;
}

View File

@@ -311,7 +311,7 @@ Oqtane.Interop = {
}
return files;
},
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) {
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) {
var success = true;
var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id);
@@ -344,16 +344,22 @@ Oqtane.Interop = {
const totalParts = Math.ceil(file.size / chunkSize);
let partCount = 0;
let filename = file.name;
if (anonymizeuploadfilenames) {
filename = crypto.randomUUID() + '.' + filename.split('.').pop();
}
const uploadPart = () => {
const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
return new Promise((resolve, reject) => {
let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name);
formdata.append('formfile', chunk, filename);
var credentials = 'same-origin';
var headers = new Headers();

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>6.1.3</version>
<version>6.1.4</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/v6.1.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Framework</id>
<version>6.1.3</version>
<version>6.1.4</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/v6.1.3/Oqtane.Framework.6.1.3.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v6.1.4/Oqtane.Framework.6.1.4.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane framework</tags>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Server</id>
<version>6.1.3</version>
<version>6.1.4</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/v6.1.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>6.1.3</version>
<version>6.1.4</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/v6.1.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>6.1.3</version>
<version>6.1.4</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/v6.1.3</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.3.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.4.Install.zip" -Force

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.3.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.4.Upgrade.zip" -Force

View File

@@ -345,6 +345,7 @@
DateTime expiry = DateTime.MinValue;
if (visitorCookieValue != null && visitorCookieValue.Contains("|"))
{
// visitor cookies contain the visitor id and an expiry date separated by a pipe symbol
var values = visitorCookieValue.Split('|');
int.TryParse(values[0], out _visitorId);
DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry);

View File

@@ -444,9 +444,14 @@ namespace Oqtane.Controllers
}
// ensure filename is valid
if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName))
string fileName = formfile.FileName;
if (Path.GetExtension(fileName).Contains(':'))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
fileName = fileName.Substring(0, fileName.LastIndexOf(':')); // remove invalid suffix from extension
}
if (!fileName.IsPathOrFileValid() || !HasValidFileExtension(fileName))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", fileName);
return StatusCode((int)HttpStatusCode.Forbidden);
}
@@ -458,8 +463,8 @@ namespace Oqtane.Controllers
return StatusCode((int)HttpStatusCode.Forbidden);
}
// create file name using header values
string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000");
// create file name using header part values
fileName += ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000");
string folderPath = "";
try
@@ -532,13 +537,13 @@ namespace Oqtane.Controllers
string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999"
int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1));
filename = Path.GetFileNameWithoutExtension(filename); // base filename
filename = Path.GetFileNameWithoutExtension(filename); // base filename including original file extension
string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist (note that file parts can arrive out of order)
if (fileparts.Length == totalparts && CanAccessFiles(fileparts))
{
// merge file parts into temp file (in case another user is trying to get the file)
// merge file parts into temp file (in case another user is trying to read the file)
bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{
@@ -559,25 +564,22 @@ namespace Oqtane.Controllers
}
// clean up file parts
foreach (var file in Directory.GetFiles(folder, "*" + token + "*"))
foreach (var file in fileparts)
{
if (fileparts.Contains(file))
try
{
try
{
System.IO.File.Delete(file);
}
catch
{
// unable to delete part - ignore
}
System.IO.File.Delete(file);
}
catch
{
// unable to delete part - ignore
}
}
// rename temp file
if (success)
{
// remove file if it already exists (as well as any thumbnails which may exist)
// remove existing file (as well as any thumbnails)
foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*"))
{
if (Path.GetExtension(file) != ".tmp")

View File

@@ -43,7 +43,6 @@ namespace Oqtane.Controllers
private readonly ILogManager _logger;
private readonly Alias _alias;
private readonly string _visitorCookie;
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager,
IOptions<CookieAuthenticationOptions> cookieOptions, IOptionsSnapshot<CookieAuthenticationOptions> cookieOptionsSnapshot, IOptionsMonitorCache<CookieAuthenticationOptions> cookieOptionsMonitorCache,
@@ -70,7 +69,6 @@ namespace Oqtane.Controllers
_identityOptionsMonitorCache = identityOptionsMonitorCache;
_logger = logger;
_alias = tenantManager.GetAlias();
_visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString();
}
// GET: api/<controller>
@@ -299,11 +297,8 @@ namespace Oqtane.Controllers
authorized = User.IsInRole(RoleNames.Admin);
if (!authorized)
{
// a visitor may have cookies disabled
if (int.TryParse(Request.Cookies[_visitorCookie], out int visitorId))
{
authorized = (visitorId == entityId);
}
var visitorCookieName = Constants.VisitorCookiePrefix + _alias.SiteId.ToString();
authorized = (entityId == GetVisitorCookieId(Request.Cookies[visitorCookieName]));
}
break;
default: // custom entity
@@ -344,11 +339,8 @@ namespace Oqtane.Controllers
case EntityNames.Visitor:
if (!User.IsInRole(RoleNames.Admin))
{
filter = true;
if (int.TryParse(Request.Cookies[_visitorCookie], out int visitorId))
{
filter = (visitorId != entityId);
}
var visitorCookieName = Constants.VisitorCookiePrefix + _alias.SiteId.ToString();
filter = (entityId != GetVisitorCookieId(Request.Cookies[visitorCookieName]));
}
break;
default: // custom entity
@@ -358,6 +350,13 @@ namespace Oqtane.Controllers
return filter;
}
private int GetVisitorCookieId(string visitorCookie)
{
// visitor cookies contain the visitor id and an expiry date separated by a pipe symbol
visitorCookie = (visitorCookie.Contains("|")) ? visitorCookie.Split('|')[0] : visitorCookie;
return (int.TryParse(visitorCookie, out int visitorId)) ? visitorId : -1;
}
private void AddSyncEvent(string EntityName, int EntityId, int SettingId, string Action)
{
_syncManager.AddSyncEvent(_alias, EntityName + "Setting", SettingId, Action);

View File

@@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class TimeZoneController : Controller
{
public TimeZoneController() {}
// GET: api/<controller>
[HttpGet]
public IEnumerable<Models.TimeZone> Get()
{
return TimeZoneInfo.GetSystemTimeZones()
.Select(item => new Models.TimeZone
{
Id = item.Id,
DisplayName = item.DisplayName
})
.OrderBy(item => item.DisplayName);
}
}
}

View File

@@ -165,14 +165,13 @@ namespace Oqtane.Controllers
bool allowregistration;
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{
user.EmailConfirmed = true;
user.IsAuthenticated = true;
user.IsAuthenticated = true; // admins can add any existing user to a site
allowregistration = true;
}
else
{
user.EmailConfirmed = false;
user.IsAuthenticated = false;
user.EmailConfirmed = false; // standard users cannot specify that their email is verified
user.IsAuthenticated = false; // existing users can only be added to a site if they provide a valid username and password
allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
}

View File

@@ -51,11 +51,8 @@ namespace Oqtane.Controllers
bool authorized = User.IsInRole(RoleNames.Admin);
if (!authorized)
{
var visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString();
if (int.TryParse(Request.Cookies[visitorCookie], out int visitorId))
{
authorized = (visitorId == id);
}
var visitorCookieName = Constants.VisitorCookiePrefix + _alias.SiteId.ToString();
authorized = (id == GetVisitorCookieId(Request.Cookies[visitorCookieName]));
}
var visitor = _visitors.GetVisitor(id);
@@ -77,5 +74,12 @@ namespace Oqtane.Controllers
return null;
}
}
private int GetVisitorCookieId(string visitorCookie)
{
// visitor cookies contain the visitor id and an expiry date separated by a pipe symbol
visitorCookie = (visitorCookie.Contains("|")) ? visitorCookie.Split('|')[0] : visitorCookie;
return (int.TryParse(visitorCookie, out int visitorId)) ? visitorId : -1;
}
}
}

View File

@@ -61,12 +61,12 @@ namespace Oqtane.Databases
public abstract IDataReader ExecuteReader(string connectionString, string query);
public virtual string RewriteName(string name)
public virtual string DelimitName(string name)
{
return name;
}
public virtual string RewriteName(string name, bool isQuery)
public virtual string RewriteName(string name)
{
return name;
}

View File

@@ -26,9 +26,9 @@ namespace Oqtane.Databases.Interfaces
public IDataReader ExecuteReader(string connectionString, string query);
public string RewriteName(string name);
public string DelimitName(string name); // only used in conjunction with method using MigrationBuilder.Sql()
public string RewriteName(string name, bool isQuery);
public string RewriteName(string name);
public string RewriteValue(string value, string type);

View File

@@ -228,11 +228,12 @@ namespace Microsoft.Extensions.DependencyInjection
options.Lockout.AllowedForNewUsers = false;
// SignIn settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedPhoneNumber = false;
// User settings
options.User.RequireUniqueEmail = false; // changing to true will cause issues for legacy data
options.User.RequireUniqueEmail = false;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
});

View File

@@ -404,13 +404,13 @@ namespace Oqtane.Extensions
else if (!string.IsNullOrEmpty(name)) // name claim provided
{
username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss");
emailaddress = ""; // unknown - will need to be requested from user later
emailaddress = username + "@unknown.com";
displayname = name;
}
else // neither email nor name provided
{
username = Guid.NewGuid().ToString("N");
emailaddress = ""; // unknown - will need to be requested from user later
emailaddress = username + "@unknown.com";
displayname = username;
}

View File

@@ -738,7 +738,7 @@ namespace Oqtane.Infrastructure
databases += "{ \"Name\": \"LocalDB\", \"ControlType\": \"Oqtane.Installer.Controls.LocalDBConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer\" },";
databases += "{ \"Name\": \"SQL Server\", \"ControlType\": \"Oqtane.Installer.Controls.SqlServerConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer\" },";
databases += "{ \"Name\": \"SQLite\", \"ControlType\": \"Oqtane.Installer.Controls.SqliteConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.Sqlite.SqliteDatabase, Oqtane.Database.Sqlite\" },";
databases += "{ \"Name\": \"MySQL\", \"ControlType\": \"Oqtane.Installer.Controls.MySQLConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.MySQL.SqlServerDatabase, Oqtane.Database.MySQL\" },";
databases += "{ \"Name\": \"MySQL\", \"ControlType\": \"Oqtane.Installer.Controls.MySQLConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.MySQL.MySQLDatabase, Oqtane.Database.MySQL\" },";
databases += "{ \"Name\": \"PostgreSQL\", \"ControlType\": \"Oqtane.Installer.Controls.PostgreSQLConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.PostgreSQL.PostgreSQLDatabase, Oqtane.Database.PostgreSQL\" }";
databases += "]";
_configManager.AddOrUpdateSetting(SettingKeys.AvailableDatabasesSection, databases, true);

View File

@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
using MailKit.Net.Smtp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using MimeKit;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
using MailKit.Security;
namespace Oqtane.Infrastructure
{
@@ -23,7 +26,7 @@ namespace Oqtane.Infrastructure
}
// job is executed for each tenant in installation
public override string ExecuteJob(IServiceProvider provider)
public async override Task<string> ExecuteJobAsync(IServiceProvider provider)
{
string log = "";
@@ -44,124 +47,175 @@ namespace Oqtane.Infrastructure
if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True")
{
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") != "" &&
settingRepository.GetSettingValue(settings, "SMTPPort", "") != "" &&
settingRepository.GetSettingValue(settings, "SMTPSender", "") != "")
bool valid = true;
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
{
// construct SMTP Client
var client = new SmtpClient()
// basic
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
DeliveryMethod = SmtpDeliveryMethod.Network,
UseDefaultCredentials = false,
Host = settingRepository.GetSettingValue(settings, "SMTPHost", ""),
Port = int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")),
EnableSsl = bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False"))
};
if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "")
{
client.Credentials = new NetworkCredential(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), settingRepository.GetSettingValue(settings, "SMTPPassword", ""));
log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "<br />";
valid = false;
}
// iterate through undelivered notifications
int sent = 0;
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
foreach (Notification notification in notifications)
{
// get sender and receiver information from user object if not provided
if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null)
{
var user = userRepository.GetUser(notification.FromUserId.Value);
if (user != null)
{
notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail;
notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName;
}
}
if ((string.IsNullOrEmpty(notification.ToEmail) || string.IsNullOrEmpty(notification.ToDisplayName)) && notification.ToUserId != null)
{
var user = userRepository.GetUser(notification.ToUserId.Value);
if (user != null)
{
notification.ToEmail = (string.IsNullOrEmpty(notification.ToEmail)) ? user.Email : notification.ToEmail;
notification.ToDisplayName = (string.IsNullOrEmpty(notification.ToDisplayName)) ? user.DisplayName : notification.ToDisplayName;
}
}
// validate recipient
if (string.IsNullOrEmpty(notification.ToEmail) || !MailAddress.TryCreate(notification.ToEmail, out _))
{
log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}<br />";
notification.IsDeleted = true;
notificationRepository.UpdateNotification(notification);
}
else
{
MailMessage mailMessage = new MailMessage();
// sender
if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True" && !string.IsNullOrEmpty(notification.FromEmail))
{
if (!string.IsNullOrEmpty(notification.FromDisplayName))
{
mailMessage.From = new MailAddress(notification.FromEmail, notification.FromDisplayName);
}
else
{
mailMessage.From = new MailAddress(notification.FromEmail);
}
}
else
{
mailMessage.From = new MailAddress(settingRepository.GetSettingValue(settings, "SMTPSender", ""), (!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name);
}
// recipient
if (!string.IsNullOrEmpty(notification.ToDisplayName))
{
mailMessage.To.Add(new MailAddress(notification.ToEmail, notification.ToDisplayName));
}
else
{
mailMessage.To.Add(new MailAddress(notification.ToEmail));
}
// subject
mailMessage.Subject = notification.Subject;
//body
mailMessage.Body = notification.Body;
if (!mailMessage.Body.Contains("<") || !mailMessage.Body.Contains(">"))
{
// plain text messages should convert line breaks to HTML tags to preserve formatting
mailMessage.Body = mailMessage.Body.Replace("\n", "<br />");
}
// encoding
mailMessage.SubjectEncoding = System.Text.Encoding.UTF8;
mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
mailMessage.IsBodyHtml = true;
// send mail
try
{
client.Send(mailMessage);
sent++;
notification.IsDelivered = true;
notification.DeliveredOn = DateTime.UtcNow;
notificationRepository.UpdateNotification(notification);
}
catch (Exception ex)
{
// error
log += $"NotificationId: {notification.NotificationId} - {ex.Message}<br />";
}
}
}
log += "Notifications Delivered: " + sent + "<br />";
}
else
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "<br />";
// oauth
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPAuthority", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientId", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientSecret", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPScopes", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, Authority, Client ID, Client Secret, Scopes, And Sender Are All Required" + "<br />";
valid = false;
}
}
if (valid)
{
// construct SMTP Client
using var client = new SmtpClient();
await client.ConnectAsync(settingRepository.GetSettingValue(settings, "SMTPHost", ""),
int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")),
bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
{
// it is possible to use basic without any authentication (not recommended)
if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "")
{
await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""),
settingRepository.GetSettingValue(settings, "SMTPPassword", ""));
}
}
else
{
// oauth authentication
var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(settingRepository.GetSettingValue(settings, "SMTPClientId", ""))
.WithAuthority(settingRepository.GetSettingValue(settings, "SMTPAuthority", ""))
.WithClientSecret(settingRepository.GetSettingValue(settings, "SMTPClientSecret", ""))
.Build();
try
{
var result = await confidentialClientApplication.AcquireTokenForClient(settingRepository.GetSettingValue(settings, "SMTPScopes", "").Split(',')).ExecuteAsync();
var oauth2 = new SaslMechanismOAuth2(settingRepository.GetSettingValue(settings, "SMTPSender", ""), result.AccessToken);
await client.AuthenticateAsync(oauth2);
}
catch (Exception ex)
{
log += "SMTP Not Configured Properly In Site Settings - OAuth Token Could Not Be Retrieved From Authority - " + ex.Message + "<br />";
valid = false;
}
}
if (valid)
{
// iterate through undelivered notifications
int sent = 0;
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
foreach (Notification notification in notifications)
{
// get sender and receiver information from user object if not provided
if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null)
{
var user = userRepository.GetUser(notification.FromUserId.Value);
if (user != null)
{
notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail;
notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName;
}
}
if ((string.IsNullOrEmpty(notification.ToEmail) || string.IsNullOrEmpty(notification.ToDisplayName)) && notification.ToUserId != null)
{
var user = userRepository.GetUser(notification.ToUserId.Value);
if (user != null)
{
notification.ToEmail = (string.IsNullOrEmpty(notification.ToEmail)) ? user.Email : notification.ToEmail;
notification.ToDisplayName = (string.IsNullOrEmpty(notification.ToDisplayName)) ? user.DisplayName : notification.ToDisplayName;
}
}
// validate recipient
if (string.IsNullOrEmpty(notification.ToEmail) || !MailboxAddress.TryParse(notification.ToEmail, out _))
{
log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}<br />";
notification.IsDeleted = true;
notificationRepository.UpdateNotification(notification);
}
else
{
MimeMessage mailMessage = new MimeMessage();
// sender
if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True" && !string.IsNullOrEmpty(notification.FromEmail))
{
if (!string.IsNullOrEmpty(notification.FromDisplayName))
{
mailMessage.From.Add(new MailboxAddress(notification.FromDisplayName, notification.FromEmail));
}
else
{
mailMessage.From.Add(new MailboxAddress("", notification.FromEmail));
}
}
else
{
mailMessage.From.Add(new MailboxAddress((!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name,
settingRepository.GetSettingValue(settings, "SMTPSender", "")));
}
// recipient
if (!string.IsNullOrEmpty(notification.ToDisplayName))
{
mailMessage.To.Add(new MailboxAddress(notification.ToDisplayName, notification.ToEmail));
}
else
{
mailMessage.To.Add(new MailboxAddress("", notification.ToEmail));
}
// subject
mailMessage.Subject = notification.Subject;
//body
var bodyText = notification.Body;
if (!bodyText.Contains('<') || !bodyText.Contains('>'))
{
// plain text messages should convert line breaks to HTML tags to preserve formatting
bodyText = bodyText.Replace("\n", "<br />");
}
mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8)
{
Text = bodyText
};
// send mail
try
{
await client.SendAsync(mailMessage);
sent++;
notification.IsDelivered = true;
notification.DeliveredOn = DateTime.UtcNow;
notificationRepository.UpdateNotification(notification);
}
catch (Exception ex)
{
// error
log += $"NotificationId: {notification.NotificationId} - {ex.Message}<br />";
}
}
}
await client.DisconnectAsync(true);
log += "Notifications Delivered: " + sent + "<br />";
}
}
}
else

View File

@@ -180,7 +180,7 @@ namespace Oqtane.Managers
if (User != null)
{
string siteName = _sites.GetSite(user.SiteId).Name;
if (!user.EmailConfirmed)
if (!user.EmailConfirmed && bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")))
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
@@ -252,29 +252,32 @@ namespace Oqtane.Managers
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
}
if (user.EmailConfirmed)
if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")))
{
if (!identityuser.EmailConfirmed)
if (user.EmailConfirmed)
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
if (!identityuser.EmailConfirmed)
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password.";
string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password.";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
}
else
{
identityuser.EmailConfirmed = false;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
}
else
{
identityuser.EmailConfirmed = false;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
@@ -354,15 +357,14 @@ namespace Oqtane.Managers
if (!user.IsDeleted)
{
var alias = _tenantManager.GetAlias();
var twoFactorSetting = _settings.GetSetting(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor")?.SettingValue ?? "false";
var twoFactorRequired = twoFactorSetting == "required" || user.TwoFactorRequired;
string siteName = _sites.GetSite(alias.SiteId).Name;
var twoFactorRequired = _settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired;
if (twoFactorRequired)
{
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
user.TwoFactorCode = token;
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
_users.UpdateUser(user);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["TwoFactorEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["TwoFactorEmailBody"].Value;
@@ -377,7 +379,7 @@ namespace Oqtane.Managers
}
else
{
if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
if (!bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")) || await _identityUserManager.IsEmailConfirmedAsync(identityuser))
{
user = GetUser(identityuser.UserName, alias.SiteId);
if (user != null)
@@ -400,13 +402,25 @@ namespace Oqtane.Managers
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Is Not An Active Member Of Site {SiteId}", user.Username, alias.SiteId);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Denied - User {Username} Is Not An Active Member Of Site {SiteId}", user.Username, alias.SiteId);
}
}
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Denied - User Email Address Not Verified For {Username}", user.Username);
// send verification email again
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string subject = _localizer["VerificationEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["VerificationEmailBody"].Value;
body = body.Replace("[UserDisplayName]", user.DisplayName);
body = body.Replace("[URL]", url);
body = body.Replace("[SiteName]", siteName);
var notification = new Notification(alias.SiteId, user, subject, body);
_notifications.AddNotification(notification);
}
}
}
@@ -538,8 +552,7 @@ namespace Oqtane.Managers
if (user != null)
{
var alias = _tenantManager.GetAlias();
var twoFactorSetting = _settings.GetSetting(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor")?.SettingValue ?? "false";
var twoFactorRequired = twoFactorSetting == "required" || user.TwoFactorRequired;
var twoFactorRequired = _settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired;
if (twoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{
user.IsAuthenticated = true;

View File

@@ -33,28 +33,28 @@ namespace Oqtane.Migrations.EntityBuilders
protected string Schema { get; init; }
private string RewriteSqlEntityTableName(string name)
private string AddSchema(string name)
{
if (Schema == null)
if (string.IsNullOrEmpty(Schema))
{
return RewriteName(name);
return name;
}
else
{
return $"{Schema}.{RewriteName(name)}";
return $"{Schema}.{name}";
}
}
private string DelimitName(string name)
{
return ActiveDatabase.DelimitName(name);
}
private string RewriteName(string name)
{
return ActiveDatabase.RewriteName(name);
}
private string RewriteName(string name, bool isQuery)
{
return ActiveDatabase.RewriteName(name, isQuery);
}
private string RewriteValue(string value, string type)
{
return ActiveDatabase.RewriteValue(value, type);
@@ -468,9 +468,10 @@ namespace Oqtane.Migrations.EntityBuilders
public void DeleteFromTable(string condition = "")
{
var deleteSql = $"DELETE FROM {RewriteSqlEntityTableName(EntityTableName)} ";
var deleteSql = $"DELETE FROM {AddSchema(DelimitName(RewriteName(EntityTableName)))} ";
if(!string.IsNullOrEmpty(condition))
{
// note that condition values must be created using RewriteName(), DelimitName(), RewriteValue() if targeting multiple database platforms
deleteSql += $"WHERE {condition}";
}
_migrationBuilder.Sql(deleteSql);
@@ -488,9 +489,10 @@ namespace Oqtane.Migrations.EntityBuilders
public void UpdateColumn(string columnName, string value, string type, string condition)
{
var updateSql = $"UPDATE {RewriteSqlEntityTableName(EntityTableName)} SET {RewriteName(columnName, true)} = {RewriteValue(value, type)} ";
var updateSql = $"UPDATE {AddSchema(DelimitName(RewriteName(EntityTableName)))} SET {DelimitName(RewriteName(columnName))} = {RewriteValue(value, type)} ";
if (!string.IsNullOrEmpty(condition))
{
// note that condition values must be created using RewriteName(), DelimitName(), RewriteValue() if targeting multiple database platforms
updateSql += $"WHERE {condition}";
}
_migrationBuilder.Sql(updateSql);

View File

@@ -12,14 +12,19 @@ namespace Oqtane.Migrations
protected IDatabase ActiveDatabase { get; }
protected string RewriteName(string name)
protected string DelimitName(string name)
{
return ActiveDatabase.RewriteName(name, false);
return ActiveDatabase.DelimitName(name);
}
protected string RewriteName(string name, bool isQuery)
protected string RewriteName(string name)
{
return ActiveDatabase.RewriteName(name, isQuery);
return ActiveDatabase.RewriteName(name);
}
protected string RewriteValue(string value, string type)
{
return ActiveDatabase.RewriteValue(value, type);
}
}
}

View File

@@ -21,7 +21,7 @@ namespace Oqtane.Migrations.Tenant
notificationEntityBuilder.AddDateTimeColumn("SendOn", true);
//Update new Column
notificationEntityBuilder.UpdateColumn("SendOn", $"{ActiveDatabase.RewriteName("CreatedOn")}", $"{ActiveDatabase.RewriteName("SendOn")} IS NULL");
notificationEntityBuilder.UpdateColumn("SendOn", $"{RewriteName("CreatedOn")}", $"{DelimitName(RewriteName("SendOn"))} IS NULL");
}
protected override void Down(MigrationBuilder migrationBuilder)

View File

@@ -18,8 +18,8 @@ namespace Oqtane.Migrations.Tenant
{
///Update Icon Field in Page
var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase);
var updateSql = ActiveDatabase.ConcatenateSql("'oi oi-'", $"{ActiveDatabase.RewriteName("Icon")}");
pageEntityBuilder.UpdateColumn("Icon", updateSql, $"{ActiveDatabase.RewriteName("Icon")} <> ''" );
var updateSql = ActiveDatabase.ConcatenateSql("'oi oi-'", $"{DelimitName(RewriteName("Icon"))}");
pageEntityBuilder.UpdateColumn("Icon", updateSql, $"{DelimitName(RewriteName("Icon"))} <> ''" );
}
}
}

View File

@@ -20,18 +20,18 @@ namespace Oqtane.Migrations.Tenant
{
//Update DefaultContainerType In Site
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'");
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'");
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'");
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'");
//Update DefaultContainerType in Page
var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase);
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'");
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'");
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'");
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'");
//Update ContainerType in PageModule
var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase);
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'");
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'");
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'");
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.NoTitle, Oqtane.Client'");
}
}

View File

@@ -29,22 +29,22 @@ namespace Oqtane.Migrations.Tenant
siteEntityBuilder.DropColumn("DefaultLayoutType");
//Update DefaultContainerType In Site
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'");
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'");
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'");
siteEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'");
//Drop Column from Page Table
var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase);
pageEntityBuilder.DropColumn("LayoutType");
//Update DefaultContainerType in Page
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'");
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "DefaultContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'");
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'");
pageEntityBuilder.UpdateColumn("DefaultContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("DefaultContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'");
//Update ContainerType in PageModule
var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase);
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'");
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", "ContainerType = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'");
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultTitle, Oqtane.Client'");
pageModuleEntityBuilder.UpdateColumn("ContainerType", "'Oqtane.Themes.OqtaneTheme.Container, Oqtane.Client'", $"{DelimitName(RewriteName("ContainerType"))} = 'Oqtane.Themes.OqtaneTheme.DefaultNoTitle, Oqtane.Client'");
}
}

View File

@@ -20,7 +20,7 @@ namespace Oqtane.Migrations.Tenant
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
folderEntityBuilder.AddIntegerColumn("Capacity", true);
folderEntityBuilder.UpdateColumn("Capacity", "0");
folderEntityBuilder.UpdateColumn("Capacity", Constants.UserFolderCapacity.ToString(), $"{ActiveDatabase.RewriteName("Name")} = 'My Folder'");
folderEntityBuilder.UpdateColumn("Capacity", Constants.UserFolderCapacity.ToString(), $"{DelimitName(RewriteName("Name"))} = 'My Folder'");
folderEntityBuilder.AddStringColumn("ImageSizes", 512, true, true);
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);

View File

@@ -18,13 +18,13 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'");
settingEntityBuilder.UpdateColumn("IsPublic", "1", "bool", $"{DelimitName(RewriteName("SettingName"))} NOT LIKE 'SMTP%'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", $"{RewriteName("SettingName")} NOT LIKE 'SMTP%'");
settingEntityBuilder.UpdateColumn("IsPublic", "0", "bool", $"{DelimitName(RewriteName("SettingName"))} NOT LIKE 'SMTP%'");
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Oqtane.Migrations.Tenant
var settingEntityBuilder = new SettingEntityBuilder(migrationBuilder, ActiveDatabase);
settingEntityBuilder.AddBooleanColumn("IsPrivate", true);
settingEntityBuilder.UpdateColumn("IsPrivate", "0", "bool", "");
settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", $"{RewriteName("EntityName")} = 'Site' AND { RewriteName("SettingName")} LIKE 'SMTP%'");
settingEntityBuilder.UpdateColumn("IsPrivate", "1", "bool", $"{DelimitName(RewriteName("EntityName"))} = 'Site' AND { DelimitName(RewriteName("SettingName"))} LIKE 'SMTP%'");
settingEntityBuilder.DropColumn("IsPublic");
}

View File

@@ -0,0 +1,30 @@
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.06.01.04.01")]
public class RemoveUniqueEmailIndex : MultiDatabaseMigration
{
public RemoveUniqueEmailIndex(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
// framework uses RequireUniqueEmail = False in .NET Identity configuration
var aspNetUsersEntityBuilder = new AspNetUsersEntityBuilder(migrationBuilder, ActiveDatabase);
aspNetUsersEntityBuilder.DropIndex("EmailIndex");
aspNetUsersEntityBuilder.AddIndex("EmailIndex", "NormalizedEmail", false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -0,0 +1,32 @@
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.06.01.04.02")]
public class ResetTimeZone : MultiDatabaseMigration
{
public ResetTimeZone(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
// resetting value as framework now uses IANA ID consistently for time zones
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.UpdateColumn("TimeZoneId", "''");
var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase);
userEntityBuilder.UpdateColumn("TimeZoneId", "''");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@@ -34,21 +34,22 @@
<EmbeddedResource Include="Scripts\MigrateTenant.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.5" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.7" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.7" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="MailKit" Version="4.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />

View File

@@ -16,5 +16,6 @@ namespace Oqtane.Repository
void DeleteSetting(string entityName, int settingId);
void DeleteSettings(string entityName, int entityId);
string GetSettingValue(IEnumerable<Setting> settings, string settingName, string defaultValue);
string GetSettingValue(string entityName, int entityId, string settingName, string defaultValue);
}
}

View File

@@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Oqtane.Models;
using Oqtane.Modules.Admin.Users;
using Oqtane.Shared;
namespace Oqtane.Repository
{
@@ -71,6 +73,19 @@ namespace Oqtane.Repository
public void DeleteRole(int roleId)
{
using var db = _dbContextFactory.CreateDbContext();
// remove userroles for role
foreach (var userrole in db.UserRole.Where(item => item.RoleId == roleId))
{
db.UserRole.Remove(userrole);
}
// remove permissions for role
foreach (var permission in db.Permission.Where(item => item.RoleId == roleId))
{
db.Permission.Remove(permission);
}
Role role = db.Role.Find(roleId);
db.Role.Remove(role);
db.SaveChanges();

View File

@@ -180,6 +180,19 @@ namespace Oqtane.Repository
}
}
public string GetSettingValue(string entityName, int entityId, string settingName, string defaultValue)
{
var setting = GetSetting(entityName, entityId, settingName);
if (setting != null)
{
return setting.SettingValue;
}
else
{
return defaultValue;
}
}
private bool IsMaster(string EntityName)
{
return (EntityName == EntityNames.ModuleDefinition || EntityName == EntityNames.Host);

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Oqtane.Models;
using Oqtane.Modules.Admin.Users;
using Oqtane.Shared;
namespace Oqtane.Repository
@@ -131,6 +132,13 @@ namespace Oqtane.Repository
public void DeleteUser(int userId)
{
using var db = _dbContextFactory.CreateDbContext();
// remove permissions for user
foreach (var permission in db.Permission.Where(item => item.UserId == userId))
{
db.Permission.Remove(permission);
}
var user = db.User.Find(userId);
db.User.Remove(user);
db.SaveChanges();

View File

@@ -13,11 +13,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.7" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -19,10 +19,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -17,9 +17,9 @@ namespace [Owner].Theme.[Theme]
Resources = new List<Resource>()
{
// obtained from https://cdnjs.com/libraries
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", CrossOrigin = "anonymous" },
new StyleSheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"),
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js", Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==", CrossOrigin = "anonymous" }
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
}
};

View File

@@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -75,6 +75,10 @@ app {
color: gray;
}
.app-moduleactions .dropdown-menu {
z-index: 9999;
}
.app-moduleactions .dropdown-submenu {
position: relative;
}
@@ -274,11 +278,11 @@ app {
}
/* cookie consent */
.gdpr-consent-bar .btn-show{
.gdpr-consent-bar .btn-show {
bottom: -3px;
left: 5px;
}
.gdpr-consent-bar .btn-hide{
.gdpr-consent-bar .btn-hide {
top: 0;
right: 5px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

View File

@@ -311,7 +311,7 @@ Oqtane.Interop = {
}
return files;
},
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) {
uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) {
var success = true;
var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id);
@@ -344,16 +344,22 @@ Oqtane.Interop = {
const totalParts = Math.ceil(file.size / chunkSize);
let partCount = 0;
let filename = file.name;
if (anonymizeuploadfilenames) {
filename = crypto.randomUUID() + '.' + filename.split('.').pop();
}
const uploadPart = () => {
const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
return new Promise((resolve, reject) => {
let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name);
formdata.append('formfile', chunk, filename);
var credentials = 'same-origin';
var headers = new Headers();

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>6.1.3</Version>
<Version>6.1.4</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/v6.1.3</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
@@ -19,11 +19,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="NodaTime" Version="3.2.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.5" />
<PackageReference Include="System.Text.Json" Version="9.0.7" />
</ItemGroup>
</Project>

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