- Backend : ajout de design-requirements a la liste blanche des documents exposes (libelle Exigences techniques) ; embarque via le Dockerfile (*.md) - Frontend : entree Exigences techniques dans le menu deroulant Specs ; le menu lateral de la page Specs se remplit dynamiquement - design.md : note renvoyant vers requirements.md et design-requirements.md
109 lines
3.9 KiB
C#
109 lines
3.9 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-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<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/<feature>/</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;
|
|
}
|
|
}
|