feat: gate premium CV themes
CI and Deploy / test (push) Successful in 2m35s
CI and Deploy / deploy (push) Successful in 1m1s

This commit is contained in:
cesnimda
2026-07-30 23:24:19 +02:00
parent fd2c60e70f
commit 09fc2b03f7
6 changed files with 49 additions and 12 deletions
@@ -38,8 +38,11 @@ public sealed class CvVariantController : ControllerBase
public sealed record AiAssistResult(string Original, string Result);
[HttpGet("themes")]
public ActionResult<IEnumerable<object>> GetThemes()
public async Task<ActionResult<IEnumerable<object>>> GetThemes()
{
var user = await _users.GetUserAsync(User);
if (user is null) return Unauthorized();
var premiumThemes = AccountPlans.ForRoles(await _users.GetRolesAsync(user)).PremiumThemes;
var themes = CvThemeCatalog.Themes.Select(t => new
{
id = t.Id,
@@ -51,6 +54,8 @@ public sealed class CvVariantController : ControllerBase
photoShape = t.PhotoShape,
supportsIcons = t.DefaultIcons,
atsFriendly = t.AtsFriendly,
premium = t.Premium,
available = premiumThemes || !t.Premium,
swatches = new[] { t.Accent, t.SidebarBg, t.Paper },
});
return Ok(themes);
@@ -81,6 +86,8 @@ public sealed class CvVariantController : ControllerBase
if (user is null) return Unauthorized();
if (request?.Settings is not null && !CvThemeCatalog.Exists(request.Settings.ThemeId))
return BadRequest("Unknown theme.");
if (request?.Settings is not null && !await CanUseThemeAsync(user, request.Settings.ThemeId))
return StatusCode(StatusCodes.Status403Forbidden, "This theme requires Premium.");
var variant = await _variants.CreateAsync(user.Id, request?.Name, request?.JobApplicationId, request?.Settings, ct);
return Ok(ToDto(variant));
}
@@ -101,6 +108,13 @@ public sealed class CvVariantController : ControllerBase
if (user is null) return Unauthorized();
var settings = CvVariantSettingsJson.Normalize(request.Settings);
if (!CvThemeCatalog.Exists(settings.ThemeId)) return BadRequest("Unknown theme.");
if (!await CanUseThemeAsync(user, settings.ThemeId))
{
var current = await _variants.GetAsync(user.Id, id, ct);
if (current is null) return NotFound();
if (!string.Equals(CvVariantSettingsJson.Deserialize(current.SettingsJson).ThemeId, settings.ThemeId, StringComparison.OrdinalIgnoreCase))
return StatusCode(StatusCodes.Status403Forbidden, "This theme requires Premium.");
}
var variant = await _variants.SaveAsync(user.Id, id, request.Name, settings, request.Source ?? "autosave", ct);
return variant is null ? NotFound() : Ok(ToDto(variant));
}
@@ -164,6 +178,7 @@ public sealed class CvVariantController : ControllerBase
var user = await _users.GetUserAsync(User);
if (user is null) return Unauthorized();
var settings = CvVariantSettingsJson.Normalize(request.Settings);
if (!CvThemeCatalog.Exists(settings.ThemeId)) return BadRequest("Unknown theme.");
var render = await _variants.RenderSettingsAsync(user.Id, settings, Person(user), ct);
return Ok(new RenderDto(render.ThemeId, render.Html, render.SuggestedFileName));
}
@@ -222,6 +237,9 @@ public sealed class CvVariantController : ControllerBase
return $"{task} Preserve every factual claim — never invent employers, titles, dates, or metrics. Write in {lang}. Return only the rewritten text with no preamble.{extra}";
}
private async Task<bool> CanUseThemeAsync(ApplicationUser user, string? themeId) =>
CvThemeCatalog.CanUse(themeId, AccountPlans.ForRoles(await _users.GetRolesAsync(user)).PremiumThemes);
private static CvRenderPerson Person(ApplicationUser user)
{
var name = string.Join(" ", new[] { user.FirstName?.Trim(), user.LastName?.Trim() }.Where(x => !string.IsNullOrWhiteSpace(x)));