test
This commit is contained in:
@ -1,10 +1,11 @@
|
|||||||
import express from 'express'
|
// server.js (ESM) — deployable on Vercel
|
||||||
import cors from 'cors'
|
|
||||||
import multer from 'multer'
|
|
||||||
import pinataSDK from '@pinata/sdk'
|
import pinataSDK from '@pinata/sdk'
|
||||||
|
import cors from 'cors'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import { Readable } from 'stream'
|
import express from 'express'
|
||||||
|
import multer from 'multer'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { Readable } from 'stream'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
@ -15,33 +16,47 @@ dotenv.config({ path: path.resolve(__dirname, '.env') })
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
// Allow local + prod (comma-separated in env), or * by default for dev
|
/**
|
||||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '*')
|
* CORS
|
||||||
.split(',')
|
* - Allows localhost for local dev
|
||||||
|
* - Allows your main frontend via FRONTEND_ORIGIN
|
||||||
|
*/
|
||||||
|
const allowedOrigins = [
|
||||||
|
'http://localhost:5173',
|
||||||
|
process.env.FRONTEND_ORIGIN, // e.g. https://tokenize-rwa-template.vercel.app
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
.map((o) => o.trim())
|
.map((o) => o.trim())
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: (origin, cb) => {
|
origin: (origin, cb) => {
|
||||||
if (!origin) return cb(null, true) // same-origin, curl, Postman
|
// allow server-to-server / curl (no origin)
|
||||||
if (allowedOrigins.includes('*')) return cb(null, true)
|
if (!origin) return cb(null, true)
|
||||||
|
|
||||||
if (allowedOrigins.includes(origin)) return cb(null, true)
|
if (allowedOrigins.includes(origin)) return cb(null, true)
|
||||||
|
|
||||||
// Allow any frontend on vercel.app (great for forks)
|
// Allow any frontend on vercel.app (great for forks + previews)
|
||||||
try {
|
try {
|
||||||
const host = new URL(origin).hostname
|
const host = new URL(origin).hostname
|
||||||
if (host.endsWith('.vercel.app')) return cb(null, true)
|
if (host.endsWith('.vercel.app')) return cb(null, true)
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
// ignore URL parse errors
|
||||||
|
}
|
||||||
|
|
||||||
return cb(null, false)
|
return cb(null, false)
|
||||||
},
|
},
|
||||||
|
methods: ['GET', 'POST', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
credentials: false,
|
credentials: false,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// IMPORTANT: for multipart/form-data, multer populates req.body
|
// Preflight for all routes
|
||||||
// but JSON parsing is still useful for other endpoints
|
app.options('*', cors())
|
||||||
app.use(express.json())
|
|
||||||
|
// For non-multipart endpoints (multer handles multipart/form-data)
|
||||||
|
app.use(express.json({ limit: '15mb' }))
|
||||||
|
|
||||||
// Pinata client
|
// Pinata client
|
||||||
const pinata = process.env.PINATA_JWT
|
const pinata = process.env.PINATA_JWT
|
||||||
@ -51,8 +66,12 @@ const pinata = process.env.PINATA_JWT
|
|||||||
// Optional: test credentials at cold start
|
// Optional: test credentials at cold start
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const auth = await pinata.testAuthentication?.()
|
if (typeof pinata.testAuthentication === 'function') {
|
||||||
console.log('Pinata auth OK:', auth || 'ok')
|
await pinata.testAuthentication()
|
||||||
|
console.log('Pinata auth OK')
|
||||||
|
} else {
|
||||||
|
console.log('Pinata SDK loaded (no testAuthentication method)')
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Pinata authentication FAILED. Check env vars.', e?.message || e)
|
console.error('Pinata authentication FAILED. Check env vars.', e?.message || e)
|
||||||
}
|
}
|
||||||
@ -61,18 +80,15 @@ const pinata = process.env.PINATA_JWT
|
|||||||
// health
|
// health
|
||||||
app.get('/health', (_req, res) => {
|
app.get('/health', (_req, res) => {
|
||||||
res.set('Cache-Control', 'no-store')
|
res.set('Cache-Control', 'no-store')
|
||||||
res.json({ ok: true, ts: Date.now() })
|
res.status(200).json({ ok: true, ts: Date.now() })
|
||||||
})
|
})
|
||||||
|
|
||||||
// uploads
|
// uploads
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage: multer.memoryStorage(),
|
storage: multer.memoryStorage(),
|
||||||
limits: {
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||||
fileSize: 10 * 1024 * 1024, // 10MB safety limit (matches frontend hint)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Small helpers
|
|
||||||
function safeTrim(v) {
|
function safeTrim(v) {
|
||||||
return typeof v === 'string' ? v.trim() : ''
|
return typeof v === 'string' ? v.trim() : ''
|
||||||
}
|
}
|
||||||
@ -91,21 +107,20 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
|
|||||||
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' })
|
||||||
|
|
||||||
// NEW: read optional fields from multipart form-data (sent by frontend)
|
// Optional multipart fields from frontend
|
||||||
const metaName = safeTrim(req.body?.metaName) || 'NFT Example'
|
const metaName = safeTrim(req.body?.metaName) || 'NFT Example'
|
||||||
const metaDescription = safeTrim(req.body?.metaDescription) || 'This is an unchangeable NFT'
|
const metaDescription = safeTrim(req.body?.metaDescription) || 'This is an unchangeable NFT'
|
||||||
const properties = safeJsonParse(req.body?.properties, {})
|
const properties = safeJsonParse(req.body?.properties, {})
|
||||||
|
|
||||||
// Pin image
|
// Pin image
|
||||||
const stream = Readable.from(file.buffer)
|
const stream = Readable.from(file.buffer)
|
||||||
// Pinata SDK expects a path/filename sometimes
|
// Pinata SDK sometimes expects a .path/filename
|
||||||
// @ts-ignore
|
|
||||||
stream.path = file.originalname || 'upload'
|
stream.path = file.originalname || 'upload'
|
||||||
|
|
||||||
const imageOptions = {
|
const imageResult = await pinata.pinFileToIPFS(stream, {
|
||||||
pinataMetadata: { name: file.originalname || `${metaName} Image` },
|
pinataMetadata: { name: file.originalname || `${metaName} Image` },
|
||||||
}
|
})
|
||||||
const imageResult = await pinata.pinFileToIPFS(stream, imageOptions)
|
|
||||||
const imageUrl = `ipfs://${imageResult.IpfsHash}`
|
const imageUrl = `ipfs://${imageResult.IpfsHash}`
|
||||||
|
|
||||||
// Pin metadata JSON
|
// Pin metadata JSON
|
||||||
@ -116,18 +131,16 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
|
|||||||
properties,
|
properties,
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonOptions = { pinataMetadata: { name: `${metaName} Metadata` } }
|
const jsonResult = await pinata.pinJSONToIPFS(metadata, {
|
||||||
const jsonResult = await pinata.pinJSONToIPFS(metadata, jsonOptions)
|
pinataMetadata: { name: `${metaName} Metadata` },
|
||||||
|
})
|
||||||
|
|
||||||
const metadataUrl = `ipfs://${jsonResult.IpfsHash}`
|
const metadataUrl = `ipfs://${jsonResult.IpfsHash}`
|
||||||
|
|
||||||
res.status(200).json({ metadataUrl })
|
return res.status(200).json({ metadataUrl })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg =
|
const msg = error?.response?.data?.error || error?.response?.data || error?.message || 'Failed to pin to IPFS.'
|
||||||
error?.response?.data?.error ||
|
return res.status(500).json({ error: msg })
|
||||||
error?.response?.data ||
|
|
||||||
error?.message ||
|
|
||||||
'Failed to pin to IPFS.'
|
|
||||||
res.status(500).json({ error: msg })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
"name": "nft_mint_server",
|
"name": "nft_mint_server",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": { "node": ">=18 <21" },
|
"engines": {
|
||||||
|
"node": ">=18 <21"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node server.js",
|
"dev": "node server.js",
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
|
|||||||
@ -46,7 +46,12 @@ type TransferMode = 'manual' | 'algo' | 'usdc'
|
|||||||
function resolveBackendBase(): string {
|
function resolveBackendBase(): string {
|
||||||
// 1) Respect explicit env (Vercel or custom)
|
// 1) Respect explicit env (Vercel or custom)
|
||||||
const env = import.meta.env.VITE_API_URL?.trim()
|
const env = import.meta.env.VITE_API_URL?.trim()
|
||||||
if (env) return env.replace(/\/$/, '')
|
if (env) {
|
||||||
|
const cleaned = env.replace(/\/$/, '')
|
||||||
|
// If someone pastes "my-backend.vercel.app" (no protocol),
|
||||||
|
// the browser will treat it as a relative path. Force https.
|
||||||
|
return cleaned.startsWith('http://') || cleaned.startsWith('https://') ? cleaned : `https://${cleaned}`
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Codespaces: convert current host to port 3001
|
// 2) Codespaces: convert current host to port 3001
|
||||||
// e.g. https://abc-5173.app.github.dev -> https://abc-3001.app.github.dev
|
// e.g. https://abc-5173.app.github.dev -> https://abc-3001.app.github.dev
|
||||||
@ -602,8 +607,12 @@ export default function TokenizeAsset() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
resetDefaults()
|
resetDefaults()
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
enqueueSnackbar('Failed to tokenize asset (ASA creation failed).', { variant: 'error' })
|
console.error('[ASA create] error:', error)
|
||||||
|
|
||||||
|
const msg = error?.response?.body?.message || error?.response?.text || error?.message || String(error)
|
||||||
|
|
||||||
|
enqueueSnackbar(`ASA creation failed: ${msg}`, { variant: 'error' })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user