require('dotenv').config(); const express = require('express'); const cors = require('cors'); const swaggerUi = require('swagger-ui-express'); const swaggerSpecs = require('./swagger.config'); const redemption = require('./components/redemption'); // Route imports const cashuRoutes = require('./routes/cashu'); const redemptionRoutes = require('./routes/redemption'); const lightningRoutes = require('./routes/lightning'); const healthRoutes = require('./routes/health'); const app = express(); const PORT = process.env.PORT || 3000; // Get API domain for CORS configuration const apiDomain = process.env.API_DOMAIN || 'localhost:3000'; const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; // Middleware app.use(express.json({ limit: '10mb' })); // Enhanced CORS configuration for Swagger UI app.use(cors({ origin: process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) : [`${protocol}://${apiDomain}`], methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'], credentials: true, optionsSuccessStatus: 200 })); // Additional middleware for Swagger UI preflight requests app.use((req, res, next) => { if (req.method === 'OPTIONS') { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET', 'POST', 'OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'); res.status(200).end(); return; } next(); }); // Debug endpoint to test CORS app.get('/api/cors-test', (req, res) => { res.json({ success: true, message: 'CORS test successful', timestamp: new Date().toISOString(), origin: req.headers.origin, host: req.headers.host }); }); /** * @swagger * /openapi.json: * get: * summary: OpenAPI specification * description: Returns the full OpenAPI 3.0 specification for this API as JSON. * tags: [Status & Monitoring] * responses: * 200: * description: OpenAPI specification * content: * application/json: * schema: * type: object * description: OpenAPI 3.0 specification document */ app.get('/openapi.json', (req, res) => { res.setHeader('Content-Type', 'application/json'); res.json(swaggerSpecs); }); // Swagger Documentation app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { customCss: '.swagger-ui .topbar { display: none }', customSiteTitle: 'Cashu Redeem API Documentation', swaggerOptions: { filter: true, showRequestHeaders: true, tryItOutEnabled: true } })); // Basic rate limiting (simple in-memory implementation) const rateLimitMap = new Map(); const RATE_LIMIT = parseInt(process.env.RATE_LIMIT) || 100; function rateLimit(req, res, next) { const clientId = req.ip || req.connection.remoteAddress; const now = Date.now(); const windowStart = now - 60000; if (!rateLimitMap.has(clientId)) { rateLimitMap.set(clientId, []); } const requests = rateLimitMap.get(clientId); const validRequests = requests.filter(time => time > windowStart); rateLimitMap.set(clientId, validRequests); if (validRequests.length >= RATE_LIMIT) { return res.status(429).json({ success: false, error: 'Rate limit exceeded. Please try again later.' }); } validRequests.push(now); next(); } // Apply rate limiting to all routes app.use(rateLimit); // Request logging middleware app.use((req, res, next) => { const timestamp = new Date().toISOString(); console.log(`${timestamp} - ${req.method} ${req.path} - ${req.ip}`); next(); }); // Root endpoint app.get('/', (req, res) => { res.json({ name: 'Cashu Redeem API', version: '2.0.0', description: 'A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol', documentation: '/docs', openapi: '/openapi.json', endpoints: { decode: 'POST /api/decode', redeem: 'POST /api/redeem', validate: 'POST /api/validate-address', health: 'GET /api/health' }, features: [ 'Decode Cashu tokens', 'Redeem tokens to Lightning addresses', 'Lightning address validation', 'Domain restrictions', 'Rate limiting', 'Comprehensive error handling' ], github: 'https://github.com/yourusername/cashu-redeem-api' }); }); // Mount API routes app.use('/api', cashuRoutes); app.use('/api', redemptionRoutes); app.use('/api', lightningRoutes); app.use('/api', healthRoutes); // 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found' }); }); // Global error handler app.use((error, req, res, next) => { console.error('Global error handler:', error); if (error.type === 'entity.parse.failed') { return res.status(400).json({ success: false, error: 'Invalid JSON payload' }); } res.status(500).json({ success: false, error: 'Internal server error' }); }); // Cleanup old redemptions periodically (every hour) setInterval(() => { try { redemption.cleanupOldRedemptions(); console.log('Cleaned up old redemptions'); } catch (error) { console.error('Error cleaning up redemptions:', error); } }, 60 * 60 * 1000); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); process.exit(0); }); // Start server app.listen(PORT, () => { console.log(`🚀 Cashu Redeem API v2.0.0 running on port ${PORT}`); console.log(`📖 API Documentation: http://localhost:${PORT}/docs`); console.log(`📋 OpenAPI spec: http://localhost:${PORT}/openapi.json`); console.log(`📍 Health check: http://localhost:${PORT}/api/health`); console.log(`🔒 Environment: ${process.env.NODE_ENV || 'development'}`); if (process.env.ALLOW_REDEEM_DOMAINS) { console.log(`🌐 Allowed domains: ${process.env.ALLOW_REDEEM_DOMAINS}`); } else { console.log('⚠️ No domain restrictions (ALLOW_REDEEM_DOMAINS not set)'); } if (process.env.DEFAULT_LIGHTNING_ADDRESS) { console.log(`⚡ Default Lightning address: ${process.env.DEFAULT_LIGHTNING_ADDRESS}`); } else { console.log('⚠️ No default Lightning address configured - Lightning address will be required for redemptions'); } }); module.exports = app;