Merge pull request #1925 from sbwalker/dev

enhanced scheduler to support one-time jobs, fixed pager component so that top/bottom have consistent UX, fixed Blazor theme z-index issues caused by input-group in Bootstrap 5, improved password reset instructions in email notification
This commit is contained in:
Shaun Walker 2022-01-10 19:49:25 -05:00 committed by GitHub
commit 66b13bdb8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 167 additions and 139 deletions

View File

@ -38,6 +38,7 @@
<option value="d">@Localizer["Day(s)"]</option>
<option value="w">@Localizer["Week(s)"]</option>
<option value="M">@Localizer["Month(s)"]</option>
<option value="O">@Localizer["Once"]</option>
</select>
</div>
</div>
@ -96,82 +97,89 @@
</form>
@code {
private ElementReference form;
private bool validated = false;
private int _jobId;
private string _name = string.Empty;
private string _jobType = string.Empty;
private string _isEnabled = "True";
private string _interval = string.Empty;
private string _frequency = string.Empty;
private DateTime? _startDate = null;
private string _startTime = string.Empty;
private DateTime? _endDate = null;
private string _endTime = string.Empty;
private string _retentionHistory = string.Empty;
private DateTime? _nextDate = null;
private string _nextTime = string.Empty;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
private ElementReference form;
private bool validated = false;
private int _jobId;
private string _name = string.Empty;
private string _jobType = string.Empty;
private string _isEnabled = "True";
private string _interval = string.Empty;
private string _frequency = string.Empty;
private DateTime? _startDate = null;
private string _startTime = string.Empty;
private DateTime? _endDate = null;
private string _endTime = string.Empty;
private string _retentionHistory = string.Empty;
private DateTime? _nextDate = null;
private string _nextTime = string.Empty;
private string createdby;
private DateTime createdon;
private string modifiedby;
private DateTime modifiedon;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
protected override async Task OnInitializedAsync()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = job.StartDate;
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
_startTime = job.StartDate.Value.ToString("HH:mm");
}
_endDate = job.EndDate;
if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0)
{
_endTime = job.EndDate.Value.ToString("HH:mm");
}
_retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution;
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{
_nextTime = job.NextExecution.Value.ToString("HH:mm");
}
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
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);
}
}
protected override async Task OnInitializedAsync()
{
try
{
_jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId);
if (job != null)
{
_name = job.Name;
_jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString();
_frequency = job.Frequency;
_startDate = job.StartDate;
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{
_startTime = job.StartDate.Value.ToString("HH:mm");
}
_endDate = job.EndDate;
if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0)
{
_endTime = job.EndDate.Value.ToString("HH:mm");
}
_retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution;
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{
_nextTime = job.NextExecution.Value.ToString("HH:mm");
}
createdby = job.CreatedBy;
createdon = job.CreatedOn;
modifiedby = job.ModifiedBy;
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()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
var job = await JobService.GetJobAsync(_jobId);
job.Name = _name;
job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency;
job.Interval = int.Parse(_interval);
private async Task SaveJob()
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
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;
if (job.StartDate != null)
{

View File

@ -49,60 +49,62 @@ else
}
@code {
private List<Job> _jobs;
private List<Job> _jobs;
public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } }
public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } }
protected override async Task OnParametersSetAsync()
{
_jobs = await JobService.GetJobsAsync();
}
protected override async Task OnParametersSetAsync()
{
_jobs = await JobService.GetJobsAsync();
}
private string DisplayStatus(bool isEnabled, bool isExecuting)
{
var status = string.Empty;
if (!isEnabled)
{
status = Localizer["Disabled"];
}
else
{
if (isExecuting)
{
status = Localizer["Executing"];
}
else
{
status = Localizer["Idle"];
}
}
private string DisplayStatus(bool isEnabled, bool isExecuting)
{
var status = string.Empty;
if (!isEnabled)
{
status = Localizer["Disabled"];
}
else
{
if (isExecuting)
{
status = Localizer["Executing"];
}
else
{
status = Localizer["Idle"];
}
}
return status;
}
return status;
}
private string DisplayFrequency(int interval, string frequency)
{
var result = $"{Localizer["Every"]} {interval.ToString()} ";
switch (frequency)
{
case "m":
result += Localizer["Minute"];
break;
case "H":
result += Localizer["Hour"];
break;
case "d":
result += Localizer["Day"];
break;
case "w":
result += Localizer["Week"];
break;
case "M":
result += Localizer["Month"];
break;
}
private string DisplayFrequency(int interval, string frequency)
{
var result = "";
switch (frequency)
{
case "m":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Minute"];
break;
case "H":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Hour"];
break;
case "d":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Day"];
break;
case "w":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Week"];
break;
case "M":
result = $"{Localizer["Every"]} {interval.ToString()} " + Localizer["Month"];
break;
case "O":
result = Localizer["Once"];
break;
}
return result;
}

View File

@ -19,9 +19,10 @@
<label for="Confirm" class="control-label">@Localizer["Password.Confirm"] </label>
<input type="password" class="form-control" placeholder="Password" @bind="@_confirm" id="Confirm" required />
</div>
<button type="button" class="btn btn-primary" @onclick="Reset">@Localizer["Password.Reset"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Reset">@Localizer["Password.Reset"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
</form>
@code {

View File

@ -107,8 +107,8 @@
<li class="page-item@((_page > 1) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => UpdateList(1))><span class="oi oi-media-step-backward" title="start" aria-hidden="true"></span></a>
</li>
@if (_pages > _displayPages)
{
@if (_pages > _displayPages && _displayPages > 1)
{
<li class="page-item@((_page > _displayPages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("back"))><span class="oi oi-media-skip-backward" title="skip back" aria-hidden="true"></span></a>
</li>
@ -135,7 +135,7 @@
<li class="page-item@((_page < _pages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => NavigateToPage("next"))><span class="oi oi-chevron-right" title="next" aria-hidden="true"></span></a>
</li>
@if (_pages > _displayPages)
@if (_pages > _displayPages && _displayPages > 1)
{
<li class="page-item@((_endPage < _pages) ? "" : " disabled")">
<a class="page-link" @onclick=@(async () => SkipPages("forward"))><span class="oi oi-media-skip-forward" title="skip forward" aria-hidden="true"></span></a>
@ -145,7 +145,7 @@
<a class="page-link" @onclick=@(async () => UpdateList(_pages))><span class="oi oi-media-step-forward" title="end" aria-hidden="true"></span></a>
</li>
<li class="page-item disabled">
<a class="page-link">Page @_page of @_pages</a>
<a class="page-link" style="white-space: nowrap;">Page @_page of @_pages</a>
</li>
</ul>
}

View File

@ -189,4 +189,7 @@
<data name="Week(s)" xml:space="preserve">
<value>Week(s)</value>
</data>
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
</root>

View File

@ -189,4 +189,7 @@
<data name="Week" xml:space="preserve">
<value>Week(s)</value>
</data>
<data name="Once" xml:space="preserve">
<value>Execute Once</value>
</data>
</root>

View File

@ -210,7 +210,7 @@
</select>
</div>
</div>
<button type="button" class="btn btn-success col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button>
<button type="button" class="btn btn-primary col-12 mt-4" @onclick="@AddModule">@Localizer["Page.Module.Add"]</button>
@((MarkupString) Message)
</div>
</div>

View File

@ -19,7 +19,6 @@ using Oqtane.Extensions;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using System.Net.Http;
// ReSharper disable StringIndexOfIsCultureSpecific.1

View File

@ -389,7 +389,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid)
{
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
if (identityuser != null && !string.IsNullOrEmpty(token))
{
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded)
@ -398,13 +398,13 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}", user.Username);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, result.Errors.ToString());
user = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}", user.Username);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}And Token {Token}", user.Username, token);
user = null;
}
}
@ -420,9 +420,14 @@ namespace Oqtane.Controllers
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nPlease Click The Link Displayed Below To Reset Your Password:\n\n" + url + "\n\nThank You!";
string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
"\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
"\n\nIf you did not request to reset your password you can safely ignore this message." +
"\n\nThank You!";
var notification = new Notification(user.SiteId, null, user, "User Password Reset", body, null);
_notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
@ -451,13 +456,13 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username}", user.Username);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, result.Errors.ToString());
user = null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username}", user.Username);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} And Token {Token}", user.Username, token);
user = null;
}
}

View File

@ -115,6 +115,11 @@ namespace Oqtane.Infrastructure
// update the job
job.NextExecution = CalculateNextExecution(NextExecution, job);
if (job.Frequency == "O") // one time
{
job.EndDate = DateTime.UtcNow;
job.NextExecution = null;
}
job.IsExecuting = false;
jobs.UpdateJob(job);
@ -174,6 +179,8 @@ namespace Oqtane.Infrastructure
nextExecution = nextExecution.Date.Add(job.StartDate.Value.TimeOfDay);
}
break;
case "O": // one time
break;
}
if (nextExecution < DateTime.UtcNow)
{

View File

@ -137,7 +137,7 @@
position: fixed;
left: 275px;
top: 15px;
z-index: 3
z-index: 6
}
.sidebar {
@ -145,13 +145,13 @@
height: 100vh;
position: sticky;
top: 0;
z-index: 1
z-index: 4
}
.main .top-row {
position: sticky;
top: 0;
z-index: 2
z-index: 5
}
.main > div {
@ -207,7 +207,7 @@
top: 150px;
width: 100%;
left: 0;
z-index: 1;
z-index: 4;
}
.sidebar {