// server.js - Backend API for DrumMaster Pro // This handles Stripe payments, user management, and session tracking const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const { MongoClient } = require('mongodb'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(bodyParser.json()); app.use(express.static('public')); // MongoDB connection const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/drummaster'; let db; MongoClient.connect(mongoUri, { useUnifiedTopology: true }) .then(client => { console.log('Connected to MongoDB'); db = client.db('drummaster'); }) .catch(error => console.error(error)); // Stripe webhook endpoint (must be before bodyParser for raw body) app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); } catch (err) { console.error('Webhook signature verification failed:', err); return res.status(400).send(`Webhook Error: ${err.message}`); } // Handle the event switch (event.type) { case 'checkout.session.completed': const session = event.data.object; await handleCheckoutSession(session); break; case 'customer.subscription.updated': case 'customer.subscription.deleted': const subscription = event.data.object; await updateSubscriptionStatus(subscription); break; default: console.log(`Unhandled event type ${event.type}`); } res.json({ received: true }); }); // User Registration app.post('/api/register', async (req, res) => { try { const { email, password, name } = req.body; // Check if user exists const existingUser = await db.collection('users').findOne({ email }); if (existingUser) { return res.status(400).json({ error: 'User already exists' }); } // Hash password const hashedPassword = [credential removed] bcrypt.hash(password, 10); // Create Stripe customer const customer = await stripe.customers.create({ email, name, metadata: { source: 'drummaster_pro' } }); // Create user in database const user = { email, password: [credential removed], name, stripeCustomerId: customer.id, plan: 'free', createdAt: new Date(), stats: { totalSessions: 0, totalPracticeTime: 0, highestScore: 0, achievements: [], level: 1 } }; await db.collection('users').insertOne(user); // Generate JWT const token = jwt.sign( { userId: user._id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' } ); res.json({ success: true, token, user: { email: user.email, name: user.name, plan: user.plan } }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'Registration failed' }); } }); // User Login app.post('/api/login', async (req, res) => { try { const { email, password } = req.body; const user = await db.collection('users').findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } const isValidPassword = [credential removed] bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign( { userId: user._id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' } ); res.json({ success: true, token, user: { email: user.email, name: user.name, plan: user.plan, stats: user.stats } }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'Login failed' }); } }); // Create Checkout Session app.post('/api/create-checkout-session', async (req, res) => { try { const { priceId, email, plan } = req.body; // Get or create customer let customer; const existingCustomers = await stripe.customers.list({ email, limit: 1 }); if (existingCustomers.data.length > 0) { customer = existingCustomers.data[0]; } else { customer = await stripe.customers.create({ email }); } // Create checkout session const session = await stripe.checkout.sessions.create({ customer: customer.id, payment_method_types: ['card'], line_items: [{ price: priceId, quantity: 1 }], mode: plan === 'lifetime' ? 'payment' : 'subscription', success_url: `${process.env.DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.DOMAIN}/cancel`, metadata: { plan, email } }); res.json({ sessionId: session.id }); } catch (error) { console.error('Checkout session error:', error); res.status(500).json({ error: 'Failed to create checkout session' }); } }); // Get User Stats app.get('/api/stats/:userId', authenticateToken, async (req, res) => { try { const user = await db.collection('users').findOne({ _id: req.params.userId }); if (!user) { return res.status(404).json({ error: 'User not found' }); } const sessions = await db.collection('sessions') .find({ userId: req.params.userId }) .sort({ createdAt: -1 }) .limit(10) .toArray(); res.json({ stats: user.stats, recentSessions: sessions }); } catch (error) { console.error('Stats error:', error); res.status(500).json({ error: 'Failed to fetch stats' }); } }); // Save Practice Session app.post('/api/sessions', authenticateToken, async (req, res) => { try { const session = { userId: req.user.userId, ...req.body, createdAt: new Date() }; await db.collection('sessions').insertOne(session); // Update user stats await db.collection('users').updateOne( { _id: req.user.userId }, { $inc: { 'stats.totalSessions': 1, 'stats.totalPracticeTime': session.duration }, $max: { 'stats.highestScore': session.score } } ); res.json({ success: true, sessionId: session._id }); } catch (error) { console.error('Session save error:', error); res.status(500).json({ error: 'Failed to save session' }); } }); // Get Leaderboard app.get('/api/leaderboard', async (req, res) => { try { const leaderboard = await db.collection('users') .find({}, { projection: { name: 1, 'stats.highestScore': 1, 'stats.level': 1 } }) .sort({ 'stats.highestScore': -1 }) .limit(100) .toArray(); res.json(leaderboard); } catch (error) { console.error('Leaderboard error:', error); res.status(500).json({ error: 'Failed to fetch leaderboard' }); } }); // Update Achievements app.post('/api/achievements', authenticateToken, async (req, res) => { try { const { achievements } = req.body; await db.collection('users').updateOne( { _id: req.user.userId }, { $addToSet: { 'stats.achievements': { $each: achievements } } } ); res.json({ success: true }); } catch (error) { console.error('Achievement update error:', error); res.status(500).json({ error: 'Failed to update achievements' }); } }); // Cancel Subscription app.post('/api/cancel-subscription', authenticateToken, async (req, res) => { try { const user = await db.collection('users').findOne({ _id: req.user.userId }); if (!user.stripeSubscriptionId) { return res.status(400).json({ error: 'No active subscription' }); } const subscription = await stripe.subscriptions.update( user.stripeSubscriptionId, { cancel_at_period_end: true } ); res.json({ success: true, cancelAt: subscription.cancel_at }); } catch (error) { console.error('Cancel subscription error:', error); res.status(500).json({ error: 'Failed to cancel subscription' }); } }); // Helper Functions async function handleCheckoutSession(session) { const { customer, metadata } = session; const { plan, email } = metadata; // Update user subscription in database await db.collection('users').updateOne( { email }, { $set: { plan, stripeCustomerId: customer, stripeSubscriptionId: session.subscription, subscriptionUpdatedAt: new Date() } } ); // Send welcome email (implement email service) // await sendWelcomeEmail(email, plan); } async function updateSubscriptionStatus(subscription) { const { customer, status, id } = subscription; const user = await db.collection('users').findOne({ stripeCustomerId: customer }); if (!user) return; const plan = status === 'active' ? 'pro' : 'free'; await db.collection('users').updateOne( { stripeCustomerId: customer }, { $set: { plan, stripeSubscriptionId: id, subscriptionStatus: status, subscriptionUpdatedAt: new Date() } } ); } function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Access token required' }); } jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ error: 'Invalid token' }); } req.user = user; next(); }); } app.listen(PORT, () => { console.log(`DrumMaster Pro API running on port ${PORT}`); }); // Export for testing module.exports = app;