screentinker/server/routes/stripe.js
ScreenTinker 1594a9d4a4 Initial open source release
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 12:14:53 -05:00

174 lines
6.7 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { db } = require('../db/database');
const { requireAuth } = require('../middleware/auth');
const config = require('../config');
const appUrl = process.env.APP_URL || '';
let stripe = null;
if (config.stripeSecretKey) {
stripe = require('stripe')(config.stripeSecretKey);
}
// Create checkout session - user clicks "Upgrade" on a plan
router.post('/checkout', requireAuth, async (req, res) => {
if (!stripe) return res.status(503).json({ error: 'Stripe not configured' });
const { plan_id, interval } = req.body; // interval: 'monthly' or 'yearly'
if (!plan_id) return res.status(400).json({ error: 'plan_id required' });
const plan = db.prepare('SELECT * FROM plans WHERE id = ?').get(plan_id);
if (!plan) return res.status(404).json({ error: 'Plan not found' });
const priceId = interval === 'yearly' ? plan.stripe_price_yearly : plan.stripe_price_monthly;
if (!priceId) return res.status(400).json({ error: `No Stripe price configured for ${plan_id} (${interval || 'monthly'})` });
try {
// Get or create Stripe customer
let customerId = req.user.stripe_customer_id;
if (!customerId) {
const customer = await stripe.customers.create({
email: req.user.email,
metadata: { user_id: req.user.id, name: req.user.name || '' },
});
customerId = customer.id;
db.prepare('UPDATE users SET stripe_customer_id = ? WHERE id = ?').run(customerId, req.user.id);
}
// If user already has an active subscription, create a portal session to manage it
if (req.user.stripe_subscription_id) {
const portal = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${req.headers.origin || appUrl}/#/settings`,
});
return res.json({ url: portal.url, type: 'portal' });
}
// Create checkout session for new subscription
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${req.headers.origin || appUrl}/#/settings?payment=success`,
cancel_url: `${req.headers.origin || appUrl}/#/settings?payment=cancelled`,
metadata: { user_id: req.user.id, plan_id },
subscription_data: {
metadata: { user_id: req.user.id, plan_id },
},
});
res.json({ url: session.url, type: 'checkout' });
} catch (err) {
console.error('Stripe checkout error:', err.message);
res.status(500).json({ error: 'Failed to create checkout session' });
}
});
// Customer portal - manage existing subscription (change plan, cancel, update payment)
router.post('/portal', requireAuth, async (req, res) => {
if (!stripe) return res.status(503).json({ error: 'Stripe not configured' });
const customerId = req.user.stripe_customer_id;
if (!customerId) return res.status(400).json({ error: 'No billing account found' });
try {
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${req.headers.origin || appUrl}/#/settings`,
});
res.json({ url: session.url });
} catch (err) {
console.error('Stripe portal error:', err.message);
res.status(500).json({ error: 'Failed to create portal session' });
}
});
// Stripe webhook - handles all subscription lifecycle events
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
if (!stripe) return res.status(404).json({ error: 'Stripe not configured' });
let event;
try {
if (config.stripeWebhookSecret) {
event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], config.stripeWebhookSecret);
} else {
event = JSON.parse(req.body.toString());
}
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
console.log(`Stripe webhook: ${event.type}`);
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
const userId = session.metadata?.user_id;
const planId = session.metadata?.plan_id;
if (userId && session.subscription) {
db.prepare(`UPDATE users SET stripe_subscription_id = ?, plan_id = ?, subscription_status = 'active', updated_at = strftime('%s','now') WHERE id = ?`)
.run(session.subscription, planId || 'starter', userId);
console.log(`User ${userId} subscribed to ${planId} (sub: ${session.subscription})`);
}
break;
}
case 'customer.subscription.updated': {
const sub = event.data.object;
const userId = sub.metadata?.user_id;
if (!userId) break;
// Find plan by stripe price ID
const priceId = sub.items?.data?.[0]?.price?.id;
let planId = sub.metadata?.plan_id;
if (priceId && !planId) {
const plan = db.prepare('SELECT id FROM plans WHERE stripe_price_monthly = ? OR stripe_price_yearly = ?').get(priceId, priceId);
if (plan) planId = plan.id;
}
const status = sub.status === 'active' ? 'active' : sub.status === 'past_due' ? 'past_due' : sub.status;
const ends = sub.current_period_end || null;
db.prepare(`UPDATE users SET plan_id = COALESCE(?, plan_id), subscription_status = ?, subscription_ends = ?, updated_at = strftime('%s','now') WHERE id = ?`)
.run(planId, status, ends, userId);
console.log(`Subscription updated for ${userId}: ${planId} (${status})`);
break;
}
case 'customer.subscription.deleted': {
const sub = event.data.object;
const userId = sub.metadata?.user_id;
if (userId) {
db.prepare(`UPDATE users SET plan_id = 'free', subscription_status = 'cancelled', stripe_subscription_id = NULL, updated_at = strftime('%s','now') WHERE id = ?`)
.run(userId);
console.log(`Subscription cancelled for ${userId}`);
}
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
const subId = invoice.subscription;
if (subId) {
const user = db.prepare('SELECT id FROM users WHERE stripe_subscription_id = ?').get(subId);
if (user) {
db.prepare("UPDATE users SET subscription_status = 'past_due', updated_at = strftime('%s','now') WHERE id = ?").run(user.id);
console.log(`Payment failed for user ${user.id}`);
}
}
break;
}
}
} catch (err) {
console.error('Webhook processing error:', err.message);
}
res.json({ received: true });
});
module.exports = router;