using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; namespace JobTrackerApi.Controllers; // Anonymous read-only surface for public CVs: /cv/{slug} in the SPA fetches this. Only variants the // owner has explicitly toggled public are served; noindex by default. docs/architecture/cv-builder.md. [ApiController] [Route("api/public-cv")] [AllowAnonymous] public sealed class PublicCvController : ControllerBase { private readonly UserManager _users; private readonly ICvVariantService _variants; private readonly ICvPdfExporter _pdf; public PublicCvController(UserManager users, ICvVariantService variants, ICvPdfExporter pdf) { _users = users; _variants = variants; _pdf = pdf; } [HttpGet("{slug}")] public async Task Get(string slug, CancellationToken ct) { var ownerId = await _variants.GetPublicOwnerAsync(slug, ct); if (ownerId is null) return NotFound(); var owner = await _users.FindByIdAsync(ownerId); var person = Person(owner); var result = await _variants.RenderPublicAsync(slug, person, ct); if (result is null) return NotFound(); Response.Headers["X-Robots-Tag"] = "noindex, nofollow"; return Ok(new { html = result.Value.render.Html, name = person.FallbackName }); } [HttpGet("{slug}/pdf")] [EnableRateLimiting("public-pdf")] public async Task DownloadPdf(string slug, CancellationToken ct) { var ownerId = await _variants.GetPublicOwnerAsync(slug, ct); if (ownerId is null) return NotFound(); var person = Person(await _users.FindByIdAsync(ownerId)); var result = await _variants.RenderPublicAsync(slug, person, ct); if (result is null) return NotFound(); var render = result.Value.render; var artifact = await _pdf.ExportAsync(new TailoredCvRenderResult(render.ThemeId, render.SuggestedFileName, render.Html), ct); return File(artifact.Bytes, "application/pdf", artifact.FileName); } private static CvRenderPerson Person(ApplicationUser? user) { if (user is null) return new CvRenderPerson("Candidate", null); 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 = "Candidate"; return new CvRenderPerson(name!, AvatarStorage.Resolve(user.AvatarImageDataUrl)); } }