Files
Cashu-redeem-api/server.js
2026-02-17 03:46:46 +00:00

229 lines
6.5 KiB
JavaScript

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;