using SchotzIadKmTracker.Api.Models.DTOs; namespace SchotzIadKmTracker.Api.Services; /// public class SpecDocumentService : ISpecDocumentService { /// /// Whitelist des documents exposés : clé sûre -> (nom de fichier, libellé lisible). /// La clé provient du client ; seules ces valeurs sont acceptées, ce qui interdit /// tout parcours de chemin (path traversal) : aucun chemin n'est construit à partir /// d'une entrée utilisateur libre. /// private static readonly IReadOnlyList<(string Cle, string Fichier, string Titre)> Documents = new[] { ("requirements", "requirements.md", "Exigences"), ("design-requirements", "design-requirements.md", "Exigences techniques"), ("design", "design.md", "Conception technique"), ("tasks", "tasks.md", "Plan de tâches") }; private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; public SpecDocumentService(IWebHostEnvironment environment, ILogger logger) { _environment = environment; _logger = logger; } /// public IReadOnlyList Lister() { return Documents .Select(d => new SpecDocumentDto(d.Cle, d.Titre, File.Exists(ResoudreChemin(d.Fichier)))) .ToList(); } /// public async Task ObtenirAsync(string cle, CancellationToken cancellationToken = default) { var doc = Documents.FirstOrDefault(d => d.Cle == cle); if (doc.Cle is null) { // Clé hors whitelist : document inconnu. return null; } var chemin = ResoudreChemin(doc.Fichier); if (!File.Exists(chemin)) { _logger.LogWarning("Document de spécification introuvable : {Chemin}", chemin); return null; } var markdown = await File.ReadAllTextAsync(chemin, cancellationToken); return new SpecContenuDto(doc.Cle, doc.Titre, markdown); } /// /// Résout le chemin absolu d'un fichier de spécification. En priorité le dossier /// specs/ embarqué à la racine de contenu de l'application (copié dans l'image /// Docker au build) ; à défaut, le dossier .kiro/specs/<feature>/ du dépôt /// (utile en développement, où l'API tourne depuis l'arborescence du projet). /// private string ResoudreChemin(string fichier) { // 1) Dossier embarqué à côté de l'application publiée (Docker). var embarque = Path.Combine(_environment.ContentRootPath, "specs", fichier); if (File.Exists(embarque)) { return embarque; } // 2) Repli développement : remonter jusqu'au dépôt et lire .kiro/specs//. var racineDepot = TrouverRacineDepot(_environment.ContentRootPath); if (racineDepot is not null) { var devPath = Path.Combine( racineDepot, ".kiro", "specs", "schotziad-km-tracker", fichier); if (File.Exists(devPath)) { return devPath; } } // Chemin embarqué par défaut (inexistant) : File.Exists renverra false en amont. return embarque; } /// /// Remonte l'arborescence depuis jusqu'à trouver un dossier /// contenant .kiro (racine du dépôt), ou null si non trouvé. /// private static string? TrouverRacineDepot(string depart) { var dir = new DirectoryInfo(depart); while (dir is not null) { if (Directory.Exists(Path.Combine(dir.FullName, ".kiro"))) { return dir.FullName; } dir = dir.Parent; } return null; } }