Merge pull request #5975 from oqtane/dev

10.0.4 Release
This commit is contained in:
Shaun Walker
2026-01-20 14:56:21 -05:00
committed by GitHub
43 changed files with 654 additions and 503 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>10.0.3</Version> <Version>10.0.4</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018-2025 .NET Foundation Copyright (c) 2018-2026 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -12,10 +12,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.3" /> <PackageReference Include="Oqtane.Client" Version="10.0.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata> <metadata>
<id>Oqtane.Application.Template</id> <id>Oqtane.Application.Template</id>
<version>10.0.3</version> <version>10.0.4</version>
<title>Oqtane Application Template For Blazor</title> <title>Oqtane Application Template For Blazor</title>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@@ -22,9 +22,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -33,7 +33,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.3" /> <PackageReference Include="Oqtane.Server" Version="10.0.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -11,7 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="10.0.3" /> <PackageReference Include="Oqtane.Shared" Version="10.0.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,100 +5,109 @@
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> @if (_initialized)
<div class="container"> {
<div class="row mb-1 align-items-center"> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<Label Class="col-sm-3" For="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label> <div class="container">
<div class="col-sm-9"> @if (!_editable)
<input id="name" class="form-control" @bind="@_name" maxlength="200" required /> {
<ModuleMessage Message="@Localizer["JobNotEditable"]" Type="MessageType.Warning" />
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label>
<div class="col-sm-9">
<input id="type" class="form-control" @bind="@_jobType" required disabled />
</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="name" HelpText="Enter the job name" ResourceKey="Name">Name: </Label>
<Label Class="col-sm-3" For="type" HelpText="The fully qualified job type name" ResourceKey="Type">Type: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="name" class="form-control" @bind="@_name" maxlength="200" required disabled="@(!_editable)" />
<input id="type" class="form-control" @bind="@_jobType" readonly /> </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="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label>
<Label Class="col-sm-3" For="enabled" HelpText="Select whether you want the job enabled or not" ResourceKey="Enabled">Enabled? </Label> <div class="col-sm-9">
<div class="col-sm-9"> <select id="enabled" class="form-select" @bind="@_isEnabled" required disabled="@(!_editable)">
<select id="enabled" class="form-select" @bind="@_isEnabled" required> <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="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label>
<Label Class="col-sm-3" For="runs-every" HelpText="Select how often you want the job to run" ResourceKey="RunsEvery">Runs Every: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required disabled="@(!_editable)" />
<input id="runs-every" class="form-control mb-1" @bind="@_interval" maxlength="4" required /> <select id="runs-every" class="form-select" @bind="@_frequency" required disabled="@(!_editable)">
<select id="runs-every" class="form-select" @bind="@_frequency" required> <option value="m">@Localizer["Minute(s)"]</option>
<option value="m">@Localizer["Minute(s)"]</option> <option value="H">@Localizer["Hour(s)"]</option>
<option value="H">@Localizer["Hour(s)"]</option> <option value="d">@Localizer["Day(s)"]</option>
<option value="d">@Localizer["Day(s)"]</option> <option value="w">@Localizer["Week(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option> <option value="M">@Localizer["Month(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option> <option value="O">@Localizer["Once"]</option>
<option value="O">@Localizer["Once"]</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="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="retention" type="number" min="0" step="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required disabled="@(!_editable)" />
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="3" required /> </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="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label>
<Label Class="col-sm-3" For="starting" HelpText="Optionally enter the date and time when this job should start executing" ResourceKey="Starting">Starting: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <input id="starting" type="date" class="form-control" @bind="@_startDate" disabled="@(!_editable)" />
<input id="starting" type="date" class="form-control" @bind="@_startDate" /> </div>
</div> <div class="col">
<div class="col"> <input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" disabled="@(!_editable)" />
<input id="starting" type="time" class="form-control" @bind="@_startTime" placeholder="hh:mm" required="@(_startDate.HasValue)" /> </div>
</div> </div>
</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="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label>
<Label Class="col-sm-3" For="ending" HelpText="Optionally enter the date and time when this job should stop executing" ResourceKey="Ending">Ending: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <div class="row">
<div class="row"> <div class="col">
<div class="col"> <input id="ending" type="date" class="form-control" @bind="@_endDate" disabled="@(!_editable)" />
<input id="ending" type="date" class="form-control" @bind="@_endDate" /> </div>
</div> <div class="col">
<div class="col"> <input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" disabled="@(!_editable)" />
<input id="ending" type="time" class="form-control" placeholder="hh:mm" @bind="@_endTime" required="@(_endDate.HasValue)" /> </div>
</div> </div>
</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="next" HelpText="The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job." ResourceKey="NextExecution">Next Execution: </Label>
<Label Class="col-sm-3" For="next" HelpText="Optionally modify the date and time when this job should execute next" ResourceKey="NextExecution">Next Execution: </Label> <div class="col-sm-9">
<div class="col-sm-9"> <input id="next" class="form-control" @bind="@_nextDate" disabled />
<div class="row">
<div class="col">
<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" required="@(_nextDate.HasValue)" />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <br />
<br /> @if (_editable)
<button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button> {
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <button type="button" class="btn btn-success" @onclick="SaveJob">@SharedLocalizer["Save"]</button>
<br /> }
<br /> else
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo> {
</form> <button type="button" class="btn btn-danger" @onclick="DisableJob">@Localizer["Disable"]</button>
}
<NavLink class="btn btn-secondary ms-1" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br />
<br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
</form>
}
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
private bool _initialized = false;
private bool _editable = true;
private int _jobId; private int _jobId;
private string _name = string.Empty; private string _name = string.Empty;
private string _jobType = string.Empty; private string _jobType = string.Empty;
@@ -113,26 +122,32 @@
private DateTime? _nextDate = null; private DateTime? _nextDate = null;
private DateTime? _nextTime = null; private DateTime? _nextTime = null;
private string createdby; private string createdby;
private DateTime createdon; private DateTime createdon;
private string modifiedby; private string modifiedby;
private DateTime modifiedon; private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try await LoadJob();
{ }
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId); protected async Task LoadJob()
if (job != null) {
{ try
_name = job.Name; {
_jobType = job.JobType; _jobId = Int32.Parse(PageState.QueryString["id"]);
_isEnabled = job.IsEnabled.ToString(); Job job = await JobService.GetJobAsync(_jobId);
_interval = job.Interval.ToString(); if (job != null)
_frequency = job.Frequency; {
_startDate = UtcToLocal(job.StartDate); _editable = !job.IsEnabled && !job.IsExecuting;
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = UtcToLocal(job.StartDate);
_startTime = UtcToLocal(job.StartDate); _startTime = UtcToLocal(job.StartDate);
_endDate = UtcToLocal(job.EndDate); _endDate = UtcToLocal(job.EndDate);
_endTime = UtcToLocal(job.EndDate); _endTime = UtcToLocal(job.EndDate);
@@ -140,70 +155,107 @@
_nextDate = UtcToLocal(job.NextExecution); _nextDate = UtcToLocal(job.NextExecution);
_nextTime = UtcToLocal(job.NextExecution); _nextTime = UtcToLocal(job.NextExecution);
createdby = job.CreatedBy; createdby = job.CreatedBy;
createdon = job.CreatedOn; createdon = job.CreatedOn;
modifiedby = job.ModifiedBy; modifiedby = job.ModifiedBy;
modifiedon = job.ModifiedOn; modifiedon = job.ModifiedOn;
} }
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
}
}
private async Task SaveJob() _initialized = true;
{
if (!Utilities.ValidateEffectiveExpiryDates(_startDate, _endDate))
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
return;
} }
validated = true; catch (Exception ex)
var interop = new Interop(JSRuntime); {
if (await interop.FormValid(form)) await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
{ AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
var job = await JobService.GetJobAsync(_jobId); }
job.Name = _name; }
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate.HasValue && _startTime.HasValue
? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
: null;
job.EndDate = _endDate.HasValue && _endTime.HasValue private async Task SaveJob()
? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay)) {
: null; try
{
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.IsEnabled = bool.Parse(_isEnabled);
job.NextExecution = _nextDate.HasValue && _nextTime.HasValue job.Frequency = _frequency;
? LocalToUtc(_nextDate.GetValueOrDefault().Date.Add(_nextTime.GetValueOrDefault().TimeOfDay)) if (job.Frequency == "O") // once
: null; {
job.RetentionHistory = int.Parse(_retentionHistory); job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
try job.StartDate = _startDate.HasValue && _startTime.HasValue
{ ? LocalToUtc(_startDate.GetValueOrDefault().Date.Add(_startTime.GetValueOrDefault().TimeOfDay))
job = await JobService.UpdateJobAsync(job); : null;
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl()); job.EndDate = _endDate.HasValue && _endTime.HasValue
} ? LocalToUtc(_endDate.GetValueOrDefault().Date.Add(_endTime.GetValueOrDefault().TimeOfDay))
catch (Exception ex) : null;
{
await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message); job.RetentionHistory = int.Parse(_retentionHistory);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
} if (!job.IsEnabled || Utilities.ValidateEffectiveExpiryDates(job.StartDate, job.EndDate))
} {
else if (!job.IsEnabled || (job.StartDate >= DateTime.UtcNow || job.StartDate == null))
{ {
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning); job.NextExecution = null;
} job = await JobService.UpdateJobAsync(job);
} await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
else
{
AddModuleMessage(Localizer["Message.StartDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.StartEndDateError"], MessageType.Warning);
}
}
else
{
AddModuleMessage(Localizer["Message.Required.JobInfo"], MessageType.Warning);
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
private async Task DisableJob()
{
try
{
var job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
if (job.IsExecuting)
{
AddModuleMessage(Localizer["Message.ExecutingError"], MessageType.Warning);
}
else
{
job.IsEnabled = false;
job.NextExecution = null;
job = await JobService.UpdateJobAsync(job);
await logger.LogInformation("Job Updated {Job}", job);
NavigationManager.NavigateTo(NavigateUrl());
}
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Updating Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Update"], MessageType.Error);
}
}
} }

