Files
jobtrackingapp/JobTrackerApi/Controllers/BackupController.cs
T

146 lines
6.7 KiB
C#

using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
namespace JobTrackerApi.Controllers
{
[ApiController]
[Route("api/backup")]
[Authorize(AuthenticationSchemes = "local")]
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)
{
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 coverLetterVersions = await _db.CoverLetterVersions.AsNoTracking()
.Where(version => jobIds.Contains(version.JobApplicationId))
.OrderBy(version => version.JobApplicationId).ThenBy(version => version.Version)
.Select(version => new
{
version.Id, version.OwnerUserId, version.JobApplicationId, version.Version,
version.Text, version.Source, version.AiAction, version.CreatedAtUtc, version.DocumentSettingsJson,
})
.ToListAsync(cancellationToken);
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 emailSendAttempts = await _db.EmailSendAttempts.AsNoTracking()
.Where(attempt => jobIds.Contains(attempt.JobApplicationId))
.OrderBy(attempt => attempt.CreatedAtUtc)
.Select(attempt => new EmailSendAttemptExport(
attempt.Id,
attempt.JobApplicationId,
attempt.Provider,
attempt.ClientRequestId,
attempt.Status,
attempt.ProviderMessageId,
attempt.FailureCategory,
attempt.CreatedAtUtc,
attempt.StartedAtUtc,
attempt.CompletedAtUtc))
.ToListAsync(cancellationToken);
var emailDrafts = await _db.EmailDrafts.AsNoTracking()
.Where(draft => jobIds.Contains(draft.JobApplicationId))
.OrderBy(draft => draft.UpdatedAtUtc)
.Select(draft => new EmailDraftExport(
draft.Id,
draft.JobApplicationId,
draft.Provider,
draft.To,
draft.Subject,
draft.BodyText,
draft.ThreadId,
draft.ClientRequestId,
draft.Revision,
draft.CreatedAtUtc,
draft.UpdatedAtUtc))
.ToListAsync(cancellationToken);
var rules = await _db.RuleSettings.AsNoTracking().FirstOrDefaultAsync(cancellationToken);
return new
{
Companies = companies,
JobApplications = jobs,
CoverLetterVersions = coverLetterVersions,
SubmittedApplicationPackages = await _db.SubmittedApplicationPackages.AsNoTracking()
.Where(package => jobIds.Contains(package.JobApplicationId))
.OrderBy(package => package.Id)
.Select(package => new
{
package.Id, package.OwnerUserId, package.JobApplicationId, package.Version,
package.CvVariantId, package.CvVariantName, package.CvVariantVersion, package.CvThemeId,
package.CvSettingsJson, package.CareerProfileJson, package.RenderedCvHtml,
package.CoverLetterText, package.CoverLetterDocumentSettingsJson, package.RenderedCoverLetterHtml,
package.ApplicationMaterialJson, package.CreatedAtUtc,
Attachments = package.Attachments.Select(file => new { file.Id, file.FileName, file.FileType, file.Purpose, file.FileSize, file.Sha256 }),
}).ToListAsync(cancellationToken),
Correspondence = correspondence,
Attachments = attachments,
Events = events,
EmailSendAttempts = emailSendAttempts,
EmailDrafts = emailDrafts,
Rules = rules
};
}
}
}