import fs from "node:fs" import path from "node:path" import process from "node:process" const ROOT_DIR = process.cwd() function loadDotEnv(filePath = path.join(ROOT_DIR, ".env")) { if (!fs.existsSync(filePath)) { return } const content = fs.readFileSync(filePath, "utf8") for (const line of content.split(/\r?\n/)) { const trimmed = line.trim() if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { continue } const index = trimmed.indexOf("=") const key = trimmed.slice(0, index).trim() let value = trimmed.slice(index + 1).trim() if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1) } if (!process.env[key]) { process.env[key] = value } } } function requireEnv(name) { const value = process.env[name] if (!value) { throw new Error(`Variable d'environnement manquante : ${name}`) } return value } async function getAccessToken() { const tenantId = requireEnv("MICROSOFT_TENANT_ID") const clientId = requireEnv("MICROSOFT_CLIENT_ID") const clientSecret = requireEnv("MICROSOFT_CLIENT_SECRET") const body = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials", }) const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }) if (!response.ok) { throw new Error(`Erreur token Microsoft Graph (${response.status}) : ${await response.text()}`) } const data = await response.json() return data.access_token } function decodeJwtPayload(token) { const [, payload] = token.split(".") if (!payload) { return {} } const normalizedPayload = payload.replaceAll("-", "+").replaceAll("_", "/") const paddedPayload = normalizedPayload.padEnd(normalizedPayload.length + ((4 - normalizedPayload.length % 4) % 4), "=") return JSON.parse(Buffer.from(paddedPayload, "base64").toString("utf8")) } function parseSharePointSiteUrl(rawUrl) { const url = new URL(rawUrl) const normalizedPath = url.pathname.replace(/\/+$/, "") if (!normalizedPath) { throw new Error("L'URL SharePoint doit contenir le chemin du site, par exemple https://tenant.sharepoint.com/sites/nom-du-site") } return { hostname: url.hostname, sitePath: normalizedPath, } } async function graphGet(token, url) { const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, }, }) if (!response.ok) { const details = await response.text() const hint = response.status === 401 ? "\nVérifie que l'application Entra ID a des permissions Microsoft Graph de type Application, par exemple Sites.Read.All ou Sites.ReadWrite.All, avec Grant admin consent. Vérifie aussi que le tenant correspond bien au SharePoint." : "" const error = new Error(`Erreur Microsoft Graph (${response.status}) : ${details}${hint}`) error.status = response.status error.details = details throw error } return response.json() } async function main() { loadDotEnv() const sharePointUrl = process.argv[2] || process.env.SHAREPOINT_SITE_URL if (!sharePointUrl) { throw new Error("Usage : npm run graph:find-drive -- https://tenant.sharepoint.com/sites/nom-du-site") } const token = await getAccessToken() const claims = decodeJwtPayload(token) console.log(`Token Microsoft Graph obtenu pour tenant=${claims.tid || "inconnu"} app=${claims.appid || claims.azp || "inconnue"}`) console.log(`Permissions applicatives dans le token : ${(claims.roles || []).join(", ") || "aucune"}`) console.log("") const { hostname, sitePath } = parseSharePointSiteUrl(sharePointUrl) let site try { site = await graphGet( token, `https://graph.microsoft.com/v1.0/sites/${hostname}:${sitePath}` ) } catch (error) { if (error.status !== 404) { throw error } const searchTerm = sitePath.split("/").filter(Boolean).at(-1) console.log(`Site introuvable avec le chemin ${sitePath}. Recherche des sites contenant "${searchTerm}"...`) console.log("") const searchResults = await graphGet( token, `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchTerm)}` ) if (!searchResults.value?.length) { throw error } console.log("Sites trouvés :") for (const result of searchResults.value) { console.log(`- ${result.displayName || result.name}`) console.log(` SITE_ID=${result.id}`) console.log(` webUrl=${result.webUrl}`) } console.log("") console.log("Relance ensuite la commande avec l'URL webUrl exacte du bon site.") return } const drives = await graphGet( token, `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(site.id)}/drives` ) console.log(`SITE_ID=${site.id}`) console.log("") console.log("Bibliothèques SharePoint trouvées :") for (const drive of drives.value || []) { console.log(`- ${drive.name}`) console.log(` GRAPH_DRIVE_ID=${drive.id}`) console.log(` webUrl=${drive.webUrl}`) } } main().catch((error) => { console.error(error) process.exit(1) })