Add minimal add-in playgrounds and orchestrator
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Continue'
|
||||
while ($true) {
|
||||
$t = (Get-Date).ToString('o')
|
||||
try {
|
||||
$r = curl.exe -k https://localhost:3001/health 2>&1
|
||||
Write-Output "$t - $r"
|
||||
} catch {
|
||||
Write-Output "$t - ERROR: $_"
|
||||
}
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
+1402
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.8.4",
|
||||
"body-parser": "^2.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
|
||||
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)');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"jobNumber": "E2E-TEST",
|
||||
"jobFullName": "E2E Test",
|
||||
"fileName": "e2e-test.pdf",
|
||||
"pdfBase64": "JVBERi0xLjQKJYGBgYEKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9Db3VudCAxIC9LaWRzIFsgMyAwIFIgXSAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSA+PgplbmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9NZWRpYUJveCBbMCAwIDYxMiA3OTJdIC9Db250ZW50cyA0IDAgUiAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiA+PgplbmRvYmoKNCAwIG9iago8PCAvTGVuZ3RoIDY2ID4+CnN0cmVhbQpCBTAgMCB0ZiA8PC9GMSA0MCAvRjEgNDAgPj4KQk4KZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgNQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAxMTAgMDAwMDAgbiAKMDAwMDAwMDY1IDAwMDAwIG4gCjAwMDAwMDAxNjAgMDAwMDAgbiAKMDAwMDAwMDIyMCAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDUgL1Jvb3QgMSAwIFIgL0luZm8gPDwgL1N1YmplY3QgPj4gPj4Kc3RhcnR4cmVmCjI0NQolJUVPRgo="
|
||||
}
|
||||
Reference in New Issue
Block a user