Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
Leigh Pointer
2025-07-31 17:10:52 +02:00
35 changed files with 599 additions and 303 deletions

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
@inject IAliasService AliasService @inject IAliasService AliasService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@@ -193,80 +194,125 @@
<Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings"> <Section Name="SMTP" Heading="SMTP Settings" ResourceKey="SMTPSettings">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<div class="col-sm-3"> <Label Class="col-sm-3" For="smtpenabled" HelpText="Specify if SMTP is enabled for this site" ResourceKey="SmtpEnabled">Enabled? </Label>
</div>
<div class="col-sm-9"> <div class="col-sm-9">
<strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br /> <select id="smtpenabled" class="form-select" value="@_smtpenabled" @onchange="(e => SMTPEnabledChanged(e))">
</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" >
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option> <option value="False">@SharedLocalizer["No"]</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> @if (_smtpenabled == "True")
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label> {
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/> <div class="col-sm-3">
</div> </div>
</div> <div class="col-sm-9">
<div class="row mb-1 align-items-center"> <strong>@Localizer["Smtp.Required.EnableNotificationJob"]</strong><br />
<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>
</div> <div class="row mb-1 align-items-center">
<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>
<Label Class="col-sm-3" For="sender" HelpText="Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server." ResourceKey="SmtpSender">Email Sender: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="host" class="form-control" @bind="@_smtphost" />
<input id="sender" class="form-control" @bind="@_smtpsender" /> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="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>
<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">
<div class="col-sm-9"> <input id="port" class="form-control" @bind="@_smtpport" />
<select id="relay" class="form-select" @bind="@_smtprelay" required> </div>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="smtpssl" HelpText="Specify if SSL is required for your SMTP server" ResourceKey="SmtpSSL">SSL Required: </Label>
<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">
<div class="col-sm-9"> <select id="smtpssl" class="form-select" @bind="@_smtpssl" >
<select id="smtpenabled" class="form-select" @bind="@_smtpenabled"> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="False">@SharedLocalizer["No"]</option>
<option value="False">@SharedLocalizer["No"]</option> </select>
</select> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<div class="row mb-1 align-items-center"> <Label Class="col-sm-3" For="smtpauthentication" HelpText="Specify the SMTP authentication type" ResourceKey="SMTPAuthentication">Authentication: </Label>
<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">
<div class="col-sm-9"> <select id="smtpauthentication" class="form-select" value="@_smtpauthentication" @onchange="(e => SMTPAuthenticationChanged(e))">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" /> <option value="Basic">@Localizer["Basic"]</option>
<option value="OAuth2">@Localizer["OAuth2"]</option>
</select>
</div>
</div> </div>
</div> @if (_smtpauthentication == "Basic")
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button> {
<br /><br /> <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> </div>
</Section> </Section>
<Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings"> <Section Name="PWA" Heading="Progressive Web Application Settings" ResourceKey="PWASettings">
@@ -454,16 +500,23 @@
private string _headcontent = string.Empty; private string _headcontent = string.Empty;
private string _bodycontent = string.Empty; private string _bodycontent = string.Empty;
private string _smtpenabled = "False";
private string _smtpauthentication = "Basic";
private string _smtphost = string.Empty; private string _smtphost = string.Empty;
private string _smtpport = string.Empty; private string _smtpport = string.Empty;
private string _smtpssl = "False"; private string _smtpssl = "True";
private string _smtpusername = string.Empty; private string _smtpusername = string.Empty;
private string _smtppassword = string.Empty; private string _smtppassword = string.Empty;
private string _smtppasswordtype = "password"; private string _smtppasswordtype = "password";
private string _togglesmtppassword = string.Empty; 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 _smtpsender = string.Empty;
private string _smtprelay = "False"; private string _smtprelay = "False";
private string _smtpenabled = "True";
private int _retention = 30; private int _retention = 30;
private string _pwaisenabled; private string _pwaisenabled;
@@ -507,7 +560,7 @@
Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId);
if (site != null) if (site != null)
{ {
_timezones = Utilities.GetTimeZones(); _timezones = TimeZoneService.GetTimeZones();
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
_pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId);
@@ -555,15 +608,21 @@
_bodycontent = site.BodyContent; _bodycontent = site.BodyContent;
// SMTP // SMTP
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "False");
_smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty); _smtphost = SettingService.GetSetting(settings, "SMTPHost", string.Empty);
_smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty); _smtpport = SettingService.GetSetting(settings, "SMTPPort", string.Empty);
_smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False"); _smtpssl = SettingService.GetSetting(settings, "SMTPSSL", "False");
_smtpauthentication = SettingService.GetSetting(settings, "SMTPAuthentication", "Basic");
_smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty); _smtpusername = SettingService.GetSetting(settings, "SMTPUsername", string.Empty);
_smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty); _smtppassword = SettingService.GetSetting(settings, "SMTPPassword", string.Empty);
_togglesmtppassword = SharedLocalizer["ShowPassword"]; _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); _smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False"); _smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
// PWA // PWA
@@ -744,8 +803,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, 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, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, 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, "SMTPSender", _smtpsender, true);
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true); settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true); settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
@@ -812,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() private async Task SendEmail()
{ {
if (_smtphost != "" && _smtpport != "" && _smtpsender != "") if (_smtphost != "" && _smtpport != "" && _smtpsender != "")
@@ -822,8 +926,13 @@
settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true); settings = SettingService.SetSetting(settings, "SMTPHost", _smtphost, true);
settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true); settings = SettingService.SetSetting(settings, "SMTPPort", _smtpport, true);
settings = SettingService.SetSetting(settings, "SMTPSSL", _smtpssl, 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, "SMTPUsername", _smtpusername, true);
settings = SettingService.SetSetting(settings, "SMTPPassword", _smtppassword, 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, "SMTPSender", _smtpsender, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
await logger.LogInformation("Site SMTP Settings Saved"); await logger.LogInformation("Site SMTP Settings Saved");
@@ -844,20 +953,6 @@
} }
} }
private void ToggleSMTPPassword()
{
if (_smtppasswordtype == "password")
{
_smtppasswordtype = "text";
_togglesmtppassword = SharedLocalizer["HidePassword"];
}
else
{
_smtppasswordtype = "password";
_togglesmtppassword = SharedLocalizer["ShowPassword"];
}
}
private async Task GetAliases() private async Task GetAliases()
{ {
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

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

View File

@@ -5,6 +5,7 @@
@inject IUserService UserService @inject IUserService UserService
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject ITimeZoneService TimeZoneService
@inject IStringLocalizer<Add> Localizer @inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -27,6 +28,15 @@
<input id="email" class="form-control" @bind="@_email" /> <input id="email" class="form-control" @bind="@_email" />
</div> </div>
</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"> <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> <Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -119,6 +129,7 @@
private bool _initialized = false; private bool _initialized = false;
private string _username = string.Empty; private string _username = string.Empty;
private string _email = string.Empty; private string _email = string.Empty;
private string _confirmed = "True";
private string _displayname = string.Empty; private string _displayname = string.Empty;
private string _timezoneid = string.Empty; private string _timezoneid = string.Empty;
private string _notify = "True"; private string _notify = "True";
@@ -132,7 +143,7 @@
{ {
try try
{ {
_timezones = Utilities.GetTimeZones(); _timezones = TimeZoneService.GetTimeZones();
_profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
_settings = new Dictionary<string, string>(); _settings = new Dictionary<string, string>();
_timezoneid = PageState.Site.TimeZoneId; _timezoneid = PageState.Site.TimeZoneId;
@@ -168,6 +179,7 @@
user.Username = _username; user.Username = _username;
user.Password = ""; // will be auto generated user.Password = ""; // will be auto generated
user.Email = _email; user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid; user.TimeZoneId = _timezoneid;
user.PhotoFileId = null; user.PhotoFileId = null;

View File

@@ -6,6 +6,7 @@
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IFileService FileService @inject IFileService FileService
@inject ITimeZoneService TimeZoneService
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@@ -47,7 +48,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="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"> <div class="col-sm-9">
<select id="confirmed" class="form-select" @bind="@_confirmed"> <select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
@@ -158,7 +159,7 @@
} }
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") @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 /> <br /><br />
<AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo> <AuditInfo CreatedBy="@_createdby" CreatedOn="@_createdon" ModifiedBy="@_modifiedby" ModifiedOn="@_modifiedon" DeletedBy="@_deletedby" DeletedOn="@_deletedon"></AuditInfo>
@@ -203,7 +204,7 @@
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
_togglepassword = SharedLocalizer["ShowPassword"]; _togglepassword = SharedLocalizer["ShowPassword"];
_profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId);
_timezones = Utilities.GetTimeZones(); _timezones = TimeZoneService.GetTimeZones();
if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) 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" /> <input id="profileurl" class="form-control" @bind="@_profileurl" />
</div> </div>
</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)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@_twofactor"> <select id="twofactor" class="form-select" @bind="@_twofactor">
<option value="false">@Localizer["Disabled"]</option> <option value="false">@Localizer["Disabled"]</option>
@@ -490,6 +499,7 @@ else
private string _allowregistration; private string _allowregistration;
private string _registerurl; private string _registerurl;
private string _profileurl; private string _profileurl;
private string _requireconfirmedemail;
private string _twofactor; private string _twofactor;
private string _cookiename; private string _cookiename;
private string _cookieexpiration; private string _cookieexpiration;
@@ -560,6 +570,7 @@ else
_allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower();
_registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", "");
_profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", "");
_requireconfirmedemail = SettingService.GetSetting(settings, "LoginOptions:RequireConfirmedEmail", "true");
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) 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:RegisterUrl", _registerurl, false);
settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, 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:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);

