Files
pn-new-crm/src/app/api/v1/files/upload/route.ts

58 lines
2.0 KiB
TypeScript
Raw Normal View History

import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { errorResponse, ValidationError } from '@/lib/errors';
import { uploadFile } from '@/lib/services/files';
import { uploadFileSchema } from '@/lib/validators/files';
export const POST = withAuth(
withPermission('files', 'upload', async (req, ctx) => {
try {
const formData = await req.formData();
const file = formData.get('file') as File | null;
if (!file) {
throw new ValidationError('No file provided');
}
const buffer = Buffer.from(await file.arrayBuffer());
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
const folderIdRaw = formData.get('folderId') as string | undefined;
const metadata = uploadFileSchema.parse({
filename: (formData.get('filename') as string | null) ?? file.name,
clientId: formData.get('clientId') as string | undefined,
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
yachtId: formData.get('yachtId') as string | undefined,
companyId: formData.get('companyId') as string | undefined,
category: formData.get('category') as string | undefined,
entityType: formData.get('entityType') as string | undefined,
entityId: formData.get('entityId') as string | undefined,
feat(documents-wizard): replace UUID-paste fields with searchable pickers + inline upload Reps no longer have to copy/paste UUIDs into the New-document wizard. Three UUID inputs replaced: - Template id Input → DocumentTemplatePicker (queries /api/v1/document-templates with name search; filters to isActive=true) - Uploaded file id Input → inline FileUploadZone (drop or browse PDF; surfaces the uploaded file id directly to the wizard via the new onUploadComplete signature) - Subject id Input → conditional picker: ClientPicker / CompanyPicker / YachtPicker / InterestPicker depending on the subject-type dropdown. Reservation falls back to Input for now (no ReservationPicker yet). Other polish in the wizard: - SIGNER_ROLES labels capitalized in the role select (client → Client, etc.) via a formatSignerRole() helper. Internal values stay lowercase. - Pinned h-9 on Select triggers so the type/subject row + signer-role select vertically align with their adjacent inputs. - Subject-type change now resets subjectId — picker options are type-specific and a stale id from a different entity table would be invalid. Infrastructure for hub uploads (will be consumed in a follow-up dropdown + drag-drop pass): - /api/v1/files/upload route now parses folderId from FormData (schema already supported it). - FileUploadZone accepts a folderId prop and forwards it, plus a new onUploadComplete(file) callback shape that surfaces { id, filename } on each successful upload. Existing per-entity callers (Files tab on clients, companies, yachts, interests) ignore the arg, no behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:17:02 +02:00
// Hub uploads pass the current folderId so the file lands inside
// the user's currently-selected folder. Empty string ⇒ root (null).
folderId: folderIdRaw && folderIdRaw.length > 0 ? folderIdRaw : undefined,
});
const result = await uploadFile(
ctx.portId,
ctx.portSlug,
{
buffer,
originalName: file.name,
mimeType: file.type,
size: file.size,
},
metadata,
{
userId: ctx.userId,
portId: ctx.portId,
ipAddress: ctx.ipAddress,
userAgent: ctx.userAgent,
},
);
return NextResponse.json({ data: result }, { status: 201 });
} catch (error) {
return errorResponse(error);
}
}),
);