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 _users; private readonly IApplicationAssetsService _assets; private readonly ISubmittedApplicationPackageService _packages; public ApplicationAssetsController(UserManager users, IApplicationAssetsService assets, ISubmittedApplicationPackageService packages) { _users = users; _assets = assets; _packages = packages; } [HttpGet("cv")] public async Task> 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> 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> 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> 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> 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> 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>> 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> 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> 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 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 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)); } }