View File

@@ -144,7 +144,6 @@ else
private string _code = string.Empty; private string _code = string.Empty;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Anonymous;
public override bool? Prerender => true;
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
@@ -222,6 +221,14 @@ else
{ {
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning); AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning);
} }
else
{
if (_allowexternallogin && !_allowsitelogin)
{
// external login
ExternalLogin();
}
}
} }
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -14,6 +14,7 @@
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IJobService JobService
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IOutputCacheService CacheService @inject IOutputCacheService CacheService
@@ -207,13 +208,6 @@
</div> </div>
@if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (_smtpenabled == "True" && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
<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 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="host" HelpText="Enter the host name of the SMTP server" ResourceKey="Host">Host: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -264,15 +258,6 @@
</div> </div>
</div> </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 else
{ {
@@ -534,7 +519,6 @@
private string _togglesmtpclientsecret = string.Empty; private string _togglesmtpclientsecret = string.Empty;
private string _smtpscopes = string.Empty; private string _smtpscopes = string.Empty;
private string _smtpsender = string.Empty; private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private int _retention = 30; private int _retention = 30;
private string _pwaisenabled; private string _pwaisenabled;
@@ -653,7 +637,6 @@
_togglesmtpclientsecret = SharedLocalizer["ShowPassword"]; _togglesmtpclientsecret = SharedLocalizer["ShowPassword"];
_smtpscopes = SettingService.GetSetting(settings, "SMTPScopes", string.Empty); _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");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30")); _retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
} }
@@ -846,8 +829,18 @@
settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true); settings = SettingService.SetSetting(settings, "SMTPClientSecret", _smtpclientsecret, true);
settings = SettingService.SetSetting(settings, "SMTPScopes", _smtpscopes, 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, "NotificationRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
if (_smtpenabled == "True")
{
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
}
} }
//cookie consent //cookie consent
@@ -957,7 +950,10 @@
{ {
try try
{ {
_smtpenabled = "True";
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
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);
@@ -972,6 +968,14 @@
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");
var jobs = await JobService.GetJobsAsync();
var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.NotificationJob, Oqtane.Server");
if (job != null && !job.IsEnabled)
{
job.IsEnabled = true;
await JobService.UpdateJobAsync(job);
}
await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly.")); await NotificationService.AddNotificationAsync(new Notification(PageState.Site.SiteId, PageState.User, PageState.Site.Name + " SMTP Configuration Test", "SMTP Server Is Configured Correctly."));
AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info); AddModuleMessage(Localizer["Info.Smtp.SaveSettings"], MessageType.Info);
await ScrollToPageTop(); await ScrollToPageTop();

View File

@@ -83,14 +83,14 @@ else
{ {
@if (_connection != "-") @if (_connection != "-")
{ {
@if (!string.IsNullOrEmpty(_tenant)) <div class="row mb-1 align-items-center">
{ <Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="row mb-1 align-items-center"> <div class="col-sm-9">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label> <input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
</div> </div>
</div>
@if (!string.IsNullOrEmpty(_tenant))
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label> <Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -150,130 +150,149 @@ else
} }
@code { @code {
private string _connection = "-"; private string _connection = "-";
private Dictionary<string, object> _connections; private Dictionary<string, object> _connections;
private List<Tenant> _tenants; private List<Tenant> _tenants;
private List<Database> _databases; private List<Database> _databases;
private string _name = string.Empty; private string _name = string.Empty;
private string _databasetype = string.Empty; private string _databasetype = string.Empty;
private Type _databaseConfigType; private Type _databaseConfigType;
private object _databaseConfig; private object _databaseConfig;
private RenderFragment DatabaseConfigComponent { get; set; } private RenderFragment DatabaseConfigComponent { get; set; }
private bool _showConnectionString = false; private bool _showConnectionString = false;
private string _tenant = string.Empty; private string _tenant = string.Empty;
private string _connectionstring = string.Empty; private string _connectionstring = string.Empty;
private string _connectionstringtype = "password"; private string _connectionstringtype = "password";
private string _connectionstringtoggle = string.Empty; private string _connectionstringtoggle = string.Empty;
private string _sql = string.Empty; private string _sql = string.Empty;
private List<Dictionary<string, string>> _results; private List<Dictionary<string, string>> _results;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_connections = await SystemService.GetSystemInfoAsync("connectionstrings"); _connections = await SystemService.GetSystemInfoAsync("connectionstrings");
_tenants = await TenantService.GetTenantsAsync(); _tenants = await TenantService.GetTenantsAsync();
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
_connectionstringtoggle = SharedLocalizer["ShowPassword"]; _connectionstringtoggle = SharedLocalizer["ShowPassword"];
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message); await logger.LogError(ex, "Error Loading Tenants {Error}", ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
} }
} }
private async void ConnectionChanged(ChangeEventArgs e) private async void ConnectionChanged(ChangeEventArgs e)
{ {
try try
{ {
_connection = (string)e.Value; _connection = (string)e.Value;
if (_connection != "-" && _connection != "+") if (_connection != "-" && _connection != "+")
{ {
_connectionstring = _connections[_connection].ToString(); _connectionstring = _connections[_connection].ToString();
_tenant = ""; _tenant = "";
_databasetype = ""; _databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection); var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null) if (tenant != null)
{ {
_tenant = tenant.Name; _tenant = tenant.Name;
// hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order // hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order
_databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name; _databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name;
} }
} else
else {
{ if (_connection.Contains(" ("))
if (_databases.Exists(item => item.IsDefault)) {
{ _databasetype = _connection.Substring(_connection.LastIndexOf(" (") + 2).Replace(")", "");
_databasetype = _databases.Find(item => item.IsDefault).Name; }
} else
else {
{ if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName;
}
}
}
}
else
{
if (_databases.Exists(item => item.IsDefault))
{
_databasetype = _databases.Find(item => item.IsDefault).Name;
}
else
{
_databasetype = Constants.DefaultDBName; _databasetype = Constants.DefaultDBName;
} }
_showConnectionString = false; _showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
StateHasChanged(); StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message); await logger.LogError(ex, "Error Loading Connection {Connection} {Error}", _connection, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
} }
} }
private void DatabaseTypeChanged(ChangeEventArgs eventArgs) private void DatabaseTypeChanged(ChangeEventArgs eventArgs)
{ {
try try
{ {
_databasetype = (string)eventArgs.Value; _databasetype = (string)eventArgs.Value;
_showConnectionString = false; _showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
{ {
AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error); AddModuleMessage(Localizer["Error.Database.LoadConfig"], MessageType.Error);
} }
} }
private void LoadDatabaseConfigComponent() private void LoadDatabaseConfigComponent()
{ {
var database = _databases.SingleOrDefault(d => d.Name == _databasetype); var database = _databases.SingleOrDefault(d => d.Name == _databasetype);
if (database != null) if (database != null)
{ {
_databaseConfigType = Type.GetType(database.ControlType); _databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder => DatabaseConfigComponent = builder =>
{ {
builder.OpenComponent(0, _databaseConfigType); builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
} }
private void ShowConnectionString() private void ShowConnectionString()
{ {
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
_connectionstring = databaseConfigControl.GetConnectionString(); _connectionstring = databaseConfigControl.GetConnectionString();
} }
_showConnectionString = !_showConnectionString; _showConnectionString = !_showConnectionString;
} }
private async Task Add() private async Task Add()
{ {
var connectionstring = _connectionstring; var connectionstring = _connectionstring;
if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl) if (!_showConnectionString && _databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
connectionstring = databaseConfigControl.GetConnectionString(); connectionstring = databaseConfigControl.GetConnectionString();
} }
if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring)) if (!string.IsNullOrEmpty(_name) && !string.IsNullOrEmpty(connectionstring))
{ {
var settings = new Dictionary<string, object>(); _name = _name + " (" + _databasetype +")";
var settings = new Dictionary<string, object>();
settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring); settings.Add($"{SettingKeys.ConnectionStringsSection}:{_name}", connectionstring);
await SystemService.UpdateSystemInfoAsync(settings); await SystemService.UpdateSystemInfoAsync(settings);
_connections = await SystemService.GetSystemInfoAsync("connectionstrings"); _connections = await SystemService.GetSystemInfoAsync("connectionstrings");

View File

@@ -11,11 +11,22 @@
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Download" ResourceKey="Download"> <TabPanel Name="Download" ResourceKey="Download">
@if (_package != null && _upgradeavailable) @if (_versions.Count > 0 && _upgradeable)
{ {
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label> <Label Class="col-sm-3" For="version" HelpText="Select the framework upgrade version" ResourceKey="Version">Version: </Label>
<div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_version">
@foreach (var version in _versions)
{
<option value="@version">@version</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="backup" HelpText="Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade." ResourceKey="Backup">Backup Files? </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="backup" class="form-select" @bind="@_backup"> <select id="backup" class="form-select" @bind="@_backup">
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>
@@ -27,7 +38,7 @@
<br /> <br />
@if (!_downloaded) @if (!_downloaded)
{ {
<button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, @_package.Version))>@SharedLocalizer["Download"] @_package.Version</button> <button type="button" class="btn btn-primary" @onclick=@(async () => await Download(Constants.PackageId, _version))>@SharedLocalizer["Download"] @_version</button>
} }
else else
{ {
@@ -66,8 +77,9 @@
@code { @code {
private bool _initialized = false; private bool _initialized = false;
private bool _downloaded = false; private bool _downloaded = false;
private Package _package; private List<string> _versions = new List<string>();
private bool _upgradeavailable = false; private string _version;
private bool _upgradeable = false;
private string _backup = "True"; private string _backup = "True";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@@ -84,18 +96,18 @@
{ {
AddModuleMessage(Localizer["Disclaimer.Text"], MessageType.Warning); AddModuleMessage(Localizer["Disclaimer.Text"], MessageType.Warning);
List<Package> packages = await PackageService.GetPackagesAsync("framework", "", "", ""); var packages = await PackageService.GetPackagesAsync("framework", "", "", "");
if (packages != null) if (packages != null)
{ {
_package = packages.Where(item => item.PackageId.StartsWith(Constants.PackageId)).FirstOrDefault(); _version = packages.First(item => item.PackageId.StartsWith(Constants.PackageId)).Version;
if (_package != null) foreach (var version in Constants.ReleaseVersions.Split(','))
{ {
_upgradeavailable = (Version.Parse(_package.Version).CompareTo(Version.Parse(Constants.Version)) > 0); if (Version.Parse(version).CompareTo(Version.Parse(Constants.Version)) > 0)
} {
else _versions.Add(version);
{ }
_package = new Package { Name = Constants.PackageId, Version = Constants.Version };
} }
_upgradeable = (Version.Parse(_version).CompareTo(Version.Parse(Constants.Version)) > 0);
} }
_initialized = true; _initialized = true;
} }

View File

@@ -21,7 +21,7 @@
@if (_style == MessageStyle.Toast) @if (_style == MessageStyle.Toast)
{ {
<div class="app-modulemessage-toast bottom-0 end-0" @key="DateTime.UtcNow"> <div class="app-modulemessage-toast bottom-0 end-0">
<div class="@_classname alert-dismissible fade show mb-3 rounded-end-0" role="alert"> <div class="@_classname alert-dismissible fade show mb-3 rounded-end-0" role="alert">
@((MarkupString)Message) @((MarkupString)Message)
@if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) @if (Type == MessageType.Error && PageState != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@@ -86,11 +86,19 @@
/// <summary> /// <summary>
/// Determines if a tab should be visible based on user permissions. /// Determines if a tab should be visible based on user permissions.
/// Authorization hierarchy: /// Authorization follows this hierarchy:
/// 1. Host and Admin roles ALWAYS have access (bypass all checks) /// 1. Host tabs (Security == Host): Only users with Host role can access (Admins excluded)
/// 2. Check standard SecurityAccessLevel (View, Edit, etc.) /// 2. Admin users: Bypass all other checks (except Host restrictions)
/// 3. If RoleName specified AND user is not Admin/Host, check RoleName /// 3. SecurityAccessLevel check (null/Anonymous/View/Edit/Host):
/// 4. If PermissionName specified AND user is not Admin/Host, check PermissionName /// - null: No security level restriction (proceeds to step 4)
/// - Anonymous: No authentication required
/// - View/Edit: Requires corresponding module permission
/// - Host: Only Host role can access
/// 4. Additional RoleName requirement (if specified)
/// 5. Additional PermissionName requirement (if specified)
///
/// Important: When Security is null, RoleName and PermissionName checks STILL apply
/// (Security = null doesn't mean unrestricted, it means "no security level required")
/// </summary> /// </summary>
/// <param name="tabPanel">The tab panel to check authorization for</param> /// <param name="tabPanel">The tab panel to check authorization for</param>
/// <returns>True if user is authorized to see this tab, false otherwise</returns> /// <returns>True if user is authorized to see this tab, false otherwise</returns>
@@ -99,24 +107,40 @@
// Step 1: Check for Host-only restriction // Step 1: Check for Host-only restriction
if (tabPanel.Security == SecurityAccessLevel.Host) if (tabPanel.Security == SecurityAccessLevel.Host)
{ {
// Only Host users can access Host-level security tabs (Admin users are excluded)
return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host); return UserSecurity.IsAuthorized(PageState.User, RoleNames.Host);
} }
// Step 2: Admin bypass all other restrictions // Step 2: Admin bypass all restrictions except Host
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{ {
return true; return true;
} }
var authorized = false; // Step 3: If Security is null, check only RoleName and PermissionName
if (tabPanel.Security == null)
{
// Start with authorized = true for null security
bool isAuthorized = true;
// Step 3: Check standard SecurityAccessLevel // Only apply RoleName check if provided
if (!string.IsNullOrEmpty(tabPanel.RoleName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
}
// Only apply PermissionName check if provided
if (isAuthorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{
isAuthorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);
}
return isAuthorized;
}
// Handle other SecurityAccessLevel values
bool authorized = false; // Use different variable name or move declaration
switch (tabPanel.Security) switch (tabPanel.Security)
{ {
case null:
authorized = true;
break;
case SecurityAccessLevel.Anonymous: case SecurityAccessLevel.Anonymous:
authorized = true; authorized = true;
break; break;
@@ -131,13 +155,13 @@
break; break;
} }
// Step 4: Check RoleName if provided (additional requirement) // Step 4: Additional RoleName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName)) if (authorized && !string.IsNullOrEmpty(tabPanel.RoleName))
{ {
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName); authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.RoleName);
} }
// Step 5: Check PermissionName if provided (additional requirement) // Step 5: Additional PermissionName requirement
if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName)) if (authorized && !string.IsNullOrEmpty(tabPanel.PermissionName))
{ {
authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList); authorized = UserSecurity.IsAuthorized(PageState.User, tabPanel.PermissionName, ModuleState.PermissionList);

View File

@@ -8,11 +8,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" />
<PackageReference Include="Radzen.Blazor" Version="8.4.0" /> <PackageReference Include="Radzen.Blazor" Version="8.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -139,7 +139,7 @@
<value>Error Updating Job</value> <value>Error Updating Job</value>
</data> </data>
<data name="Message.Required.JobInfo" xml:space="preserve"> <data name="Message.Required.JobInfo" xml:space="preserve">
<value>You Must Provide The Job Name, Type, Frequency, and Retention</value> <value>You Must Provide The Job Name, Frequency, and Retention</value>
</data> </data>
<data name="Name.HelpText" xml:space="preserve"> <data name="Name.HelpText" xml:space="preserve">
<value>Enter the job name</value> <value>Enter the job name</value>
@@ -154,7 +154,7 @@
<value>Select how often you want the job to run</value> <value>Select how often you want the job to run</value>
</data> </data>
<data name="Starting.HelpText" xml:space="preserve"> <data name="Starting.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should start executing</value> <value>Optionally enter the date and time when this job should start executing. If no date or time is specified, the job will execute immediately.</value>
</data> </data>
<data name="Ending.HelpText" xml:space="preserve"> <data name="Ending.HelpText" xml:space="preserve">
<value>Optionally enter the date and time when this job should stop executing</value> <value>Optionally enter the date and time when this job should stop executing</value>
@@ -163,7 +163,7 @@
<value>Number of log entries to retain for this job</value> <value>Number of log entries to retain for this job</value>
</data> </data>
<data name="NextExecution.HelpText" xml:space="preserve"> <data name="NextExecution.HelpText" xml:space="preserve">
<value>Optionally modify the date and time when this job should execute next</value> <value>The date and time when this job will execute next. This value cannot be modified. Use the settings above to control the execution of the job.</value>
</data> </data>
<data name="Type.Text" xml:space="preserve"> <data name="Type.Text" xml:space="preserve">
<value>Type: </value> <value>Type: </value>
@@ -193,6 +193,15 @@
<value>Execute Once</value> <value>Execute Once</value>
</data> </data>
<data name="Message.StartEndDateError" xml:space="preserve"> <data name="Message.StartEndDateError" xml:space="preserve">
<value>Start Date cannot be after End Date.</value> <value>The Start Date Cannot Be Later Than The End Date</value>
</data>
<data name="Message.StartDateError" xml:space="preserve">
<value>The Start Date Cannot Be Prior To The Current Date</value>
</data>
<data name="Message.ExecutingError" xml:space="preserve">
<value>The Job Is Currently Executing. You Must Wait Until The Job Has Completed.</value>
</data>
<data name="JobNotEditable" xml:space="preserve">
<value>The Job Cannot Be Modified As It Is Currently Enabled. You Must Disable The Job To Change Its Settings.</value>
</data> </data>
</root> </root>

View File

@@ -132,9 +132,6 @@
<data name="DefaultAdminContainer" xml:space="preserve"> <data name="DefaultAdminContainer" xml:space="preserve">
<value>Default Admin Container</value> <value>Default Admin Container</value>
</data> </data>
<data name="Smtp.Required.EnableNotificationJob" xml:space="preserve">
<value>** Please Note That SMTP Requires The Notification Job To Be Enabled In Scheduled Jobs</value>
</data>
<data name="Smtp.TestConfig" xml:space="preserve"> <data name="Smtp.TestConfig" xml:space="preserve">
<value>Test SMTP Configuration</value> <value>Test SMTP Configuration</value>
</data> </data>
@@ -342,12 +339,6 @@
<data name="HomePage.Text" xml:space="preserve"> <data name="HomePage.Text" xml:space="preserve">
<value>Home Page:</value> <value>Home Page:</value>
</data> </data>
<data name="SmtpRelay.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="SmtpRelay.Text" xml:space="preserve">
<value>Relay Configured?</value>
</data>
<data name="SiteMap.HelpText" xml:space="preserve"> <data name="SiteMap.HelpText" xml:space="preserve">
<value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value> <value>The site map url for this site which can be submitted to search engines for indexing. The sitemap is cached for 5 minutes and the cache can be manually cleared.</value>
</data> </data>

View File

@@ -156,4 +156,10 @@
<data name="Backup.HelpText" xml:space="preserve"> <data name="Backup.HelpText" xml:space="preserve">
<value>Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade.</value> <value>Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade.</value>
</data> </data>
<data name="Version.Text" xml:space="preserve">
<value>Version:</value>
</data>
<data name="Version.HelpText" xml:space="preserve">
<value>Select the framework upgrade version</value>
</data>
</root> </root>

View File

@@ -16,7 +16,7 @@
{ {
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top") @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top")
{ {
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" /> <ModuleMessage @key="_messageVersionTop" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
} }
@DynamicComponent @DynamicComponent
@if (_progressIndicator) @if (_progressIndicator)
@@ -25,7 +25,7 @@
} }
@if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom") @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom")
{ {
<ModuleMessage Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" /> <ModuleMessage @key="_messageVersionBottom" Message="@_messageContent" Type="@_messageType" Parent="@this" Style="@_messageStyle" />
} }
} }
} }
@@ -52,6 +52,8 @@
private MessageStyle _messageStyle; private MessageStyle _messageStyle;
private bool _progressIndicator = false; private bool _progressIndicator = false;
private string _error; private string _error;
private string _messageVersionTop = Guid.NewGuid().ToString();
private string _messageVersionBottom = Guid.NewGuid().ToString();
[Parameter] [Parameter]
public SiteState SiteState { get; set; } public SiteState SiteState { get; set; }
@@ -143,6 +145,18 @@
_messageStyle = style; _messageStyle = style;
_progressIndicator = false; _progressIndicator = false;
if (style == MessageStyle.Toast && !string.IsNullOrEmpty(_messageContent))
{
if (_messagePosition == "top")
{
_messageVersionTop = Guid.NewGuid().ToString();
}
else if (_messagePosition == "bottom")
{
_messageVersionBottom = Guid.NewGuid().ToString();
}
}
StateHasChanged(); StateHasChanged();
} }
} }

