UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Zero-config local development and project scaffolding

404 lines (357 loc) • 12.5 kB
#!/usr/bin/env node /** * SPAPS Local Development Server * Minimal, zero-config server for local development */ const express = require('express'); const cors = require('cors'); const chalk = require('chalk'); const { generateDocsHTML } = require('./docs-html'); const StripeLocalManager = require('./stripe-local'); class LocalServer { constructor(options = {}) { this.port = options.port || process.env.PORT || 3300; this.json = options.json || false; this.app = express(); this.stripeManager = null; this.setupMiddleware(); this.setupRoutes(); this.setupStripeRoutes(); this.setupCatchAll(); } setupMiddleware() { // CORS - allow everything in local mode this.app.use(cors({ origin: true, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Test-User'], })); // Body parsing this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); // Local mode indicator this.app.use((req, res, next) => { res.setHeader('X-SPAPS-Mode', 'local-development'); // Auto-auth in local mode if (!req.headers.authorization && !req.headers['x-api-key']) { req.headers['x-api-key'] = 'local-dev-key'; req.user = { id: 'local-user-123', email: 'dev@localhost', role: req.query._user || req.headers['x-test-user'] || 'user' }; } // Log requests (unless in JSON mode) if (!this.json) { console.log(chalk.dim(`${req.method} ${req.path}`)); } next(); }); } setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'healthy', mode: 'local-development', version: '0.2.0', timestamp: new Date().toISOString() }); }); // Local mode status this.app.get('/health/local-mode', (req, res) => { res.json({ enabled: true, environment: 'local-development', features: { autoAuth: true, corsEnabled: true, testUsers: ['user', 'admin', 'premium'], apiKeyRequired: false } }); }); // Mock authentication endpoints this.app.post('/api/auth/login', (req, res) => { const { email, password } = req.body; res.json({ access_token: 'local-jwt-token-' + Date.now(), refresh_token: 'local-refresh-token-' + Date.now(), user: { id: 'local-user-123', email: email || 'dev@localhost', role: 'user' } }); }); this.app.post('/api/auth/register', (req, res) => { const { email, password } = req.body; res.json({ access_token: 'local-jwt-token-' + Date.now(), refresh_token: 'local-refresh-token-' + Date.now(), user: { id: 'local-user-' + Date.now(), email: email || 'dev@localhost', role: 'user' } }); }); this.app.post('/api/auth/wallet-sign-in', (req, res) => { const { wallet_address, chain_type } = req.body; res.json({ access_token: 'local-jwt-token-' + Date.now(), refresh_token: 'local-refresh-token-' + Date.now(), user: { id: 'local-wallet-user-123', wallet_address, chain_type, role: 'user' } }); }); this.app.post('/api/auth/refresh', (req, res) => { res.json({ access_token: 'local-jwt-token-refreshed-' + Date.now(), refresh_token: 'local-refresh-token-refreshed-' + Date.now() }); }); this.app.post('/api/auth/logout', (req, res) => { res.json({ success: true, message: 'Logged out successfully' }); }); this.app.get('/api/auth/user', (req, res) => { res.json({ id: req.user?.id || 'local-user-123', email: req.user?.email || 'dev@localhost', role: req.user?.role || 'user', created_at: new Date().toISOString() }); }); // Mock Stripe endpoints this.app.post('/api/stripe/create-checkout-session', (req, res) => { res.json({ sessionId: 'cs_test_local_' + Date.now(), url: 'https://checkout.stripe.com/pay/cs_test_local' }); }); this.app.get('/api/stripe/subscription', (req, res) => { res.json({ id: 'sub_local_123', status: 'active', plan: 'premium', current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() }); }); // Mock usage endpoints this.app.get('/api/usage/balance', (req, res) => { res.json({ balance: 1000, currency: 'credits', updated_at: new Date().toISOString() }); }); // Documentation endpoint this.app.get('/docs', (req, res) => { res.send(generateDocsHTML(this.port)); }); } setupStripeRoutes() { // Mock Stripe checkout session this.app.post('/api/stripe/create-checkout-session', async (req, res) => { const { price_id, success_url, cancel_url } = req.body; const sessionId = 'cs_local_' + Date.now(); res.json({ sessionId, url: `http://localhost:${this.port}/checkout/${sessionId}?success=${encodeURIComponent(success_url)}&cancel=${encodeURIComponent(cancel_url)}` }); // Simulate webhook after delay setTimeout(async () => { try { await this.simulateCheckoutWebhook(sessionId, price_id); if (!this.json) { console.log(chalk.blue(`⚔ Webhook simulated: checkout.session.completed`)); } } catch (error) { console.error(chalk.red('Webhook simulation failed:'), error); } }, 2000); }); // Mock checkout page this.app.get('/checkout/:sessionId', (req, res) => { const { sessionId } = req.params; const { success, cancel } = req.query; res.send(` <!DOCTYPE html> <html> <head> <title>SPAPS Local - Mock Checkout</title> <style> body { font-family: system-ui; max-width: 400px; margin: 100px auto; padding: 2rem; } button { width: 100%; padding: 1rem; margin: 0.5rem 0; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; } .pay { background: #635bff; color: white; } .pay:hover { background: #4b41e0; } .cancel { background: #f5f5f5; } .cancel:hover { background: #e5e5e5; } </style> </head> <body> <h1>šŸ  Mock Checkout</h1> <p>Session: ${sessionId}</p> <p>This is a mock checkout page for local development.</p> <button class="pay" onclick="window.location='${success}'"> šŸ’³ Complete Payment </button> <button class="cancel" onclick="window.location='${cancel}'"> Cancel </button> <p style="margin-top: 2rem; color: #666; font-size: 14px;"> In production, this would be a real Stripe Checkout page. </p> </body> </html> `); }); // Mock webhook endpoint this.app.post('/api/stripe/webhooks', express.raw({ type: 'application/json' }), (req, res) => { // In local mode, accept all webhooks const event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body; if (!this.json) { console.log(chalk.blue(`⚔ Webhook received: ${event.type}`)); } // Store for testing this.lastWebhookEvent = event; res.json({ received: true }); }); // Webhook testing UI this.app.get('/api/stripe/webhooks/test', (req, res) => { res.send(` <!DOCTYPE html> <html> <head> <title>SPAPS - Stripe Webhook Tester</title> <style> body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 2rem; } button { background: #635bff; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 6px; cursor: pointer; margin: 0.25rem; } button:hover { background: #4b41e0; } .event { background: #f9f9f9; padding: 1rem; margin: 1rem 0; border-radius: 8px; border-left: 4px solid #635bff; } </style> </head> <body> <h1>šŸ  Stripe Webhook Tester</h1> <h2>Simulate Events</h2> <div> <button onclick="simulate('checkout.session.completed')">Checkout Completed</button> <button onclick="simulate('payment_intent.succeeded')">Payment Success</button> <button onclick="simulate('customer.subscription.created')">Subscription Created</button> </div> <h2>Last Event</h2> <div id="lastEvent">No events yet</div> <script> async function simulate(type) { const response = await fetch('/api/stripe/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'evt_local_' + Date.now(), type: type, data: { object: { id: type.split('.')[0] + '_' + Date.now() } } }) }); if (response.ok) { document.getElementById('lastEvent').innerHTML = '<div class="event">āœ… ' + type + ' - ' + new Date().toLocaleTimeString() + '</div>'; } } </script> </body> </html> `); }); } async simulateCheckoutWebhook(sessionId, priceId) { const event = { id: 'evt_local_' + Date.now(), type: 'checkout.session.completed', data: { object: { id: sessionId, amount_total: this.getPriceAmount(priceId), currency: 'usd', customer: 'cus_local_' + Date.now(), payment_status: 'paid', status: 'complete', metadata: { app_id: 'local-app-001', price_id: priceId } } } }; // Send to webhook endpoint const response = await fetch(`http://localhost:${this.port}/api/stripe/webhooks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }); return response.ok; } getPriceAmount(priceId) { const prices = { 'price_local_validate': 50000, 'price_local_prototype': 250000, 'price_local_strategy': 1000000, 'price_local_build': 2500000 }; return prices[priceId] || 10000; } setupCatchAll() { // Catch-all for unimplemented routes this.app.use((req, res) => { res.status(404).json({ error: 'Not found', message: `Endpoint ${req.method} ${req.path} not implemented in local mode`, suggestion: 'Check /docs for available endpoints' }); }); } start() { return new Promise((resolve, reject) => { const server = this.app.listen(this.port, (err) => { if (err) { reject(err); } else { if (!this.json) { console.log(); console.log(chalk.yellow('šŸ  SPAPS Local Development Server')); console.log(chalk.green(`✨ Running at: http://localhost:${this.port}`)); console.log(chalk.blue(`šŸ“ Documentation: http://localhost:${this.port}/docs`)); console.log(chalk.dim(' Press Ctrl+C to stop')); console.log(); } resolve(server); } }); // Handle errors server.on('error', (err) => { if (!this.json) { if (err.code === 'EADDRINUSE') { console.error(chalk.red(`āŒ Port ${this.port} is already in use`)); console.log(chalk.yellow('šŸ’” Try: spaps local --port 3301')); } else { console.error(chalk.red('āŒ Server error:'), err.message); } } reject(err); }); }); } } // Export for use in CLI module.exports = LocalServer; // Run directly if called as script if (require.main === module) { const server = new LocalServer(); server.start().catch(console.error); // Graceful shutdown process.on('SIGINT', () => { console.log(chalk.yellow('\nšŸ‘‹ Shutting down...')); process.exit(0); }); }