139 lines
5.6 KiB
C#
139 lines
5.6 KiB
C#
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
|
|
{
|
|
[ApiController]
|
|
[Route("api/export")]
|
|
[Authorize(AuthenticationSchemes = "local")]
|
|
public class ExportController : ControllerBase
|
|
{
|
|
private readonly JobTrackerContext _db;
|
|
private readonly AccountDataExportService? _accountExport;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
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")]
|
|
public async Task<IActionResult> ExportJobs(
|
|
[FromQuery] string format = "json",
|
|
[FromQuery] bool includeDeleted = false,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
var query = _db.JobApplications
|
|
.AsNoTracking()
|
|
.Include(j => j.Company)
|
|
.AsQueryable();
|
|
|
|
if (!includeDeleted) query = query.Where(j => !j.IsDeleted);
|
|
|
|
var jobs = await query
|
|
.OrderByDescending(j => j.DateApplied)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
var stamp = DateTime.Now.ToString("yyyy-MM-dd");
|
|
|
|
if (string.Equals(format, "csv", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
static string Esc(string? s)
|
|
{
|
|
s ??= "";
|
|
var needs = s.Contains(',') || s.Contains('"') || s.Contains('\n') || s.Contains('\r');
|
|
var q = s.Replace("\"", "\"\"");
|
|
return needs ? $"\"{q}\"" : q;
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine(string.Join(",",
|
|
"Company",
|
|
"CompanyLocation",
|
|
"CompanySource",
|
|
"JobTitle",
|
|
"Status",
|
|
"DateApplied",
|
|
"Location",
|
|
"Salary",
|
|
"SalaryMin",
|
|
"SalaryMax",
|
|
"SalaryCurrency",
|
|
"SalaryPeriod",
|
|
"NextAction",
|
|
"FollowUpAt",
|
|
"JobUrl",
|
|
"Notes",
|
|
"CoverLetterText"
|
|
));
|
|
|
|
foreach (var j in jobs)
|
|
{
|
|
sb.AppendLine(string.Join(",",
|
|
Esc(j.Company?.Name),
|
|
Esc(j.Company?.Location),
|
|
Esc(j.Company?.Source),
|
|
Esc(j.JobTitle),
|
|
Esc(j.Status),
|
|
Esc(j.DateApplied?.ToString("o")),
|
|
Esc(j.Location),
|
|
Esc(j.Salary),
|
|
Esc(j.SalaryMin?.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
|
Esc(j.SalaryMax?.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
|
Esc(j.SalaryCurrency),
|
|
Esc(j.SalaryPeriod),
|
|
Esc(j.NextAction),
|
|
Esc(j.FollowUpAt?.ToString("o")),
|
|
Esc(j.JobUrl),
|
|
Esc(j.Notes),
|
|
Esc(j.CoverLetterText)
|
|
));
|
|
}
|
|
|
|
return File(Encoding.UTF8.GetBytes(sb.ToString()), "text/csv", $"job-tracker-export-{stamp}.csv");
|
|
}
|
|
|
|
return File(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(jobs), "application/json", $"job-tracker-export-{stamp}.json");
|
|
}
|
|
}
|
|
}
|
|
|