Add minimal add-in playgrounds and orchestrator
This commit is contained in:
-26251
File diff suppressed because it is too large
Load Diff
Generated
+2439
File diff suppressed because it is too large
Load Diff
@@ -18,11 +18,15 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.8.4",
|
||||
"axios": "^1.13.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"hono": "^4.10.6",
|
||||
"joi": "^18.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"pino": "^10.1.0",
|
||||
"pocketbase": "^0.26.3",
|
||||
"qs": "^6.14.0",
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'dotenv/config.js';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// MSAL configuration for client credentials
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: process.env.CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
},
|
||||
};
|
||||
|
||||
const cca = new ConfidentialClientApplication(msalConfig);
|
||||
|
||||
// Endpoint to exchange On-Behalf-Of token (legacy)
|
||||
app.post('/api/obo-exchange', async (req, res) => {
|
||||
const { userToken } = req.body;
|
||||
if (!userToken) {
|
||||
return res.status(400).json({ error: 'User token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await cca.acquireTokenOnBehalfOf({
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
oboAssertion: userToken,
|
||||
});
|
||||
res.json({ token: response.accessToken });
|
||||
} catch (error) {
|
||||
console.error('OBO exchange error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Main endpoint: create folder + upload PDF
|
||||
// Uses On-Behalf-Of flow: frontend sends user token → backend exchanges for Graph token
|
||||
app.post('/api/upload-pdf', async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
||||
}
|
||||
|
||||
const userAssertionToken = authHeader.substring(7); // Remove "Bearer " prefix
|
||||
const { jobNumber, jobFullName, fileName, pdfBase64 } = req.body;
|
||||
|
||||
if (!jobNumber || !jobFullName || !fileName || !pdfBase64) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Exchange user token for Graph token using On-Behalf-Of flow
|
||||
console.log('Exchanging user token via OBO flow...');
|
||||
const tokenResponse = await cca.acquireTokenOnBehalfOf({
|
||||
oboAssertion: userAssertionToken,
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
});
|
||||
const accessToken = tokenResponse.accessToken;
|
||||
console.log('Token exchanged successfully. Proceeding with Graph API calls...');
|
||||
|
||||
const driveId = process.env.DRIVE_ID;
|
||||
const parentId = process.env.PARENT_ITEM_ID;
|
||||
|
||||
// Create main folder: "Job_Number - Job_Full_Name"
|
||||
const mainFolderName = `${jobNumber} - ${jobFullName}`;
|
||||
const createFolderUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
|
||||
const createFolderBody = {
|
||||
name: mainFolderName,
|
||||
folder: {},
|
||||
'@microsoft.graph.conflictBehavior': 'rename',
|
||||
};
|
||||
|
||||
const createFolderResp = await fetch(createFolderUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(createFolderBody),
|
||||
});
|
||||
|
||||
if (!createFolderResp.ok) {
|
||||
const errorData = await createFolderResp.text();
|
||||
console.error('Create folder error:', errorData);
|
||||
return res.status(500).json({ error: `Create folder failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const mainFolder = await createFolderResp.json();
|
||||
const mainFolderId = mainFolder.id;
|
||||
console.log(`✓ Created main folder: ${mainFolderName} (ID: ${mainFolderId})`);
|
||||
|
||||
// Create "Managers Info" subfolder
|
||||
const subfolderName = 'Managers Info';
|
||||
const createSubfolderUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${mainFolderId}/children`;
|
||||
const createSubfolderBody = {
|
||||
name: subfolderName,
|
||||
folder: {},
|
||||
'@microsoft.graph.conflictBehavior': 'rename',
|
||||
};
|
||||
|
||||
const createSubfolderResp = await fetch(createSubfolderUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(createSubfolderBody),
|
||||
});
|
||||
|
||||
if (!createSubfolderResp.ok) {
|
||||
const errorData = await createSubfolderResp.text();
|
||||
console.error('Create subfolder error:', errorData);
|
||||
return res.status(500).json({ error: `Create subfolder failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const subfolder = await createSubfolderResp.json();
|
||||
const subfolderId = subfolder.id;
|
||||
console.log(`✓ Created subfolder: ${subfolderName} (ID: ${subfolderId})`);
|
||||
|
||||
// Upload PDF to subfolder
|
||||
const pdfBuffer = Buffer.from(pdfBase64, 'base64');
|
||||
const uploadUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subfolderId}:/${encodeURIComponent(fileName)}:/content`;
|
||||
|
||||
const uploadResp = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
body: pdfBuffer,
|
||||
});
|
||||
|
||||
if (!uploadResp.ok) {
|
||||
const errorData = await uploadResp.text();
|
||||
console.error('Upload PDF error:', errorData);
|
||||
return res.status(500).json({ error: `Upload PDF failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const uploadedItem = await uploadResp.json();
|
||||
console.log(`✓ Uploaded PDF: ${fileName}`);
|
||||
|
||||
// Create share link for the subfolder
|
||||
const shareLinkUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subfolderId}/createLink`;
|
||||
const shareLinkBody = {
|
||||
type: 'view',
|
||||
scope: 'organization',
|
||||
};
|
||||
|
||||
const shareLinkResp = await fetch(shareLinkUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(shareLinkBody),
|
||||
});
|
||||
|
||||
if (!shareLinkResp.ok) {
|
||||
const errorData = await shareLinkResp.text();
|
||||
console.error('Create share link error:', errorData);
|
||||
return res.status(500).json({ error: `Create share link failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const shareLink = await shareLinkResp.json();
|
||||
console.log(`✓ Created share link`);
|
||||
|
||||
res.json({
|
||||
uploadedItem,
|
||||
shareLink: shareLink.link.webUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload PDF error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'Backend server running' });
|
||||
});
|
||||
|
||||
// Fallback to HTTP (dev server doesn't need HTTPS for localhost)
|
||||
app.listen(PORT, () => {
|
||||
console.log(`✅ Backend listening on http://localhost:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user