View File

@@ -18,7 +18,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId> <ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>10.0.3</ApplicationDisplayVersion> <ApplicationDisplayVersion>10.0.4</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@@ -54,11 +54,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" /> <PackageReference Include="System.Net.Http.Json" Version="10.0.2" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>10.0.3</version> <version>10.0.4</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,18 +12,18 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
<dependencies> <dependencies>
<group targetFramework="net10.0"> <group targetFramework="net10.0">
<dependency id="Oqtane.Shared" version="10.0.0" exclude="Build,Analyzers" /> <dependency id="Oqtane.Shared" version="10.0.4" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Authentication" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Localization" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Localization" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.Http" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.Http" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Radzen.Blazor" version="8.4.0" exclude="Build,Analyzers" /> <dependency id="Radzen.Blazor" version="8.6.0" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Framework</id> <id>Oqtane.Framework</id>
<version>10.0.3</version> <version>10.0.4</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright> <copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.0.3/Oqtane.Framework.10.0.3.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane framework</tags> <tags>oqtane framework</tags>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Server</id> <id>Oqtane.Server</id>
<version>10.0.3</version> <version>10.0.4</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,29 +12,29 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
<dependencies> <dependencies>
<group targetFramework="net10.0"> <group targetFramework="net10.0">
<dependency id="Oqtane.Client" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Oqtane.Client" version="10.0.4" exclude="Build,Analyzers" />
<dependency id="Oqtane.Shared" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Oqtane.Shared" version="10.0.4" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Components.WebAssembly.Server" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Identity.EntityFrameworkCore" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.Relational" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="SixLabors.ImageSharp" version="3.1.12" exclude="Build,Analyzers" /> <dependency id="SixLabors.ImageSharp" version="3.1.12" exclude="Build,Analyzers" />
<dependency id="HtmlAgilityPack" version="1.12.4" exclude="Build,Analyzers" /> <dependency id="HtmlAgilityPack" version="1.12.4" exclude="Build,Analyzers" />
<dependency id="Swashbuckle.AspNetCore" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Swashbuckle.AspNetCore" version="10.1.0" exclude="Build,Analyzers" />
<dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" /> <dependency id="MailKit" version="4.14.1" exclude="Build,Analyzers" />
<dependency id="MySql.Data" version="9.5.0" exclude="Build,Analyzers" /> <dependency id="MySql.Data" version="9.5.0" exclude="Build,Analyzers" />
<dependency id="Pomelo.EntityFrameworkCore.MySql" version="9.0.0" exclude="Build,Analyzers" /> <dependency id="MySql.EntityFrameworkCore" version="10.0.0-rc" exclude="Build,Analyzers" />
<dependency id="EFCore.NamingConventions" version="10.0.0-rc.2" exclude="Build,Analyzers" /> <dependency id="EFCore.NamingConventions" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0" exclude="Build,Analyzers" /> <dependency id="Npgsql.EntityFrameworkCore.PostgreSQL" version="10.0.0" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.Sqlite" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.Data.Sqlite.Core" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Data.Sqlite.Core" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="10.0.2" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
<frameworkReferences> <frameworkReferences>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Shared</id> <id>Oqtane.Shared</id>
<version>10.0.3</version> <version>10.0.4</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,15 +12,15 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
<dependencies> <dependencies>
<group targetFramework="net10.0"> <group targetFramework="net10.0">
<dependency id="Microsoft.EntityFrameworkCore" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.EntityFrameworkCore" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.1" exclude="Build,Analyzers" /> <dependency id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.2" exclude="Build,Analyzers" />
<dependency id="NodaTime" version="3.2.3" exclude="Build,Analyzers" /> <dependency id="NodaTime" version="3.3.0" exclude="Build,Analyzers" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>10.0.3</version> <version>10.0.4</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.3.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Install.zip" -Force

