feat: store node logo/favicon in postgres instead of S3

- Add logoData and faviconData columns to nodes table
- Create /api/admin/node/upload endpoint for direct DB storage
- Create /api/node/logo and /api/node/favicon endpoints to serve images
- Update admin page to use new upload endpoint
- Remove dependency on admin's personal S3 storage for node assets
This commit is contained in:
Christomatt
2026-02-01 16:29:39 +01:00
parent 0cea2e9e1f
commit 90dfd62434
17 changed files with 913 additions and 90 deletions
+60
View File
@@ -0,0 +1,60 @@
import { NextResponse } from 'next/server';
import { db, nodes } from '@/db';
import { eq } from 'drizzle-orm';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
if (!db) {
return NextResponse.json({ error: 'Database not available' }, { status: 500 });
}
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
// 1. Try exact match
let node = await db.query.nodes.findFirst({
where: eq(nodes.domain, domain),
});
// 2. Fallback: If not found, check if there is exactly ONE node in the system
if (!node) {
const allNodes = await db.query.nodes.findMany({ limit: 2 });
if (allNodes.length === 1) {
node = allNodes[0];
}
}
// Check if we have favicon data
if (!node?.faviconData) {
return NextResponse.json({ error: 'Favicon not found' }, { status: 404 });
}
// Parse the data URL to extract MIME type and base64 data
const dataUrl = node.faviconData;
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
if (!match) {
// If not a proper data URL, try to serve as-is (backward compatibility)
return NextResponse.json({ error: 'Invalid favicon data' }, { status: 500 });
}
const mimeType = match[1];
const base64Data = match[2];
const buffer = Buffer.from(base64Data, 'base64');
// Return the image with proper headers
return new NextResponse(buffer, {
status: 200,
headers: {
'Content-Type': mimeType,
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
'Content-Length': buffer.length.toString(),
},
});
} catch (error) {
console.error('Favicon serve error:', error);
return NextResponse.json({ error: 'Failed to serve favicon' }, { status: 500 });
}
}