import { createClient } from '@supabase/supabase-js'; const FB_SOURCE = 'srv'; function normalizePath(path) { const parts = String(path || '/').trim().split('/').filter(p => p && p !== '.' && p !== '..'); return `/${parts.join('/')}`; } function joinPath(...parts) { return normalizePath(parts.join('/')); } function safeName(v) { return String(v || '').trim() .replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-') .replace(/\s+/g, ' ') .replace(/^-+|-+$/g, ''); } function getConfig() { const url = String(process.env.FILEBROWSER_URL || '').trim().replace(/\/+$/, ''); return { url, token: process.env.FILEBROWSER_TOKEN || '', adminUser: process.env.FILEBROWSER_ADMIN_USER || '', adminPass: process.env.FILEBROWSER_ADMIN_PASS || '', clientRoot: normalizePath(process.env.FILEBROWSER_CLIENT_ROOT || '/Clients'), configured: Boolean(url), }; } let _cachedToken = ''; let _cachedTokenTs = 0; async function getToken(config) { if (config.token) return config.token; const now = Date.now(); if (_cachedToken && now - _cachedTokenTs < 25 * 60 * 1000) return _cachedToken; const res = await fetch(`${config.url}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: config.adminUser, password: config.adminPass }), }); const data = await res.json().catch(() => ({})); const tok = data?.token || data?.access_token || ''; if (tok) { _cachedToken = tok; _cachedTokenTs = now; } return tok; } async function fbFetch(config, method, endpoint, { params = {}, headers = {}, body } = {}) { const token = await getToken(config); const qs = new URLSearchParams({ source: FB_SOURCE, ...params }).toString(); const res = await fetch(`${config.url}${endpoint}?${qs}`, { method, headers: { Authorization: `Bearer ${token}`, ...headers }, body, }); if (res.status === 401) { _cachedToken = ''; const fresh = await getToken(config); return fetch(`${config.url}${endpoint}?${qs}`, { method, headers: { Authorization: `Bearer ${fresh}`, ...headers }, body, }); } return res; } async function listDir(config, path) { const res = await fbFetch(config, 'GET', '/api/resources', { params: { path } }); if (!res.ok) return null; const data = await res.json().catch(() => null); const entries = Array.isArray(data?.entries) ? data.entries : [...(data?.folders ?? []), ...(data?.files ?? [])]; return entries; } async function renameDir(config, fromPath, toPath) { const res = await fbFetch(config, 'PATCH', '/api/resources', { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'rename', items: [{ fromSource: FB_SOURCE, fromPath, toSource: FB_SOURCE, toPath }], overwrite: true }), }); return res.ok; } async function deleteDir(config, path) { const res = await fbFetch(config, 'DELETE', '/api/resources', { params: { path } }); return res.ok; } export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).json({ error: 'POST only' }); const authHeader = req.headers.authorization || ''; const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL; const supabaseKey = process.env.VITE_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY; const isServiceRole = serviceKey && authHeader === `Bearer ${serviceKey}`; if (!isServiceRole) { const sb = createClient(supabaseUrl, supabaseKey, { auth: { persistSession: false }, global: { headers: { Authorization: authHeader } }, }); const { data: { user } } = await sb.auth.getUser(); if (!user) return res.status(401).json({ error: 'Unauthorized' }); const { data: profile } = await sb.from('profiles').select('role').eq('id', user.id).single(); if (profile?.role !== 'team') return res.status(403).json({ error: 'Team only' }); } const sb = createClient(supabaseUrl, serviceKey || supabaseKey, { auth: { persistSession: false } }); const config = getConfig(); if (!config.configured) return res.status(500).json({ error: 'FileBrowser not configured' }); const { data: tasks, error } = await sb .from('tasks') .select('title, projects!project_id(name, companies!company_id(name))'); if (error) return res.status(500).json({ error: error.message }); const log = []; let migrated = 0, skipped = 0, failed = 0; for (const task of tasks || []) { const co = safeName(task.projects?.companies?.name || ''); const pr = safeName(task.projects?.name || ''); const tk = safeName(task.title || ''); if (!co || !pr || !tk) { skipped++; continue; } const base = joinPath(config.clientRoot, co, 'Projects', pr, tk); const src = `${base}/Old Book`; const dst = `${base}/Old Books`; const srcItems = await listDir(config, src); if (srcItems === null) { skipped++; continue; } if (srcItems.length === 0) { const ok = await deleteDir(config, src); ok ? (migrated++, log.push(`deleted empty: ${src}`)) : (failed++, log.push(`delete-fail: ${src}`)); } else { for (const item of srcItems) { await renameDir(config, `${src}/${item.name}`, `${dst}/${item.name}`); } const ok = await deleteDir(config, src); ok ? (migrated++, log.push(`merged+deleted: ${src}`)) : (failed++, log.push(`merge-fail: ${src}`)); } } return res.status(200).json({ migrated, skipped, failed, log }); }