mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
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>
174 lines
6.7 KiB
JavaScript
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;
|