API - CRUD complet sur /api/livres : liste filtrable (format, statut) et cherchable (titre, auteur), consultation, creation, modification, changement de statut isole, suppression. - Aucune lecture ne filtre sur AjoutePar : la bibliotheque est commune au foyer. Le critere n'existe meme pas dans CritereLivres, pour qu'on ne puisse pas s'en servir par inadvertance. Deux tests verrouillent l'invariant, dont un par reflexion sur le type des criteres. - AjoutePar est renseigne par le serveur a la creation, jamais par le client, et n'est pas reecrit a l'edition (pas plus que DateAjout). - Identite lue dans les en-tetes SSOwat (YNH_USER, YNH_USER_EMAIL, YNH_USER_FULLNAME), avec repli sur un utilisateur simule configure en developpement uniquement. Sans en-tete ni simulation, l'identite reste inconnue plutot qu'inventee. Expose par GET /api/moi. - Migration appliquee au demarrage : pas d'etape manuelle a l'installation. Interface Blazor, pensee mobile d'abord - Catalogue unique (physiques et numeriques melanges, distingues par le format) avec recherche, filtres et changement de statut directement dans la liste. - Ajout par ISBN : quand la cascade rend plusieurs notices, un ecran de choix les presente en mettant en avant editeur et annee, seuls elements qui les departagent. Le formulaire reste ensuite entierement modifiable. - Ajout manuel, edition, suppression avec confirmation explicite. - Couvertures : l'URL OpenLibrary n'est jamais validee par un appel prealable (502 intermittents mesures en phase 2) ; un attribut onerror masque l'image cassee et laisse apparaitre un substitut. Decisions prises la ou CLAUDE.md etait muet - Tri alphabetique par titre. - Un ISBN saisi doit etre valide, mais reste facultatif. - Ecran de choix affiche seulement a partir de deux candidats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
3.4 KiB
C#
98 lines
3.4 KiB
C#
using MaBibli.Shared.Dtos;
|
|
|
|
namespace MaBibli.Api.Services.Identite;
|
|
|
|
public interface IFournisseurUtilisateur
|
|
{
|
|
/// <summary>Identité de la requête en cours, jamais <c>null</c> (au pire <see cref="UtilisateurCourant.Anonyme"/>).</summary>
|
|
UtilisateurCourant Obtenir();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options de l'identité, section <c>Identite</c> de la configuration.
|
|
/// </summary>
|
|
public sealed class OptionsIdentite
|
|
{
|
|
public const string Section = "Identite";
|
|
|
|
/// <summary>
|
|
/// Utilisateur simulé en développement, où SSOwat n'existe pas.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// CLAUDE.md : « prévoir un utilisateur simulé plutôt que de désactiver l'auth ».
|
|
/// Il n'est utilisé que si les en-têtes SSOwat sont absents, et n'est renseigné que dans
|
|
/// <c>appsettings.Development.json</c> — en production, aucune valeur, donc aucun repli.
|
|
/// </remarks>
|
|
public string? UtilisateurSimule { get; set; }
|
|
|
|
public string? EmailSimule { get; set; }
|
|
|
|
public string? NomCompletSimule { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lit l'identité dans les en-têtes HTTP injectés par SSOwat.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// nginx authentifie le visiteur via le portail YunoHost puis pousse <c>YNH_USER</c>,
|
|
/// <c>YNH_USER_EMAIL</c> et <c>YNH_USER_FULLNAME</c>. L'application se contente de les lire.
|
|
/// <para>
|
|
/// ⚠️ Ces en-têtes ne sont dignes de confiance que parce que le service n'écoute que sur
|
|
/// <c>127.0.0.1</c> derrière nginx : exposé directement, n'importe qui pourrait forger
|
|
/// <c>YNH_USER</c>. Cette contrainte se règle dans <c>systemd.service</c> / <c>nginx.conf</c>,
|
|
/// qui vivent dans le dépôt <c>mabibli_ynh</c> (phase 7) — pas ici.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class FournisseurUtilisateurSsowat(
|
|
IHttpContextAccessor accesseur,
|
|
Microsoft.Extensions.Options.IOptions<OptionsIdentite> options) : IFournisseurUtilisateur
|
|
{
|
|
public const string EnteteUtilisateur = "YNH_USER";
|
|
public const string EnteteEmail = "YNH_USER_EMAIL";
|
|
public const string EnteteNomComplet = "YNH_USER_FULLNAME";
|
|
|
|
public UtilisateurCourant Obtenir()
|
|
{
|
|
var entetes = accesseur.HttpContext?.Request.Headers;
|
|
var identifiant = Valeur(entetes, EnteteUtilisateur);
|
|
|
|
if (identifiant is not null)
|
|
{
|
|
return new UtilisateurCourant
|
|
{
|
|
Identifiant = identifiant,
|
|
Email = Valeur(entetes, EnteteEmail),
|
|
NomComplet = Valeur(entetes, EnteteNomComplet),
|
|
Simule = false,
|
|
};
|
|
}
|
|
|
|
var simule = options.Value;
|
|
if (!string.IsNullOrWhiteSpace(simule.UtilisateurSimule))
|
|
{
|
|
return new UtilisateurCourant
|
|
{
|
|
Identifiant = simule.UtilisateurSimule,
|
|
Email = simule.EmailSimule,
|
|
NomComplet = simule.NomCompletSimule,
|
|
Simule = true,
|
|
};
|
|
}
|
|
|
|
// Ni en-tête, ni utilisateur simulé : on n'invente pas d'identité.
|
|
// Le livre sera enregistré sans traçabilité plutôt que sous un faux nom.
|
|
return UtilisateurCourant.Anonyme;
|
|
}
|
|
|
|
private static string? Valeur(IHeaderDictionary? entetes, string nom)
|
|
{
|
|
if (entetes is null || !entetes.TryGetValue(nom, out var valeurs))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var valeur = valeurs.ToString().Trim();
|
|
return valeur.Length == 0 ? null : valeur;
|
|
}
|
|
}
|