feat(export): add readable account archive
CI and Deploy / test (pull_request) Successful in 5m13s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 18:36:40 +02:00
parent cdcc7163fa
commit 1ec9dd037e
15 changed files with 864 additions and 35 deletions
+37 -1
View File
@@ -1,8 +1,11 @@
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Data;
using JobTrackerApi.Services;
using System.Security.Claims;
namespace JobTrackerApi.Controllers
{
@@ -12,10 +15,43 @@ namespace JobTrackerApi.Controllers
public class ExportController : ControllerBase
{
private readonly JobTrackerContext _db;
private readonly AccountDataExportService? _accountExport;
private readonly TimeProvider _timeProvider;
public ExportController(JobTrackerContext db)
public ExportController(JobTrackerContext db, AccountDataExportService? accountExport = null, TimeProvider? timeProvider = null)
{
_db = db;
_accountExport = accountExport;
_timeProvider = timeProvider ?? TimeProvider.System;
}
[HttpPost("account")]
[EnableRateLimiting("account-data")]
public async Task<IActionResult> ExportAccount(CancellationToken cancellationToken)
{
var ownerUserId = LocalAuthIdentity.GetRequiredUserId(User);
if (ownerUserId is null) return Unauthorized();
if (_accountExport is null) throw new InvalidOperationException("Account export is not configured.");
var sessionId = User.FindFirstValue("sid");
var recentCutoff = _timeProvider.GetUtcNow().AddMinutes(-15);
var currentSession = string.IsNullOrWhiteSpace(sessionId)
? null
: await _db.UserSessions.IgnoreQueryFilters().AsNoTracking()
.FirstOrDefaultAsync(item => item.Id == sessionId && item.UserId == ownerUserId, cancellationToken);
var recentlyAuthenticated = currentSession is { RevokedAtUtc: null } && currentSession.CreatedAtUtc >= recentCutoff;
if (!recentlyAuthenticated)
{
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetails
{
Title = "Recent sign-in required",
Detail = "Sign in again before downloading a complete account export.",
});
}
var artifact = await _accountExport.CreateAsync(ownerUserId, cancellationToken);
var stream = new FileStream(artifact.StoragePath, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, FileOptions.Asynchronous | FileOptions.DeleteOnClose);
return File(stream, "application/zip", artifact.DownloadFileName);
}
[HttpGet("jobs")]