View File

@@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.3.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Upgrade.zip" -Force

View File

@@ -128,7 +128,7 @@ namespace Oqtane.Controllers
} }
break; break;
default: default:
_configManager.AddOrUpdateSetting(key, value, false); _configManager.AddOrUpdateSetting(key, value, true);
break; break;
} }
} }

View File

@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
using MySql.EntityFrameworkCore.Metadata;
using Oqtane.Databases; using Oqtane.Databases;
namespace Oqtane.Database.MySQL namespace Oqtane.Database.MySQL
@@ -25,7 +26,7 @@ namespace Oqtane.Database.MySQL
public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name) public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name)
{ {
return table.Column<int>(name: name, nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); return table.Column<int>(name: name, nullable: false).Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn);
} }
public override string ConcatenateSql(params string[] values) public override string ConcatenateSql(params string[] values)
@@ -96,7 +97,7 @@ namespace Oqtane.Database.MySQL
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{ {
return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); return optionsBuilder.UseMySQL(connectionString);
} }
private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query) private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query)

View File

@@ -84,16 +84,13 @@ namespace Oqtane.Extensions
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "") if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{ {
options.Events = new OpenIdConnectEvents options.Events.OnRedirectToIdentityProvider = context =>
{ {
OnRedirectToIdentityProvider = context => foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{ {
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(",")) context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
{
context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
}
return Task.FromResult(0);
} }
return Task.FromResult(0);
}; };
} }
} }
@@ -132,18 +129,15 @@ namespace Oqtane.Extensions
options.Events.OnRemoteFailure = OnRemoteFailure; options.Events.OnRemoteFailure = OnRemoteFailure;
if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "") if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
{ {
options.Events = new OAuthEvents options.Events.OnRedirectToAuthorizationEndpoint = context =>
{ {
OnRedirectToAuthorizationEndpoint = context => var url = context.RedirectUri;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{ {
var url = context.RedirectUri; url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
{
url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
}
context.Response.Redirect(url);
return Task.FromResult(0);
} }
context.Response.Redirect(url);
return Task.FromResult(0);
}; };
} }
} }

