Files
SchotzIAD/backend/SchotzIadKmTracker.Api/Services/SpecDocumentService.cs
T
MadJax 9d2b676c84 Consultation des specs dans l'app (menu Specs + rendu Markdown/Mermaid)
Nouvelle fonctionnalité : consulter les documents de spécification (exigences,
conception technique, plan de tâches) en lecture seule dans le frontend.

Backend :
- SpecsController (GET /api/specs, GET /api/specs/{cle}) protégé par l'auth
  globale ; SpecDocumentService avec whitelist (pas de path traversal) et
  lecture depuis le dossier specs/ embarqué, repli sur .kiro/specs en dev.
- DTOs SpecDocumentDto / SpecContenuDto ; service enregistré dans Program.cs.

Docker :
- Contexte de build backend = racine du dépôt (dockerfile: backend/Dockerfile)
  pour embarquer .kiro/specs/*.md dans l'image (app autonome).
- .dockerignore racine ajouté (exclut frontend/node_modules, bin/obj, etc.).

Frontend :
- SpecService + SpecsComponent (lazy-loadé) : page dédiée avec menu latéral par
  document ; rendu Markdown via marked (GFM) assaini par DOMPurify.
- Diagrammes Mermaid (ex. graph TB) rendus en SVG via mermaid.js importé
  dynamiquement ; déclenchement après résolution du @ViewChild (setTimeout).
- Barre de navigation : simple lien « Specs » (le choix du document se fait dans
  le menu latéral de la page, pas dans la barre).
- Routes /specs et /specs/:cle protégées par authGuard.

Specs : exigence 19 (consultation des documents) ajoutée à requirements.md ;
design.md complété (DTOs, endpoints, composant, rendu Mermaid, note Docker).
2026-09-16 08:10:41 +02:00

108 lines
3.8 KiB
C#

using SchotzIadKmTracker.Api.Models.DTOs;
namespace SchotzIadKmTracker.Api.Services;
/// <inheritdoc />
public class SpecDocumentService : ISpecDocumentService
{
/// <summary>
/// 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.
/// </summary>
private static readonly IReadOnlyList<(string Cle, string Fichier, string Titre)> Documents = new[]
{
("requirements", "requirements.md", "Exigences"),
("design", "design.md", "Conception technique"),
("tasks", "tasks.md", "Plan de tâches")
};
private readonly IWebHostEnvironment _environment;
private readonly ILogger<SpecDocumentService> _logger;
public SpecDocumentService(IWebHostEnvironment environment, ILogger<SpecDocumentService> logger)
{
_environment = environment;
_logger = logger;
}
/// <inheritdoc />
public IReadOnlyList<SpecDocumentDto> Lister()
{
return Documents
.Select(d => new SpecDocumentDto(d.Cle, d.Titre, File.Exists(ResoudreChemin(d.Fichier))))
.ToList();
}
/// <inheritdoc />
public async Task<SpecContenuDto?> 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);
}
/// <summary>
/// Résout le chemin absolu d'un fichier de spécification. En priorité le dossier
/// <c>specs/</c> embarqué à la racine de contenu de l'application (copié dans l'image
/// Docker au build) ; à défaut, le dossier <c>.kiro/specs/&lt;feature&gt;/</c> du dépôt
/// (utile en développement, où l'API tourne depuis l'arborescence du projet).
/// </summary>
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/<feature>/.
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;
}
/// <summary>
/// Remonte l'arborescence depuis <paramref name="depart"/> jusqu'à trouver un dossier
/// contenant <c>.kiro</c> (racine du dépôt), ou <c>null</c> si non trouvé.
/// </summary>
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;
}
}