Add performer public permalink actions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e7e6b375f5
commit
d192705f29
5 changed files with 187 additions and 24 deletions
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -1,12 +1,12 @@
|
|||
# Changelog
|
||||
|
||||
## [1.0.8-rc30] - unstable
|
||||
## [1.0.8-rc31] - unstable
|
||||
|
||||
### 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.
|
||||
* [Yavsc.Org] Permalien public de podcasts performer avec jeton a durée
|
||||
limitée, page Razor de presentation et bandeau de prospection vers
|
||||
l'inscription Yavsc et le téléchargement de PostIt.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -16,20 +16,31 @@
|
|||
* avoid NRE in MailSender on rdv query notifications
|
||||
* Android emulator smoke test skipped in CI because Calabash instrumentation signature mismatches the target APK on hosted runners
|
||||
|
||||
|
||||
## [1.0.8-rc30] - unstable
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
* Podcast availability from Android
|
||||
|
||||
## [1.0.8-rc29] - unstable
|
||||
|
||||
### Added
|
||||
|
||||
* [PostIt.Android] Lecture des podcasts directs et replays dans la page
|
||||
video partagee avec le desktop : surface LibVLC native Android et moteur
|
||||
embarque, transport authentifie commun et controles uniques. Liberation
|
||||
du lecteur en arriere-plan ; reouverture du flux au retour dans l'application.
|
||||
video partagée avec le desktop : surface LibVLC native Android et moteur
|
||||
embarque, transport authentifie commun et contrôles uniques. Liberation
|
||||
du lecteur en arrière-plan ; réouverture du flux au retour dans l'application.
|
||||
|
||||
* [PostIt.Android] Emission depuis la camera et le microphone Android
|
||||
(Android 8 minimum), encodage H.264/AAC natif et segmentation MPEG-TS
|
||||
sur images cles. Interface, envoi authentifie, retries et finalisation
|
||||
sur images clés. Interface, envoi authentifie, retries et finalisation
|
||||
du replay partages avec le desktop. Service de premier plan avec
|
||||
notification d'arret et maintien de la capture en arriere-plan.
|
||||
notification d'arrêt et maintien de la capture en arrière-plan.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -39,7 +50,7 @@
|
|||
|
||||
* [Yavsc.Api, Yavsc.Server] Une demande de rendez-vous ne provoque plus
|
||||
d'erreur 500 (NullReferenceException dans MailSender) : l'API lie
|
||||
desormais la section Smtp, le modele appsettings-api fournit Site.Owner
|
||||
désormais la section Smtp, le modèle appsettings-api fournit Site.Owner
|
||||
et Smtp, et un envoi sans configuration mail complete est journalise
|
||||
puis ignore.
|
||||
|
||||
|
|
@ -51,38 +62,38 @@
|
|||
* [Yavsc.Api, Yavsc.Server, Yavsc.Api.Client] Creation explicite et
|
||||
idempotente d'une facture depuis un devis doublement signe, avec lignes,
|
||||
total et parties figes. Historique des encaissements hors ligne attestes
|
||||
par le prestataire et des transactions PayPal verifiees cote serveur ;
|
||||
references uniques, cumul atomique et protection contre les ecritures
|
||||
concurrentes. Routes documentees, client HTTP et migration associes.
|
||||
par le prestataire et des transactions PayPal vérifiées cote serveur ;
|
||||
references uniques, cumul atomique et protection contre les écritures
|
||||
concurrentes. Routes documentées, client HTTP et migration associes.
|
||||
|
||||
* [Yavsc.Abstract, Yavsc.Server] Modele de solde des commandes facturables :
|
||||
montants dus et payes persistants, reste a payer et etats non paye,
|
||||
* [Yavsc.Abstract, Yavsc.Server] Modèle de solde des commandes facturables :
|
||||
montants dus et payes persistants, reste a payer et états non paye,
|
||||
partiellement paye ou acquitte. L'acquittement depend des montants,
|
||||
et non du seul statut PayPal ; migration sans montant historique invente.
|
||||
|
||||
* [PostIt.Desktop, Yavsc.Api] Podcasts en direct avec replay HLS conserve :
|
||||
capture camera et microphone par LibVLC (H.264/AAC, segments MPEG-TS),
|
||||
envoi ordonne et idempotent, catalogue et lecture authentifiee dans PostIt.
|
||||
envoi ordonne et idempotent, catalogue et lecture authentifiée dans PostIt.
|
||||
L'auteur peut terminer ou supprimer ses emissions ; stockage prive sur
|
||||
disque, quotas dedies et conservation des segments recus apres interruption.
|
||||
disque, quotas dédiés et conservation des segments reçus apres interruption.
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
* [Facturation] Les commandes facturees et leur devis ne peuvent plus etre
|
||||
* [Facturation] Les commandes facturées et leur devis ne peuvent plus être
|
||||
supprimes via les routes de commande usuelles. Les additions distinguent
|
||||
montant verse, solde restant et acquittement ; les anciennes routes de
|
||||
generation exigent une facture etablie depuis un devis signe.
|
||||
generation exigent une facture établie depuis un devis signe.
|
||||
|
||||
## [1.0.8-rc27] - unstable
|
||||
|
||||
### Added
|
||||
|
||||
* [PostIt.Desktop] Installeur NSIS Windows x64 par utilisateur, sans
|
||||
elevation : publication autonome, raccourci du menu Demarrer,
|
||||
protocole `postit://` pour la connexion et desinstallation via Windows.
|
||||
Instructions de generation ajoutees dans `INSTALL.md`.
|
||||
elevation : publication autonome, raccourci du menu Démarrer,
|
||||
protocole `postit://` pour la connexion et désinstallation via Windows.
|
||||
Instructions de generation ajoutées dans `INSTALL.md`.
|
||||
|
||||
* [PostIt, Yavsc.Api] Portage des outils des contrôleurs web
|
||||
d’administration : tableau de bord, rôles et affectations, clients OIDC
|
||||
|
|
|
|||
|
|
@ -65,6 +65,20 @@ Rituels recommandes:
|
|||
- Mensuel: mise a jour du present guide et du playbook.
|
||||
- A chaque incident: ajouter un anti-pattern dans le playbook.
|
||||
|
||||
### Reseau de versionnement / git flow pour le CHANGELOG
|
||||
|
||||
Regle obligatoire:
|
||||
- La section courante du CHANGELOG représente la prochaine version a publier, pas un tag deja créé.
|
||||
- Un tag deja publie bloque définitivement la section correspondante du CHANGELOG.
|
||||
- Au commit suivant, si la modification impacte le CHANGELOG, il faut incrémenter la version exposée dans le changelog (version du prochain tag attendu), par exemple: `1.0.8-rc30 -> 1.0.8-rc31`.
|
||||
|
||||
Ordre a respecter a chaque commit qui modifie le produit:
|
||||
1. Verifier si le dernier tag du CHANGELOG existe deja; si oui, incrémenter la version de la section suivante au lieu de re-écrire la dernière section bloquée.
|
||||
2. Ouvrir une nouvelle section du CHANGELOG avec ce numero de version incrémenté.
|
||||
3. Apres seulement, alimenter cette section avec la description du changement.
|
||||
|
||||
En pratique: la version du CHANGELOG est la version "future" a tagger, et elle est incrémentée avant l'écriture de la description; le tag n'est pas créé avant la section, il est seulement établi ensuite lors de la publication effective.
|
||||
|
||||
## Check-list de validation
|
||||
|
||||
- Le changement indique son impact architecture.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue