Files
fourge-portal/api/filebrowser.js
Krao Hasanee 04e0911e9f fix: crash bugs in TeamInvoices + faster load
Bug fixes:
- TeamInvoices: add useAuth import/currentUser (invoice-create crash)
- TeamInvoices: setChartYear -> setExportYear (year-dropdown crash)
- TeamDashboard: drop redundant setState-in-effect (cascading renders)
- Companies: delete dead _UnusedClientCompanies (illegal hook calls)
- annotate intentional empty PDF-fallback catches

Load speed:
- preconnect/dns-prefetch to Supabase origin
- lazy-load heic-to in Converters: page chunk 2737KB -> 9KB
- split recharts into its own 'charts' vendor chunk

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:59:11 -04:00

412 lines
15 KiB
JavaScript

import { createClient } from '@supabase/supabase-js';
const FB_SOURCE = 'srv';
const PROJECT_DEFAULT_SUBFOLDERS = ['00 Project Files'];
const REQUEST_DEFAULT_SUBFOLDERS = ['Old Books', 'Working Files', 'Survey'];
export const config = { api: { bodyParser: false, sizeLimit: '50mb' } };
function json(res, status, body) {
res.status(status).setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'no-store');
res.send(JSON.stringify(body));
}
function normalizePath(path) {
const raw = String(path || '/').trim();
const parts = raw.split('/').filter(Boolean);
const clean = [];
for (const part of parts) {
if (part === '.') continue;
if (part === '..') throw new Error('Invalid path: path traversal not allowed');
clean.push(part);
}
return `/${clean.join('/')}`;
}
function joinPath(...parts) {
return normalizePath(parts.join('/'));
}
function safeName(value, fallback = '') {
const cleaned = String(value || '')
.trim()
.replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-')
.replace(/\s+/g, ' ')
.replace(/^-+|-+$/g, '');
return cleaned || fallback;
}
function parseBody(body) {
if (!body) return {};
if (typeof body === 'string') {
try {
return JSON.parse(body);
} catch {
return {};
}
}
return body;
}
async function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
function getConfig() {
const url = String(process.env.FILEBROWSER_URL || '').trim().replace(/\/+$/, '');
return {
url,
token: process.env.FILEBROWSER_TOKEN || '',
clientRoot: normalizePath(process.env.FILEBROWSER_CLIENT_ROOT || '/fourgebranding/Clients'),
subsRoot: normalizePath(process.env.FILEBROWSER_SUBS_ROOT || '/fourgebranding/team'),
configured: Boolean(url),
};
}
function getToken(config) {
const token = String(config.token || '').trim();
if (!token) throw new Error('FILEBROWSER_TOKEN not configured');
return token;
}
async function fbFetch(config, method, endpoint, { params = {}, headers = {}, body } = {}) {
const qs = new URLSearchParams({ source: FB_SOURCE, ...params }).toString();
const url = `${config.url}${endpoint}?${qs}`;
const token = getToken(config);
const response = await fetch(url, {
method,
headers: { Authorization: `Bearer ${token}`, ...headers },
body,
});
if (!response.ok) {
const text = await response.text().catch(() => '');
const error = new Error(text || `FileBrowser ${response.status}`);
error.status = response.status;
throw error;
}
const text = await response.text();
try {
return text ? JSON.parse(text) : null;
} catch {
return text;
}
}
async function createCallerClient(authHeader) {
const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.VITE_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) throw new Error('Supabase env not configured');
return createClient(supabaseUrl, supabaseAnonKey, {
auth: { persistSession: false, autoRefreshToken: false },
global: { headers: { Authorization: authHeader } },
});
}
async function requirePortalUser(authHeader) {
const callerClient = await createCallerClient(authHeader);
const { data: userData, error: userError } = await callerClient.auth.getUser();
if (userError || !userData?.user) return { ok: false, status: 401, message: 'Unauthorized' };
const { data: profile, error: profileError } = await callerClient
.from('profiles')
.select('id, role, company:companies(id, name)')
.eq('id', userData.user.id)
.single();
if (profileError) return { ok: false, status: 500, message: profileError.message };
if (!['team', 'client'].includes(profile?.role)) {
return { ok: false, status: 403, message: 'Forbidden' };
}
const clientCompanies = [];
if (profile.role === 'client') {
const seen = new Set();
if (profile.company?.id) {
clientCompanies.push(profile.company);
seen.add(profile.company.id);
}
const { data: memberships } = await callerClient
.from('company_members')
.select('company:companies(id, name)')
.eq('profile_id', userData.user.id);
for (const row of memberships || []) {
if (row.company?.id && !seen.has(row.company.id)) {
seen.add(row.company.id);
clientCompanies.push(row.company);
}
}
}
return {
ok: true,
profile: { ...profile, clientCompanies },
};
}
function createAdminClient() {
const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !serviceKey) throw new Error('Supabase admin env not configured');
return createClient(supabaseUrl, serviceKey, {
auth: { persistSession: false, autoRefreshToken: false },
});
}
function isAlreadyExistsError(error) {
const message = String(error?.message || '').toLowerCase();
return error?.status === 409 || message.includes('exist') || message.includes('conflict');
}
async function ensureDirectory(config, fullPath) {
const parts = normalizePath(fullPath).split('/').filter(Boolean);
let current = '/';
for (const part of parts) {
current = joinPath(current, part);
try {
await fbFetch(config, 'POST', '/api/resources', {
params: { path: current, isDir: 'true' },
});
} catch (error) {
if (!isAlreadyExistsError(error)) throw error;
}
}
}
async function renameDirectory(config, fromPath, toPath) {
const sourcePath = normalizePath(fromPath);
const targetPath = normalizePath(toPath);
if (sourcePath === targetPath) return { renamed: false, skipped: true, path: targetPath };
try {
await fbFetch(config, 'PATCH', '/api/resources', {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'rename',
items: [{ fromSource: FB_SOURCE, fromPath: sourcePath, toSource: FB_SOURCE, toPath: targetPath }],
overwrite: false,
}),
});
return { renamed: true, path: targetPath };
} catch (error) {
if (error?.status === 404) {
await ensureDirectory(config, targetPath);
return { renamed: false, ensured: true, path: targetPath };
}
throw error;
}
}
export default async function handler(req, res) {
const action = String(req.query?.action || '').trim();
if (req.method !== 'POST' || ![
'ensure-company-folder',
'ensure-project-folder',
'ensure-request-folder',
'ensure-profile-folder',
'upload-request-file',
'rename-company-folder',
'rename-project-folder',
'rename-profile-folder',
].includes(action)) {
return json(res, 405, { error: 'Method not allowed' });
}
const authHeader = req.headers.authorization || '';
if (!authHeader.startsWith('Bearer ')) {
return json(res, 401, { error: 'Unauthorized' });
}
const config = getConfig();
if (!config.configured) {
return json(res, 200, { ok: false, configured: false, warning: 'FileBrowser is not configured.' });
}
try {
const rawBody = await readBody(req);
const body = (() => {
if (!rawBody.length) return {};
const contentType = String(req.headers['content-type'] || '').split(';')[0].trim();
if (contentType === 'application/json') {
try {
return JSON.parse(rawBody.toString());
} catch {
return {};
}
}
return {};
})();
const auth = await requirePortalUser(authHeader);
if (!auth.ok) return json(res, auth.status, { error: auth.message });
const admin = createAdminClient();
if (action === 'ensure-company-folder') {
if (auth.profile.role !== 'team') {
return json(res, 403, { error: 'Forbidden' });
}
const companyId = String(body.companyId || '').trim();
if (!companyId) return json(res, 400, { error: 'companyId is required.' });
const { data: company, error: companyError } = await admin
.from('companies')
.select('id, name')
.eq('id', companyId)
.single();
if (companyError || !company) return json(res, 404, { error: 'Company not found.' });
const fullPath = joinPath(config.clientRoot, safeName(company.name, company.id));
await ensureDirectory(config, fullPath);
return json(res, 200, { ok: true, configured: true, path: fullPath });
}
if (action === 'rename-company-folder') {
if (auth.profile.role !== 'team') {
return json(res, 403, { error: 'Forbidden' });
}
const companyId = String(body.companyId || '').trim();
const oldName = safeName(body.oldName, '');
const newName = safeName(body.newName, '');
if (!companyId || !oldName || !newName) return json(res, 400, { error: 'companyId, oldName, and newName are required.' });
const { data: company, error: companyError } = await admin
.from('companies')
.select('id')
.eq('id', companyId)
.single();
if (companyError || !company) return json(res, 404, { error: 'Company not found.' });
const result = await renameDirectory(config, joinPath(config.clientRoot, oldName), joinPath(config.clientRoot, newName));
return json(res, 200, { ok: true, configured: true, ...result });
}
if (action === 'ensure-profile-folder') {
if (auth.profile.role !== 'team') {
return json(res, 403, { error: 'Forbidden' });
}
const profileId = String(body.profileId || '').trim();
if (!profileId) return json(res, 400, { error: 'profileId is required.' });
const { data: profile, error: profileError } = await admin
.from('profiles')
.select('id, name, role')
.eq('id', profileId)
.single();
if (profileError || !profile) return json(res, 404, { error: 'Profile not found.' });
if (profile.role !== 'external') return json(res, 400, { error: 'Only subcontractor folders are supported here.' });
const fullPath = joinPath(config.subsRoot, safeName(profile.name, profile.id));
await ensureDirectory(config, fullPath);
return json(res, 200, { ok: true, configured: true, path: fullPath });
}
if (action === 'rename-profile-folder') {
if (auth.profile.role !== 'team') {
return json(res, 403, { error: 'Forbidden' });
}
const profileId = String(body.profileId || '').trim();
const oldName = safeName(body.oldName, '');
const newName = safeName(body.newName, '');
if (!profileId || !oldName || !newName) return json(res, 400, { error: 'profileId, oldName, and newName are required.' });
const { data: profile, error: profileError } = await admin
.from('profiles')
.select('id, role')
.eq('id', profileId)
.single();
if (profileError || !profile) return json(res, 404, { error: 'Profile not found.' });
if (profile.role !== 'external') return json(res, 400, { error: 'Only subcontractor folders are supported here.' });
const result = await renameDirectory(config, joinPath(config.subsRoot, oldName), joinPath(config.subsRoot, newName));
return json(res, 200, { ok: true, configured: true, ...result });
}
const projectId = String(req.query?.projectId || body.projectId || '').trim();
if (!projectId) return json(res, 400, { error: 'projectId is required.' });
const { data: project, error: projectError } = await admin
.from('projects')
.select('id, name, company_id, company:companies(id, name)')
.eq('id', projectId)
.single();
if (projectError || !project) return json(res, 404, { error: 'Project not found.' });
if (auth.profile.role === 'client') {
const allowedCompanyIds = new Set((auth.profile.clientCompanies || []).map(company => company.id));
if (!allowedCompanyIds.has(project.company_id)) return json(res, 403, { error: 'Forbidden' });
}
const companyName = safeName(project.company?.name, String(project.company_id || 'company'));
const projectName = safeName(project.name, projectId);
if (action === 'rename-project-folder') {
const oldName = safeName(body.oldName, '');
const newName = safeName(body.newName, '');
if (!oldName || !newName) return json(res, 400, { error: 'oldName and newName are required.' });
const result = await renameDirectory(
config,
joinPath(config.clientRoot, companyName, 'Projects', oldName),
joinPath(config.clientRoot, companyName, 'Projects', newName)
);
return json(res, 200, { ok: true, configured: true, ...result });
}
if (action === 'ensure-project-folder') {
const fullPath = joinPath(config.clientRoot, companyName, 'Projects', projectName);
await ensureDirectory(config, fullPath);
for (const folderName of PROJECT_DEFAULT_SUBFOLDERS) {
await ensureDirectory(config, joinPath(fullPath, folderName));
}
return json(res, 200, { ok: true, configured: true, path: fullPath });
}
const taskTitle = safeName(req.query?.taskTitle || body.taskTitle, '');
if (!taskTitle) return json(res, 400, { error: 'taskTitle is required.' });
const fullPath = joinPath(config.clientRoot, companyName, 'Projects', projectName, taskTitle);
if (action === 'upload-request-file') {
const bucket = String(body.bucket || 'submissions').trim();
const storagePath = String(body.storagePath || '').trim();
const fileName = safeName(body.fileName || req.query?.fileName || req.headers['x-file-name'], '');
if (!storagePath) return json(res, 400, { error: 'storagePath is required.' });
if (!fileName) return json(res, 400, { error: 'fileName is required.' });
const surveyPath = joinPath(fullPath, 'Survey');
await ensureDirectory(config, surveyPath);
const { data: downloadedFile, error: downloadError } = await admin.storage.from(bucket).download(storagePath);
if (downloadError || !downloadedFile) {
return json(res, 500, { error: downloadError?.message || 'Failed to download request file from storage.' });
}
const fileBuffer = Buffer.from(await downloadedFile.arrayBuffer());
const uploadUrl = `${config.url}/api/resources?source=${FB_SOURCE}&path=${encodeURIComponent(joinPath(surveyPath, fileName))}&override=true`;
const contentType = String(downloadedFile.type || '').trim() || 'application/octet-stream';
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${getToken(config)}`,
'Content-Type': contentType,
},
body: fileBuffer,
});
if (!response.ok) {
const text = await response.text().catch(() => '');
return json(res, response.status, { error: text || `Upload failed (${response.status})` });
}
return json(res, 200, { ok: true, configured: true, path: joinPath(surveyPath, fileName) });
}
await ensureDirectory(config, fullPath);
for (const folderName of REQUEST_DEFAULT_SUBFOLDERS) {
await ensureDirectory(config, joinPath(fullPath, folderName));
}
return json(res, 200, { ok: true, configured: true, path: fullPath });
} catch (error) {
return json(res, error?.status || 500, { error: error?.message || 'Failed to ensure request folder.' });
}
}