Add performer public permalink actions
All checks were successful
Dotnet build and test / build (push) Successful in 15m8s
Forgejo Release / release (push) Successful in 8m23s

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-21 17:25:07 +01:00
commit d192705f29
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
5 changed files with 187 additions and 24 deletions

View file

@ -113,6 +113,37 @@ namespace Yavsc.Controllers
return View(podcasts);
}
[AllowAnonymous]
public async Task<ActionResult> PerformerActivity(string id, string activityCode, string? token)
{
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(activityCode)) return NotFound();
if (!TryValidateToken(id, activityCode, token, out var expireAt))
return BadRequest("Ce lien public pour cette activité a expire ou est invalide.");
var performer = await _context.Performers
.Include(p => p.Performer)
.FirstOrDefaultAsync(p => p.PerformerId == id);
var activity = await _context.Activities
.FirstOrDefaultAsync(a => a.Code == activityCode);
if (performer == null || activity == null) return NotFound();
var profile = (await _context.ListPerformersAsync(_billing, activityCode))
.Where(p => string.Equals(p.PerformerId, id, StringComparison.Ordinal))
.ToList();
ViewBag.Activity = activity;
ViewBag.PerformerName = performer.Performer?.UserName ?? performer.PerformerId;
ViewBag.PerformerId = id;
ViewBag.PermalinkExpiry = expireAt;
ViewBag.Permalink = Url.Action(nameof(PerformerActivity), "FrontOffice", new { id, activityCode, token = CreateActivityToken(id, activityCode) }, Request.Scheme, Request.Host.Value);
return View("Profiles", profile);
}
[HttpPost]
[Authorize]
public IActionResult GeneratePerformerPodcastPermalink(string id)
{
@ -124,6 +155,18 @@ namespace Yavsc.Controllers
return RedirectToAction(nameof(PerformerPodcasts), new { id, token });
}
[HttpPost]
[Authorize]
public IActionResult GeneratePerformerActivityPermalink(string id, string activityCode)
{
var currentUserId = User.GetUserId();
if (string.IsNullOrWhiteSpace(currentUserId) || !string.Equals(currentUserId, id, StringComparison.Ordinal))
return Forbid();
var token = CreateActivityToken(id, activityCode);
return RedirectToAction(nameof(PerformerActivity), new { id, activityCode, token });
}
private string CreateToken(string performerId)
{
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
@ -136,20 +179,45 @@ namespace Yavsc.Controllers
return protector.Protect(JsonSerializer.Serialize(payload));
}
private string CreateActivityToken(string performerId, string activityCode)
{
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
var protector = _dataProtectionProvider.CreateProtector("Yavsc.Org.PerformerActivityLink");
var payload = new
{
performerId,
activityCode,
exp = expiresAt.ToUnixTimeSeconds(),
};
return protector.Protect(JsonSerializer.Serialize(payload));
}
private bool TryValidateToken(string performerId, string? token, out DateTimeOffset validUntil)
=> TryValidateTokenInternal(performerId, null, token, out validUntil, "Yavsc.Org.PerformerPodcastLink");
private bool TryValidateToken(string performerId, string activityCode, string? token, out DateTimeOffset validUntil)
=> TryValidateTokenInternal(performerId, activityCode, token, out validUntil, "Yavsc.Org.PerformerActivityLink");
private bool TryValidateTokenInternal(string performerId, string? activityCode, string? token, out DateTimeOffset validUntil, string protectorName)
{
validUntil = DateTimeOffset.MinValue;
if (string.IsNullOrWhiteSpace(token)) return false;
try
{
var protector = _dataProtectionProvider.CreateProtector("Yavsc.Org.PerformerPodcastLink");
var protector = _dataProtectionProvider.CreateProtector(protectorName);
var payload = JsonDocument.Parse(protector.Unprotect(token));
var ownerId = payload.RootElement.GetProperty("performerId").GetString();
var expiry = payload.RootElement.GetProperty("exp").GetInt64();
if (!string.Equals(ownerId, performerId, StringComparison.Ordinal)) return false;
if (!string.IsNullOrWhiteSpace(activityCode))
{
if (!payload.RootElement.TryGetProperty("activityCode", out var activityPayload)) return false;
if (!string.Equals(activityPayload.GetString(), activityCode, StringComparison.Ordinal)) return false;
}
validUntil = DateTimeOffset.FromUnixTimeSeconds(expiry);
return validUntil > DateTimeOffset.UtcNow;
}

View file

@ -2,6 +2,7 @@
@{
ViewBag.Title = $"Podcasts de {ViewBag.PerformerName}";
var performerName = ViewBag.PerformerName ?? "ce performer";
var permalinkExpiry = ViewBag.PermalinkExpiry is DateTimeOffset expiry ? expiry : DateTimeOffset.MinValue;
}
<div class="container py-4">
@ -9,10 +10,11 @@
<div>
<h1 class="h3 mb-1">Découvrez les podcasts de @performerName</h1>
<p class="mb-0 text-muted">
Ce lien public est valable jusqu'au @(((DateTimeOffset)ViewBag.PermalinkExpiry).ToLocalTime().ToString("g")).
Ce lien public est valable jusqu'au @permalinkExpiry.ToLocalTime().ToString("g").
</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary" onclick="copyPermalink()">Copier le permalien</button>
<a class="btn btn-primary" href="https://www.yavsc.fr/" target="_blank" rel="noreferrer">S'inscrire</a>
<a class="btn btn-outline-primary" href="https://postit.yavsc.fr/" target="_blank" rel="noreferrer">Installer PostIt</a>
</div>
@ -57,3 +59,29 @@
</div>
}
</div>
<script>
function copyPermalink() {
const url = '@(ViewBag.Permalink ?? string.Empty)';
if (!url) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).catch(() => fallbackCopy(url));
return;
}
fallbackCopy(url);
}
function fallbackCopy(value) {
const temp = document.createElement('textarea');
temp.value = value;
temp.setAttribute('readonly', '');
temp.style.position = 'fixed';
temp.style.left = '-9999px';
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
document.body.removeChild(temp);
}
</script>

View file

@ -2,10 +2,26 @@
@{
ViewBag.Title = (ViewBag.Activity?.Name ?? "Any") ;
var activityCode = ViewBag.Activity?.Code as string;
var performerId = ViewBag.PerformerId as string;
var permalinkExpiry = ViewBag.PermalinkExpiry is DateTimeOffset expiry ? expiry : DateTimeOffset.MinValue;
}
<h1>@ViewBag.Title</h1>
<em>@ViewBag.Activity.Description</em>
@if (!string.IsNullOrWhiteSpace(performerId) && !string.IsNullOrWhiteSpace(activityCode))
{
<div class="alert alert-info d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mt-3" role="alert">
<div>
<div class="fw-semibold">Permalien public</div>
<small class="text-muted">Valable jusqu'au @permalinkExpiry.ToLocalTime().ToString("g")</small>
</div>
<button type="button" class="btn btn-primary" onclick="copyPermalink()">
Copier le permalien
</button>
</div>
}
@foreach (var profile in Model) {
<hr/>
@Html.DisplayFor(m=>profile)
@ -15,3 +31,29 @@
</a>
}
<script>
function copyPermalink() {
const url = '@(ViewBag.Permalink ?? string.Empty)';
if (!url) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).catch(() => fallbackCopy(url));
return;
}
fallbackCopy(url);
}
function fallbackCopy(value) {
const temp = document.createElement('textarea');
temp.value = value;
temp.setAttribute('readonly', '');
temp.style.position = 'fixed';
temp.style.left = '-9999px';
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
document.body.removeChild(temp);
}
</script>