View File

@@ -133,7 +133,7 @@
<value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value> <value>External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions.</value>
</data> </data>
<data name="Error.Login.Fail" xml:space="preserve"> <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>
<data name="Message.Required.UserInfo" xml:space="preserve"> <data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value> <value>Please Provide All Required Fields</value>

View File

@@ -192,7 +192,7 @@
<data name="Port.HelpText" xml:space="preserve"> <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> <value>Enter the port number for the SMTP server. Please note this field is required if you provide a host name.</value>
</data> </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> <value>Specify if SSL is required for your SMTP server</value>
</data> </data>
<data name="SmtpUsername.HelpText" xml:space="preserve"> <data name="SmtpUsername.HelpText" xml:space="preserve">
@@ -202,7 +202,7 @@
<value>Enter the password for your SMTP account</value> <value>Enter the password for your SMTP account</value>
</data> </data>
<data name="SmtpSender.HelpText" xml:space="preserve"> <data name="SmtpSender.HelpText" xml:space="preserve">
<value>Enter the email which emails will be sent from. Please note that this email address may need to be authorized with the SMTP server.</value> <value>Enter the email 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>
<data name="EnablePWA.HelpText" xml:space="preserve"> <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> <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"> <data name="Port.Text" xml:space="preserve">
<value>Port: </value> <value>Port: </value>
</data> </data>
<data name="UseSsl.Text" xml:space="preserve"> <data name="SmtpSSL.Text" xml:space="preserve">
<value>SSL Enabled: </value> <value>SSL Required: </value>
</data> </data>
<data name="SmtpUsername.Text" xml:space="preserve"> <data name="SmtpUsername.Text" xml:space="preserve">
<value>Username: </value> <value>Username: </value>
@@ -372,10 +372,10 @@
<data name="PageContent.Heading" xml:space="preserve"> <data name="PageContent.Heading" xml:space="preserve">
<value>Page Content</value> <value>Page Content</value>
</data> </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> <value>Specify if SMTP is enabled for this site</value>
</data> </data>
<data name="SMTPEnabled.Text" xml:space="preserve"> <data name="SmtpEnabled.Text" xml:space="preserve">
<value>Enabled?</value> <value>Enabled?</value>
</data> </data>
<data name="Version.HelpText" xml:space="preserve"> <data name="Version.HelpText" xml:space="preserve">
@@ -453,4 +453,40 @@
<data name="TimeZone.HelpText" xml:space="preserve"> <data name="TimeZone.HelpText" xml:space="preserve">
<value>The default time zone for the site</value> <value>The default time zone for the site</value>
</data> </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> </root>