View File

@@ -198,41 +198,27 @@ namespace Oqtane.Infrastructure
{ {
case "m": // minutes case "m": // minutes
nextExecution = nextExecution.AddMinutes(job.Interval); nextExecution = nextExecution.AddMinutes(job.Interval);
if (nextExecution < DateTime.UtcNow) nextExecution = DateTime.UtcNow;
break; break;
case "H": // hours case "H": // hours
nextExecution = nextExecution.AddHours(job.Interval); nextExecution = nextExecution.AddHours(job.Interval);
if (nextExecution < DateTime.UtcNow) nextExecution = DateTime.UtcNow;
break; break;
case "d": // days case "d": // days
nextExecution = DateTime.UtcNow.Date.Add(nextExecution.TimeOfDay); // preserve time of day
nextExecution = nextExecution.AddDays(job.Interval); nextExecution = nextExecution.AddDays(job.Interval);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
// set the start time
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break; break;
case "w": // weeks case "w": // weeks
nextExecution = DateTime.UtcNow.Date.Add(nextExecution.TimeOfDay); // preserve time of day
nextExecution = nextExecution.AddDays(job.Interval * 7); nextExecution = nextExecution.AddDays(job.Interval * 7);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
// set the start time
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break; break;
case "M": // months case "M": // months
nextExecution = DateTime.UtcNow.Date.Add(nextExecution.TimeOfDay); // preserve time of day
nextExecution = nextExecution.AddMonths(job.Interval); nextExecution = nextExecution.AddMonths(job.Interval);
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
// set the start time
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break; break;
case "O": // one time case "O": // one time
break; break;
} }
if (nextExecution < DateTime.UtcNow)
{
nextExecution = DateTime.UtcNow;
}
return nextExecution; return nextExecution;
} }

