Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
SUPABASE_URL=
SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
SUPABASE_BUCKET=financial-documents

NOTION_TOKEN=
NOTION_DATABASE_ID=

GOOGLE_PROJECT_ID=
GOOGLE_CLIENT_EMAIL=
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
GOOGLE_DRIVE_FOLDER_ID=

PORT=3000
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,49 @@ A comprehensive workspace application that combines credit analysis tools for co
- **File Upload**: Multer middleware
- **OCR**: Framework ready for integration with tesseract.js or similar

## Integrations

The app can integrate with Supabase Storage, Notion, and Google Drive. Use the checkboxes in the UI to opt-in per action. Availability is shown by enabling/disabling those checkboxes based on server configuration.

### Environment Variables

Copy `.env.example` to `.env` and fill values:

```bash
cp .env.example .env
```

- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (or `SUPABASE_ANON_KEY`): Supabase project credentials
- `SUPABASE_BUCKET`: Storage bucket name (default: `financial-documents`)
- `NOTION_TOKEN`, `NOTION_DATABASE_ID`: Notion internal integration token and database ID
- `GOOGLE_CLIENT_EMAIL`, `GOOGLE_PRIVATE_KEY`, `GOOGLE_DRIVE_FOLDER_ID`: Google service account and a target folder ID

Notes:
- For `GOOGLE_PRIVATE_KEY`, keep it on one line with `\n` for newlines, e.g. `"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"`.
- The app automatically converts `\n` to real newlines at runtime.

### Supabase Setup
- Create a Supabase project and a Storage bucket (e.g. `financial-documents`).
- Optionally make the bucket public if you need public links, or keep private and serve via signed URLs (current code uses public URLs).
- Get the Service Role key for server-side uploads and set `SUPABASE_SERVICE_ROLE_KEY`.

### Notion Setup
- Create a Notion internal integration and get the token.
- Create a database (or use an existing one) and share it with the integration.
- Copy the database ID to `NOTION_DATABASE_ID`.

### Google Drive Setup
- Create a Google Cloud project and enable the Drive API.
- Create a Service Account and generate a JSON key.
- Use the service account email to share a target Drive folder (or its parent) with at least `Writer` access, and set that folder's ID in `GOOGLE_DRIVE_FOLDER_ID`.
- Put the JSON key fields into `.env`:
- `GOOGLE_CLIENT_EMAIL`
- `GOOGLE_PRIVATE_KEY` (escaped with `\n` as above)

### Runtime
- Start the server and visit `/api/integrations` to see booleans for configured services.
- In the UI, checkboxes will be disabled if a service is not configured.

## Installation

1. Clone the repository:
Expand Down
155 changes: 155 additions & 0 deletions config/integrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use strict';

require('dotenv').config();

const { createClient } = require('@supabase/supabase-js');
const { Client: NotionClient } = require('@notionhq/client');
const { google } = require('googleapis');

const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const supabaseBucket = process.env.SUPABASE_BUCKET || 'financial-documents';

const notionToken = process.env.NOTION_TOKEN;
const notionDatabaseId = process.env.NOTION_DATABASE_ID;

