From 12ffdd45f5c0034df9fdd2b38bb4830c22398447 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 14:58:41 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20Add=20Discord=20OAuth=20=E2=86=92=20Stri?= =?UTF-8?q?pe=20checkout=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- services/arbiter-3.0/src/routes/auth.js | 13 ++++ services/arbiter-3.0/src/routes/stripe.js | 92 ++++++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/services/arbiter-3.0/src/routes/auth.js b/services/arbiter-3.0/src/routes/auth.js index 6c514fe..6a391d8 100644 --- a/services/arbiter-3.0/src/routes/auth.js +++ b/services/arbiter-3.0/src/routes/auth.js @@ -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'); }); diff --git a/services/arbiter-3.0/src/routes/stripe.js b/services/arbiter-3.0/src/routes/stripe.js index eca2ac2..45f3118 100644 --- a/services/arbiter-3.0/src/routes/stripe.js +++ b/services/arbiter-3.0/src/routes/stripe.js @@ -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,