import fs from 'fs' import path from 'path' import crypto from 'crypto' const APP_PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..') const APP_SOURCE_ROOT = path.join(APP_PACKAGE_ROOT, 'src') const APP_DIST_ROOT = path.join(APP_PACKAGE_ROOT, 'dist') const DEFAULT_STORAGE_ROOT = path.join(APP_PACKAGE_ROOT, 'storage') const LEGACY_STORAGE_ROOTS = [ path.join(APP_SOURCE_ROOT, 'lib', 'storage'), ] export type StorageVisibility = 'public' | 'private' export function getStorageRoot(): string { return process.env.FILE_STORAGE_ROOT ? path.resolve(process.env.FILE_STORAGE_ROOT) : DEFAULT_STORAGE_ROOT } export function getPublicStorageRoot(): string { return path.join(getStorageRoot(), 'public') } export function getPrivateStorageRoot(): string { return path.join(getStorageRoot(), 'private') } export function getLegacyStorageRoots(): string[] { return LEGACY_STORAGE_ROOTS } function isWithinPath(targetPath: string, parentPath: string): boolean { const relative = path.relative(parentPath, targetPath) return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)) } export function assertStorageConfiguration(): string { const storageRoot = getStorageRoot() if (process.env.NODE_ENV === 'production' && !process.env.FILE_STORAGE_ROOT) { throw new Error('FILE_STORAGE_ROOT must be set in production so uploads are stored on the mounted volume.') } if (process.env.NODE_ENV === 'production') { const forbiddenRoots = [APP_PACKAGE_ROOT, APP_SOURCE_ROOT, APP_DIST_ROOT] const invalidRoot = forbiddenRoots.find((root) => isWithinPath(storageRoot, root)) if (invalidRoot) { throw new Error( `FILE_STORAGE_ROOT must point outside the API app tree in production. Received ${storageRoot}, which is inside ${invalidRoot}.` ) } } return storageRoot } function ensureStorageRoot(visibility: StorageVisibility): string { assertStorageConfiguration() const root = visibility === 'public' ? getPublicStorageRoot() : getPrivateStorageRoot() fs.mkdirSync(root, { recursive: true }) return root } function inferVisibility(folder: string): StorageVisibility { const normalized = folder.replace(/\\/g, '/') if (/\/customers\//.test(`/${normalized}/`)) return 'private' if (/\/licenses?\//.test(`/${normalized}/`)) return 'private' if (/\/contracts?\//.test(`/${normalized}/`)) return 'private' if (/\/documents?\//.test(`/${normalized}/`)) return 'private' if (/\/inspections?\//.test(`/${normalized}/`)) return 'private' if (/\/internal\//.test(`/${normalized}/`)) return 'private' return 'public' } function getApiBase(): string { return (process.env.API_URL ?? 'http://localhost:4000').replace(/\/$/, '') } function getDashboardBase(): string { return ( process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard' ).replace(/\/$/, '') } function getStorageRelativePath(imageUrl: string): string | null { const marker = '/storage/' const idx = imageUrl.indexOf(marker) if (idx === -1) return null return imageUrl.slice(idx + marker.length) } export function getProtectedCustomerLicenseImageUrl(customerId: string): string { return `${getDashboardBase()}/api/v1/customers/${customerId}/license-image` } export async function uploadImage( buffer: Buffer, folder: string, publicId?: string, visibility: StorageVisibility = inferVisibility(folder), ): Promise { const storageRoot = ensureStorageRoot(visibility) const folderPath = path.join(storageRoot, folder) if (!isWithinPath(folderPath, storageRoot)) { throw new Error('Upload path escapes storage root') } fs.mkdirSync(folderPath, { recursive: true }) const filename = publicId ? `${publicId}.jpg` : `${crypto.randomBytes(16).toString('hex')}.jpg` fs.writeFileSync(path.join(folderPath, filename), buffer) return `${getApiBase()}/storage/${folder}/${filename}` } export function resolveStoredFilePath(imageUrl: string): string | null { const relative = getStorageRelativePath(imageUrl) if (!relative) return null const roots = [getPublicStorageRoot(), getPrivateStorageRoot(), ...getLegacyStorageRoots(), assertStorageConfiguration()] for (const root of roots) { const filePath = path.join(root, relative) if (!isWithinPath(filePath, root)) continue if (fs.existsSync(filePath)) { return filePath } } return null } export async function deleteImage(imageUrl: string): Promise { const filePath = resolveStoredFilePath(imageUrl) if (filePath && fs.existsSync(filePath)) { fs.unlinkSync(filePath) } }