View File

@@ -160,7 +160,7 @@ namespace Oqtane.Infrastructure
var toEmail = notification.ToEmail ?? ""; var toEmail = notification.ToEmail ?? "";
var toName = notification.ToDisplayName ?? ""; var toName = notification.ToDisplayName ?? "";
// get sender and receiver information from user information if available // get sender from user information if "from" email or name is not specified and user id is available
if ((string.IsNullOrEmpty(fromEmail) || string.IsNullOrEmpty(fromName)) && notification.FromUserId != null) if ((string.IsNullOrEmpty(fromEmail) || string.IsNullOrEmpty(fromName)) && notification.FromUserId != null)
{ {
var user = userRepository.GetUser(notification.FromUserId.Value); var user = userRepository.GetUser(notification.FromUserId.Value);
@@ -170,6 +170,9 @@ namespace Oqtane.Infrastructure
fromName = string.IsNullOrEmpty(fromName) ? user.DisplayName ?? "" : fromName; fromName = string.IsNullOrEmpty(fromName) ? user.DisplayName ?? "" : fromName;
} }
} }
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
// get recipient from user information if "to" email or name is not specified and user id is available
if ((string.IsNullOrEmpty(toEmail) || string.IsNullOrEmpty(toName)) && notification.ToUserId != null) if ((string.IsNullOrEmpty(toEmail) || string.IsNullOrEmpty(toName)) && notification.ToUserId != null)
{ {
var user = userRepository.GetUser(notification.ToUserId.Value); var user = userRepository.GetUser(notification.ToUserId.Value);
@@ -181,30 +184,34 @@ namespace Oqtane.Infrastructure
} }
// create mailbox addresses // create mailbox addresses
MailboxAddress to = null;
MailboxAddress from = null; MailboxAddress from = null;
MailboxAddress to = null;
MailboxAddress replyTo = null;
var mailboxAddressValidationError = ""; var mailboxAddressValidationError = "";
// sender // always send from SMTP Sender
if ((settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True") && string.IsNullOrEmpty(fromEmail)) if (MailboxAddress.TryParse(settingRepository.GetSettingValue(settings, "SMTPSender", ""), out from))
{ {
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", ""); from.Name = fromName;
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
}
if (MailboxAddress.TryParse(fromEmail, out from))
{
from.Name = fromName;
} }
else else
{ {
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{settingRepository.GetSettingValue(settings, "SMTPSender", "")}&gt;";
}
mailboxAddressValidationError += $" Invalid Sender: {fromName} &lt;{fromEmail}&gt;"; // reply to
if (!string.IsNullOrEmpty(fromEmail) && fromEmail != from.Address)
{
if (MailboxAddress.TryParse(fromEmail, out replyTo))
{
replyTo.Name = fromName;
}
} }
// recipient // recipient
if (MailboxAddress.TryParse(toEmail, out to)) if (MailboxAddress.TryParse(toEmail, out to))
{ {
to.Name = toName; to.Name = toName;
} }
else else
{ {
@@ -218,6 +225,10 @@ namespace Oqtane.Infrastructure
MimeMessage mailMessage = new MimeMessage(); MimeMessage mailMessage = new MimeMessage();
mailMessage.From.Add(from); mailMessage.From.Add(from);
mailMessage.To.Add(to); mailMessage.To.Add(to);
if (replyTo != null)
{
mailMessage.ReplyTo.Add(replyTo);
}
// subject // subject
mailMessage.Subject = notification.Subject; mailMessage.Subject = notification.Subject;

View File

@@ -66,7 +66,7 @@ namespace Oqtane.Infrastructure.SiteTemplates
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
Content = "<p>Copyright (c) 2018-2025 .NET Foundation</p>" + Content = "<p>Copyright (c) 2018-2026 .NET Foundation</p>" +
"<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>" + "<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>" +
"<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" + "<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" +
"<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>" "<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>"

View File

@@ -78,9 +78,6 @@ namespace Oqtane.Infrastructure
case "5.2.1": case "5.2.1":
Upgrade_5_2_1(tenant, scope); Upgrade_5_2_1(tenant, scope);
break; break;
case "6.1.0":
Upgrade_6_1_0(tenant, scope);
break;
case "6.1.1": case "6.1.1":
Upgrade_6_1_1(tenant, scope); Upgrade_6_1_1(tenant, scope);
break; break;
@@ -93,6 +90,9 @@ namespace Oqtane.Infrastructure
case "6.2.1": case "6.2.1":
Upgrade_6_2_1(tenant, scope); Upgrade_6_2_1(tenant, scope);
break; break;
case "10.0.4":
Upgrade_10_0_4(tenant, scope);
break;
} }
} }
} }
@@ -447,16 +447,6 @@ namespace Oqtane.Infrastructure
AddPagesToSites(scope, tenant, pageTemplates); AddPagesToSites(scope, tenant, pageTemplates);
} }
private void Upgrade_6_1_0(Tenant tenant, IServiceScope scope)
{
// remove MySql.EntityFrameworkCore package (replaced by Pomelo.EntityFrameworkCore.MySql)
string[] assemblies = {
"MySql.EntityFrameworkCore.dll"
};
RemoveAssemblies(tenant, assemblies, "6.1.0");
}
private void Upgrade_6_1_1(Tenant tenant, IServiceScope scope) private void Upgrade_6_1_1(Tenant tenant, IServiceScope scope)
{ {
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<AdminSiteTemplate>>(); var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<AdminSiteTemplate>>();
@@ -602,6 +592,16 @@ namespace Oqtane.Infrastructure
RemoveFiles(tenant, files, "6.2.1"); RemoveFiles(tenant, files, "6.2.1");
} }
private void Upgrade_10_0_4(Tenant tenant, IServiceScope scope)
{
// remove Pomelo.EntityFrameworkCore.MySql package (replaced by MySql.EntityFrameworkCore)
string[] assemblies = {
"Pomelo.EntityFrameworkCore.MySql.dll"
};
RemoveAssemblies(tenant, assemblies, "10.0.4");
}
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates) private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates)
{ {
var tenants = scope.ServiceProvider.GetRequiredService<ITenantManager>(); var tenants = scope.ServiceProvider.GetRequiredService<ITenantManager>();

View File

@@ -181,13 +181,23 @@ namespace Oqtane.Managers
succeeded = true; succeeded = true;
if (!user.IsAuthenticated) if (!user.IsAuthenticated)
{ {
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false); // validate if the user already exists for the site
succeeded = result.Succeeded; succeeded = string.IsNullOrEmpty(GetUser(user.Username, user.SiteId).Roles);
if (!succeeded) if (succeeded)
{ {
errors = "Password Not Valid For User"; // a user is registering for a new site - ensure their password is valid
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
succeeded = result.Succeeded;
if (!succeeded)
{
errors = "User Already Exists In Installation But Cannot Be Added To A Site Because The Password Provided Is Not Valid";
}
user.EmailConfirmed = succeeded;
}
else
{
errors = "User Already Exists In Site";
} }
user.EmailConfirmed = succeeded;
} }
} }

