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:
@@ -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("\"", "\\\"") + '"';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user