Files
PS_Launcher/src/PSLauncher.Models/License.cs
j.foucher c371a79f93 v0.26.0 — Channels per-license + tag BÊTA sur versions
DEUX features liées :

1. CHANNELS : chaque license peut être attribuée à un manifest distinct
   (« channel »). Permet de servir des versions différentes selon le
   client. Le serveur lit ?channel=X et sert manifest/versions-{X}.json
   avec fallback transparent sur versions.json. NULL = default.

2. BÊTA : nouveau flag isBeta + betaNotes par version dans le manifest.
   Visible uniquement par les licenses avec can_see_betas=1. Affichage
   d'une pill orange « BÊTA » sur la row + tooltip avec les notes pour
   les testeurs. Les installations locales déjà présentes restent
   visibles même si l'accès BÊTA est retiré ensuite (on n'efface pas
   le disque du client).

DB :
  002_channel_betas.sql ajoute channel + can_see_betas sur licenses.
  Idempotent (ALTER TABLE IF NOT EXISTS), zero data migration.

Serveur PHP :
  - ValidateLicense.php signe channel + canSeeBetas dans la réponse
    (ordre des clés CRITIQUE pour matcher le canonical client).
  - Manifest.php : whitelist regex anti-traversal sur ?channel=, fallback
    silencieux sur versions.json si channel inconnu (évite leak de la
    liste de channels par probing).
  - SignManifest.php prend un channel optionnel → l'admin peut signer
    chaque manifest indépendamment.
  - admin/licenses.php : dropdown channel + checkbox bêta sur create,
    bouton détails repliable par-row pour edit.
  - admin/versions.php : channel switcher en tête, badge BÊTA sur chaque
    row, dialog repliable « Bêta » avec checkbox + notes des testeurs.

Client C# :
  - License.Channel + License.CanSeeBetas (dans le canonical signé).
  - VersionManifest.IsBeta + BetaNotes.
  - ManifestService prend un channelProvider via DI, lu depuis license
    cachée à chaque fetch (lazy, pas de circular dep).
  - MainViewModel.RebuildList filtre les versions IsBeta si !CanSeeBetas
    (mais conserve les installées locales — on ne retire pas l'accès
    rétroactivement à ce qui est déjà sur disque).
  - VersionRowViewModel : props IsBeta / BetaNotes / BetaTooltip.
  - MainWindow.xaml : pill orange à côté du n° version pour le featured
    et les rows compactes, tooltip dynamique avec les notes testeurs.

Backward compat signature :
  Anciennes licenses cachées (signées sans channel/canSeeBetas) sont
  toujours validées via un fallback canonical legacy dans VerifySignature.
  Sans ce fallback, le passage à v0.26 invaliderait toutes les caches
  hors-ligne et bloquerait les users en mobilité.

Migration côté admin : jouer 002_channel_betas.sql sur la base, déployer
les fichiers PHP, créer manifest/versions-{channel}.json pour les
nouveaux channels (l'admin versions.php propose un input « Créer/utiliser
un nouveau channel »). Les licenses existantes restent en channel=NULL
= default = comportement actuel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:38:00 +02:00

71 lines
2.5 KiB
C#

using System.Text.Json.Serialization;
namespace PSLauncher.Models;
public sealed class LicenseValidationResponse
{
[JsonPropertyName("status")]
public string Status { get; set; } = "invalid";
[JsonPropertyName("licenseId")]
public string? LicenseId { get; set; }
[JsonPropertyName("ownerName")]
public string? OwnerName { get; set; }
[JsonPropertyName("issuedAt")]
public DateTime? IssuedAt { get; set; }
[JsonPropertyName("downloadEntitlementUntil")]
public DateTime? DownloadEntitlementUntil { get; set; }
[JsonPropertyName("maxMachines")]
public int? MaxMachines { get; set; }
/// <summary>
/// Channel de manifest associé à la license. NULL = manifest default
/// (versions.json). Sinon le launcher fetch versions-{Channel}.json. Le
/// champ est inclus dans la signature Ed25519, donc un user ne peut pas
/// modifier sa réponse cachée pour basculer sur un autre channel.
/// </summary>
[JsonPropertyName("channel")]
public string? Channel { get; set; }
/// <summary>
/// True si la license autorise l'affichage des versions taggées
/// <c>isBeta=true</c> dans le launcher. Le filtrage est côté client (le
/// serveur peut servir le manifest complet — la signature Ed25519
/// protège contre toute modif locale du flag).
/// </summary>
[JsonPropertyName("canSeeBetas")]
public bool CanSeeBetas { get; set; }
[JsonPropertyName("serverTime")]
public DateTime? ServerTime { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("signature")]
public string? Signature { get; set; }
public bool IsValid => Status == "valid";
public bool IsExpired => Status == "expired";
public bool CanDownload(VersionManifest version)
{
if (!IsValid && !IsExpired) return false;
if (DownloadEntitlementUntil is null) return false;
if (version.MinLicenseDate is null) return true; // pas de minimum imposé
return DownloadEntitlementUntil >= version.MinLicenseDate;
}
}
public sealed class LicenseValidationRequest
{
[JsonPropertyName("licenseKey")] public string LicenseKey { get; set; } = string.Empty;
[JsonPropertyName("machineId")] public string MachineId { get; set; } = string.Empty;
[JsonPropertyName("machineLabel")] public string? MachineLabel { get; set; }
[JsonPropertyName("launcherVersion")] public string LauncherVersion { get; set; } = string.Empty;
}