From cceda1db1eae40bcc08a8a96f436c9d61a521a06 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 28 Jul 2025 09:06:36 -0400 Subject: [PATCH 01/20] add OAuth support to Notification Job (#5372) --- .../Infrastructure/Jobs/NotificationJob.cs | 260 +++++++++++------- 1 file changed, 154 insertions(+), 106 deletions(-) diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index e1d3e17a..6b755e89 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; - +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 { @@ -27,7 +26,7 @@ namespace Oqtane.Infrastructure } // job is executed for each tenant in installation - public override string ExecuteJob(IServiceProvider provider) + public async override Task ExecuteJobAsync(IServiceProvider provider) { string log = ""; @@ -48,126 +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") + { + // basic + if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" || + settingRepository.GetSettingValue(settings, "SMTPSender", "") == "") + { + log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "
"; + valid = false; + } + } + else + { + // 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" + "
"; + valid = false; + } + } + + + if (valid) { // construct SMTP Client using var client = new SmtpClient(); - client.Connect(host: settingRepository.GetSettingValue(settings, "SMTPHost", ""), - port: int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), - options: bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? MailKit.Security.SecureSocketOptions.StartTls : MailKit.Security.SecureSocketOptions.None); + 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, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") + if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") { - client.Authenticate(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), - settingRepository.GetSettingValue(settings, "SMTPPassword", "")); + // 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 + "
"; + valid = false; + } } - // iterate through undelivered notifications - int sent = 0; - List notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList(); - foreach (Notification notification in notifications) + if (valid) { - // get sender and receiver information from user object if not provided - if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null) + // iterate through undelivered notifications + int sent = 0; + List notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList(); + foreach (Notification notification in notifications) { - var user = userRepository.GetUser(notification.FromUserId.Value); - if (user != null) + // get sender and receiver information from user object if not provided + if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != 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}
"; - 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)) + var user = userRepository.GetUser(notification.FromUserId.Value); + if (user != null) { - mailMessage.From.Add(new MailboxAddress(notification.FromDisplayName, notification.FromEmail)); + 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}
"; + 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("", notification.FromEmail)); + 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", "
"); + } + + 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}
"; } } - 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", "
"); - } - - mailMessage.Body = new TextPart("html", System.Text.Encoding.UTF8) - { - Text = bodyText - }; - - // 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}
"; - } } + await client.DisconnectAsync(true); + log += "Notifications Delivered: " + sent + "
"; } - client.Disconnect(true); - log += "Notifications Delivered: " + sent + "
"; - } - else - { - log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "
"; } } else From 91c53098552c93ab547678056355e4235c17db36 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 28 Jul 2025 10:26:18 -0400 Subject: [PATCH 02/20] fix #5372 - add support for sending SMTP emails using OAuth --- Oqtane.Client/Modules/Admin/Site/Index.razor | 252 ++++++++++++------ .../Resources/Modules/Admin/Site/Index.resx | 48 +++- 2 files changed, 215 insertions(+), 85 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index fcc9557f..8b7c9ad2 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -193,80 +193,125 @@
-
-
+
- @Localizer["Smtp.Required.EnableNotificationJob"]
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-
- -
- -
-
-
- -
-
- - + @if (_smtpenabled == "True") + { +
+
+
+
+ @Localizer["Smtp.Required.EnableNotificationJob"]
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
- -

+ @if (_smtpauthentication == "Basic") + { +
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ } + else + { +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ } +
+ +
+ +
+
+
+ +
+ +
+
+ +

+ }
@@ -454,16 +499,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; @@ -555,15 +607,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 @@ -744,8 +802,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); @@ -812,6 +875,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 != "") @@ -822,8 +925,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"); @@ -844,20 +952,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)) diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index acfd022e..7e2fae82 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -192,7 +192,7 @@ Enter the port number for the SMTP server. Please note this field is required if you provide a host name. - + Specify if SSL is required for your SMTP server @@ -202,7 +202,7 @@ Enter the password for your SMTP account - Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server. + 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. Select whether you would like this site to be available as a Progressive Web Application (PWA) @@ -240,8 +240,8 @@ Port: - - SSL Enabled: + + SSL Required: Username: @@ -372,10 +372,10 @@ Page Content - + Specify if SMTP is enabled for this site - + Enabled? @@ -453,4 +453,40 @@ The default time zone for the site + + Basic + + + OAuth + + + Authentication: + + + Specify the SMTP authentication type + + + Client ID: + + + The Client ID for the SMTP provider + + + Client Secret: + + + The Client Secret for the SMTP provider + + + Scopes: + + + A list of Scopes for the SMTP provider (separated by commas) + + + Authority Url: + + + The Authority Url for the SMTP provider + \ No newline at end of file From e179976fe8478bb7c5d95d1a8d3a1840aca1d90d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 28 Jul 2025 17:00:27 -0400 Subject: [PATCH 03/20] improve TimeZoneService --- .../Resources/TimeZoneResources.resx | 9 ----- Oqtane.Client/Services/TimeZoneService.cs | 38 ++++++++++++++----- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Oqtane.Client/Resources/TimeZoneResources.resx b/Oqtane.Client/Resources/TimeZoneResources.resx index d736dcea..1af7de15 100644 --- a/Oqtane.Client/Resources/TimeZoneResources.resx +++ b/Oqtane.Client/Resources/TimeZoneResources.resx @@ -117,13 +117,4 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - (UTC) Coordinated Universal Time - - - (UTC-05:00) Eastern Time (US & Canada) - - - (UTC-08:00) Pacific Time (US & Canada) - \ No newline at end of file diff --git a/Oqtane.Client/Services/TimeZoneService.cs b/Oqtane.Client/Services/TimeZoneService.cs index f6cab35b..753e455b 100644 --- a/Oqtane.Client/Services/TimeZoneService.cs +++ b/Oqtane.Client/Services/TimeZoneService.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; 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 { @@ -17,18 +19,36 @@ namespace Oqtane.Services _TimeZoneLocalizer = TimeZoneLocalizer; } - public List GetTimeZones() + public List GetTimeZones() { - var _timezones = new List(); - foreach (var timezone in Utilities.GetTimeZones()) + var timezones = new List(); + + 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)) { - _timezones.Add(new TimeZone + // get localized display name + var displayname = _TimeZoneLocalizer[tz.Id].Value; + if (displayname == tz.Id) { - Id = timezone.Id, - DisplayName = _TimeZoneLocalizer[timezone.Id] + // use default "friendly" display format + displayname = displayname.Replace("_", " ").Replace("/", " / "); + } + + // 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.OrderBy(item => item.DisplayName).ToList(); + + return timezones; } } } From 9f097521f6179f0370070c1e5674c09fa9238b0e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 08:11:42 -0400 Subject: [PATCH 04/20] fix #5348 - ensure time zones work consistently on all platforms --- Oqtane.Client/Modules/Admin/Register/Index.razor | 3 ++- Oqtane.Client/Modules/Admin/Site/Index.razor | 3 ++- Oqtane.Client/Modules/Admin/Users/Add.razor | 3 ++- Oqtane.Client/Modules/Admin/Users/Edit.razor | 3 ++- Oqtane.Shared/Shared/Utilities.cs | 10 ---------- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 025c86c0..2f7c47e7 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -6,6 +6,7 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @if (_initialized) { @@ -114,7 +115,7 @@ { _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.Site.TimeZoneId; _initialized = true; } diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 8b7c9ad2..67316ae4 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -10,6 +10,7 @@ @inject IAliasService AliasService @inject IThemeService ThemeService @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject INotificationService NotificationService @@ -559,7 +560,7 @@ Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); if (site != null) { - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index 2387e88e..cb5c7224 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -5,6 +5,7 @@ @inject IUserService UserService @inject IProfileService ProfileService @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -132,7 +133,7 @@ { try { - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _settings = new Dictionary(); _timezoneid = PageState.Site.TimeZoneId; diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 002e64df..353a474f 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -6,6 +6,7 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject IFileService FileService +@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -203,7 +204,7 @@ _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _togglepassword = SharedLocalizer["ShowPassword"]; _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); - _timezones = Utilities.GetTimeZones(); + _timezones = TimeZoneService.GetTimeZones(); if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) { diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index f5faff87..44cccffc 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -692,16 +692,6 @@ namespace Oqtane.Shared return (localDateTime?.Date, localTime); } - public static List GetTimeZones() - { - return [.. DateTimeZoneProviders.Tzdb.GetAllZones() - .Select(tz => new TimeZone() - { - Id = tz.Id, - DisplayName = tz.Id - })]; - } - public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate) { DateTime currentUtcTime = DateTime.UtcNow; From b1770ebb762bc85567c001c7379875434099651e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 08:40:38 -0400 Subject: [PATCH 05/20] fix #5346 - deleting role should remove associated permissions --- Oqtane.Server/Repository/RoleRepository.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Oqtane.Server/Repository/RoleRepository.cs b/Oqtane.Server/Repository/RoleRepository.cs index 1c4aa0c4..e9d1590d 100644 --- a/Oqtane.Server/Repository/RoleRepository.cs +++ b/Oqtane.Server/Repository/RoleRepository.cs @@ -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,7 +73,19 @@ namespace Oqtane.Repository public void DeleteRole(int roleId) { using var db = _dbContextFactory.CreateDbContext(); + Role role = db.Role.Find(roleId); + + // remove permissions for this role + var permissions = db.Permission.Where(item => item.SiteId == role.SiteId).ToList(); + foreach (var permission in permissions) + { + if (permission.RoleId == roleId) + { + db.Permission.Remove(permission); + } + } + db.Role.Remove(role); db.SaveChanges(); } From 658059806bfbe8ddca058ecd2da589d0babddd61 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 09:05:37 -0400 Subject: [PATCH 06/20] fix #5346 - deleting role should remove associated useroles --- Oqtane.Client/Modules/Admin/Users/Edit.razor | 2 +- Oqtane.Server/Repository/RoleRepository.cs | 19 ++++++++++--------- Oqtane.Server/Repository/UserRepository.cs | 8 ++++++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 353a474f..cc497086 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -159,7 +159,7 @@ } @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") { - + }

