137 lines
6.3 KiB
C#
137 lines
6.3 KiB
C#
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JobTrackerApi.Controllers;
|
|
|
|
// Phase 5.4 — Application Assets. Connects existing career outputs to one application.
|
|
//
|
|
// Deliberately thin: CV variant CRUD, preview, PDF export, themes and version history all stay on
|
|
// /api/cv (CvVariantController), and documents stay on /api/attachments. These routes only cover what
|
|
// is genuinely application-scoped — which variant this application uses, what to tailor, and the
|
|
// cover letter with its history. Every route is tenant-scoped and returns 404 for another user's
|
|
// application. docs/architecture/application-workspace.md.
|
|
[ApiController]
|
|
[Route("api/jobapplications/{jobId:int}")]
|
|
[Authorize(AuthenticationSchemes = "local")]
|
|
public sealed class ApplicationAssetsController : ControllerBase
|
|
{
|
|
public sealed record AttachVariantRequest(int? VariantId);
|
|
public sealed record SaveCoverLetterRequest(string? Text, string? Source, string? AiAction);
|
|
|
|
private readonly UserManager<ApplicationUser> _users;
|
|
private readonly IApplicationAssetsService _assets;
|
|
private readonly ISubmittedApplicationPackageService _packages;
|
|
|
|
public ApplicationAssetsController(UserManager<ApplicationUser> users, IApplicationAssetsService assets, ISubmittedApplicationPackageService packages)
|
|
{
|
|
_users = users;
|
|
_assets = assets;
|
|
_packages = packages;
|
|
}
|
|
|
|
[HttpGet("cv")]
|
|
public async Task<ActionResult<ApplicationCvDto>> GetCv(int jobId, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _assets.GetCvAsync(userId, jobId, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpPut("cv")]
|
|
public async Task<ActionResult<ApplicationCvDto>> AttachVariant(int jobId, [FromBody] AttachVariantRequest request, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _assets.AttachVariantAsync(userId, jobId, request?.VariantId, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpGet("tailoring")]
|
|
public async Task<ActionResult<TailoringPlanDto>> GetTailoring(int jobId, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _assets.GetTailoringPlanAsync(userId, jobId, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpGet("cover-letter")]
|
|
public async Task<ActionResult<CoverLetterDto>> GetCoverLetter(int jobId, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _assets.GetCoverLetterAsync(userId, jobId, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpPut("cover-letter")]
|
|
public async Task<ActionResult<CoverLetterDto>> SaveCoverLetter(int jobId, [FromBody] SaveCoverLetterRequest request, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _assets.SaveCoverLetterAsync(
|
|
userId, jobId, request?.Text, request?.Source ?? CoverLetterSources.Manual, request?.AiAction, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpPost("cover-letter/versions/{version:int}/restore")]
|
|
public async Task<ActionResult<CoverLetterDto>> RestoreCoverLetter(int jobId, int version, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _assets.RestoreCoverLetterAsync(userId, jobId, version, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpGet("submitted-packages")]
|
|
public async Task<ActionResult<IReadOnlyList<SubmittedPackageDto>>> ListSubmittedPackages(int jobId, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _packages.ListAsync(userId, jobId, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpPost("submitted-packages")]
|
|
public async Task<ActionResult<SubmittedPackageDto>> CaptureSubmittedPackage(int jobId, CancellationToken ct)
|
|
{
|
|
var user = await _users.GetUserAsync(User);
|
|
if (user is null) return Unauthorized();
|
|
var result = await _packages.CaptureAsync(user.Id, jobId, Person(user), ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpGet("submitted-packages/{packageId:int}")]
|
|
public async Task<ActionResult<SubmittedPackageDetailDto>> GetSubmittedPackage(int jobId, int packageId, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var result = await _packages.GetAsync(userId, jobId, packageId, ct);
|
|
return result is null ? NotFound() : Ok(result);
|
|
}
|
|
|
|
[HttpGet("submitted-packages/{packageId:int}/attachments/{attachmentId:int}")]
|
|
public async Task<IActionResult> DownloadSubmittedAttachment(int jobId, int packageId, int attachmentId, CancellationToken ct)
|
|
{
|
|
var userId = await CurrentUserIdAsync();
|
|
if (userId is null) return Unauthorized();
|
|
var file = await _packages.GetAttachmentAsync(userId, jobId, packageId, attachmentId, ct);
|
|
return file is null ? NotFound() : PhysicalFile(file.Path, file.ContentType, file.FileName);
|
|
}
|
|
|
|
private async Task<string?> CurrentUserIdAsync() => (await _users.GetUserAsync(User))?.Id;
|
|
|
|
private static CvRenderPerson Person(ApplicationUser user)
|
|
{
|
|
var name = string.Join(" ", new[] { user.FirstName?.Trim(), user.LastName?.Trim() }.Where(x => !string.IsNullOrWhiteSpace(x)));
|
|
if (string.IsNullOrWhiteSpace(name)) name = user.DisplayName?.Trim();
|
|
if (string.IsNullOrWhiteSpace(name)) name = user.UserName?.Trim();
|
|
if (string.IsNullOrWhiteSpace(name)) name = user.Email?.Trim();
|
|
return new CvRenderPerson(string.IsNullOrWhiteSpace(name) ? "Your Name" : name, AvatarStorage.Resolve(user.AvatarImageDataUrl));
|
|
}
|
|
}
|