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
|
# Changelog
|
||||||
|
|
||||||
## [1.0.8-rc30] - unstable
|
## [1.0.8-rc31] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* [Yavsc.Org] Permalien public de podcasts performer avec jeton a duree
|
* [Yavsc.Org] Permalien public de podcasts performer avec jeton a durée
|
||||||
limitee, page Razor de presentation et bandeau de prospection vers
|
limitée, page Razor de presentation et bandeau de prospection vers
|
||||||
l'inscription Yavsc et le telechargement de PostIt.
|
l'inscription Yavsc et le téléchargement de PostIt.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
@ -16,20 +16,31 @@
|
||||||
* avoid NRE in MailSender on rdv query notifications
|
* 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
|
* 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
|
## [1.0.8-rc29] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* [PostIt.Android] Lecture des podcasts directs et replays dans la page
|
* [PostIt.Android] Lecture des podcasts directs et replays dans la page
|
||||||
video partagee avec le desktop : surface LibVLC native Android et moteur
|
video partagée avec le desktop : surface LibVLC native Android et moteur
|
||||||
embarque, transport authentifie commun et controles uniques. Liberation
|
embarque, transport authentifie commun et contrôles uniques. Liberation
|
||||||
du lecteur en arriere-plan ; reouverture du flux au retour dans l'application.
|
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
|
* [PostIt.Android] Emission depuis la camera et le microphone Android
|
||||||
(Android 8 minimum), encodage H.264/AAC natif et segmentation MPEG-TS
|
(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
|
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
|
### Changed
|
||||||
|
|
||||||
|
|
@ -39,7 +50,7 @@
|
||||||
|
|
||||||
* [Yavsc.Api, Yavsc.Server] Une demande de rendez-vous ne provoque plus
|
* [Yavsc.Api, Yavsc.Server] Une demande de rendez-vous ne provoque plus
|
||||||
d'erreur 500 (NullReferenceException dans MailSender) : l'API lie
|
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
|
et Smtp, et un envoi sans configuration mail complete est journalise
|
||||||
puis ignore.
|
puis ignore.
|
||||||
|
|
||||||
|
|
@ -51,38 +62,38 @@
|
||||||
* [Yavsc.Api, Yavsc.Server, Yavsc.Api.Client] Creation explicite et
|
* [Yavsc.Api, Yavsc.Server, Yavsc.Api.Client] Creation explicite et
|
||||||
idempotente d'une facture depuis un devis doublement signe, avec lignes,
|
idempotente d'une facture depuis un devis doublement signe, avec lignes,
|
||||||
total et parties figes. Historique des encaissements hors ligne attestes
|
total et parties figes. Historique des encaissements hors ligne attestes
|
||||||
par le prestataire et des transactions PayPal verifiees cote serveur ;
|
par le prestataire et des transactions PayPal vérifiées cote serveur ;
|
||||||
references uniques, cumul atomique et protection contre les ecritures
|
references uniques, cumul atomique et protection contre les écritures
|
||||||
concurrentes. Routes documentees, client HTTP et migration associes.
|
concurrentes. Routes documentées, client HTTP et migration associes.
|
||||||
|
|
||||||
* [Yavsc.Abstract, Yavsc.Server] Modele de solde des commandes facturables :
|
* [Yavsc.Abstract, Yavsc.Server] Modèle de solde des commandes facturables :
|
||||||
montants dus et payes persistants, reste a payer et etats non paye,
|
montants dus et payes persistants, reste a payer et états non paye,
|
||||||
partiellement paye ou acquitte. L'acquittement depend des montants,
|
partiellement paye ou acquitte. L'acquittement depend des montants,
|
||||||
et non du seul statut PayPal ; migration sans montant historique invente.
|
et non du seul statut PayPal ; migration sans montant historique invente.
|
||||||
|
|
||||||
* [PostIt.Desktop, Yavsc.Api] Podcasts en direct avec replay HLS conserve :
|
* [PostIt.Desktop, Yavsc.Api] Podcasts en direct avec replay HLS conserve :
|
||||||
capture camera et microphone par LibVLC (H.264/AAC, segments MPEG-TS),
|
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
|
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
|
### Changed
|
||||||
|
|
||||||
### Fixed
|
### 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
|
supprimes via les routes de commande usuelles. Les additions distinguent
|
||||||
montant verse, solde restant et acquittement ; les anciennes routes de
|
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
|
## [1.0.8-rc27] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
* [PostIt.Desktop] Installeur NSIS Windows x64 par utilisateur, sans
|
* [PostIt.Desktop] Installeur NSIS Windows x64 par utilisateur, sans
|
||||||
elevation : publication autonome, raccourci du menu Demarrer,
|
elevation : publication autonome, raccourci du menu Démarrer,
|
||||||
protocole `postit://` pour la connexion et desinstallation via Windows.
|
protocole `postit://` pour la connexion et désinstallation via Windows.
|
||||||
Instructions de generation ajoutees dans `INSTALL.md`.
|
Instructions de generation ajoutées dans `INSTALL.md`.
|
||||||
|
|
||||||
* [PostIt, Yavsc.Api] Portage des outils des contrôleurs web
|
* [PostIt, Yavsc.Api] Portage des outils des contrôleurs web
|
||||||
d’administration : tableau de bord, rôles et affectations, clients OIDC
|
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.
|
- Mensuel: mise a jour du present guide et du playbook.
|
||||||
- A chaque incident: ajouter un anti-pattern dans le 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
|
## Check-list de validation
|
||||||
|
|
||||||
- Le changement indique son impact architecture.
|
- Le changement indique son impact architecture.
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,37 @@ namespace Yavsc.Controllers
|
||||||
return View(podcasts);
|
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]
|
[Authorize]
|
||||||
public IActionResult GeneratePerformerPodcastPermalink(string id)
|
public IActionResult GeneratePerformerPodcastPermalink(string id)
|
||||||
{
|
{
|
||||||
|
|
@ -124,6 +155,18 @@ namespace Yavsc.Controllers
|
||||||
return RedirectToAction(nameof(PerformerPodcasts), new { id, token });
|
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)
|
private string CreateToken(string performerId)
|
||||||
{
|
{
|
||||||
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
|
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
|
||||||
|
|
@ -136,20 +179,45 @@ namespace Yavsc.Controllers
|
||||||
return protector.Protect(JsonSerializer.Serialize(payload));
|
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)
|
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;
|
validUntil = DateTimeOffset.MinValue;
|
||||||
if (string.IsNullOrWhiteSpace(token)) return false;
|
if (string.IsNullOrWhiteSpace(token)) return false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var protector = _dataProtectionProvider.CreateProtector("Yavsc.Org.PerformerPodcastLink");
|
var protector = _dataProtectionProvider.CreateProtector(protectorName);
|
||||||
var payload = JsonDocument.Parse(protector.Unprotect(token));
|
var payload = JsonDocument.Parse(protector.Unprotect(token));
|
||||||
var ownerId = payload.RootElement.GetProperty("performerId").GetString();
|
var ownerId = payload.RootElement.GetProperty("performerId").GetString();
|
||||||
var expiry = payload.RootElement.GetProperty("exp").GetInt64();
|
var expiry = payload.RootElement.GetProperty("exp").GetInt64();
|
||||||
|
|
||||||
if (!string.Equals(ownerId, performerId, StringComparison.Ordinal)) return false;
|
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);
|
validUntil = DateTimeOffset.FromUnixTimeSeconds(expiry);
|
||||||
return validUntil > DateTimeOffset.UtcNow;
|
return validUntil > DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = $"Podcasts de {ViewBag.PerformerName}";
|
ViewBag.Title = $"Podcasts de {ViewBag.PerformerName}";
|
||||||
var performerName = ViewBag.PerformerName ?? "ce performer";
|
var performerName = ViewBag.PerformerName ?? "ce performer";
|
||||||
|
var permalinkExpiry = ViewBag.PermalinkExpiry is DateTimeOffset expiry ? expiry : DateTimeOffset.MinValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
|
|
@ -9,10 +10,11 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h3 mb-1">Découvrez les podcasts de @performerName</h1>
|
<h1 class="h3 mb-1">Découvrez les podcasts de @performerName</h1>
|
||||||
<p class="mb-0 text-muted">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<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-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>
|
<a class="btn btn-outline-primary" href="https://postit.yavsc.fr/" target="_blank" rel="noreferrer">Installer PostIt</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -57,3 +59,29 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</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") ;
|
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>
|
<h1>@ViewBag.Title</h1>
|
||||||
<em>@ViewBag.Activity.Description</em>
|
<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) {
|
@foreach (var profile in Model) {
|
||||||
<hr/>
|
<hr/>
|
||||||
@Html.DisplayFor(m=>profile)
|
@Html.DisplayFor(m=>profile)
|
||||||
|
|
@ -15,3 +31,29 @@
|
||||||
</a>
|
</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