71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
|
|
/**
|
||
|
|
* Returns a presigned URL the browser can use to PUT a PDF directly to the
|
||
|
|
* active storage backend. The URL is constrained by content-length-range up
|
||
|
|
* to `system_settings.berth_pdf_max_upload_mb` (default 15 MB) per §11.1.
|
||
|
|
*
|
||
|
|
* For S3 backends this is a true signed URL; for filesystem backends it's a
|
||
|
|
* CRM-internal proxy URL with an HMAC token (see `FilesystemBackend`).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { NextResponse } from 'next/server';
|
||
|
|
|
||
|
|
import { type RouteHandler } from '@/lib/api/helpers';
|
||
|
|
import { db } from '@/lib/db';
|
||
|
|
import { berths } from '@/lib/db/schema/berths';
|
||
|
|
import { eq } from 'drizzle-orm';
|
||
|
|
import { errorResponse, NotFoundError, ValidationError } from '@/lib/errors';
|
||
|
|
import { getMaxUploadMb } from '@/lib/services/berth-pdf.service';
|
||
|
|
import { getStorageBackend } from '@/lib/storage';
|
||
|
|
|
||
|
|
interface PostBody {
|
||
|
|
fileName: string;
|
||
|
|
/** Size hint in bytes — used to early-reject oversized uploads before we
|
||
|
|
* burn a presigned URL. */
|
||
|
|
sizeBytes?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const postHandler: RouteHandler = async (req, _ctx, params) => {
|
||
|
|
try {
|
||
|
|
const body = (await req.json()) as Partial<PostBody>;
|
||
|
|
const fileName = (body.fileName ?? '').trim();
|
||
|
|
if (!fileName) throw new ValidationError('fileName is required');
|
||
|
|
|
||
|
|
const berthRow = await db.query.berths.findFirst({ where: eq(berths.id, params.id!) });
|
||
|
|
if (!berthRow) throw new NotFoundError('Berth');
|
||
|
|
|
||
|
|
const maxMb = await getMaxUploadMb(berthRow.portId);
|
||
|
|
const maxBytes = maxMb * 1024 * 1024;
|
||
|
|
if (typeof body.sizeBytes === 'number' && body.sizeBytes > maxBytes) {
|
||
|
|
throw new ValidationError(
|
||
|
|
`File exceeds ${maxMb} MB upload cap (got ${(body.sizeBytes / 1024 / 1024).toFixed(1)} MB).`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Provisional version number: the actual row insert happens in POST
|
||
|
|
// /pdf-versions and re-computes via SELECT max+1 inside a transaction,
|
||
|
|
// so a race between two reps just shifts which one wins the version
|
||
|
|
// slot. The storage key is gen_random_uuid()-namespaced so collisions
|
||
|
|
// in the storage layer are impossible.
|
||
|
|
const sanitized = fileName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 200) || 'berth.pdf';
|
||
|
|
const storageKey = `berths/${params.id!}/uploads/${crypto.randomUUID()}_${sanitized}`;
|
||
|
|
|
||
|
|
const backend = await getStorageBackend();
|
||
|
|
const presigned = await backend.presignUpload(storageKey, {
|
||
|
|
contentType: 'application/pdf',
|
||
|
|
expirySeconds: 900,
|
||
|
|
});
|
||
|
|
|
||
|
|
return NextResponse.json({
|
||
|
|
data: {
|
||
|
|
url: presigned.url,
|
||
|
|
method: presigned.method,
|
||
|
|
storageKey,
|
||
|
|
maxBytes,
|
||
|
|
backend: backend.name,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
return errorResponse(error);
|
||
|
|
}
|
||
|
|
};
|