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; public ApplicationAssetsController(UserManager users, IApplicationAssetsService assets) { _users = users; _assets = assets; } [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); } private async Task CurrentUserIdAsync() => (await _users.GetUserAsync(User))?.Id; }