Files
boc/aamos-ledger-refactor/routes/export.mjs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

115 lines
4.9 KiB
JavaScript

/**
* routes/export.mjs — Export endpoints (receipts, uploads)
*/
import { execFile } from 'child_process';
import { promisify } from 'util';
import { createWriteStream, unlinkSync } from 'fs';
const execFileAsync = promisify(execFile);
const RECEIPT_BUCKET = 'landvex-prod';
const RECEIPT_REGION = 'eu-north-1';
async function s3Put(localPath, s3Key) {
await execFileAsync('aws', [
's3', 'cp', localPath, `s3://${RECEIPT_BUCKET}/${s3Key}`,
'--region', RECEIPT_REGION,
], { env: { ...process.env, AWS_DEFAULT_REGION: RECEIPT_REGION } });
return `https://${RECEIPT_BUCKET}.s3.${RECEIPT_REGION}.amazonaws.com/${s3Key}`;
}
export default function registerExport(app, pool) {
// POST /api/ledger/journal/:id/upload — raw file upload
app.post('/api/ledger/journal/:id/upload', async (req, res) => {
const { id } = req.params;
const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group';
const filename = (req.headers['x-filename'] || 'receipt').replace(/[^a-zA-Z0-9._-]/g, '_');
const ctype = req.headers['content-type'] || 'application/octet-stream';
const localPath = `/tmp/rcpt_${Date.now()}_${filename}`;
let fileSize = 0;
try {
await new Promise((resolve, reject) => {
const ws = createWriteStream(localPath);
req.on('data', chunk => { fileSize += chunk.length; });
req.pipe(ws);
ws.on('finish', resolve);
ws.on('error', reject);
req.on('error', reject);
});
if (fileSize > 12 * 1024 * 1024) {
unlinkSync(localPath);
return res.status(400).json({ ok: false, error: 'Filen är för stor (max 12 MB)' });
}
const s3Key = `receipts/${tenant_id}/${id}/${Date.now()}_${filename}`;
const pubUrl = await s3Put(localPath, s3Key);
try { unlinkSync(localPath); } catch {}
const { rows } = await pool.query(
'SELECT metadata FROM ledger_journal_entries WHERE id=$1 AND tenant_id=$2',
[id, tenant_id]
);
if (!rows.length) return res.status(404).json({ ok: false, error: 'Verifikat ej hittat' });
const meta = rows[0].metadata || {};
const receipts = [...(meta.receipts || []), {
s3Key, filename, contentType: ctype, fileSize,
url: pubUrl,
uploaded_by: req.auth?.user_id || 'unknown',
uploaded_at: new Date().toISOString(),
}];
await pool.query(
`UPDATE ledger_journal_entries SET metadata = metadata || $1::jsonb WHERE id=$2 AND tenant_id=$3`,
[JSON.stringify({ receipts }), id, tenant_id]
);
res.json({ ok: true, filename, s3Key, url: pubUrl, receipts, count: receipts.length });
} catch (e) {
try { unlinkSync(localPath); } catch {}
res.status(500).json({ ok: false, error: e.message });
}
});
// POST /api/ledger/journal/:id/receipt — bekräfta upload
app.post('/api/ledger/journal/:id/receipt', async (req, res) => {
const { id } = req.params;
const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group';
const { s3Key, filename, contentType, fileSize } = req.body;
if (!s3Key || !filename) return res.status(400).json({ ok: false, error: 's3Key och filename krävs' });
try {
const { rows } = await pool.query(
'SELECT id, metadata FROM ledger_journal_entries WHERE id=$1 AND tenant_id=$2',
[id, tenant_id]
);
if (!rows.length) return res.status(404).json({ ok: false, error: 'Verifikat ej hittat' });
const meta = rows[0].metadata || {};
const receipts = [...(meta.receipts || []), {
s3Key, filename, contentType: contentType || 'application/octet-stream',
fileSize: fileSize || 0,
url: `https://${RECEIPT_BUCKET}.s3.${RECEIPT_REGION}.amazonaws.com/${s3Key}`,
uploaded_by: req.auth?.user_id || 'unknown',
uploaded_at: new Date().toISOString(),
}];
await pool.query(
`UPDATE ledger_journal_entries SET metadata = metadata || $1::jsonb WHERE id=$2 AND tenant_id=$3`,
[JSON.stringify({ receipts }), id, tenant_id]
);
res.json({ ok: true, receipts, count: receipts.length });
} catch (e) {
res.status(500).json({ ok: false, error: e.message });
}
});
// GET /api/ledger/journal/:id/receipts
app.get('/api/ledger/journal/:id/receipts', async (req, res) => {
const { id } = req.params;
const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group';
try {
const { rows } = await pool.query(
'SELECT metadata FROM ledger_journal_entries WHERE id=$1 AND tenant_id=$2',
[id, tenant_id]
);
if (!rows.length) return res.status(404).json({ ok: false, error: 'Ej hittat' });
const receipts = (rows[0].metadata || {}).receipts || [];
res.json({ ok: true, receipts, count: receipts.length });
} catch (e) {
res.status(500).json({ ok: false, error: e.message });
}
});
}