fix
This commit is contained in:
@ -1,99 +1,88 @@
|
||||
// server.js (ESM) — deployable on Vercel
|
||||
import pinataSDK from '@pinata/sdk'
|
||||
import cors from 'cors'
|
||||
import dotenv from 'dotenv'
|
||||
import express from 'express'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import { Readable } from 'stream'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Load local .env for dev. In Vercel, env vars come from platform.
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env') })
|
||||
dotenv.config()
|
||||
|
||||
const app = express()
|
||||
|
||||
/**
|
||||
* CORS
|
||||
* - Allows localhost for local dev
|
||||
* - Allows your main frontend via FRONTEND_ORIGIN
|
||||
* - Allows localhost dev
|
||||
* - Allows your main frontend
|
||||
* - Allows ANY *.vercel.app (so forks work for non-technical founders)
|
||||
*
|
||||
* Optional: set ALLOWED_ORIGINS in Vercel env as comma-separated list
|
||||
* Example:
|
||||
* ALLOWED_ORIGINS=https://tokenize-rwa-template.vercel.app,http://localhost:5173
|
||||
*/
|
||||
const allowedOrigins = [
|
||||
'http://localhost:5173',
|
||||
process.env.FRONTEND_ORIGIN, // e.g. https://tokenize-rwa-template.vercel.app
|
||||
]
|
||||
const explicitAllowed = (process.env.ALLOWED_ORIGINS || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.map((o) => o.trim())
|
||||
|
||||
const isAllowedOrigin = (origin: string) => {
|
||||
// explicitly allowed
|
||||
if (explicitAllowed.includes(origin)) return true
|
||||
|
||||
// local dev
|
||||
if (origin === 'http://localhost:5173') return true
|
||||
|
||||
// allow any Vercel preview/prod frontend (great for forks)
|
||||
try {
|
||||
const host = new URL(origin).hostname
|
||||
if (host.endsWith('.vercel.app')) return true
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: (origin, cb) => {
|
||||
// allow server-to-server / curl (no origin)
|
||||
// allow server-to-server / curl / same-origin (no Origin header)
|
||||
if (!origin) return cb(null, true)
|
||||
|
||||
if (allowedOrigins.includes(origin)) return cb(null, true)
|
||||
|
||||
// Allow any frontend on vercel.app (great for forks + previews)
|
||||
try {
|
||||
const host = new URL(origin).hostname
|
||||
if (host.endsWith('.vercel.app')) return cb(null, true)
|
||||
} catch {
|
||||
// ignore URL parse errors
|
||||
}
|
||||
|
||||
if (isAllowedOrigin(origin)) return cb(null, true)
|
||||
return cb(null, false)
|
||||
},
|
||||
methods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: false,
|
||||
}),
|
||||
)
|
||||
|
||||
// Preflight for all routes
|
||||
// Handle preflight requests for ALL routes
|
||||
app.options('*', cors())
|
||||
|
||||
// For non-multipart endpoints (multer handles multipart/form-data)
|
||||
app.use(express.json({ limit: '15mb' }))
|
||||
app.use(express.json())
|
||||
|
||||
// Pinata client
|
||||
const pinata = process.env.PINATA_JWT
|
||||
const pinata =
|
||||
process.env.PINATA_JWT && process.env.PINATA_JWT.trim().length > 0
|
||||
? 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
|
||||
;(async () => {
|
||||
try {
|
||||
if (typeof pinata.testAuthentication === 'function') {
|
||||
await pinata.testAuthentication()
|
||||
console.log('Pinata auth OK')
|
||||
} else {
|
||||
console.log('Pinata SDK loaded (no testAuthentication method)')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Pinata authentication FAILED. Check env vars.', e?.message || e)
|
||||
}
|
||||
})()
|
||||
|
||||
// health
|
||||
app.get('/health', (_req, res) => {
|
||||
res.set('Cache-Control', 'no-store')
|
||||
res.status(200).json({ ok: true, ts: Date.now() })
|
||||
})
|
||||
|
||||
// uploads
|
||||
// Uploads (multipart/form-data)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||
})
|
||||
|
||||
function safeTrim(v) {
|
||||
app.get('/health', (_req, res) => {
|
||||
res.set('Cache-Control', 'no-store')
|
||||
res.status(200).json({ ok: true, ts: Date.now() })
|
||||
})
|
||||
|
||||
function safeTrim(v: unknown) {
|
||||
return typeof v === 'string' ? v.trim() : ''
|
||||
}
|
||||
|
||||
function safeJsonParse(v, fallback) {
|
||||
function safeJsonParse(v: unknown, fallback: any) {
|
||||
try {
|
||||
if (typeof v !== 'string' || !v.trim()) return fallback
|
||||
return JSON.parse(v)
|
||||
@ -107,14 +96,13 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
|
||||
const file = req.file
|
||||
if (!file) return res.status(400).json({ error: 'No file uploaded' })
|
||||
|
||||
// Optional multipart fields from frontend
|
||||
// Optional form fields (friendly for vibe-coders)
|
||||
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) || 'Pinned via TokenizeRWA template'
|
||||
const properties = safeJsonParse(req.body?.properties, {})
|
||||
|
||||
// Pin image
|
||||
const stream = Readable.from(file.buffer)
|
||||
// Pinata SDK sometimes expects a .path/filename
|
||||
const stream = Readable.from(file.buffer) as any
|
||||
stream.path = file.originalname || 'upload'
|
||||
|
||||
const imageResult = await pinata.pinFileToIPFS(stream, {
|
||||
@ -138,8 +126,12 @@ app.post('/api/pin-image', upload.single('file'), async (req, res) => {
|
||||
const metadataUrl = `ipfs://${jsonResult.IpfsHash}`
|
||||
|
||||
return res.status(200).json({ metadataUrl })
|
||||
} catch (error) {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
|
||||
@ -12,7 +12,7 @@ import { getAlgodConfigFromViteEnvironment } from '../utils/network/getAlgoClien
|
||||
* Captures ASA configuration including compliance fields
|
||||
*/
|
||||
type CreatedAsset = {
|
||||
assetId: bigint
|
||||
assetId: string
|
||||
assetName: string
|
||||
unitName: string
|
||||
total: string
|
||||
@ -575,7 +575,7 @@ export default function TokenizeAsset() {
|
||||
const assetId = createResult.assetId
|
||||
|
||||
const newEntry: CreatedAsset = {
|
||||
assetId: BigInt(assetId),
|
||||
assetId: String(assetId),
|
||||
assetName: String(assetName),
|
||||
unitName: String(unitName),
|
||||
total: String(total),
|
||||
@ -900,7 +900,7 @@ export default function TokenizeAsset() {
|
||||
|
||||
// ✅ Persist minted NFT into SAME history list (NFTs are ASAs)
|
||||
const nftEntry: CreatedAsset = {
|
||||
assetId: BigInt(assetId),
|
||||
assetId: String(assetId),
|
||||
assetName: String(nftName),
|
||||
unitName: String(nftUnit),
|
||||
total: String(nftSupply),
|
||||
|
||||
Reference in New Issue
Block a user