pelagia-portal/App/lib/storage.ts
Hardik 1071cb226f feat(po): per-company logo, stamp & brand bar on exported POs
Companies can upload a logo and a stamp/seal (Admin → Companies → Edit →
Branding); both render on exported PDF and XLSX purchase orders. A fixed
brand-colour bar (#92D050, matching the sample PO) runs along the bottom of
every export.

- Company.logoKey / stampKey + migration
- buildCompanyAssetKey() deterministic storage keys (overwrite-in-place)
- uploadCompanyAsset / removeCompanyAsset server actions (≤4MB PNG/JPG/WebP,
  manage_vessels_accounts gated)
- CompanyBrandingUploader in the company edit dialog with live previews
- Export route embeds logo (top-left), stamp (signatory block) and brand bar
  in both ExcelJS and print-HTML paths
- Unit test (storage keys) + integration test (branding actions)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:17:23 +05:30

126 lines
3.6 KiB
TypeScript

import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const isDev = process.env.NODE_ENV === "development";
function getR2Client() {
return new S3Client({
region: "auto",
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
}
const DEV_BASE_URL = process.env.NEXTAUTH_URL ?? "http://localhost:3000";
export async function generateUploadUrl(
key: string,
contentType: string,
expiresIn = 300
): Promise<string> {
if (isDev) {
return `${DEV_BASE_URL}/api/files/dev/${key}`;
}
const command = new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME!,
Key: key,
ContentType: contentType,
});
return getSignedUrl(getR2Client(), command, { expiresIn });
}
export async function generateDownloadUrl(
key: string,
expiresIn = 3600
): Promise<string> {
if (isDev) {
return `${DEV_BASE_URL}/api/files/dev/${key}`;
}
const command = new GetObjectCommand({ Bucket: process.env.R2_BUCKET_NAME!, Key: key });
return getSignedUrl(getR2Client(), command, { expiresIn });
}
export function buildStorageKey(
type: "po-document" | "receipt",
poId: string,
fileName: string
): string {
const timestamp = Date.now();
const safe = fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
return `${type}/${poId}/${timestamp}-${safe}`;
}
export function buildSignatureKey(userId: string, ext: string): string {
return `signatures/${userId}.${ext}`;
}
/**
* Storage key for a company branding asset (logo or stamp/seal).
* Deterministic per company+type so a re-upload overwrites the previous file.
*/
export function buildCompanyAssetKey(
companyId: string,
type: "logo" | "stamp",
ext: string
): string {
return `company-assets/${companyId}/${type}.${ext}`;
}
/**
* Upload a file buffer directly to storage (server-side).
* In dev: writes to .dev-uploads/. In prod: PUTs to R2.
*/
export async function uploadBuffer(
key: string,
buffer: Buffer,
contentType: string
): Promise<void> {
if (isDev) {
const fs = await import("fs/promises");
const path = await import("path");
const dir = path.join(process.cwd(), ".dev-uploads", ...key.split("/").slice(0, -1));
const filePath = path.join(process.cwd(), ".dev-uploads", ...key.split("/"));
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, buffer);
} else {
const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
const s3 = new S3Client({
region: "auto",
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
await s3.send(new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME!,
Key: key,
Body: buffer,
ContentType: contentType,
}));
}
}
/**
* Fetch a stored file as a Buffer (server-side).
*/
export async function downloadBuffer(key: string): Promise<Buffer | null> {
try {
if (isDev) {
const fs = await import("fs/promises");
const path = await import("path");
const filePath = path.join(process.cwd(), ".dev-uploads", ...key.split("/"));
return await fs.readFile(filePath) as Buffer;
} else {
const url = await generateDownloadUrl(key, 60);
const res = await fetch(url);
if (!res.ok) return null;
return Buffer.from(await res.arrayBuffer());
}
} catch {
return null;
}
}