229 lines
6.5 KiB
JavaScript
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;
|