feat: complete release readiness work

- consolidate API ownership and remove dead vendor code

- add Stripe billing, learning paths, and public CV hardening

- add migration, recovery, security, audit, and browser gates
This commit is contained in:
cesnimda
2026-07-31 16:54:16 +02:00
parent a23c3dfc97
commit ce76046a29
1634 changed files with 6889 additions and 135429 deletions
@@ -1,6 +1,8 @@
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using System.Text;
namespace JobTrackerApi.Services;
@@ -28,6 +30,8 @@ public sealed record ChecklistProgressDto(int Total, int Completed, int Dismisse
public sealed record ChecklistDto(IReadOnlyList<ChecklistItemDto> Items, ChecklistProgressDto Progress);
public sealed record LearningRecommendationDto(int Id, string Keyword, string Status);
public sealed record ChecklistItemInput(string? Title, string? Description, string? Category, string? Status, string? Section);
// The signals a checklist item can auto-complete from. Computed once per read.
@@ -86,10 +90,13 @@ public interface IApplicationChecklistService
Task<ChecklistItemDto?> UpdateAsync(string ownerUserId, int jobApplicationId, int itemId, ChecklistItemInput input, CancellationToken ct);
Task<bool> DeleteAsync(string ownerUserId, int jobApplicationId, int itemId, CancellationToken ct);
Task<ChecklistDto?> ReorderAsync(string ownerUserId, int jobApplicationId, IReadOnlyList<int> orderedIds, CancellationToken ct);
Task<IReadOnlyList<LearningRecommendationDto>> SyncLearningRecommendationsAsync(
string ownerUserId, int jobApplicationId, IReadOnlyList<string> missingKeywords, CancellationToken ct);
}
public sealed class ApplicationChecklistService : IApplicationChecklistService
{
private const string LearningKeyPrefix = "learning:";
// The default system checklist. Stable keys — renaming a title must never orphan a user's item.
private sealed record Template(string Key, string Title, string Description, string Category, string? Signal, string? Section);
@@ -246,6 +253,83 @@ public sealed class ApplicationChecklistService : IApplicationChecklistService
return Project(items);
}
public async Task<IReadOnlyList<LearningRecommendationDto>> SyncLearningRecommendationsAsync(
string ownerUserId, int jobApplicationId, IReadOnlyList<string> missingKeywords, CancellationToken ct)
{
if (!await _db.JobApplications.AsNoTracking()
.AnyAsync(job => job.Id == jobApplicationId && job.OwnerUserId == ownerUserId, ct))
{
return [];
}
var keywords = missingKeywords
.Where(keyword => !string.IsNullOrWhiteSpace(keyword))
.Select(keyword => keyword.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var items = await _db.ApplicationChecklistItems
.Where(item => item.OwnerUserId == ownerUserId
&& item.JobApplicationId == jobApplicationId
&& item.SystemKey != null
&& item.SystemKey.StartsWith(LearningKeyPrefix))
.ToListAsync(ct);
var byKey = items.ToDictionary(item => item.SystemKey!, StringComparer.OrdinalIgnoreCase);
var activeKeys = keywords.Select(LearningKey).ToHashSet(StringComparer.OrdinalIgnoreCase);
var changed = false;
foreach (var keyword in keywords)
{
var key = LearningKey(keyword);
if (!byKey.TryGetValue(key, out var item))
{
item = new ApplicationChecklistItem
{
OwnerUserId = ownerUserId,
JobApplicationId = jobApplicationId,
SystemKey = key,
Title = keyword,
Description = $"Build or verify evidence for {keyword} before claiming it in an application.",
Category = ChecklistCategories.Custom,
Status = ChecklistStatuses.Pending,
Section = "match",
SortOrder = items.Count,
IsSystemGenerated = true,
};
_db.ApplicationChecklistItems.Add(item);
items.Add(item);
byKey[key] = item;
changed = true;
}
else if (item.Status == ChecklistStatuses.Done && item.IsAutoCompleted)
{
item.Status = ChecklistStatuses.Pending;
item.IsAutoCompleted = false;
item.CompletedAt = null;
item.UpdatedAtUtc = DateTimeOffset.UtcNow;
changed = true;
}
}
foreach (var item in items.Where(item => !activeKeys.Contains(item.SystemKey!)
&& item.Status == ChecklistStatuses.Pending))
{
item.Status = ChecklistStatuses.Done;
item.IsAutoCompleted = true;
item.CompletedAt = DateTimeOffset.UtcNow;
item.UpdatedAtUtc = DateTimeOffset.UtcNow;
changed = true;
}
if (changed) await _db.SaveChangesAsync(ct);
return keywords.Select(keyword => byKey[LearningKey(keyword)])
.Select(item => new LearningRecommendationDto(item.Id, item.Title, item.Status))
.ToList();
}
private static string LearningKey(string keyword) => LearningKeyPrefix
+ Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(keyword.Trim().ToLowerInvariant())))[..40];
// The next unfinished step, by category priority then the user's own ordering. This is what
// ApplicationWorkspaceService surfaces as "what do I do next" — one source, not a parallel ruleset.
public static ChecklistItemDto? NextPending(ChecklistDto checklist) =>