First Commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user