fix: Add Discord OAuth → Stripe checkout flow

THE BUG: Website redirected to /stripe/auth but route didn't exist.
Checkout sessions were created WITHOUT client_reference_id (Discord ID),
so webhook couldn't sync Discord roles after payment.

THE FIX:
- Added GET /stripe/auth - stores tier in session, redirects to Discord OAuth
- Added GET /stripe/checkout - creates checkout WITH client_reference_id
- Updated auth callback to redirect to /stripe/checkout after OAuth
- Legacy POST /create-checkout-session kept for compatibility

FLOW NOW:
1. User clicks Subscribe on website
2. → /stripe/auth?tier=X (stores tier, redirects to Discord)
3. → /auth/discord (Discord OAuth)
4. → /auth/discord/callback (user authenticated)
5. → /stripe/checkout?tier=X (creates Stripe session WITH Discord ID)
6. → Stripe Checkout (user pays)
7. → Webhook receives event with client_reference_id
8. → Discord role synced!

Chronicler #75
This commit is contained in:
Claude
2026-04-10 14:58:41 +00:00
parent d227bce0a8
commit 12ffdd45f5
2 changed files with 103 additions and 2 deletions

View File

@@ -2,11 +2,24 @@ const express = require('express');
const passport = require('passport');
const router = express.Router();
/**
* Standard Discord OAuth - redirects to admin after login
*/
router.get('/discord', passport.authenticate('discord'));
router.get('/discord/callback', passport.authenticate('discord', {
failureRedirect: '/'
}), (req, res) => {
// Check if this was a checkout flow (tier stored in session)
if (req.session.pendingCheckoutTier) {
const tierLevel = req.session.pendingCheckoutTier;
delete req.session.pendingCheckoutTier; // Clean up
// Redirect to checkout creation with Discord ID now available
return res.redirect(`/stripe/checkout?tier=${tierLevel}`);
}
// Standard admin redirect
res.redirect('/admin');
});

View File

@@ -3,6 +3,7 @@
* Handles checkout sessions, webhooks, and customer portal
* Date: April 3, 2026
* Updated: April 6, 2026 - Added Discord role sync (Task #87)
* Updated: April 10, 2026 - Added /auth and /checkout routes for Discord OAuth flow
*/
const express = require('express');
@@ -27,9 +28,96 @@ const corsOptions = {
router.options('/create-checkout-session', cors(corsOptions));
/**
* CREATE CHECKOUT SESSION
* STRIPE AUTH - Entry point from website
* GET /stripe/auth?tier=X
* Stores tier in session, redirects to Discord OAuth
*/
router.get('/auth', (req, res) => {
const tierLevel = req.query.tier;
if (!tierLevel || isNaN(parseInt(tierLevel))) {
return res.status(400).send('Invalid tier level. Please return to the subscribe page and try again.');
}
// Store tier in session for after OAuth callback
req.session.pendingCheckoutTier = parseInt(tierLevel);
// Redirect to Discord OAuth
res.redirect('/auth/discord');
});
/**
* CHECKOUT - Creates Stripe session after Discord OAuth
* GET /stripe/checkout?tier=X
* Called from auth callback, user is now logged in
*/
router.get('/checkout', async (req, res) => {
try {
// User must be authenticated
if (!req.user || !req.user.id) {
return res.redirect('/stripe/auth?tier=' + (req.query.tier || '1'));
}
const tierLevel = parseInt(req.query.tier);
const discordId = req.user.id;
if (!tierLevel || isNaN(tierLevel)) {
return res.status(400).send('Invalid tier level');
}
// Get Stripe Price ID from database
const productResult = await db.query(
'SELECT stripe_price_id, tier_name, billing_type FROM stripe_products WHERE tier_level = $1',
[tierLevel]
);
if (productResult.rows.length === 0) {
return res.status(404).send('Invalid tier level - product not found');
}
const product = productResult.rows[0];
const priceId = product.stripe_price_id;
const billingMode = product.billing_type === 'one-time' ? 'payment' : 'subscription';
console.log('🔍 Creating checkout session (OAuth flow):', {
discord_id: discordId,
tier_level: tierLevel,
tier_name: product.tier_name,
priceId,
billingMode
});
// Create Stripe Checkout Session WITH client_reference_id
const sessionConfig = {
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
mode: billingMode,
client_reference_id: discordId, // 🔑 THE KEY - Discord ID for webhook
success_url: 'https://firefrostgaming.com/success',
cancel_url: 'https://firefrostgaming.com/subscribe',
metadata: {
tier_level: tierLevel.toString(),
tier_name: product.tier_name,
discord_id: discordId
}
};
const session = await stripe.checkout.sessions.create(sessionConfig);
// Redirect directly to Stripe Checkout
res.redirect(session.url);
} catch (error) {
console.error('Checkout creation error:', error);
res.status(500).send('Failed to create checkout session. Please try again.');
}
});
/**
* CREATE CHECKOUT SESSION (Legacy API - kept for compatibility)
* POST /stripe/create-checkout-session
* Body: { tier_level }
* NOTE: This doesn't have Discord ID - use /stripe/auth flow instead
*/
router.post('/create-checkout-session', cors(corsOptions), async (req, res) => {
try {
@@ -53,7 +141,7 @@ router.post('/create-checkout-session', cors(corsOptions), async (req, res) => {
const priceId = product.stripe_price_id;
const billingMode = product.billing_type === 'one-time' ? 'payment' : 'subscription';
console.log('🔍 Creating checkout session:', {
console.log('🔍 Creating checkout session (legacy, no Discord ID):', {
tier_level,
tier_name: product.tier_name,
priceId,