Add performer podcast public permalink

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-21 17:15:13 +01:00
commit e7e6b375f5
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
3 changed files with 147 additions and 3 deletions

View file

@ -4,6 +4,10 @@
### Added ### Added
* [Yavsc.Org] Permalien public de podcasts performer avec jeton a duree
limitee, page Razor de presentation et bandeau de prospection vers
l'inscription Yavsc et le telechargement de PostIt.
### Changed ### Changed
### Fixed ### Fixed

View file

@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace Yavsc.Controllers namespace Yavsc.Controllers
{ {
@ -10,6 +12,7 @@ namespace Yavsc.Controllers
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Models; using Models;
using ViewModels.FrontOffice; using ViewModels.FrontOffice;
using Yavsc.Abstract.Streaming;
using Yavsc.Server.Helpers; using Yavsc.Server.Helpers;
using Yavsc.Services; using Yavsc.Services;
@ -20,18 +23,24 @@ namespace Yavsc.Controllers
readonly ILogger _logger; readonly ILogger _logger;
readonly IStringLocalizer _SR; readonly IStringLocalizer _SR;
private readonly IBillingService _billing; private readonly IBillingService _billing;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly ILiveProcessor _liveProcessor;
public FrontOfficeController(ApplicationDbContext context, public FrontOfficeController(ApplicationDbContext context,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IBillingService billing, IBillingService billing,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStringLocalizer<FrontOfficeController> SR) IDataProtectionProvider dataProtectionProvider,
ILiveProcessor liveProcessor,
IStringLocalizer<FrontOfficeController> SR)
{ {
_context = context; _context = context;
_userManager = userManager; _userManager = userManager;
_logger = loggerFactory.CreateLogger<FrontOfficeController>(); _logger = loggerFactory.CreateLogger<FrontOfficeController>();
_SR = SR; _SR = SR;
_billing = billing; _billing = billing;
_dataProtectionProvider = dataProtectionProvider;
_liveProcessor = liveProcessor;
} }
public ActionResult Index() public ActionResult Index()
@ -77,5 +86,77 @@ namespace Yavsc.Controllers
return View(result); return View(result);
} }
[AllowAnonymous]
public async Task<ActionResult> PerformerPodcasts(string id, string? token)
{
if (string.IsNullOrWhiteSpace(id)) return NotFound();
if (!TryValidateToken(id, token, out var expireAt))
return BadRequest("Ce lien public de podcast a expire ou est invalide.");
var performer = await _context.Performers
.Include(p => p.Performer)
.FirstOrDefaultAsync(p => p.PerformerId == id);
if (performer == null) return NotFound();
var podcasts = (await _liveProcessor.ListAsync(HttpContext.RequestAborted))
.Where(p => string.Equals(p.OwnerId, id, StringComparison.Ordinal))
.OrderByDescending(p => p.CreatedAt)
.ToArray();
ViewBag.PerformerName = performer.Performer?.UserName ?? performer.PerformerId;
ViewBag.PerformerId = id;
ViewBag.PermalinkExpiry = expireAt;
ViewBag.Permalink = Url.Action(nameof(PerformerPodcasts), "FrontOffice", new { id, token = CreateToken(id) }, Request.Scheme, Request.Host.Value);
return View(podcasts);
}
[Authorize]
public IActionResult GeneratePerformerPodcastPermalink(string id)
{
var currentUserId = User.GetUserId();
if (string.IsNullOrWhiteSpace(currentUserId) || !string.Equals(currentUserId, id, StringComparison.Ordinal))
return Forbid();
var token = CreateToken(id);
return RedirectToAction(nameof(PerformerPodcasts), new { id, token });
}
private string CreateToken(string performerId)
{
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
var protector = _dataProtectionProvider.CreateProtector("Yavsc.Org.PerformerPodcastLink");
var payload = new
{
performerId,
exp = expiresAt.ToUnixTimeSeconds(),
};
return protector.Protect(JsonSerializer.Serialize(payload));
}
private bool TryValidateToken(string performerId, string? token, out DateTimeOffset validUntil)
{
validUntil = DateTimeOffset.MinValue;
if (string.IsNullOrWhiteSpace(token)) return false;
try
{
var protector = _dataProtectionProvider.CreateProtector("Yavsc.Org.PerformerPodcastLink");
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;
validUntil = DateTimeOffset.FromUnixTimeSeconds(expiry);
return validUntil > DateTimeOffset.UtcNow;
}
catch
{
return false;
}
}
} }
} }

View file

@ -0,0 +1,59 @@
@model IReadOnlyList<Yavsc.Abstract.Streaming.PodcastResponse>
@{
ViewBag.Title = $"Podcasts de {ViewBag.PerformerName}";
var performerName = ViewBag.PerformerName ?? "ce performer";
}
<div class="container py-4">
<div class="alert alert-info d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3" role="alert">
<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")).
</p>
</div>
<div class="d-flex gap-2">
<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>
</div>
@if (Model == null || Model.Count == 0)
{
<div class="alert alert-light border">
Aucun podcast public n'est encore disponible pour @performerName.
</div>
}
else
{
<div class="row g-3">
@foreach (var podcast in Model)
{
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<h2 class="h5 mb-1">@podcast.Title</h2>
<small class="text-muted">
@podcast.CreatedAt.ToLocalTime().ToString("g")
</small>
</div>
<span class="badge bg-secondary">@podcast.State</span>
</div>
<p class="mt-3 mb-2 text-muted">
@podcast.DurationSeconds.ToString("0.##") s · @podcast.SegmentCount segments · @((podcast.Bytes / 1024.0 / 1024.0).ToString("0.##")) Mo
</p>
<audio controls class="w-100 mt-2" preload="metadata">
<source src="@Url.Action("Playlist", "Podcast", new { id = podcast.Id })" type="application/vnd.apple.mpegurl" />
Votre navigateur ne prend pas en charge la lecture audio.
</audio>
</div>
</div>
</div>
}
</div>
}
</div>