diff --git a/Oqtane.Server/Repository/RoleRepository.cs b/Oqtane.Server/Repository/RoleRepository.cs index e9d1590d..3d16eb35 100644 --- a/Oqtane.Server/Repository/RoleRepository.cs +++ b/Oqtane.Server/Repository/RoleRepository.cs @@ -74,18 +74,19 @@ namespace Oqtane.Repository { using var db = _dbContextFactory.CreateDbContext(); - Role role = db.Role.Find(roleId); - - // remove permissions for this role - var permissions = db.Permission.Where(item => item.SiteId == role.SiteId).ToList(); - foreach (var permission in permissions) + // remove userroles for role + foreach (var userrole in db.UserRole.Where(item => item.RoleId == roleId)) { - if (permission.RoleId == roleId) - { - db.Permission.Remove(permission); - } + 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(); } diff --git a/Oqtane.Server/Repository/UserRepository.cs b/Oqtane.Server/Repository/UserRepository.cs index 3c0a40ad..4858e7c3 100644 --- a/Oqtane.Server/Repository/UserRepository.cs +++ b/Oqtane.Server/Repository/UserRepository.cs @@ -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(); From f4cea3fe03cd1aea9c19fbedf732eaf9b04ffb98 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 29 Jul 2025 16:20:07 -0400 Subject: [PATCH 07/20] fix #5349 - send verification email if unverified user attempts to login, add ability to enable/disable email verification per site --- Oqtane.Client/Modules/Admin/Login/Index.razor | 12 +--- Oqtane.Client/Modules/Admin/Users/Add.razor | 11 ++++ Oqtane.Client/Modules/Admin/Users/Edit.razor | 2 +- Oqtane.Client/Modules/Admin/Users/Index.razor | 14 ++++- .../Resources/Modules/Admin/Login/Index.resx | 2 +- .../Resources/Modules/Admin/Users/Add.resx | 6 ++ .../Resources/Modules/Admin/Users/Edit.resx | 2 +- .../Resources/Modules/Admin/Users/Index.resx | 8 ++- Oqtane.Server/Controllers/UserController.cs | 7 +-- .../OqtaneServiceCollectionExtensions.cs | 5 +- Oqtane.Server/Managers/UserManager.cs | 63 +++++++++++-------- .../Interfaces/ISettingRepository.cs | 1 + Oqtane.Server/Repository/SettingRepository.cs | 13 ++++ 13 files changed, 101 insertions(+), 45 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index f0a057d0..66562e6c 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -21,9 +21,7 @@ else @if (_allowexternallogin) { -
- -
+

} @if (_allowsitelogin) { @@ -49,15 +47,11 @@ else -
- -
+

@if (PageState.Site.AllowRegistration) { -
- -
+

@Localizer["Register"] } } diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index cb5c7224..e5581222 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -28,6 +28,15 @@ +
+ +
+ +
+
@@ -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"; @@ -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; diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index cc497086..2d75d27a 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -48,7 +48,7 @@
- +
+
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
- +
+ @@ -499,7 +499,7 @@ else private string _allowregistration; private string _registerurl; private string _profileurl; - private string _requirevalidemail; + private string _requireconfirmedemail; private string _twofactor; private string _cookiename; private string _cookieexpiration; @@ -570,7 +570,7 @@ else _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); - _requirevalidemail = SettingService.GetSetting(settings, "LoginOptions:RequireValidEmail", "true"); + _requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true"); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { @@ -696,7 +696,7 @@ else { settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false); settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false); - settings = SettingService.SetSetting(settings, "LoginOptions:RequireValidEmail", _requirevalidemail, 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); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 2bbf1f75..79144098 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -372,11 +372,11 @@ Two Factor Authentication? - - Do you want to require registered users to validate their email address before they are allowed to log in? + + Do you want to require registered users to verify their email address before they are allowed to log in? - - Require Valid Email? + + Require Verified Email? Disabled diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 7a60e785..5f9cd8dc 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -180,7 +180,7 @@ namespace Oqtane.Managers if (User != null) { string siteName = _sites.GetSite(user.SiteId).Name; - if (!user.EmailConfirmed && bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireValidEmail", "true"))) + 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,7 +252,7 @@ namespace Oqtane.Managers await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated } - if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireValidEmail", "true"))) + if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true"))) { if (user.EmailConfirmed) { @@ -379,7 +379,7 @@ namespace Oqtane.Managers } else { - if (!bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireValidEmail", "true")) || 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) From 6c0e2a62e7af7285b194c96ac94cafeec2565644 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 30 Jul 2025 12:53:59 +0200 Subject: [PATCH 09/20] Discussion #5426 updated and returned to https://cdnjs.com/ Updated and styles tested - reload.js needs still testing? --- .../Themes/Templates/External/Client/ThemeInfo.cs | 2 +- Oqtane.Shared/Shared/Constants.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs index d4d395cd..5517a501 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs @@ -16,7 +16,7 @@ namespace [Owner].Theme.[Theme] ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane", Resources = new List() { - // obtained from https://www.jsdelivr.com/ + // obtained from https://cdnjs.com/libraries new Script(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 10be4133..c24d54a5 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -85,11 +85,11 @@ namespace Oqtane.Shared public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" }; public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client"; - - public const string BootstrapScriptUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"; - public const string BootstrapScriptIntegrity = "sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq"; - public const string BootstrapStylesheetUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"; - public const string BootstrapStylesheetIntegrity = "sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"; + //Obtained from https://cdnjs.com/libraries/bootstrap + public const string BootstrapScriptUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/js/bootstrap.bundle.min.js"; + public const string BootstrapScriptIntegrity = "sha512-Tc0i+vRogmX4NN7tuLbQfBxa8JkfUSAxSFVzmU31nVdHyiHElPPy2cWfFacmCJKw0VqovrzKhdd2TSTMdAxp2g=="; + public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css"; + public const string BootstrapStylesheetIntegrity = "sha512-fw7f+TcMjTb7bpbLJZlP8g2Y4XcCyFZW8uy8HsRZsH/SwbMw0plKHFHr99DN3l04VsYNwvzicUX/6qurvIxbxw=="; public const string CookieConsentCookieName = "Oqtane.CookieConsent"; public const string CookieConsentCookieValue = "yes"; From cf9b4b869cf23e5d2e5fdffe6c47b91b5f115ac7 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 08:16:07 -0400 Subject: [PATCH 10/20] use margin rather than padding --- Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor | 4 ++-- Oqtane.Client/Modules/Admin/Themes/Index.razor | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor index fcfe6114..bbb2698e 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor @@ -17,8 +17,8 @@ else
- - + +
- +
diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 7e2fae82..97e4e8f3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -456,8 +456,8 @@ Basic - - OAuth + + OAuth 2.0 (OAuth2) Authentication: diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 79144098..1a008727 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -508,7 +508,7 @@ Info - OAuth 2.0 + OAuth 2.0 (OAuth2) OpenID Connect (OIDC) From 662a1817f23fd09a7222c8cbc9f2dd08d02284ff Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 10:43:36 -0400 Subject: [PATCH 15/20] fix #5364 - add ability to specify preferred Container per Pane --- Oqtane.Client/UI/ContainerBuilder.razor | 7 +++++++ Oqtane.Client/UI/Pane.razor | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/Oqtane.Client/UI/ContainerBuilder.razor b/Oqtane.Client/UI/ContainerBuilder.razor index 76565c6b..bc9b1198 100644 --- a/Oqtane.Client/UI/ContainerBuilder.razor +++ b/Oqtane.Client/UI/ContainerBuilder.razor @@ -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; diff --git a/Oqtane.Client/UI/Pane.razor b/Oqtane.Client/UI/Pane.razor index 633cd4fd..3e5ddfd3 100644 --- a/Oqtane.Client/UI/Pane.razor +++ b/Oqtane.Client/UI/Pane.razor @@ -26,6 +26,9 @@ else [Parameter] public string Name { get; set; } + [Parameter] + public string ContainerType { get; set; } + RenderFragment DynamicComponent { get; set; } protected override void OnParametersSet() @@ -119,6 +122,7 @@ else { builder.OpenComponent(0, typeof(ContainerBuilder)); builder.AddAttribute(1, "ModuleState", module); + builder.AddAttribute(2, "ContainerType", ContainerType); builder.SetKey(module.PageModuleId); builder.CloseComponent(); } From bfe57c3ac7c60313fdeaeaaa407edd564b2f83f6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 13:35:39 -0400 Subject: [PATCH 16/20] synchronize interop,js with .NET MAUI --- Oqtane.Maui/wwwroot/js/interop.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js index 191d9823..fecc4c99 100644 --- a/Oqtane.Maui/wwwroot/js/interop.js +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -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(); From eae8b431eeb9f4b457a7a67f2e68cf19926148b4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 13:40:25 -0400 Subject: [PATCH 17/20] synchronize app.css with .NET MAUI --- Oqtane.Maui/wwwroot/css/app.css | 17 ++++++++++++++--- Oqtane.Server/wwwroot/css/app.css | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css index ab9c6adb..46e749f7 100644 --- a/Oqtane.Maui/wwwroot/css/app.css +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -239,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 { @@ -275,3 +276,13 @@ 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; +} diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css index 00461f64..46e749f7 100644 --- a/Oqtane.Server/wwwroot/css/app.css +++ b/Oqtane.Server/wwwroot/css/app.css @@ -278,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; } From d95104cb924008d928366c7e6313fbe5af8f124c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 30 Jul 2025 15:23:16 -0400 Subject: [PATCH 18/20] update Azure ARM template to 6.1.4 --- azuredeploy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredeploy.json b/azuredeploy.json index d7970211..f4718349 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -220,7 +220,7 @@ "apiVersion": "2024-04-01", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "properties": { - "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Install.zip" + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.4/Oqtane.Framework.6.1.4.Install.zip" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" From 752083e9eb2b89af9988f473a3e869915ba289c5 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 30 Jul 2025 15:29:19 -0400 Subject: [PATCH 19/20] Update README.md --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 10112a56..cdad0222 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) was released on May 29, 2025 and is a maintenance release including 59 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 6600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[6.1.4](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4) was released on July 30, 2025 and is a maintenance release including 49 pull requests by 4 different contributors, pushing the total number of project commits all-time to over 6700. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! @@ -26,7 +26,7 @@ A free ASP.NET hosting account. No hidden fees. No credit card required. **Installing using source code from the Dev/Master branch:** -- Install **[.NET 9.0.5 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. +- Install **[.NET 9.0.7 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. @@ -92,8 +92,15 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... +[6.1.4](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4) (Jul 30, 2025) +- [x] Stabilization improvements +- [x] SMTP OAuth2 Support + [6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) (May 29, 2025) -- [x] Stabilization improvements +- [x] Stabilization improvements +- [x] Time zone support +- [x] Module header/footer content +- [x] Module import/export from files [6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) (Apr 10, 2025) - [x] Stabilization improvements From 50fa95dff9e0b6f5b8798ee382a47376d741cbf4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 31 Jul 2025 11:04:22 -0400 Subject: [PATCH 20/20] resolve interactive rendering issue --- Oqtane.Client/UI/Routes.razor | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Oqtane.Client/UI/Routes.razor b/Oqtane.Client/UI/Routes.razor index 6081966c..baf8596a 100644 --- a/Oqtane.Client/UI/Routes.razor +++ b/Oqtane.Client/UI/Routes.razor @@ -48,18 +48,12 @@ private bool _initialized = false; private bool _installed = false; - private string _display = ""; + private string _display = "display: none;"; // prevents flash on initial interactive page load when using prerendering 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; @@ -89,9 +83,10 @@ protected override void OnAfterRender(bool firstRender) { - if (firstRender) + if (firstRender && _display == "display: none;") { _display = ""; + StateHasChanged(); // required or else the UI will not refresh } }