View File

@@ -162,4 +162,10 @@
<data name="TimeZone.HelpText" xml:space="preserve"> <data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value> <value>The user's time zone</value>
</data> </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> </root>

View File

@@ -217,7 +217,7 @@
<value>The user's time zone</value> <value>The user's time zone</value>
</data> </data>
<data name="Confirmed.Text" xml:space="preserve"> <data name="Confirmed.Text" xml:space="preserve">
<value>Confirmed?</value> <value>Verified?</value>
</data> </data>
<data name="Confirmed.HelpText" xml:space="preserve"> <data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value> <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> <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>
<data name="TwoFactor.Text" xml:space="preserve"> <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>
<data name="Disabled" xml:space="preserve"> <data name="Disabled" xml:space="preserve">
<value>Disabled</value> <value>Disabled</value>
@@ -502,7 +508,7 @@
<value>Info</value> <value>Info</value>
</data> </data>
<data name="OAuth2" xml:space="preserve"> <data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0</value> <value>OAuth 2.0 (OAuth2)</value>
</data> </data>
<data name="OIDC" xml:space="preserve"> <data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value> <value>OpenID Connect (OIDC)</value>

View File

@@ -117,13 +117,4 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Universal" xml:space="preserve">
<value>(UTC) Coordinated Universal Time</value>
</data>
<data name="US/Eastern" xml:space="preserve">
<value>(UTC-05:00) Eastern Time (US &amp; Canada)</value>
</data>
<data name="US/Pacific" xml:space="preserve">
<value>(UTC-08:00) Pacific Time (US &amp; Canada)</value>
</data>
</root> </root>

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using NodaTime.TimeZones;
using NodaTime;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Models; using NodaTime.Extensions;
using Oqtane.Shared;
namespace Oqtane.Services namespace Oqtane.Services
{ {
@@ -17,18 +19,40 @@ namespace Oqtane.Services
_TimeZoneLocalizer = TimeZoneLocalizer; _TimeZoneLocalizer = TimeZoneLocalizer;
} }
public List<TimeZone> GetTimeZones() public List<Models.TimeZone> GetTimeZones()
{ {
var _timezones = new List<TimeZone>(); var timezones = new List<Models.TimeZone>();
foreach (var timezone in Utilities.GetTimeZones())
foreach (var tz in DateTimeZoneProviders.Tzdb.GetAllZones()
// only include timezones which have a country code defined or are US timezones
.Where(item => !string.IsNullOrEmpty(TzdbDateTimeZoneSource.Default.ZoneLocations.FirstOrDefault(l => l.ZoneId == item.Id)?.CountryCode) || item.Id.ToLower().Contains("us/"))
// order by UTC offset (ie. -11:00 to +14:00)
.OrderBy(item => item.GetUtcOffset(Instant.FromDateTimeUtc(DateTime.UtcNow)).Ticks))
{ {
_timezones.Add(new TimeZone // get localized display name
var displayname = _TimeZoneLocalizer[tz.Id].Value;
if (displayname == tz.Id)
{ {
Id = timezone.Id, // use default "friendly" display format
DisplayName = _TimeZoneLocalizer[timezone.Id] 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.OrderBy(item => item.DisplayName).ToList();
return timezones;
} }
} }
} }

View File

@@ -16,8 +16,8 @@ namespace Oqtane.Themes.OqtaneTheme
ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client", ContainerSettingsType = "Oqtane.Themes.OqtaneTheme.ContainerSettings, Oqtane.Client",
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
// obtained from https://www.jsdelivr.com/package/npm/bootswatch // obtained from https://cdnjs.com/libraries/bootswatch
new Stylesheet("https://cdn.jsdelivr.net/npm/bootswatch@5.3.5/dist/cyborg/bootstrap.min.css"), 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 Stylesheet("~/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
} }

