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
@@ -315,7 +315,7 @@ public sealed partial class ProfileCvController : ControllerBase
[NonAction]
public async Task ProcessQueuedRunAsync(int runId, CancellationToken cancellationToken)
{
var run = await _db.CvExtractionRuns.FirstOrDefaultAsync(x => x.Id == runId, cancellationToken);
var run = await _db.CvExtractionRuns.IgnoreQueryFilters().FirstOrDefaultAsync(x => x.Id == runId, cancellationToken);
if (run is null) return;
var user = await _users.FindByIdAsync(run.OwnerUserId);
if (user is null)
@@ -367,7 +367,7 @@ public sealed partial class ProfileCvController : ControllerBase
}
case "reprocess":
{
var artifact = await _db.CvUploadArtifacts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == run.ArtifactId && x.OwnerUserId == user.Id, cancellationToken);
var artifact = await _db.CvUploadArtifacts.IgnoreQueryFilters().AsNoTracking().FirstOrDefaultAsync(x => x.Id == run.ArtifactId && x.OwnerUserId == user.Id, cancellationToken);
if (artifact is null) throw new InvalidOperationException("Upload a CV before reprocessing it.");
if (string.IsNullOrWhiteSpace(artifact.StoragePath) || !System.IO.File.Exists(artifact.StoragePath))
{
@@ -416,14 +416,46 @@ public sealed partial class ProfileCvController : ControllerBase
private async Task PruneExtractionRunsAsync(string ownerUserId, CancellationToken cancellationToken)
{
var expired = await _db.CvExtractionRuns
var expired = await _db.CvExtractionRuns.IgnoreQueryFilters()
.Where(x => x.OwnerUserId == ownerUserId && x.Status != "queued" && x.Status != "running")
.OrderByDescending(x => x.StartedAtUtc)
.Skip(ExtractionRunRetentionCount)
.ToListAsync(cancellationToken);
if (expired.Count == 0) return;
_db.CvExtractionRuns.RemoveRange(expired);
if (expired.Count > 0)
{
_db.CvExtractionRuns.RemoveRange(expired);
await _db.SaveChangesAsync(cancellationToken);
}
var referencedArtifactIds = await _db.CvExtractionRuns.IgnoreQueryFilters()
.Where(x => x.OwnerUserId == ownerUserId && x.ArtifactId != null)
.Select(x => x.ArtifactId!.Value)
.Distinct()
.ToListAsync(cancellationToken);
var user = await _users.FindByIdAsync(ownerUserId);
if (user?.CurrentCvUploadArtifactId is int currentArtifactId)
{
referencedArtifactIds.Add(currentArtifactId);
}
var orphanedArtifacts = await _db.CvUploadArtifacts.IgnoreQueryFilters()
.Where(x => x.OwnerUserId == ownerUserId && !referencedArtifactIds.Contains(x.Id))
.ToListAsync(cancellationToken);
if (orphanedArtifacts.Count == 0) return;
_db.CvUploadArtifacts.RemoveRange(orphanedArtifacts);
await _db.SaveChangesAsync(cancellationToken);
foreach (var artifact in orphanedArtifacts)
{
try
{
if (!string.IsNullOrWhiteSpace(artifact.StoragePath)) System.IO.File.Delete(artifact.StoragePath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not delete unreferenced CV artifact {ArtifactId} at {Path}", artifact.Id, artifact.StoragePath);
}
}
}
private async Task SendRunCompletionEmailAsync(ApplicationUser user, CvExtractionRun run, bool success, CancellationToken cancellationToken)