6.1.4 Release

6.1.4 Release
This commit is contained in:
Shaun Walker
2025-07-30 15:10:42 -04:00
committed by GitHub
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