View File

@@ -31,6 +31,9 @@
[Parameter] [Parameter]
public Module ModuleState { get; set; } public Module ModuleState { get; set; }
[Parameter]
public string ContainerType { get; set; }
protected override bool ShouldRender() protected override bool ShouldRender()
{ {
return PageState?.RenderId == ModuleState?.RenderId; return PageState?.RenderId == ModuleState?.RenderId;
@@ -44,6 +47,10 @@
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
string container = ModuleState.ContainerType; string container = ModuleState.ContainerType;
if (!string.IsNullOrEmpty(ContainerType))
{
container = ContainerType;
}
if (PageState.ModuleId != -1 && PageState.Route.Action != "" && ModuleState.UseAdminContainer) if (PageState.ModuleId != -1 && PageState.Route.Action != "" && ModuleState.UseAdminContainer)
{ {
container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer; container = (!string.IsNullOrEmpty(PageState.Site.AdminContainerType)) ? PageState.Site.AdminContainerType : Constants.DefaultAdminContainer;

View File

@@ -26,6 +26,9 @@ else
[Parameter] [Parameter]
public string Name { get; set; } public string Name { get; set; }
[Parameter]
public string ContainerType { get; set; }
RenderFragment DynamicComponent { get; set; } RenderFragment DynamicComponent { get; set; }
protected override void OnParametersSet() protected override void OnParametersSet()
@@ -119,6 +122,7 @@ else
{ {
builder.OpenComponent(0, typeof(ContainerBuilder)); builder.OpenComponent(0, typeof(ContainerBuilder));
builder.AddAttribute(1, "ModuleState", module); builder.AddAttribute(1, "ModuleState", module);
builder.AddAttribute(2, "ContainerType", ContainerType);
builder.SetKey(module.PageModuleId); builder.SetKey(module.PageModuleId);
builder.CloseComponent(); builder.CloseComponent();
} }

View File

@@ -48,18 +48,12 @@
private bool _initialized = false; private bool _initialized = false;
private bool _installed = false; private bool _installed = false;
private string _display = ""; private string _display = "display: none;"; // prevents flash on initial interactive page load when using prerendering
private PageState _pageState { get; set; } private PageState _pageState { get; set; }
protected override async Task OnParametersSetAsync() 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.AntiForgeryToken = AntiForgeryToken;
SiteState.AuthorizationToken = AuthorizationToken; SiteState.AuthorizationToken = AuthorizationToken;
SiteState.Platform = Platform; SiteState.Platform = Platform;
@@ -89,9 +83,10 @@
protected override void OnAfterRender(bool firstRender) protected override void OnAfterRender(bool firstRender)
{ {
if (firstRender) if (firstRender && _display == "display: none;")
{ {
_display = ""; _display = "";
StateHasChanged(); // required or else the UI will not refresh
} }
} }

View File

@@ -239,18 +239,19 @@ app {
.app-form-inline { .app-form-inline {
display: inline; display: inline;
} }
.app-search{
.app-search {
display: inline-block; display: inline-block;
position: relative; position: relative;
} }
.app-search input + button{ .app-search input + button {
background: none; background: none;
border: none; border: none;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
} }
.app-search input + button .oi{ .app-search input + button .oi {
top: 0; top: 0;
} }
.app-search-noinput { .app-search-noinput {
@@ -275,3 +276,13 @@ app {
.app-logo .navbar-brand { .app-logo .navbar-brand {
padding: 5px 20px 5px 20px; 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; 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 success = true;
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
@@ -344,16 +344,22 @@ Oqtane.Interop = {
const totalParts = Math.ceil(file.size / chunkSize); const totalParts = Math.ceil(file.size / chunkSize);
let partCount = 0; let partCount = 0;
let filename = file.name;
if (anonymizeuploadfilenames) {
filename = crypto.randomUUID() + '.' + filename.split('.').pop();
}
const uploadPart = () => { const uploadPart = () => {
const start = partCount * chunkSize; const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size); const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); const chunk = file.slice(start, end);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let formdata = new FormData(); let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken); formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder); formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name); formdata.append('formfile', chunk, filename);
var credentials = 'same-origin'; var credentials = 'same-origin';
var headers = new Headers(); var headers = new Headers();

View File

@@ -165,14 +165,13 @@ namespace Oqtane.Controllers
bool allowregistration; bool allowregistration;
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin)) if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
{ {
user.EmailConfirmed = true; user.IsAuthenticated = true; // admins can add any existing user to a site
user.IsAuthenticated = true;
allowregistration = true; allowregistration = true;
} }
else else
{ {
user.EmailConfirmed = false; user.EmailConfirmed = false; // standard users cannot specify that their email is verified
user.IsAuthenticated = false; 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; allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
} }

View File

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

View File

@@ -1,16 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using MailKit.Net.Smtp; using MailKit.Net.Smtp;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;
using MimeKit; using MimeKit;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Shared; using Oqtane.Shared;
using MailKit.Security;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
@@ -27,7 +26,7 @@ namespace Oqtane.Infrastructure
} }
// job is executed for each tenant in installation // job is executed for each tenant in installation
public override string ExecuteJob(IServiceProvider provider) public async override Task<string> ExecuteJobAsync(IServiceProvider provider)
{ {
string log = ""; string log = "";
@@ -48,126 +47,175 @@ namespace Oqtane.Infrastructure
if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True")
{ {
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") != "" && bool valid = true;
settingRepository.GetSettingValue(settings, "SMTPPort", "") != "" && if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
settingRepository.GetSettingValue(settings, "SMTPSender", "") != "") {
// basic
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "<br />";
valid = false;
}
}
else
{
// oauth
if (settingRepository.GetSettingValue(settings, "SMTPHost", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPPort", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPAuthority", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientId", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPClientSecret", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPScopes", "") == "" ||
settingRepository.GetSettingValue(settings, "SMTPSender", "") == "")
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, Authority, Client ID, Client Secret, Scopes, And Sender Are All Required" + "<br />";
valid = false;
}
}
if (valid)
{ {
// construct SMTP Client // construct SMTP Client
using var client = new SmtpClient(); using var client = new SmtpClient();
client.Connect(host: settingRepository.GetSettingValue(settings, "SMTPHost", ""), await client.ConnectAsync(settingRepository.GetSettingValue(settings, "SMTPHost", ""),
port: int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")),
options: bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? MailKit.Security.SecureSocketOptions.StartTls : MailKit.Security.SecureSocketOptions.None); bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic")
{ {
client.Authenticate(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), // it is possible to use basic without any authentication (not recommended)
settingRepository.GetSettingValue(settings, "SMTPPassword", "")); 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;
}
} }
// iterate through undelivered notifications if (valid)
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 // iterate through undelivered notifications
if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null) int sent = 0;
List<Notification> notifications = notificationRepository.GetNotifications(site.SiteId, -1, -1).ToList();
foreach (Notification notification in notifications)
{ {
var user = userRepository.GetUser(notification.FromUserId.Value); // get sender and receiver information from user object if not provided
if (user != null) if ((string.IsNullOrEmpty(notification.FromEmail) || string.IsNullOrEmpty(notification.FromDisplayName)) && notification.FromUserId != null)
{ {
notification.FromEmail = (string.IsNullOrEmpty(notification.FromEmail)) ? user.Email : notification.FromEmail; var user = userRepository.GetUser(notification.FromUserId.Value);
notification.FromDisplayName = (string.IsNullOrEmpty(notification.FromDisplayName)) ? user.DisplayName : notification.FromDisplayName; if (user != null)
}
}
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)); 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 else
{ {
mailMessage.From.Add(new MailboxAddress("", notification.FromEmail)); mailMessage.From.Add(new MailboxAddress((!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name,
settingRepository.GetSettingValue(settings, "SMTPSender", "")));
}
// recipient
if (!string.IsNullOrEmpty(notification.ToDisplayName))
{
mailMessage.To.Add(new MailboxAddress(notification.ToDisplayName, notification.ToEmail));
}
else
{
mailMessage.To.Add(new MailboxAddress("", notification.ToEmail));
}
// subject
mailMessage.Subject = notification.Subject;
//body
var bodyText = notification.Body;
if (!bodyText.Contains('<') || !bodyText.Contains('>'))
{
// plain text messages should convert line breaks to HTML tags to preserve formatting
bodyText = bodyText.Replace("\n", "<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 />";
} }
} }
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
{
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 />";
}
} }
await client.DisconnectAsync(true);
log += "Notifications Delivered: " + sent + "<br />";
} }
client.Disconnect(true);
log += "Notifications Delivered: " + sent + "<br />";
}
else
{
log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "<br />";
} }
} }
else else

