217 lines
9.1 KiB
JavaScript
217 lines
9.1 KiB
JavaScript
|
|
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 <token>
|
|
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)');
|
|
});
|
|
}
|