fix(app): harden account and workflow state

This commit is contained in:
cesnimda
2026-08-24 20:21:09 +02:00
parent e7cacad7d6
commit dca5daa1a2
32 changed files with 811 additions and 86 deletions
@@ -473,6 +473,7 @@ Canonical profile:
[FromQuery] int? companyId = null,
[FromQuery] string? location = null,
[FromQuery] bool needsFollowUp = false,
[FromQuery] string? readiness = null,
[FromQuery] bool includeDeleted = false,
[FromQuery] bool deletedOnly = false,
[FromQuery] string? sortBy = null,
@@ -482,6 +483,9 @@ Canonical profile:
{
if (page < 1) page = 1;
if (pageSize is not (15 or 20 or 25)) pageSize = 15;
var readinessFilter = (readiness ?? string.Empty).Trim().ToLowerInvariant();
if (readinessFilter is not ("" or "needs-work" or "interview"))
return BadRequest("Readiness must be 'needs-work' or 'interview'.");
var query = _db.JobApplications
.AsNoTracking()
@@ -542,16 +546,25 @@ Canonical profile:
var dirDesc = string.Equals(sortDir, "desc", StringComparison.OrdinalIgnoreCase);
var key = (sortBy ?? "dateApplied").Trim();
if (needsFollowUp)
if (needsFollowUp || readinessFilter.Length > 0)
{
// NeedsFollowUp depends on rules + last correspondence date; evaluate in memory so filtering is correct.
// Both filters depend on rule evaluation and workflow signals that include normalized
// note content, so evaluate before pagination to keep totals/pages correct.
var pre = await query.ToListAsync(cancellationToken);
var filtered = new List<JobApplication>();
foreach (var j in pre)
{
lastMsg.TryGetValue(j.Id, out var lm);
var d = RulesEngine.Evaluate(settings, j, now, lm);
if (d.NeedsFollowUp) filtered.Add(j);
var signal = BuildWorkflowSignal(j, d);
var matchesFollowUp = !needsFollowUp || d.NeedsFollowUp;
var matchesReadiness = readinessFilter switch
{
"interview" => signal.NeedsInterviewPrep,
"needs-work" => signal.HasPackageGap || signal.NeedsInterviewPrep,
_ => true,
};
if (matchesFollowUp && matchesReadiness) filtered.Add(j);
}
filtered = key switch
@@ -0,0 +1,34 @@
using JobTrackerApi.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace JobTrackerApi.Controllers;
[ApiController]
[Route("api/notification-settings")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class NotificationSettingsController(UserManager<ApplicationUser> users) : ControllerBase
{
public sealed record NotificationSettingsDto(bool EmailFollowUpRemindersEnabled);
public sealed record SaveNotificationSettingsRequest(bool EmailFollowUpRemindersEnabled);
[HttpGet]
public async Task<ActionResult<NotificationSettingsDto>> Get()
{
var user = await users.GetUserAsync(User);
return user is null ? Unauthorized() : Ok(new NotificationSettingsDto(user.EmailFollowUpRemindersEnabled));
}
[HttpPut]
public async Task<ActionResult<NotificationSettingsDto>> Put([FromBody] SaveNotificationSettingsRequest request)
{
var user = await users.GetUserAsync(User);
if (user is null) return Unauthorized();
user.EmailFollowUpRemindersEnabled = request.EmailFollowUpRemindersEnabled;
var result = await users.UpdateAsync(user);
if (!result.Succeeded)
return Problem("Notification settings could not be saved.", statusCode: StatusCodes.Status500InternalServerError);
return Ok(new NotificationSettingsDto(user.EmailFollowUpRemindersEnabled));
}
}
@@ -179,6 +179,13 @@ public sealed class UsersController : ControllerBase
if (!removeRoles.Succeeded) return IdentityFailure(removeRoles);
}
// Role claims are embedded in issued JWTs. Without revoking the target user's live
// sessions, a removed Admin role would remain effective until those tokens expired.
// The application DI path always supplies the context; the null branch only supports
// isolated controller construction in older tests.
if (_db is not null && (toAdd.Count > 0 || toRemove.Count > 0))
await SessionRevocation.RevokeAllAsync(_db, u.Id, trustedDeviceHashToKeep: null, cancellationToken);
return NoContent();
}