View File

@@ -180,7 +180,7 @@ namespace Oqtane.Managers
if (User != null) if (User != null)
{ {
string siteName = _sites.GetSite(user.SiteId).Name; 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 token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); 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 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); if (!identityuser.EmailConfirmed)
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); {
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); var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification); _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); user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
@@ -354,15 +357,14 @@ namespace Oqtane.Managers
if (!user.IsDeleted) if (!user.IsDeleted)
{ {
var alias = _tenantManager.GetAlias(); var alias = _tenantManager.GetAlias();
var twoFactorSetting = _settings.GetSetting(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor")?.SettingValue ?? "false"; string siteName = _sites.GetSite(alias.SiteId).Name;
var twoFactorRequired = twoFactorSetting == "required" || user.TwoFactorRequired; var twoFactorRequired = _settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired;
if (twoFactorRequired) if (twoFactorRequired)
{ {
var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email"); var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
user.TwoFactorCode = token; user.TwoFactorCode = token;
user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10); user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
_users.UpdateUser(user); _users.UpdateUser(user);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["TwoFactorEmailSubject"]; string subject = _localizer["TwoFactorEmailSubject"];
subject = subject.Replace("[SiteName]", siteName); subject = subject.Replace("[SiteName]", siteName);
string body = _localizer["TwoFactorEmailBody"].Value; string body = _localizer["TwoFactorEmailBody"].Value;
@@ -377,7 +379,7 @@ namespace Oqtane.Managers
} }
else 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); user = GetUser(identityuser.UserName, alias.SiteId);
if (user != null) if (user != null)
@@ -400,13 +402,25 @@ namespace Oqtane.Managers
} }
else 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 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) if (user != null)
{ {
var alias = _tenantManager.GetAlias(); var alias = _tenantManager.GetAlias();
var twoFactorSetting = _settings.GetSetting(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor")?.SettingValue ?? "false"; var twoFactorRequired = _settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired;
var twoFactorRequired = twoFactorSetting == "required" || user.TwoFactorRequired;
if (twoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) if (twoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{ {
user.IsAuthenticated = true; user.IsAuthenticated = true;

View File

@@ -16,5 +16,6 @@ namespace Oqtane.Repository
void DeleteSetting(string entityName, int settingId); void DeleteSetting(string entityName, int settingId);
void DeleteSettings(string entityName, int entityId); void DeleteSettings(string entityName, int entityId);
string GetSettingValue(IEnumerable<Setting> settings, string settingName, string defaultValue); 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 System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Modules.Admin.Users;
using Oqtane.Shared;
namespace Oqtane.Repository namespace Oqtane.Repository
{ {
@@ -71,6 +73,19 @@ namespace Oqtane.Repository
public void DeleteRole(int roleId) public void DeleteRole(int roleId)
{ {
using var db = _dbContextFactory.CreateDbContext(); 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); Role role = db.Role.Find(roleId);
db.Role.Remove(role); db.Role.Remove(role);
db.SaveChanges(); 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) private bool IsMaster(string EntityName)
{ {
return (EntityName == EntityNames.ModuleDefinition || EntityName == EntityNames.Host); return (EntityName == EntityNames.ModuleDefinition || EntityName == EntityNames.Host);

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Modules.Admin.Users;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Repository namespace Oqtane.Repository
@@ -131,6 +132,13 @@ namespace Oqtane.Repository
public void DeleteUser(int userId) public void DeleteUser(int userId)
{ {
using var db = _dbContextFactory.CreateDbContext(); 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); var user = db.User.Find(userId);
db.User.Remove(user); db.User.Remove(user);
db.SaveChanges(); db.SaveChanges();

View File

@@ -16,8 +16,8 @@ namespace [Owner].Theme.[Theme]
ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane", ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane",
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
// obtained from https://www.jsdelivr.com/ // obtained from https://cdnjs.com/libraries
new Script(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), new StyleSheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"),
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")

View File

@@ -278,11 +278,11 @@ app {
} }
/* cookie consent */ /* cookie consent */
.gdpr-consent-bar .btn-show{ .gdpr-consent-bar .btn-show {
bottom: -3px; bottom: -3px;
left: 5px; left: 5px;
} }
.gdpr-consent-bar .btn-hide{ .gdpr-consent-bar .btn-hide {
top: 0; top: 0;
right: 5px; right: 5px;
} }

View File

@@ -85,11 +85,11 @@ namespace Oqtane.Shared
public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" }; public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" };
public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client"; public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
//Obtained from https://cdnjs.com/libraries/bootstrap
public const string BootstrapScriptUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"; public const string BootstrapScriptUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/js/bootstrap.bundle.min.js";
public const string BootstrapScriptIntegrity = "sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq"; public const string BootstrapScriptIntegrity = "sha512-Tc0i+vRogmX4NN7tuLbQfBxa8JkfUSAxSFVzmU31nVdHyiHElPPy2cWfFacmCJKw0VqovrzKhdd2TSTMdAxp2g==";
public const string BootstrapStylesheetUrl = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"; public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css";
public const string BootstrapStylesheetIntegrity = "sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7"; public const string BootstrapStylesheetIntegrity = "sha512-fw7f+TcMjTb7bpbLJZlP8g2Y4XcCyFZW8uy8HsRZsH/SwbMw0plKHFHr99DN3l04VsYNwvzicUX/6qurvIxbxw==";
public const string CookieConsentCookieName = "Oqtane.CookieConsent"; public const string CookieConsentCookieName = "Oqtane.CookieConsent";
public const string CookieConsentCookieValue = "yes"; public const string CookieConsentCookieValue = "yes";

View File

@@ -692,16 +692,6 @@ namespace Oqtane.Shared
return (localDateTime?.Date, localTime); return (localDateTime?.Date, localTime);
} }
public static List<TimeZone> GetTimeZones()
{
return [.. DateTimeZoneProviders.Tzdb.GetAllZones()
.Select(tz => new TimeZone()
{
Id = tz.Id,
DisplayName = tz.Id
})];
}
public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate) public static bool IsEffectiveAndNotExpired(DateTime? effectiveDate, DateTime? expiryDate)
{ {
DateTime currentUtcTime = DateTime.UtcNow; DateTime currentUtcTime = DateTime.UtcNow;

View File

@@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline
# Latest Release # Latest Release
[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) was released on May 29, 2025 and is a maintenance release including 59 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 6600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. [6.1.4](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4) was released on July 30, 2025 and is a maintenance release including 49 pull requests by 4 different contributors, pushing the total number of project commits all-time to over 6700. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
# Try It Now! # Try It Now!
@@ -26,7 +26,7 @@ A free ASP.NET hosting account. No hidden fees. No credit card required.
**Installing using source code from the Dev/Master branch:** **Installing using source code from the Dev/Master branch:**
- Install **[.NET 9.0.5 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install **[.NET 9.0.7 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**.
- Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
@@ -92,8 +92,15 @@ Connect with other developers, get support, and share ideas by joining the Oqtan
# Roadmap # Roadmap
This project is open source, and therefore is a work in progress... This project is open source, and therefore is a work in progress...
[6.1.4](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.4) (Jul 30, 2025)
- [x] Stabilization improvements
- [x] SMTP OAuth2 Support
[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) (May 29, 2025) [6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) (May 29, 2025)
- [x] Stabilization improvements - [x] Stabilization improvements
- [x] Time zone support
- [x] Module header/footer content
- [x] Module import/export from files
[6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) (Apr 10, 2025) [6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) (Apr 10, 2025)
- [x] Stabilization improvements - [x] Stabilization improvements

View File

@@ -220,7 +220,7 @@
"apiVersion": "2024-04-01", "apiVersion": "2024-04-01",
"name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]",
"properties": { "properties": {
"packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Install.zip" "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.4/Oqtane.Framework.6.1.4.Install.zip"
}, },
"dependsOn": [ "dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"