require("dotenv").config(); const tenantId = process.env.TENANT_ID; const clientId = process.env.CLIENT_ID; const clientSecret = process.env.CLIENT_SECRET; const fs = require("fs"); const path = require("path"); const https = require("https"); const express = require("express"); const cors = require("cors"); const { ConfidentialClientApplication } = require("@azure/msal-node"); const fetch = require('node-fetch'); const app = express(); // Use built-in JSON parser app.use(express.json()); // Allow your task pane origin (HTTPS localhost:3000) app.use(cors({ origin: "https://localhost:3000", methods: ["POST", "OPTIONS"], // frontend will send Authorization header when doing OBO/upload calls allowedHeaders: ["Content-Type", "Authorization"], })); const msalConfig = { auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, clientSecret } }; const cca = new ConfidentialClientApplication(msalConfig); const driveId = process.env.DRIVE_ID; const parentItemId = process.env.PARENT_ITEM_ID; // Helper to mask tokens in logs function maskToken(token) { if (!token || token.length < 12) return '***'; return token.slice(0, 6) + '...' + token.slice(-6); } // Endpoint to receive a PDF (base64) and upload it to Graph using app credentials app.post('/api/upload-pdf', async (req, res) => { try { const { jobNumber, jobFullName, fileName, pdfBase64 } = req.body || {}; console.log(`[upload-pdf] incoming request jobNumber=${jobNumber} fileName=${fileName}`); if (!jobNumber || !jobFullName || !fileName || !pdfBase64) { console.warn('[upload-pdf] missing required fields'); return res.status(400).json({ error: 'Missing required fields' }); } // Require a user token (OBO) — reject requests without an Authorization: Bearer const authHeader = (req.headers.authorization || req.headers.Authorization || ''); if (!authHeader.toLowerCase().startsWith('bearer ')) { console.warn('[upload-pdf] no bearer token supplied; rejecting because OBO is required'); return res.status(401).json({ error: 'Authorization bearer token required for OBO (user delegated access)' }); } const userAssertion = authHeader.substring(7).trim(); console.log(`[upload-pdf] Authorization present, token=${maskToken(userAssertion)}`); // Perform OBO exchange — fail fast if OBO fails since we now require OBO let accessToken; try { console.log('[upload-pdf] attempting OBO token exchange'); const oboRequest = { oboAssertion: userAssertion, scopes: [ 'User.Read', 'Files.ReadWrite.All', 'Sites.ReadWrite.All' ] }; const oboResult = await cca.acquireTokenOnBehalfOf(oboRequest); accessToken = oboResult.accessToken; console.log('[upload-pdf] OBO exchange successful; token=' + maskToken(accessToken)); } catch (oboErr) { console.error('[upload-pdf] OBO exchange failed:', oboErr && oboErr.message ? oboErr.message : oboErr); // include suberror/code if available for diagnostics const details = { error: oboErr && oboErr.errorCode ? oboErr.errorCode : undefined, suberror: oboErr && oboErr.suberror ? oboErr.suberror : undefined, message: oboErr && oboErr.message ? oboErr.message : String(oboErr) }; return res.status(401).json({ error: 'OBO exchange failed', details }); } // 1) Create main folder under configured parent const mainFolderName = `${jobNumber} - ${jobFullName}`; const createFolderEndpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentItemId}/children`; console.log('[upload-pdf] creating main folder:', mainFolderName); const folderResp = await fetch(createFolderEndpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: mainFolderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" }) }); const folderData = await folderResp.json(); console.log('[upload-pdf] create folder response', { status: folderResp.status, body: folderData }); if (!folderResp.ok) { console.error('[upload-pdf] create main folder failed', folderData); return res.status(500).json({ error: 'Create main folder failed', details: folderData }); } const mainFolderId = folderData.id; // 2) Create subfolder Managers Info console.log('[upload-pdf] creating subfolder: Managers Info under', mainFolderId); const subFolderResp = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${mainFolderId}/children`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Managers Info', folder: {}, "@microsoft.graph.conflictBehavior": "rename" }) }); const subFolderData = await subFolderResp.json(); console.log('[upload-pdf] create subfolder response', { status: subFolderResp.status, body: subFolderData }); if (!subFolderResp.ok) { console.error('[upload-pdf] create subfolder failed', subFolderData); return res.status(500).json({ error: 'Create subfolder failed', details: subFolderData }); } const subFolderId = subFolderData.id; // 3) Upload PDF content const fileBuffer = Buffer.from(pdfBase64, 'base64'); const uploadEndpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subFolderId}:/${encodeURIComponent(fileName)}:/content`; console.log('[upload-pdf] uploading file to', uploadEndpoint, 'sizeBytes=', fileBuffer.length); const uploadResp = await fetch(uploadEndpoint, { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' }, body: fileBuffer }); const uploadData = await uploadResp.json(); console.log('[upload-pdf] upload response', { status: uploadResp.status, body: uploadData }); if (!uploadResp.ok) { console.error('[upload-pdf] upload failed', uploadData); return res.status(500).json({ error: 'Upload failed', details: uploadData }); } // 4) Create share link for subfolder console.log('[upload-pdf] creating share link for subfolder', subFolderId); const shareResp = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subFolderId}/createLink`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'view', scope: 'organization' }) }); const shareData = await shareResp.json(); console.log('[upload-pdf] share link response', { status: shareResp.status, body: shareData }); res.json({ uploadedItem: uploadData, shareLink: shareData.link && shareData.link.webUrl ? shareData.link.webUrl : null }); } catch (err) { console.error('[upload-pdf] error:', err && err.message ? err.message : err); res.status(500).json({ error: err.message || 'server error', details: err }); } }); // Simple health check for local development app.get('/health', (req, res) => { res.json({ status: 'ok', time: new Date().toISOString() }); }); // 🔐 OBO endpoint (delegated scopes) app.post("/api/obo-exchange", async (req, res) => { try { const { officeToken } = req.body || {}; if (!officeToken) { return res.status(400).json({ error: "Missing officeToken" }); } const oboRequest = { oboAssertion: officeToken, // ✅ Use standard scope strings (delegated) scopes: [ "User.Read", "Files.ReadWrite.All", "Sites.ReadWrite.All" ] }; const result = await cca.acquireTokenOnBehalfOf(oboRequest); res.json({ graphToken: result.accessToken }); } catch (err) { console.error("OBO exchange error:", err); const status = (err.status || err.statusCode || 500); res.status(status).json({ error: err.message || "OBO failed", // helpful diagnostics in dev details: err.errorCode || err.code || undefined, suberror: err.suberror || undefined }); } }); // Try to start HTTPS using the dev cert created by office-addin-dev-certs. // If certs are not available, fall back to plain HTTP so the server doesn't crash at startup. const certDir = path.join(process.env.USERPROFILE || process.env.HOME, ".office-addin-dev-certs"); const keyPath = path.join(certDir, "localhost.key"); const certPath = path.join(certDir, "localhost.crt"); try { if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { const key = fs.readFileSync(keyPath); const cert = fs.readFileSync(certPath); https.createServer({ key, cert }, app).listen(3001, () => { console.log("✅ Backend listening on https://localhost:3001"); }); } else { throw new Error('Dev cert files not found'); } } catch (err) { console.warn('⚠️ Could not start HTTPS server on 3001:', err.message); console.warn('Starting HTTP server on port 3001 as a fallback. Update your frontend to use http://localhost:3001 if necessary.'); app.listen(3001, () => { console.log('⚠️ Backend listening on http://localhost:3001 (insecure fallback)'); }); }