production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-03-20 13:46:05 -05:00
parent b275b19f08
commit fc24313f15
187 changed files with 20977 additions and 767 deletions
+264
View File
@@ -0,0 +1,264 @@
// ─────────────────────────────────────────────────────────────────────────────
// 🔥 ASSET MANAGER API — File Browser & Upload for Applications CMS
// ─────────────────────────────────────────────────────────────────────────────
// Place this file at: /src/app/api/assets/route.ts (or /app/api/assets/route.ts)
//
// Endpoints:
// GET /api/assets?slug=textile-drying&path=images
// → Lists all files and folders inside /public/applications/{slug}/{path}
//
// POST /api/assets (multipart FormData with: slug, path, file)
// → Uploads a file to /public/applications/{slug}/{path}/
//
// PUT /api/assets (JSON body: { slug, folderName, parentPath })
// → Creates a new folder at /public/applications/{slug}/{parentPath}/{folderName}
//
// DELETE /api/assets (JSON body: { slug, filePath })
// → Deletes a file at /public/applications/{slug}/{filePath}
//
// Security:
// - All paths are sanitized and clamped to /public/applications/{slug}/
// - No traversal (../) allowed
// - Only known media extensions are served
// ─────────────────────────────────────────────────────────────────────────────
// /src/app/api/assets/route.ts — ASSET MANAGER API v3
// Supports scope=applications (/public/applications/{slug}/)
// scope=cases (/public/cases/{slug}/)
// scope=news (/public/news/{slug}/)
import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const SCOPE_ROOTS: Record<string, string> = {
applications: path.join(process.cwd(), "public", "applications"),
cases: path.join(process.cwd(), "public", "cases"),
news: path.join(process.cwd(), "public", "news"),
// 🔥 NUEVO: Scope para el Component Matrix
parts: path.join(process.cwd(), "public", "parts"),
};
const MEDIA_TYPES: Record<string, string[]> = {
image: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif"],
video: [".mp4", ".webm", ".mov"],
model: [".glb", ".gltf", ".usdz"],
document: [".pdf"],
};
const ALL_EXTENSIONS = Object.values(MEDIA_TYPES).flat();
function getFileType(filename: string): string {
const ext = path.extname(filename).toLowerCase();
for (const [type, exts] of Object.entries(MEDIA_TYPES)) {
if (exts.includes(ext)) return type;
}
return "unknown";
}
function getFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function sanitizePath(input: string): string {
return input
.replace(/\.\./g, "")
.replace(/[<>:"|?*]/g, "")
.replace(/\/+/g, "/")
.replace(/^\/+|\/+$/g, "");
}
function buildSafePath(scope: string, slug: string, subPath?: string): string | null {
const root = SCOPE_ROOTS[scope];
if (!root || !slug) return null;
const appDir = path.join(root, slug);
if (!subPath || subPath === "" || subPath === "/") return appDir;
const cleaned = sanitizePath(subPath);
const fullPath = path.join(appDir, cleaned);
if (!path.resolve(fullPath).startsWith(path.resolve(appDir))) return null;
return fullPath;
}
function buildBreadcrumbs(subPath: string) {
const parts = subPath ? subPath.split("/").filter(Boolean) : [];
const crumbs = [{ name: "Root", path: "" }];
let acc = "";
for (const p of parts) {
acc += (acc ? "/" : "") + p;
crumbs.push({ name: p, path: acc });
}
return crumbs;
}
// GET — List files and folders
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const scope = searchParams.get("scope") || "applications";
const slug = searchParams.get("slug");
const subPath = searchParams.get("path") || "";
if (!slug) return NextResponse.json({ error: "Missing slug" }, { status: 400 });
if (!SCOPE_ROOTS[scope]) return NextResponse.json({ error: `Invalid scope "${scope}"` }, { status: 400 });
const dirPath = buildSafePath(scope, slug, subPath);
if (!dirPath) return NextResponse.json({ error: "Invalid path" }, { status: 400 });
if (!fs.existsSync(dirPath)) {
return NextResponse.json({
success: true, scope, slug,
currentPath: subPath || "/",
items: [],
breadcrumbs: buildBreadcrumbs(subPath),
});
}
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
const items = entries
.filter(e => !e.name.startsWith(".") && e.name !== "Thumbs.db")
.map(entry => {
const entryPath = path.join(dirPath, entry.name);
const rel = subPath ? `${subPath}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
let childCount = 0;
try { childCount = fs.readdirSync(entryPath).filter(f => !f.startsWith(".")).length; } catch {}
return { name: entry.name, type: "folder" as const, path: rel, childCount };
}
const stats = fs.statSync(entryPath);
return {
name: entry.name,
type: "file" as const,
mediaType: getFileType(entry.name),
extension: path.extname(entry.name).toLowerCase(),
path: rel,
publicUrl: `/${scope}/${slug}/${rel}`,
size: getFileSize(stats.size),
sizeBytes: stats.size,
modifiedAt: stats.mtime.toISOString(),
};
})
.sort((a, b) => {
if (a.type === "folder" && b.type !== "folder") return -1;
if (a.type !== "folder" && b.type === "folder") return 1;
return a.name.localeCompare(b.name);
});
return NextResponse.json({
success: true, scope, slug,
currentPath: subPath || "/",
items,
breadcrumbs: buildBreadcrumbs(subPath),
});
} catch (error) {
console.error("Asset GET error:", error);
return NextResponse.json({ error: "Failed to list directory" }, { status: 500 });
}
}
// POST — Upload a file
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const scope = (formData.get("scope") as string) || "applications";
const slug = formData.get("slug") as string;
const subPath = formData.get("path") as string || "";
const file = formData.get("file") as File;
if (!slug || !file) return NextResponse.json({ error: "Missing slug or file" }, { status: 400 });
if (!SCOPE_ROOTS[scope]) return NextResponse.json({ error: "Invalid scope" }, { status: 400 });
const ext = path.extname(file.name).toLowerCase();
if (!ALL_EXTENSIONS.includes(ext)) {
return NextResponse.json({ error: `Type "${ext}" not allowed. Accepted: ${ALL_EXTENSIONS.join(", ")}` }, { status: 400 });
}
if (file.size > 50 * 1024 * 1024) {
return NextResponse.json({ error: "File exceeds 50MB limit" }, { status: 400 });
}
const dirPath = buildSafePath(scope, slug, subPath);
if (!dirPath) return NextResponse.json({ error: "Invalid path" }, { status: 400 });
fs.mkdirSync(dirPath, { recursive: true });
const safeName = file.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "");
const filePath = path.join(dirPath, safeName);
const existed = fs.existsSync(filePath);
fs.writeFileSync(filePath, Buffer.from(await file.arrayBuffer()));
const rel = subPath ? `${subPath}/${safeName}` : safeName;
return NextResponse.json({
success: true,
file: {
name: safeName,
publicUrl: `/${scope}/${slug}/${rel}`,
path: rel,
mediaType: getFileType(safeName),
size: getFileSize(file.size),
overwritten: existed,
}
});
} catch (error) {
console.error("Asset POST error:", error);
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
}
}
// PUT — Create a new folder
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
const { scope = "applications", slug, folderName, parentPath = "" } = body;
if (!slug || !folderName) return NextResponse.json({ error: "Missing slug or folderName" }, { status: 400 });
if (!SCOPE_ROOTS[scope]) return NextResponse.json({ error: "Invalid scope" }, { status: 400 });
const safe = folderName.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "");
if (!safe) return NextResponse.json({ error: "Invalid folder name" }, { status: 400 });
const targetPath = buildSafePath(scope, slug, parentPath ? `${parentPath}/${safe}` : safe);
if (!targetPath) return NextResponse.json({ error: "Invalid path" }, { status: 400 });
if (fs.existsSync(targetPath)) return NextResponse.json({ error: "Folder already exists" }, { status: 409 });
fs.mkdirSync(targetPath, { recursive: true });
return NextResponse.json({
success: true,
folder: { name: safe, path: parentPath ? `${parentPath}/${safe}` : safe }
});
} catch (error) {
console.error("Asset PUT error:", error);
return NextResponse.json({ error: "Failed to create folder" }, { status: 500 });
}
}
// DELETE — Remove a file
export async function DELETE(request: NextRequest) {
try {
const body = await request.json();
const { scope = "applications", slug, filePath } = body;
if (!slug || !filePath) return NextResponse.json({ error: "Missing params" }, { status: 400 });
if (!SCOPE_ROOTS[scope]) return NextResponse.json({ error: "Invalid scope" }, { status: 400 });
const targetPath = buildSafePath(scope, slug, filePath);
if (!targetPath) return NextResponse.json({ error: "Invalid path" }, { status: 400 });
if (!fs.existsSync(targetPath)) return NextResponse.json({ error: "File not found" }, { status: 404 });
if (fs.statSync(targetPath).isDirectory()) return NextResponse.json({ error: "Cannot delete folders via API" }, { status: 400 });
fs.unlinkSync(targetPath);
return NextResponse.json({ success: true, deleted: filePath });
} catch (error) {
console.error("Asset DELETE error:", error);
return NextResponse.json({ error: "Delete failed" }, { status: 500 });
}
}