View File

@@ -27,28 +27,28 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
<PackageReference Include="MailKit" Version="4.14.1" /> <PackageReference Include="MailKit" Version="4.14.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- MySQL Database Provider Dependencies --> <!-- MySQL Database Provider Dependencies -->
<PackageReference Include="MySql.Data" Version="9.5.0" /> <PackageReference Include="MySql.Data" Version="9.5.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" /> <PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.0-rc" />
<!-- PostgreSQL Database Provider Dependencies --> <!-- PostgreSQL Database Provider Dependencies -->
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0-rc.2" /> <PackageReference Include="EFCore.NamingConventions" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<!-- SQLite Database Provider Dependencies --> <!-- SQLite Database Provider Dependencies -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.1" /> <PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.2" />
<!-- SQL Server Database Provider Dependencies --> <!-- SQL Server Database Provider Dependencies -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
</ItemGroup> </ItemGroup>
<!-- Suppress EF Core internal warnings for Database Providers --> <!-- Suppress EF Core internal warnings for Database Providers -->

View File

@@ -43,14 +43,6 @@ namespace Oqtane.Repository
{ {
optionsBuilder.ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>(); optionsBuilder.ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();
// specify the SchemaVersion for .NET Identity as it is not being persisted when using AddIdentityCore()
var services = new ServiceCollection();
services.AddIdentityCore<IdentityUser>(options =>
{
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
});
optionsBuilder.UseApplicationServiceProvider(services.BuildServiceProvider());
if (string.IsNullOrEmpty(_connectionString)) if (string.IsNullOrEmpty(_connectionString))
{ {
Tenant tenant = _tenantManager.GetTenant(); Tenant tenant = _tenantManager.GetTenant();
@@ -75,6 +67,22 @@ namespace Oqtane.Repository
ActiveDatabase = Activator.CreateInstance(type) as IDatabase; ActiveDatabase = Activator.CreateInstance(type) as IDatabase;
} }
// specify the SchemaVersion for .NET Identity as it is not being persisted when using AddIdentityCore()
var services = new ServiceCollection();
services.AddIdentityCore<IdentityUser>(options =>
{
if (!string.IsNullOrEmpty(_databaseType) && _databaseType.ToLower().Contains("mysql"))
{
// MySQL does not support some of the newer features of .NET Identity (ie. Passkeys)
options.Stores.SchemaVersion = IdentitySchemaVersions.Version2;
}
else
{
options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
}
});
optionsBuilder.UseApplicationServiceProvider(services.BuildServiceProvider());
if (!string.IsNullOrEmpty(_connectionString) && ActiveDatabase != null) if (!string.IsNullOrEmpty(_connectionString) && ActiveDatabase != null)
{ {
optionsBuilder.UseOqtaneDatabase(ActiveDatabase, _connectionString); optionsBuilder.UseOqtaneDatabase(ActiveDatabase, _connectionString);

View File

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

View File

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

View File

@@ -14,9 +14,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="10.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -5,9 +5,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageReference Include="NodaTime" Version="3.2.3" /> <PackageReference Include="NodaTime" Version="3.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,8 +4,8 @@ namespace Oqtane.Shared
{ {
public class Constants public class Constants
{ {
public static readonly string Version = "10.0.3"; public static readonly string Version = "10.0.4";
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2,10.0.3"; public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2,10.0.3,10.0.4";
public const string PackageId = "Oqtane.Framework"; public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client"; public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater"; public const string UpdaterPackageId = "Oqtane.Updater";

View File

@@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline
# Latest Release # Latest Release
[10.0.3](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3) was released on December 24, 2025 and is a maintenance release including 19 pull requests by 2 different contributors, pushing the total number of project commits all-time to nearly 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. [10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) was released on January 20, 2026 and is a maintenance release including 27 pull requests by 3 different contributors, pushing the total number of project commits all-time over 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
# Try It Now! # Try It Now!
@@ -111,6 +111,9 @@ 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...
[10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) (Jan 20, 2026)
- [x] Stabilization improvements
[10.0.3](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3) (Dec 24, 2025) [10.0.3](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.3) (Dec 24, 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/v10.0.3/Oqtane.Framework.10.0.3.Install.zip" "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Install.zip"
}, },
"dependsOn": [ "dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"