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,4 +1,5 @@
using System.Diagnostics;
using System.Globalization;
using System.Text;
namespace JobTrackerApi.Services;
@@ -26,16 +27,19 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
private readonly AppPaths _paths;
private readonly ILogger<PlaywrightCvPdfExporter> _logger;
private readonly int _retentionDays;
public PlaywrightCvPdfExporter(AppPaths paths, ILogger<PlaywrightCvPdfExporter> logger)
public PlaywrightCvPdfExporter(AppPaths paths, ILogger<PlaywrightCvPdfExporter> logger, IConfiguration configuration)
{
_paths = paths;
_logger = logger;
_retentionDays = Math.Clamp(configuration.GetValue("CvExports:RetainDays", 30), 1, 365);
}
public async Task<CvPdfArtifact> ExportAsync(TailoredCvRenderResult renderResult, CancellationToken cancellationToken)
{
var now = DateTimeOffset.UtcNow;
PruneExpiredExports(DateOnly.FromDateTime(now.UtcDateTime).AddDays(-_retentionDays));
var folder = Path.Combine(_paths.CvExportsRoot, now.ToString("yyyyMMdd"));
Directory.CreateDirectory(folder);
@@ -46,10 +50,8 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
var tempRoot = Path.Combine(Path.GetTempPath(), "jobtracker-cv-pdf", Guid.NewGuid().ToString("n"));
var htmlPath = Path.Combine(tempRoot, "document.html");
var userDataDir = Path.Combine(tempRoot, "profile");
Directory.CreateDirectory(tempRoot);
Directory.CreateDirectory(userDataDir);
try
{
@@ -61,10 +63,12 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
throw new InvalidOperationException("CV PDF export is unavailable. Install Chromium/Google Chrome or set CV_PDF_BROWSER_PATH.");
}
var arguments = BuildArguments(userDataDir, storagePath, htmlPath);
var startInfo = new ProcessStartInfo();
startInfo.FileName = browserPath;
startInfo.Arguments = arguments;
foreach (var argument in BuildArguments(storagePath, htmlPath))
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.UseShellExecute = false;
@@ -73,7 +77,14 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
using var process = new Process();
process.StartInfo = startInfo;
process.Start();
await process.WaitForExitAsync(cancellationToken);
try
{
await process.WaitForExitAsync(cancellationToken).WaitAsync(TimeSpan.FromSeconds(60), cancellationToken);
}
finally
{
if (!process.HasExited) process.Kill(entireProcessTree: true);
}
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
@@ -102,9 +113,27 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
}
}
private static string BuildArguments(string userDataDir, string storagePath, string htmlPath)
private void PruneExpiredExports(DateOnly cutoff)
{
var parts = new List<string>
foreach (var directory in Directory.EnumerateDirectories(_paths.CvExportsRoot))
{
var name = Path.GetFileName(directory);
if (!DateOnly.TryParseExact(name, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) || date >= cutoff) continue;
try
{
Directory.Delete(directory, recursive: true);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not prune expired CV export directory {Directory}", directory);
}
}
}
private static IReadOnlyList<string> BuildArguments(string storagePath, string htmlPath)
{
return new[]
{
"--headless=new",
"--disable-gpu",
@@ -112,12 +141,9 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
"--disable-dev-shm-usage",
"--allow-file-access-from-files",
"--enable-local-file-accesses",
"--user-data-dir=" + Quote(userDataDir),
"--print-to-pdf=" + Quote(storagePath),
Quote(htmlPath)
$"--print-to-pdf={storagePath}",
new Uri(htmlPath).AbsoluteUri
};
return string.Join(' ', parts);
}
private static string? ResolveBrowserPath()
@@ -171,8 +197,4 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter
}
}
private static string Quote(string value)
{
return '"' + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + '"';
}
}