relv-space4 / src /server /routes /reports.ts
relv-dev's picture
Deploy latest verified Turnitin worker
521a9b6 verified
Raw
History Blame Contribute Delete
4.53 kB
import { Router, Response } from 'express';
import { authenticateUser, AuthenticatedRequest } from '../middleware/auth';
import { getJobById } from '../../db/jobs';
import { createSignedUrl } from '../../db/storage';
import { config } from '../../config';
import { logger } from '../../utils/logger';
const router = Router();
function buildReportDownloadName(inputFileName: string): string {
const base = String(inputFileName || 'report')
.replace(/\.[^.]+$/, '')
.replace(/[\\/:*?"<>|]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 140) || 'report';
return `RelVDev_${base}.pdf`;
}
function buildReceiptDownloadName(inputFileName: string): string {
const base = String(inputFileName || 'receipt')
.replace(/\.[^.]+$/, '')
.replace(/[\\/:*?"<>|]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 132) || 'receipt';
return `RelVDev_Receipt_${base}.pdf`;
}
/**
* GET /api/jobs/:jobId/report-url
*
* Creates a short-lived signed URL for a completed report PDF. The browser
* should not sign private storage objects directly because storage RLS can hide
* private files as "not found"; this endpoint verifies job ownership first and
* then signs with the backend service role.
*/
router.get(
'/api/jobs/:jobId/report-url',
authenticateUser,
async (req, res: Response): Promise<void> => {
const authReq = req as AuthenticatedRequest;
const jobId = String(authReq.params.jobId || '');
try {
const job = await getJobById(jobId);
if (!job) {
res.status(404).json({ error: 'Job not found' });
return;
}
if (job.user_id !== authReq.userId) {
res.status(403).json({ error: 'Forbidden' });
return;
}
if (!job.output_pdf_path) {
res.status(404).json({ error: 'Report PDF is not available yet' });
return;
}
if (
job.output_pdf_expires_at &&
new Date(job.output_pdf_expires_at).getTime() < Date.now()
) {
res.status(410).json({ error: 'Report PDF has expired' });
return;
}
const signedUrl = await createSignedUrl(
config.reportBucket,
job.output_pdf_path,
300,
buildReportDownloadName(job.input_file_name),
);
res.json({
signedUrl,
fileName: buildReportDownloadName(job.input_file_name),
expiresIn: 300,
expiresAt: new Date(Date.now() + 300 * 1000).toISOString(),
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
logger.error('Failed to create report signed URL', {
jobId,
userId: authReq.userId,
error: message,
});
res.status(500).json({ error: 'Failed to create report download URL' });
}
},
);
/**
* GET /api/jobs/:jobId/receipt-url
*
* Creates a short-lived signed URL for a legacy Digital Receipt PDF.
*/
router.get(
'/api/jobs/:jobId/receipt-url',
authenticateUser,
async (req, res: Response): Promise<void> => {
const authReq = req as AuthenticatedRequest;
const jobId = String(authReq.params.jobId || '');
try {
const job = await getJobById(jobId);
if (!job) {
res.status(404).json({ error: 'Job not found' });
return;
}
if (job.user_id !== authReq.userId) {
res.status(403).json({ error: 'Forbidden' });
return;
}
if (!job.receipt_pdf_path) {
res.status(404).json({ error: 'Digital Receipt is not available yet' });
return;
}
if (
job.receipt_pdf_expires_at &&
new Date(job.receipt_pdf_expires_at).getTime() < Date.now()
) {
res.status(410).json({ error: 'Digital Receipt has expired' });
return;
}
const fileName = buildReceiptDownloadName(job.input_file_name);
const signedUrl = await createSignedUrl(
config.reportBucket,
job.receipt_pdf_path,
300,
fileName,
);
res.json({
signedUrl,
fileName,
expiresIn: 300,
expiresAt: new Date(Date.now() + 300 * 1000).toISOString(),
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
logger.error('Failed to create Digital Receipt signed URL', {
jobId,
userId: authReq.userId,
error: message,
});
res.status(500).json({ error: 'Failed to create Digital Receipt download URL' });
}
},
);
export default router;