Files
jobtrackingapp/JobTrackerApi/Controllers/BackupController.cs
T
2026-03-21 11:55:27 +01:00

93 lines
3.6 KiB
C#

using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Data;
namespace JobTrackerApi.Controllers
{
[ApiController]
[Route("api/backup")]
public class BackupController : ControllerBase
{
private readonly JobTrackerContext _db;
private readonly ILogger<BackupController> _logger;
private readonly IDataProtector _protector;
public BackupController(JobTrackerContext db, ILogger<BackupController> logger, IDataProtectionProvider dp)
{
_db = db;
_logger = logger;
_protector = dp.CreateProtector("JobTrackerApi.Backup.v1");
}
public sealed record BackupEnvelope(
string Version,
DateTime CreatedAt,
object Data
);
[HttpPost("encrypted")]
public async Task<IActionResult> Encrypted(CancellationToken cancellationToken)
{
if (!OperatingSystem.IsWindows())
{
return StatusCode(501, "Encrypted backups are only implemented for Windows (DPAPI) in this build.");
}
var data = await BuildExport(cancellationToken);
var envelope = new BackupEnvelope("jtbackup.v1", DateTime.Now, data);
var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(
envelope,
new JsonSerializerOptions { WriteIndented = true }
);
// Data Protection encrypts payload using the app's key ring.
// On Windows, keys are encrypted at rest for the current user.
var protectedText = _protector.Protect(Convert.ToBase64String(jsonBytes));
var cipher = Encoding.UTF8.GetBytes(protectedText);
var fileName = $"jobtracker_backup_{DateTime.Now:yyyyMMdd_HHmmss}.jtbackup";
_logger.LogInformation("Generated encrypted backup {FileName} ({Bytes} bytes).", fileName, cipher.Length);
return File(cipher, "application/octet-stream", fileName);
}
private async Task<object> BuildExport(CancellationToken cancellationToken)
{
// Avoid navigation cycles by exporting as plain graphs / DTOs.
var companies = await _db.Companies.AsNoTracking().OrderBy(c => c.Name).ToListAsync(cancellationToken);
var jobs = await _db.JobApplications.AsNoTracking().OrderByDescending(j => j.DateApplied).ToListAsync(cancellationToken);
var jobIds = jobs.Select(j => j.Id).ToList();
var correspondence = await _db.Correspondences.AsNoTracking()
.Where(c => jobIds.Contains(c.JobApplicationId))
.OrderBy(c => c.Date)
.ToListAsync(cancellationToken);
var attachments = await _db.Attachments.AsNoTracking()
.Where(a => jobIds.Contains(a.JobApplicationId))
.OrderBy(a => a.UploadDate)
.ToListAsync(cancellationToken);
var events = await _db.JobEvents.AsNoTracking()
.Where(e => jobIds.Contains(e.JobApplicationId))
.OrderBy(e => e.At)
.ToListAsync(cancellationToken);
var rules = await _db.RuleSettings.AsNoTracking().FirstOrDefaultAsync(cancellationToken);
return new
{
Companies = companies,
JobApplications = jobs,
Correspondence = correspondence,
Attachments = attachments,
Events = events,
Rules = rules
};
}
}
}