feat: upload any validated file to the asset library

Activos now supports multi-file/folder upload to private Storage
(PDF, Excel, CSV, images, ZIP, etc.) with per-tenant folders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-22 15:26:33 +02:00
parent 8182402d96
commit ce9498e63c
9 changed files with 931 additions and 63 deletions

68
src/lib/assets.ts Normal file
View File

@@ -0,0 +1,68 @@
import { createClient } from '@supabase/supabase-js';
import {
serviceHeaders,
supabaseUrl,
} from '@/lib/agency';
export const ASSETS_BUCKET = 'tenant-assets';
export const MAX_ASSET_BYTES = 80 * 1024 * 1024; // 80 MB
export function serviceSupabase() {
const key = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim();
if (!key) return null;
return createClient(supabaseUrl(), key, {
auth: { persistSession: false, autoRefreshToken: false },
});
}
export async function ensureAssetsBucket(): Promise<{ ok: boolean; error?: string }> {
const headers = serviceHeaders();
if (!headers) return { ok: false, error: 'no service role' };
const list = await fetch(`${supabaseUrl()}/storage/v1/bucket`, {
headers,
cache: 'no-store',
});
if (list.ok) {
const buckets = (await list.json()) as { id: string }[];
if (buckets.some((b) => b.id === ASSETS_BUCKET)) {
return { ok: true };
}
}
const create = await fetch(`${supabaseUrl()}/storage/v1/bucket`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
id: ASSETS_BUCKET,
name: ASSETS_BUCKET,
public: false,
file_size_limit: MAX_ASSET_BYTES,
allowed_mime_types: null,
}),
});
if (!create.ok) {
const text = await create.text();
// race: already exists
if (text.toLowerCase().includes('already') || create.status === 409) {
return { ok: true };
}
return { ok: false, error: text.slice(0, 300) };
}
return { ok: true };
}
export function sanitizeFolder(raw: string): string {
return raw
.trim()
.replace(/\\/g, '/')
.replace(/\.\./g, '')
.replace(/^\/+|\/+$/g, '')
.replace(/[^a-zA-Z0-9/_ .-]/g, '-')
.slice(0, 180);
}
export function sanitizeFileName(name: string): string {
const base = name.split(/[/\\]/).pop() || 'file';
return base.replace(/[^\w.\- ()áéíóúÁÉÍÓÚñÑ]+/gi, '_').slice(0, 180);
}