From fd2c60e70f46fead78c4ca6dab82918ae5377dcb Mon Sep 17 00:00:00 2001 From: cesnimda Date: Thu, 30 Jul 2026 23:19:17 +0200 Subject: [PATCH] feat: complete public CV sharing --- JobTrackerApi.Tests/CvBuilderTests.cs | 17 +++++++++++++++++ JobTrackerApi/Services/CvVariantService.cs | 1 + docs/implementation-roadmap.md | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/JobTrackerApi.Tests/CvBuilderTests.cs b/JobTrackerApi.Tests/CvBuilderTests.cs index b7e6917..6a88022 100644 --- a/JobTrackerApi.Tests/CvBuilderTests.cs +++ b/JobTrackerApi.Tests/CvBuilderTests.cs @@ -280,4 +280,21 @@ public sealed class CvBuilderTests var v = await svc.CreateAsync("user-1", "CV", null, null, default); Assert.Null(await svc.GetPublicOwnerAsync(v.PublicSlug, default)); } + + [Fact] + public async Task Re_enabling_public_access_rotates_the_revoked_link() + { + var (db, svc) = NewService("user-1"); + await using var _ = db; + var v = await svc.CreateAsync("user-1", "CV", null, null, default); + + await svc.SetPublicAsync("user-1", v.Id, true, default); + var revokedSlug = v.PublicSlug; + await svc.SetPublicAsync("user-1", v.Id, false, default); + await svc.SetPublicAsync("user-1", v.Id, true, default); + + Assert.NotEqual(revokedSlug, v.PublicSlug); + Assert.Null(await svc.GetPublicOwnerAsync(revokedSlug, default)); + Assert.Equal("user-1", await svc.GetPublicOwnerAsync(v.PublicSlug, default)); + } } diff --git a/JobTrackerApi/Services/CvVariantService.cs b/JobTrackerApi/Services/CvVariantService.cs index 83ccc91..3a28e5f 100644 --- a/JobTrackerApi/Services/CvVariantService.cs +++ b/JobTrackerApi/Services/CvVariantService.cs @@ -96,6 +96,7 @@ public sealed class CvVariantService : ICvVariantService { var variant = await GetAsync(ownerUserId, id, ct); if (variant is null) return null; + if (isPublic && !variant.IsPublic) variant.PublicSlug = NewSlug(); variant.IsPublic = isPublic; variant.UpdatedAtUtc = DateTimeOffset.UtcNow; await _db.SaveChangesAsync(ct); diff --git a/docs/implementation-roadmap.md b/docs/implementation-roadmap.md index 330b2e1..e96f53a 100644 --- a/docs/implementation-roadmap.md +++ b/docs/implementation-roadmap.md @@ -194,7 +194,7 @@ Goal: commercialise. Last, per the guide's "do not over-engineer before needed. | 7.3 | **DONE (2026-07-30)** — existing AI interaction metering now enforces monthly generation limits: 25 for free accounts and 250 for Premium/Admin. Usage responses expose the active plan and limit. | **P3** | **M** | 5.2, 7.2 | Cost-bearing AI now has a clear monthly ceiling before registration opens. | | 7.4 | **DONE (2026-07-30)** — attachment uploads enforce total per-user storage entitlements (250 MB free, 5 GB Premium/Admin) in addition to the existing 10 MB per-file cap. | **P3** | **S** | 7.2 | Storage limits match the exposed capability model. | | 7.5 | **Stripe billing** | **P3** | **L** | 7.2 | Still blocked on **Stripe keys** — the only remaining hard blocker. Tiers are now decided. | -| 7.6 | **Public CV** (`/cv/{guid}`) | **P3** | **M** | 3.4, 4.2 | Documented in `docs/00-ai-context.md`; **zero code** — no route, no `IsPublic`, no slug. Privacy-first random GUID, no usernames. | +| 7.6 | ✅ **DONE (2026-07-30)** — public CV (`/cv/{guid}`), privacy-first random links, revoke/rotate sharing | **P3** | **M** | 3.4, 4.2 | Anonymous rendering is isolated behind an explicit public flag, served with `noindex`, and revoked links cannot be restored accidentally. | | 7.7 | **Premium themes** | **P3** | **S** | 4.3, 7.2 | Trivial once themes are data. Impossible while they are C# methods. A decided premium lever. | | 7.8 | **DONE (2026-07-30)** — CI runs NuGet transitive vulnerability reporting and a production-only npm audit. The npm audit reports the existing no-fix advisory baseline without blocking unrelated deploys. | **P2** | **S** | none | Vulnerable dependencies are now visible before deployment. | | 7.9 | **Per-user AI provider cost controls** | **P3** | **S** | 5.2, 7.2 | With `AI_PROVIDER=gemini` the "advanced AI" tier spends real money per call. Metering (5.2) measures; this enforces. |