const googleProjectId = process.env.GOOGLE_PROJECT_ID;
const googleClientEmail = process.env.GOOGLE_CLIENT_EMAIL;
let googlePrivateKey = process.env.GOOGLE_PRIVATE_KEY;
if (googlePrivateKey && googlePrivateKey.includes('\\n')) {
googlePrivateKey = googlePrivateKey.replace(/\\n/g, '\n');
Comment on lines +20 to +21

Copilot AI Sep 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The private key processing logic should handle all common escape sequences, not just \\n. Consider also handling \\r\\n and other whitespace characters that might be escaped in environment variables.

Suggested change
if (googlePrivateKey && googlePrivateKey.includes('\\n')) {
googlePrivateKey = googlePrivateKey.replace(/\\n/g, '\n');
if (googlePrivateKey) {
// Unescape all common escape sequences (e.g., \n, \r, \t, etc.)
googlePrivateKey = JSON.parse('"' + googlePrivateKey.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"');

Copilot uses AI. Check for mistakes.
}
const googleDriveFolderId = process.env.GOOGLE_DRIVE_FOLDER_ID;

const supabaseKeyToUse = supabaseServiceKey || supabaseAnonKey;
const supabase = (supabaseUrl && supabaseKeyToUse) ? createClient(supabaseUrl, supabaseKeyToUse) : null;

const notion = notionToken ? new NotionClient({ auth: notionToken }) : null;

let drive = null;
if (googleClientEmail && googlePrivateKey) {
const jwt = new google.auth.JWT({
email: googleClientEmail,
key: googlePrivateKey,
scopes: ['https://www.googleapis.com/auth/drive.file'],
subject: undefined,
});
drive = google.drive({ version: 'v3', auth: jwt });
}

const integrations = {
supabase,
supabaseBucket,
notion,
notionDatabaseId,
drive,
googleDriveFolderId,
isSupabaseConfigured: Boolean(supabase),
isNotionConfigured: Boolean(notion),
isGoogleDriveConfigured: Boolean(drive),
};

async function uploadFileToSupabase(localFilePath, destinationPath, mimeType) {
if (!supabase) {
throw new Error('Supabase is not configured');
}
const fs = require('fs').promises;
const buffer = await fs.readFile(localFilePath);
const { data, error } = await supabase.storage.from(supabaseBucket).upload(destinationPath, buffer, {
contentType: mimeType || 'application/octet-stream',
upsert: true,
});
if (error) {
throw error;
}
const { data: publicUrlData } = supabase.storage.from(supabaseBucket).getPublicUrl(destinationPath);
return { path: data.path, publicUrl: publicUrlData.publicUrl };
}

async function uploadFileToGoogleDrive(localFilePath, destinationName, mimeType) {
if (!drive) {
throw new Error('Google Drive is not configured');
}
const fs = require('fs');
const fileMetadata = {
name: destinationName,
parents: googleDriveFolderId ? [googleDriveFolderId] : undefined,
};
const media = {
mimeType: mimeType || 'application/octet-stream',
body: fs.createReadStream(localFilePath),
};
const res = await drive.files.create({
requestBody: fileMetadata,
media,
fields: 'id, name, webViewLink, webContentLink',
});
const fileId = res.data.id;
try {
await drive.permissions.create({
fileId,
requestBody: { role: 'reader', type: 'anyone' },
});
} catch (e) {}

Copilot AI Sep 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch block silently ignores permission setting failures. Consider logging the error or at least adding a comment explaining why this failure is acceptable.

Suggested change
} catch (e) {}
} catch (e) {
console.error('Failed to set Google Drive file permissions for fileId:', fileId, e);
}

Copilot uses AI. Check for mistakes.
const linkRes = await drive.files.get({
fileId,
fields: 'id, name, webViewLink, webContentLink',
});
return linkRes.data;
}

async function exportCreditMemoToNotion(memo) {
if (!notion) {
throw new Error('Notion is not configured');
}
if (!notionDatabaseId) {
throw new Error('NOTION_DATABASE_ID is not set');
}
const page = await notion.pages.create({
parent: { database_id: notionDatabaseId },
properties: {
Title: {
title: [{ text: { content: memo.title || 'Credit Memo' } }],
},
MemoType: {
rich_text: [{ text: { content: memo.memo_type || '' } }],
},
CompanyId: {
number: Number(memo.company_id) || null,
},
CreatedAt: {
date: { start: new Date().toISOString() },
},
},
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: memo.content || '' } }],
},
},
...(memo.financial_metrics
? [
{
object: 'block',
type: 'code',
code: {
language: 'json',
rich_text: [{ type: 'text', text: { content: typeof memo.financial_metrics === 'string' ? memo.financial_metrics : JSON.stringify(memo.financial_metrics, null, 2) } }],
},
},
]
: []),
],
});
return page;
}

module.exports = {
integrations,
uploadFileToSupabase,
uploadFileToGoogleDrive,
exportCreditMemoToNotion,
};
Loading