diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbac6717..47498be79 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/doc/onboarding-agents.md b/doc/onboarding-agents.md index 14a727a17..18852fec1 100644 --- a/doc/onboarding-agents.md +++ b/doc/onboarding-agents.md @@ -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. diff --git a/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs b/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs index be0dd8942..12b1e9e7a 100644 --- a/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs +++ b/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs @@ -113,6 +113,37 @@ namespace Yavsc.Controllers return View(podcasts); } + [AllowAnonymous] + public async Task 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; } diff --git a/src/Yavsc.Org/Views/FrontOffice/PerformerPodcasts.cshtml b/src/Yavsc.Org/Views/FrontOffice/PerformerPodcasts.cshtml index 2c3e55db1..2d927c74a 100644 --- a/src/Yavsc.Org/Views/FrontOffice/PerformerPodcasts.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/PerformerPodcasts.cshtml @@ -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; }
@@ -9,10 +10,11 @@

Découvrez les podcasts de @performerName

- Ce lien public est valable jusqu'au @(((DateTimeOffset)ViewBag.PermalinkExpiry).ToLocalTime().ToString("g")). + Ce lien public est valable jusqu'au @permalinkExpiry.ToLocalTime().ToString("g").

+ S'inscrire Installer PostIt
@@ -57,3 +59,29 @@
} + + diff --git a/src/Yavsc.Org/Views/FrontOffice/Profiles.cshtml b/src/Yavsc.Org/Views/FrontOffice/Profiles.cshtml index a2f5cdab1..d5daf5afd 100644 --- a/src/Yavsc.Org/Views/FrontOffice/Profiles.cshtml +++ b/src/Yavsc.Org/Views/FrontOffice/Profiles.cshtml @@ -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; }

@ViewBag.Title

@ViewBag.Activity.Description +@if (!string.IsNullOrWhiteSpace(performerId) && !string.IsNullOrWhiteSpace(activityCode)) +{ + +} + @foreach (var profile in Model) {
@Html.DisplayFor(m=>profile) @@ -15,3 +31,29 @@ } + +