using Azure.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Graph; using SIFinance.Shared; using SIFinance.Shared.Entities; namespace SIFinance.Api; public record SharePointSiteDto(string Id, string Name, string? WebUrl); public record SharePointUploadDto(string WebUrl, string ItemId, long Size, string Nom); // Endpoints de découverte SharePoint + upload de documents dans les dossiers modules // (Achats / Sous-Traitants / Contrat Etudiant) du drive configuré (SharePoint:DriveId). public static class SharePointEndpoints { // Whitelist stricte plutôt que d'accepter n'importe quel nom de dossier envoyé par le // client — évite qu'un chemin arbitraire soit construit dans l'URL Graph. private static readonly HashSet DossiersAutorises = new(StringComparer.OrdinalIgnoreCase) { "Achats", "Sous-Traitants", "Contrat Etudiant" }; public static IEndpointRouteBuilder MapSharePointEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/sharepoint").WithTags("SharePoint").RequireAuthorization(); group.MapGet("/sites", async (SharePointGraphClientFactory graphFactory, CancellationToken ct) => { try { var graph = graphFactory.Create(); var result = await graph.Sites.GetAsync(cfg => cfg.QueryParameters.Search = "*", ct); var sites = (result?.Value ?? []) .Select(s => new SharePointSiteDto(s.Id ?? "", s.Name ?? s.DisplayName ?? "(sans nom)", s.WebUrl)) .ToList(); return Results.Ok(sites); } catch (Exception ex) { // Cas fréquents : secret pas encore renseigné, permission Sites.Read.All // absente, ou admin consent pas encore accordé (403). return Results.BadRequest(new { error = "Impossible de récupérer la liste des sites SharePoint — vérifie les identifiants et les permissions (Sites.Read.All ou Sites.ReadWrite.All, avec admin consent).", detail = ex.Message, }); } }); group.MapPost("/upload", async ( HttpRequest request, SharePointUploadService uploadService, SIFinanceDbContext db, CurrentUserService currentUser, CancellationToken ct) => { if (!request.HasFormContentType) return Results.BadRequest(new { error = "Contenu multipart attendu." }); var form = await request.ReadFormAsync(ct); var file = form.Files.GetFile("file"); var dossier = form["dossier"].ToString(); var service = form["service"].ToString(); if (file is null || file.Length == 0) return Results.BadRequest(new { error = "Fichier manquant." }); if (!DossiersAutorises.Contains(dossier)) return Results.BadRequest(new { error = "Dossier de destination invalide." }); // Le sous-dossier "collaborateur" est résolu depuis l'utilisateur authentifié, // jamais depuis une valeur envoyée par le client — sinon n'importe qui pourrait // faire apparaître son upload dans le dossier d'un collègue. var userId = await currentUser.GetOrCreateCurrentUserIdAsync(ct); var user = await db.Utilisateurs.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId, ct); var collaborateur = user is null ? "Autre" : $"{user.Prenom} {user.Nom}"; var segments = new List { dossier }; if (!string.IsNullOrWhiteSpace(service)) segments.Add(SanitizeSegment(service)); segments.Add(SanitizeSegment(collaborateur)); var cheminDossier = string.Join('/', segments); try { await using var stream = file.OpenReadStream(); var result = await uploadService.UploadAsync(cheminDossier, file.FileName, stream, file.ContentType, ct); return Results.Ok(new SharePointUploadDto(result.WebUrl, result.ItemId, result.Size, file.FileName)); } catch (Exception ex) { return Results.BadRequest(new { error = "Échec de l'upload vers SharePoint.", detail = ex.Message }); } }); return app; } private static string SanitizeSegment(string value) { var trimmed = value.Trim(); if (trimmed.Length == 0) return "Autre"; var invalides = new[] { '"', '*', ':', '<', '>', '?', '/', '\\', '|' }; var nettoye = new string(trimmed.Select(c => invalides.Contains(c) ? '-' : c).ToArray()); return nettoye.Length == 0 ? "Autre" : nettoye; } }