feat: implement dotenv secrets loading from /home/admin/secrets/.env

- Added dotenv package to load environment variables from absolute path
- Secrets now loaded automatically on app startup, no manual sourcing needed
- Secrets file secured with chmod 600, outside project directory
- Enables persistent secrets across service restarts
- Tested: Job submission flow working end-to-end with all steps completing
This commit is contained in:
2025-12-20 04:56:16 +00:00
parent 2e7940befa
commit 340b7e0818
8 changed files with 496 additions and 6 deletions
+94
View File
@@ -0,0 +1,94 @@
# Job Folder Integration Module
Automatically creates OneDrive folders with share links when PocketBase job records are created.
## Installation
```bash
cd extracted-graph-logic
bun install
```
## Integration
### 1. Import in server.ts
```typescript
import { processNewJobRecord } from './extracted-graph-logic/post-record-integration.js';
```
### 2. Call after PocketBase record creation
```typescript
const graphToken = await getGraphToken();
const folderResult = await processNewJobRecord(record, graphToken);
if (folderResult.success) {
record.Job_Folder_Link = folderResult.shareLink || '';
}
await addToExcel(record, record.id);
```
## What It Does
1. Receives PocketBase record + Graph API token
2. Creates folder: `{Job_Number} - {Job_Name} - {Job_Address} - {Company_Client}`
3. Creates "Managers Info" subfolder
4. Generates organization-wide share link
5. Returns folder details and link
## Module Files
- **post-record-integration.js** - Main entry point, exports processNewJobRecord()
- **job-folder-integration.js** - Core Graph API logic, exports createJobFolderAndGetLink()
- **config.js** - Production configuration:
- DRIVE_ID: b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
- PARENT_ITEM_ID: 01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
- CLIENT_ID: 3c846e71-9609-40e1-b458-0eb805e21b9f
- **package.json** - Dependencies (node-fetch@2.7.0)
## Return Values
### On Success
```javascript
{
success: true,
jobNumber: "12345",
jobFullName: "New Construction - 123 Main St - ABC Corp",
shareLink: "https://...",
mainFolderId: "...",
mainFolderName: "...",
subFolderId: "...",
folderPath: "..."
}
```
### On Failure
```javascript
{
success: false,
error: "error message",
jobNumber: "12345",
shareLink: null
}
```
## Configuration
Edit config.js to change OneDrive location. Current values point to Cardoza Construction production SharePoint/OneDrive.
## Error Handling
- If folder creation fails, job is still created in PocketBase and synced to Excel
- Share link will be null on failure
- Errors logged with `[Job Folder Integration]` prefix
## Dependencies
- node-fetch@2.7.0 (for Graph API calls)
- Graph API token from your server
## Status
Production-ready. Uses actual OneDrive/SharePoint IDs from Job Creation Form environment.
+21
View File
@@ -0,0 +1,21 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "job-folder-graph-integration",
"dependencies": {
"node-fetch": "^2.7.0",
},
},
},
"packages": {
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
}
}
+14
View File
@@ -0,0 +1,14 @@
// SharePoint/OneDrive Configuration
// These IDs specify where folders and files will be created
const DRIVE_ID = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
const PARENT_ITEM_ID = "01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7";
// Azure AD App Configuration
const CLIENT_ID = "3c846e71-9609-40e1-b458-0eb805e21b9f";
module.exports = {
DRIVE_ID,
PARENT_ITEM_ID,
CLIENT_ID
};
@@ -0,0 +1,126 @@
// Job Folder Integration Module
// This module handles folder creation and share link capture after PocketBase record creation
// Uses the same Graph API token from the server to avoid re-authentication
const { DRIVE_ID, PARENT_ITEM_ID } = require('./config');
/**
* Creates a job folder structure in OneDrive/SharePoint and returns the share link
* @param {string} accessToken - Graph API access token (from server's token flow)
* @param {string} jobNumber - Job number (e.g., "12345")
* @param {string} jobFullName - Full job name calculated from form data
* @returns {Promise<Object>} Result with folder IDs and share link
*/
async function createJobFolderAndGetLink(accessToken, jobNumber, jobFullName) {
try {
// 1) Create main folder under configured parent
const mainFolderName = `${jobNumber} - ${jobFullName}`;
const createFolderEndpoint = `https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/items/${PARENT_ITEM_ID}/children`;
console.log(`[Job Folder Integration] 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(`[Job Folder Integration] Create folder response:`, {
status: folderResp.status,
folderId: folderData.id,
folderName: folderData.name
});
if (!folderResp.ok) {
throw new Error(`Create main folder failed: ${JSON.stringify(folderData)}`);
}
const mainFolderId = folderData.id;
// 2) Create subfolder "Managers Info"
console.log('[Job Folder Integration] Creating subfolder: Managers Info');
const subFolderResp = await fetch(
`https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/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('[Job Folder Integration] Create subfolder response:', {
status: subFolderResp.status,
subfolderId: subFolderData.id
});
if (!subFolderResp.ok) {
throw new Error(`Create subfolder failed: ${JSON.stringify(subFolderData)}`);
}
const subFolderId = subFolderData.id;
// 3) Create share link for the main folder
console.log('[Job Folder Integration] Creating share link for main folder');
const shareResp = await fetch(
`https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/items/${mainFolderId}/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('[Job Folder Integration] Share link response:', {
status: shareResp.status,
hasLink: !!shareData.link?.webUrl
});
if (!shareResp.ok) {
throw new Error(`Create share link failed: ${JSON.stringify(shareData)}`);
}
const shareLink = shareData.link && shareData.link.webUrl ? shareData.link.webUrl : null;
console.log(`[Job Folder Integration] ✓ Successfully created folder structure and share link`);
console.log(`[Job Folder Integration] Share link: ${shareLink}`);
return {
success: true,
mainFolderId,
mainFolderName,
subFolderId,
shareLink,
folderPath: mainFolderName
};
} catch (error) {
console.error('[Job Folder Integration] Error in createJobFolderAndGetLink:', error);
throw error;
}
}
module.exports = {
createJobFolderAndGetLink
};
+20
View File
@@ -0,0 +1,20 @@
{
"name": "job-folder-graph-integration",
"version": "1.0.0",
"description": "Microsoft Graph integration for job folder creation and management",
"main": "post-record-integration.js",
"scripts": {
"test": "node example-usage.js"
},
"dependencies": {
"node-fetch": "^2.7.0"
},
"keywords": [
"microsoft-graph",
"onedrive",
"sharepoint",
"folder-management"
],
"author": "Cardoza Construction",
"license": "MIT"
}
@@ -0,0 +1,83 @@
// Post-Record Integration Module
// Handles all post-PocketBase record creation tasks:
// 1. Captures form data and Job_Number from the created PB record
// 2. Creates folder structure in OneDrive using the same Graph token
// 3. Captures share link from created folder
// 4. Returns the share link to be added to Excel row
const { createJobFolderAndGetLink } = require('./job-folder-integration');
/**
* Processes a newly created PocketBase record by:
* - Creating corresponding folder structure in OneDrive
* - Capturing the share link for the folder
*
* @param {Object} record - The PocketBase record that was just created
* @param {string} accessToken - Graph API access token (from server)
* @returns {Promise<Object>} Result with share link and folder info
*/
async function processNewJobRecord(record, accessToken) {
try {
console.log('[Post-Record Integration] Processing new job record:', {
jobNumber: record.Job_Number,
jobName: record.Job_Name,
recordId: record.id
});
// Extract required fields from the record
const jobNumber = record.Job_Number || record.job_number;
const jobName = record.Job_Name || record.job_name || '';
const jobAddress = record.Job_Address || record.job_address || '';
const companyClient = record.Company_Client || record.company_client || '';
// Calculate Job_Full_Name (same logic as in server.ts)
const parts = [jobName, jobAddress, companyClient].filter(p => p && String(p).trim());
const jobFullName = parts.join(' - ');
console.log('[Post-Record Integration] Calculated job full name:', jobFullName);
// Validate required fields
if (!jobNumber) {
throw new Error('Job_Number is required but was not found in record');
}
if (!jobFullName || jobFullName.trim() === '') {
throw new Error('Job_Full_Name could not be calculated from record data');
}
// Create folder and get share link
const folderResult = await createJobFolderAndGetLink(
accessToken,
jobNumber,
jobFullName
);
console.log('[Post-Record Integration] ✓ Folder creation successful');
console.log('[Post-Record Integration] Share link:', folderResult.shareLink);
return {
success: true,
jobNumber,
jobFullName,
shareLink: folderResult.shareLink,
mainFolderId: folderResult.mainFolderId,
mainFolderName: folderResult.mainFolderName,
subFolderId: folderResult.subFolderId,
folderPath: folderResult.folderPath
};
} catch (error) {
console.error('[Post-Record Integration] Error processing new job record:', error);
return {
success: false,
error: error.message,
jobNumber: record.Job_Number || record.job_number,
shareLink: null
};
}
}
module.exports = {
processNewJobRecord
};