This commit is contained in:
SaraJane
2026-01-20 13:24:15 +00:00
parent 11286a61c2
commit 5252c3f0b0

View File

@ -1,4 +1,4 @@
// Shared Express app (no .listen here) // app.js (PURE JS - no TS syntax)
import pinataSDK from '@pinata/sdk' import pinataSDK from '@pinata/sdk'
import cors from 'cors' import cors from 'cors'
import dotenv from 'dotenv' import dotenv from 'dotenv'
@ -11,19 +11,14 @@ dotenv.config()
const app = express() const app = express()
// --- DEBUG: log every request (shows up in Vercel function logs) // --- DEBUG: log every request (shows up in Vercel logs)
app.use((req, _res, next) => { app.use((req, _res, next) => {
console.log(`[REQ] ${req.method} ${req.url} origin=${req.headers.origin || 'none'}`) console.log(`[REQ] ${req.method} ${req.url} origin=${req.headers.origin || 'none'}`)
next() next()
}) })
/** /**
* CORS * CORS
* - Allows localhost dev
* - Allows your main frontend(s) via ALLOWED_ORIGINS
* - Allows ANY *.vercel.app (so forks work for non-technical founders)
*
* Optional: set ALLOWED_ORIGINS in Vercel env as comma-separated list * Optional: set ALLOWED_ORIGINS in Vercel env as comma-separated list
* Example: * Example:
* ALLOWED_ORIGINS=https://tokenize-rwa-template.vercel.app,http://localhost:5173 * ALLOWED_ORIGINS=https://tokenize-rwa-template.vercel.app,http://localhost:5173
@ -33,33 +28,24 @@ const explicitAllowed = (process.env.ALLOWED_ORIGINS || '')
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean) .filter(Boolean)
function isAllowedOrigin(origin: string) { function isAllowedOrigin(origin) {
// explicitly allowed
if (explicitAllowed.includes('*')) return true if (explicitAllowed.includes('*')) return true
if (explicitAllowed.includes(origin)) return true if (explicitAllowed.includes(origin)) return true
// local dev
if (origin === 'http://localhost:5173') return true if (origin === 'http://localhost:5173') return true
// allow any Vercel preview/prod frontend (great for forks)
try { try {
const host = new URL(origin).hostname const host = new URL(origin).hostname
if (host.endsWith('.vercel.app')) return true if (host.endsWith('.vercel.app')) return true
} catch { } catch {
// ignore bad origins // ignore
} }
return false return false
} }
const corsOptions: cors.CorsOptions = { const corsOptions = {
origin: (origin, cb) => { origin: (origin, cb) => {
// allow server-to-server / curl / same-origin (no Origin header)
if (!origin) return cb(null, true) if (!origin) return cb(null, true)
if (isAllowedOrigin(origin)) return cb(null, true) if (isAllowedOrigin(origin)) return cb(null, true)
// IMPORTANT: return an error (not "false") so it's obvious in logs/debugging
return cb(new Error(`CORS blocked for origin: ${origin}`)) return cb(new Error(`CORS blocked for origin: ${origin}`))
}, },
methods: ['GET', 'POST', 'OPTIONS'], methods: ['GET', 'POST', 'OPTIONS'],
@ -71,7 +57,6 @@ const corsOptions: cors.CorsOptions = {
// Apply CORS to all routes // Apply CORS to all routes
app.use(cors(corsOptions)) app.use(cors(corsOptions))
// Handle preflight requests for ALL routes (with the SAME options)
app.options('*', cors(corsOptions)) app.options('*', cors(corsOptions))
app.use(express.json()) app.use(express.json())
@ -82,17 +67,17 @@ const pinata =
? new pinataSDK({ pinataJWTKey: process.env.PINATA_JWT }) ? new pinataSDK({ pinataJWTKey: process.env.PINATA_JWT })
: new pinataSDK(process.env.PINATA_API_KEY || '', process.env.PINATA_API_SECRET || '') : new pinataSDK(process.env.PINATA_API_KEY || '', process.env.PINATA_API_SECRET || '')
// Optional: test credentials at cold start (helps a LOT on Vercel) // Optional: test credentials at cold start
;(async () => { ;(async () => {
try { try {
const auth = await (pinata as any).testAuthentication?.() const auth = await pinata.testAuthentication?.()
console.log('Pinata auth OK:', auth || 'ok') console.log('Pinata auth OK:', auth || 'ok')
} catch (e) { } catch (e) {
console.error('Pinata authentication FAILED. Check env vars.', e) console.error('Pinata authentication FAILED. Check env vars.', e)
} }
})() })()
// Uploads (multipart/form-data) // Uploads
const upload = multer({ const upload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
@ -103,11 +88,21 @@ app.get('/health', (_req, res) => {
res.status(200).json({ ok: true, ts: Date.now() }) res.status(200).json({ ok: true, ts: Date.now() })
}) })
function safeTrim(v: unknown) { // DEBUG ROUTE
app.get('/api/debug', (req, res) => {
res.status(200).json({
ok: true,
message: 'Reached Express',
url: req.url,
origin: req.headers.origin || null,
})
})
function safeTrim(v) {
return typeof v === 'string' ? v.trim() : '' return typeof v === 'string' ? v.trim() : ''
} }
function safeJsonParse(v: unknown, fallback: any) { function safeJsonParse(v, fallback) {
try { try {
if (typeof v !== 'string' || !v.trim()) return fallback if (typeof v !== 'string' || !v.trim()) return fallback
return JSON.parse(v) return JSON.parse(v)
@ -116,71 +111,16 @@ function safeJsonParse(v: unknown, fallback: any) {
} }
} }
const app = express()
// 1⃣ (optional) DEBUG: log every request
app.use((req, _res, next) => {
console.log(`[REQ] ${req.method} ${req.url} origin=${req.headers.origin || 'none'}`)
next()
})
// 2⃣ CORS
app.use(cors(corsOptions))
app.options('*', cors(corsOptions))
// 3⃣ Body parsers
app.use(express.json())
// 4⃣ Health
app.get('/health', (_req, res) => {
res.set('Cache-Control', 'no-store')
res.status(200).json({ ok: true, ts: Date.now() })
})
// 🔎 5⃣ DEBUG ROUTE — ADD IT HERE
app.get('/api/debug', (req, res) => {
res.json({
ok: true,
message: 'Reached Express',
url: req.url,
origin: req.headers.origin || null,
})
})
// 6⃣ Upload middleware
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 },
})
// 7⃣ Real endpoint
app.post('/api/pin-image', upload.single('file'), async (req, res) => {
// ... your existing logic
})
// 8⃣ (optional) DEBUG: catch-all 404 at VERY bottom
app.use((req, res) => {
console.log(`[MISS] ${req.method} ${req.url}`)
res.status(404).json({
error: 'NOT_FOUND_IN_EXPRESS',
method: req.method,
url: req.url,
})
})
app.post('/api/pin-image', upload.single('file'), async (req, res) => { app.post('/api/pin-image', upload.single('file'), async (req, res) => {
try { try {
const file = req.file const file = req.file
if (!file) return res.status(400).json({ error: 'No file uploaded' }) if (!file) return res.status(400).json({ error: 'No file uploaded' })
// Optional form fields (friendly for vibe-coders)
const metaName = safeTrim(req.body?.metaName) || 'NFT Example' const metaName = safeTrim(req.body?.metaName) || 'NFT Example'
const metaDescription = safeTrim(req.body?.metaDescription) || 'Pinned via TokenizeRWA template' const metaDescription = safeTrim(req.body?.metaDescription) || 'Pinned via TokenizeRWA template'
const properties = safeJsonParse(req.body?.properties, {}) const properties = safeJsonParse(req.body?.properties, {})
// Pin image const stream = Readable.from(file.buffer)
const stream = Readable.from(file.buffer) as any
stream.path = file.originalname || 'upload' stream.path = file.originalname || 'upload'
const imageResult = await pinata.pinFileToIPFS(stream, { const imageResult = await pinata.pinFileToIPFS(stream, {
@ -189,7 +129,6 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
const imageUrl = `ipfs://${imageResult.IpfsHash}` const imageUrl = `ipfs://${imageResult.IpfsHash}`
// Pin metadata JSON
const metadata = { const metadata = {
name: metaName, name: metaName,
description: metaDescription, description: metaDescription,
@ -201,28 +140,17 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
pinataMetadata: { name: `${metaName} Metadata` }, pinataMetadata: { name: `${metaName} Metadata` },
}) })
const metadataUrl = `ipfs://${jsonResult.IpfsHash}` return res.status(200).json({ metadataUrl: `ipfs://${jsonResult.IpfsHash}` })
} catch (error) {
return res.status(200).json({ metadataUrl }) const msg = error?.response?.data?.error || error?.response?.data || error?.message || 'Failed to pin to IPFS.'
} catch (error: any) {
const msg =
error?.response?.data?.error ||
error?.response?.data ||
error?.message ||
'Failed to pin to IPFS.'
return res.status(500).json({ error: msg }) return res.status(500).json({ error: msg })
} }
}) })
// --- DEBUG: catch-all so we can see if Express is being hit at all // Catch-all 404 (so we KNOW Express is being hit)
app.use((req, res) => { app.use((req, res) => {
console.log(`[MISS] ${req.method} ${req.url} (no route matched)`) console.log(`[MISS] ${req.method} ${req.url}`)
res.status(404).json({ res.status(404).json({ error: 'NOT_FOUND_IN_EXPRESS', method: req.method, url: req.url })
error: 'NOT_FOUND_IN_EXPRESS',
method: req.method,
url: req.url,
})
}) })
export default app export default app