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 _logger; private readonly IDataProtector _protector; public BackupController(JobTrackerContext db, ILogger 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 Encrypted(CancellationToken cancellationToken) { 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 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 }; } } }