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:58:58 -05:00
parent efcfc0783c
commit 51b356cc0e
11 changed files with 167 additions and 139 deletions

View File

@ -38,6 +38,7 @@
<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>
</select> </select>
</div> </div>
</div> </div>
@ -96,82 +97,89 @@
</form> </form>
@code { @code {
private ElementReference form; private ElementReference form;
private bool validated = false; private bool validated = false;
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;
private string _isEnabled = "True"; private string _isEnabled = "True";
private string _interval = string.Empty; private string _interval = string.Empty;
private string _frequency = string.Empty; private string _frequency = string.Empty;
private DateTime? _startDate = null; private DateTime? _startDate = null;
private string _startTime = string.Empty; private string _startTime = string.Empty;
private DateTime? _endDate = null; private DateTime? _endDate = null;
private string _endTime = string.Empty; private string _endTime = string.Empty;
private string _retentionHistory = string.Empty; private string _retentionHistory = string.Empty;
private DateTime? _nextDate = null; private DateTime? _nextDate = null;
private string _nextTime = string.Empty; private string _nextTime = string.Empty;
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 try
{ {
_jobId = Int32.Parse(PageState.QueryString["id"]); _jobId = Int32.Parse(PageState.QueryString["id"]);
Job job = await JobService.GetJobAsync(_jobId); Job job = await JobService.GetJobAsync(_jobId);
if (job != null) if (job != null)
{ {
_name = job.Name; _name = job.Name;
_jobType = job.JobType; _jobType = job.JobType;
_isEnabled = job.IsEnabled.ToString(); _isEnabled = job.IsEnabled.ToString();
_interval = job.Interval.ToString(); _interval = job.Interval.ToString();
_frequency = job.Frequency; _frequency = job.Frequency;
_startDate = job.StartDate; _startDate = job.StartDate;
if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0) if (job.StartDate != null && job.StartDate.Value.TimeOfDay.TotalSeconds != 0)
{ {
_startTime = job.StartDate.Value.ToString("HH:mm"); _startTime = job.StartDate.Value.ToString("HH:mm");
} }
_endDate = job.EndDate; _endDate = job.EndDate;
if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0) if (job.EndDate != null && job.EndDate.Value.TimeOfDay.TotalSeconds != 0)
{ {
_endTime = job.EndDate.Value.ToString("HH:mm"); _endTime = job.EndDate.Value.ToString("HH:mm");
} }
_retentionHistory = job.RetentionHistory.ToString(); _retentionHistory = job.RetentionHistory.ToString();
_nextDate = job.NextExecution; _nextDate = job.NextExecution;
if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0) if (job.NextExecution != null && job.NextExecution.Value.TimeOfDay.TotalSeconds != 0)
{ {
_nextTime = job.NextExecution.Value.ToString("HH:mm"); _nextTime = job.NextExecution.Value.ToString("HH:mm");
} }
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) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message); await logger.LogError(ex, "Error Loading Job {JobId} {Error}", _jobId, ex.Message);
AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error); AddModuleMessage(Localizer["Error.Job.Load"], MessageType.Error);
} }
} }
private async Task SaveJob() private async Task SaveJob()
{ {
validated = true; validated = true;
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {
var job = await JobService.GetJobAsync(_jobId); var job = await JobService.GetJobAsync(_jobId);
job.Name = _name; job.Name = _name;
job.JobType = _jobType; job.JobType = _jobType;
job.IsEnabled = Boolean.Parse(_isEnabled); job.IsEnabled = Boolean.Parse(_isEnabled);
job.Frequency = _frequency; job.Frequency = _frequency;
job.Interval = int.Parse(_interval); if (job.Frequency == "O") // once
{
job.Interval = 1;
}
else
{
job.Interval = int.Parse(_interval);
}
job.StartDate = _startDate; job.StartDate = _startDate;
if (job.StartDate != null) if (job.StartDate != null)
{ {

View File

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

View File

@ -19,9 +19,10 @@
<label for="Confirm" class="control-label">@Localizer["Password.Confirm"] </label> <label for="Confirm" class="control-label">@Localizer["Password.Confirm"] </label>
<input type="password" class="form-control" placeholder="Password" @bind="@_confirm" id="Confirm" required /> <input type="password" class="form-control" placeholder="Password" @bind="@_confirm" id="Confirm" required />
</div> </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> </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> </form>
@code { @code {

View File

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

View File

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

View File

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

View File

@ -210,7 +210,7 @@
</select> </select>
</div> </div>
</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) @((MarkupString) Message)
</div> </div>
</div> </div>

View File

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

View File

@ -389,7 +389,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null) if (identityuser != null && !string.IsNullOrEmpty(token))
{ {
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token); var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
if (result.Succeeded) if (result.Succeeded)
@ -398,13 +398,13 @@ namespace Oqtane.Controllers
} }
else 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; user = null;
} }
} }
else 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; user = null;
} }
} }
@ -420,9 +420,14 @@ namespace Oqtane.Controllers
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null) if (identityuser != null)
{ {
user = _users.GetUser(user.Username);
string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser); string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
string url = HttpContext.Request.Scheme + "://" + _alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); 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); var notification = new Notification(user.SiteId, null, user, "User Password Reset", body, null);
_notifications.AddNotification(notification); _notifications.AddNotification(notification);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
@ -451,13 +456,13 @@ namespace Oqtane.Controllers
} }
else 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; user = null;
} }
} }
else 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; user = null;
} }
} }

View File

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

View File

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