Files
Job-Info/officeaddin/Create Job Folder/backend/server.js
T

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)');
});
}