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).
This commit is contained in:
+13
-3
@@ -1,10 +1,13 @@
|
||||
# ---- Étape build (SDK) ----
|
||||
# NOTE : le contexte de build est la RACINE du dépôt (voir docker-compose.yml),
|
||||
# afin de pouvoir embarquer les documents de spécification (.kiro/specs). Les chemins
|
||||
# sont donc relatifs à la racine (préfixe backend/).
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY *.sln ./
|
||||
COPY SchotzIadKmTracker.Api/*.csproj SchotzIadKmTracker.Api/
|
||||
COPY backend/*.sln ./
|
||||
COPY backend/SchotzIadKmTracker.Api/*.csproj SchotzIadKmTracker.Api/
|
||||
RUN dotnet restore SchotzIadKmTracker.Api/SchotzIadKmTracker.Api.csproj
|
||||
COPY . .
|
||||
COPY backend/ .
|
||||
RUN dotnet publish SchotzIadKmTracker.Api/SchotzIadKmTracker.Api.csproj -c Release -o /app/publish
|
||||
|
||||
# ---- Étape runtime (ASP.NET) ----
|
||||
@@ -18,6 +21,13 @@ RUN apt-get update \
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Documents de spécification embarqués (lus par SpecsController) : l'application est
|
||||
# ainsi autonome, sans dépendre d'un montage du dépôt à l'exécution.
|
||||
COPY .kiro/specs/schotziad-km-tracker/requirements.md \
|
||||
.kiro/specs/schotziad-km-tracker/design.md \
|
||||
.kiro/specs/schotziad-km-tracker/tasks.md \
|
||||
./specs/
|
||||
|
||||
# Répertoire du volume pour la base SQLite persistée
|
||||
RUN mkdir -p /data
|
||||
VOLUME ["/data"]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Expose en lecture seule les documents de spécification du projet (exigences,
|
||||
/// conception technique, plan de tâches) pour affichage dans le frontend. Comme tous
|
||||
/// les controllers hors AuthController, ces endpoints exigent un utilisateur
|
||||
/// authentifié (autorisation globale) : les specs ne sont pas publiques.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/specs")]
|
||||
public class SpecsController : ControllerBase
|
||||
{
|
||||
private readonly ISpecDocumentService _specDocumentService;
|
||||
|
||||
public SpecsController(ISpecDocumentService specDocumentService)
|
||||
{
|
||||
_specDocumentService = specDocumentService;
|
||||
}
|
||||
|
||||
/// <summary>Liste les documents de spécification connus et leur disponibilité (pour le sous-menu).</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<SpecDocumentDto>), StatusCodes.Status200OK)]
|
||||
public ActionResult<IReadOnlyList<SpecDocumentDto>> Lister()
|
||||
{
|
||||
return Ok(_specDocumentService.Lister());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le contenu Markdown d'un document identifié par sa clé (whitelist :
|
||||
/// "requirements", "design", "tasks"). 404 si la clé est inconnue ou le fichier absent.
|
||||
/// </summary>
|
||||
[HttpGet("{cle}")]
|
||||
[ProducesResponseType(typeof(SpecContenuDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<SpecContenuDto>> Obtenir(string cle, CancellationToken cancellationToken)
|
||||
{
|
||||
var contenu = await _specDocumentService.ObtenirAsync(cle, cancellationToken);
|
||||
if (contenu is null)
|
||||
{
|
||||
return Problem(
|
||||
detail: $"Le document de spécification '{cle}' est introuvable.",
|
||||
statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
return Ok(contenu);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace SchotzIadKmTracker.Api.Models.DTOs;
|
||||
|
||||
/// <summary>Métadonnées d'un document de spécification exposé au frontend.</summary>
|
||||
/// <param name="Cle">Clé stable et sûre (whitelist) : "requirements", "design" ou "tasks".</param>
|
||||
/// <param name="Titre">Libellé lisible pour le sous-menu (ex. « Exigences »).</param>
|
||||
/// <param name="Disponible">Vrai si le fichier Markdown correspondant est présent et lisible.</param>
|
||||
public record SpecDocumentDto(
|
||||
string Cle,
|
||||
string Titre,
|
||||
bool Disponible
|
||||
);
|
||||
|
||||
/// <summary>Contenu Markdown d'un document de spécification.</summary>
|
||||
/// <param name="Cle">Clé du document ("requirements", "design", "tasks").</param>
|
||||
/// <param name="Titre">Libellé lisible du document.</param>
|
||||
/// <param name="Markdown">Contenu brut Markdown du document, rendu côté frontend.</param>
|
||||
public record SpecContenuDto(
|
||||
string Cle,
|
||||
string Titre,
|
||||
string Markdown
|
||||
);
|
||||
@@ -79,6 +79,7 @@ builder.Services.AddScoped<IDistanceCalculator, DistanceCalculatorService>();
|
||||
// --- Cache des adresses géocodées ---
|
||||
builder.Services.AddScoped<IAdresseCacheRepository, AdresseCacheRepository>();
|
||||
builder.Services.AddScoped<IAdresseCacheService, AdresseCacheService>();
|
||||
builder.Services.AddScoped<ISpecDocumentService, SpecDocumentService>();
|
||||
|
||||
// --- Services de base ---
|
||||
// Autorisation GLOBALE par défaut : toutes les routes MVC exigent un utilisateur authentifié.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Fournit l'accès en lecture seule aux documents de spécification du projet
|
||||
/// (requirements, design, tasks) pour affichage dans le frontend.
|
||||
/// </summary>
|
||||
public interface ISpecDocumentService
|
||||
{
|
||||
/// <summary>Liste les documents de spécification connus et leur disponibilité.</summary>
|
||||
IReadOnlyList<SpecDocumentDto> Lister();
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le contenu Markdown du document identifié par <paramref name="cle"/>,
|
||||
/// ou <c>null</c> si la clé est inconnue (hors whitelist) ou le fichier absent.
|
||||
/// </summary>
|
||||
Task<SpecContenuDto?> ObtenirAsync(string cle, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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/<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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user