From d9f033aa0224ada213a78d333c0234b815778111 Mon Sep 17 00:00:00 2001 From: "Claude (Chronicler #58)" Date: Sat, 4 Apr 2026 00:18:27 +0000 Subject: [PATCH] docs: Add Stateless OAuth Bridge implementation guide The complete implementation guide from The Validator (#57) and Gemini. Used by Chronicler #58 to implement Discord-Stripe OAuth linking. Contains: - Step-by-step implementation instructions - OAuth route code (/stripe/auth, /stripe/callback) - Webhook handler updates - Website button changes - Testing checklist - Troubleshooting guide Signed-off-by: Claude --- .../stateless-oauth-bridge-implementation.md | 472 ++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 docs/implementation/stateless-oauth-bridge-implementation.md diff --git a/docs/implementation/stateless-oauth-bridge-implementation.md b/docs/implementation/stateless-oauth-bridge-implementation.md new file mode 100644 index 0000000..2b55c0e --- /dev/null +++ b/docs/implementation/stateless-oauth-bridge-implementation.md @@ -0,0 +1,472 @@ +# Discord-Stripe OAuth Linking - Complete Implementation Guide + +## ๐ŸŽฏ Mission: Automate Discord User โ†’ Stripe Subscription Linking + +**Current Problem:** Subscriptions created via Stripe checkout have NO discord_id, making them invisible in admin panel and preventing Discord role assignment. + +**Gemini's Solution:** "Stateless OAuth Bridge" - Use Discord OAuth before Stripe checkout, pass Discord ID through the entire flow via Stripe's `client_reference_id` field. + +--- + +## ๐Ÿ“‹ Prerequisites + +**Before implementing, verify these exist:** + +1. **Discord Application Settings:** + - Client ID: `1487080166969577502` + - Client Secret: `xOK9ZYgionyqd-huGJRE2Rym98zy0W-m` + - Current redirect URI: `https://discord-bot.firefrostgaming.com/auth/discord/callback` (for admin panel) + - **NEW redirect URI needed:** `https://discord-bot.firefrostgaming.com/stripe/callback` (for checkout flow) + +2. **Environment Variables in `/opt/arbiter-3.0/.env`:** + ```bash + DISCORD_CLIENT_ID=1487080166969577502 + DISCORD_CLIENT_SECRET=xOK9ZYgionyqd-huGJRE2Rym98zy0W-m + DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback + ``` + +3. **Database:** + - `stripe_products` table with `tier_level` and `stripe_price_id` columns + - `subscriptions` table with `discord_id` column + +4. **Stripe Integration:** + - Webhook endpoint at `/webhooks/stripe` + - Handles `checkout.session.completed` event + +--- + +## ๐Ÿ”ง Implementation Steps + +### Step 1: Add Discord Redirect URI + +**Action:** Go to Discord Developer Portal โ†’ Applications โ†’ OAuth2 โ†’ Redirects + +**Add:** +``` +https://discord-bot.firefrostgaming.com/stripe/callback +``` + +**Why:** Discord will redirect users back to this URL after login. + +--- + +### Step 2: Add Environment Variable + +**File:** `/opt/arbiter-3.0/.env` + +**Add this line:** +```bash +DISCORD_CHECKOUT_REDIRECT_URI=https://discord-bot.firefrostgaming.com/stripe/callback +``` + +**Verify it loaded:** +```bash +systemctl restart arbiter-3 +journalctl -u arbiter-3 -n 20 | grep -i "listening" +``` + +--- + +### Step 3: Install axios (if not already installed) + +**Command:** +```bash +cd /opt/arbiter-3.0 && npm install axios +``` + +**Why:** We need to make HTTP requests to Discord's OAuth API. + +--- + +### Step 4: Add OAuth Routes to stripe.js + +**File:** `/opt/arbiter-3.0/src/routes/stripe.js` + +**Add these TWO routes BEFORE the checkout route:** + +#### Route 1: Auth Entry Point + +```javascript +const axios = require('axios'); + +// Step 1: Entry point - User clicks subscribe button on website +router.get('/auth', (req, res) => { + const tierLevel = req.query.tier; + + if (!tierLevel) { + return res.status(400).send('โŒ Tier level is required'); + } + + // Redirect to Discord OAuth with tier in state parameter + const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.DISCORD_CHECKOUT_REDIRECT_URI)}&response_type=code&scope=identify&state=${tierLevel}`; + + res.redirect(discordAuthUrl); +}); +``` + +#### Route 2: OAuth Callback โ†’ Create Stripe Session + +```javascript +// Step 2: Discord redirects back here with user ID and tier +router.get('/callback', async (req, res) => { + const { code, state: tierLevel } = req.query; + + if (!code || !tierLevel) { + return res.redirect('https://firefrostgaming.com/subscribe?error=missing_params'); + } + + try { + // Exchange OAuth code for Discord access token + const tokenResponse = await axios.post( + 'https://discord.com/api/oauth2/token', + new URLSearchParams({ + client_id: process.env.DISCORD_CLIENT_ID, + client_secret: process.env.DISCORD_CLIENT_SECRET, + grant_type: 'authorization_code', + code: code, + redirect_uri: process.env.DISCORD_CHECKOUT_REDIRECT_URI + }), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ); + + // Fetch Discord user identity + const userResponse = await axios.get('https://discord.com/api/users/@me', { + headers: { Authorization: `Bearer ${tokenResponse.data.access_token}` } + }); + + const discordId = userResponse.data.id; + const discordUsername = userResponse.data.username; + + console.log(`โœ… Discord OAuth success: ${discordUsername} (${discordId}) purchasing tier ${tierLevel}`); + + // Get Stripe price ID from database + const productResult = await pool.query( + 'SELECT stripe_price_id, tier_name, billing_type FROM stripe_products WHERE tier_level = $1', + [tierLevel] + ); + + if (productResult.rows.length === 0) { + return res.redirect('https://firefrostgaming.com/subscribe?error=invalid_tier'); + } + + const { stripe_price_id, tier_name, billing_type } = productResult.rows[0]; + + // Create Stripe Checkout Session with Discord ID attached + const session = await stripe.checkout.sessions.create({ + mode: billing_type === 'recurring' ? 'subscription' : 'payment', + line_items: [{ price: stripe_price_id, quantity: 1 }], + success_url: 'https://firefrostgaming.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url: 'https://firefrostgaming.com/subscribe', + client_reference_id: discordId, // ๐Ÿ”ฅ THIS IS THE MAGIC - Discord ID travels to webhook! + metadata: { + tier_level: tierLevel, + tier_name: tier_name, + discord_username: discordUsername + } + }); + + console.log(`โœ… Stripe session created: ${session.id} for ${discordUsername}`); + + // Redirect user to Stripe payment page + res.redirect(session.url); + + } catch (error) { + console.error('โŒ OAuth/Checkout Error:', error.response?.data || error.message); + res.redirect('https://firefrostgaming.com/subscribe?error=auth_failed'); + } +}); +``` + +--- + +### Step 5: Update Webhook Handler + +**File:** `/opt/arbiter-3.0/src/routes/stripe.js` + +**Find the `checkout.session.completed` handler and UPDATE it:** + +```javascript +case 'checkout.session.completed': { + const session = event.data.object; + + // ๐Ÿ”ฅ CRITICAL: Extract Discord ID from client_reference_id + const discordId = session.client_reference_id; + const customerEmail = session.customer_details.email; + const tierLevel = parseInt(session.metadata.tier_level); + const tierName = session.metadata.tier_name; + + if (!discordId) { + console.error('โŒ No Discord ID in checkout session:', session.id); + return res.status(400).send('Missing Discord ID'); + } + + console.log(`โœ… Payment complete: ${session.metadata.discord_username} (${discordId}) - ${tierName}`); + + // Determine subscription ID based on billing type + let stripeSubscriptionId = null; + let status = 'lifetime'; + + if (session.mode === 'subscription') { + stripeSubscriptionId = session.subscription; + status = 'active'; + } + + // Insert/update subscription with Discord ID + await pool.query(` + INSERT INTO subscriptions ( + stripe_subscription_id, + stripe_customer_id, + discord_id, + tier_level, + status, + created_at + ) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (discord_id) + DO UPDATE SET + tier_level = EXCLUDED.tier_level, + status = EXCLUDED.status, + stripe_subscription_id = EXCLUDED.stripe_subscription_id, + stripe_customer_id = EXCLUDED.stripe_customer_id, + updated_at = NOW() + `, [stripeSubscriptionId, session.customer, discordId, tierLevel, status]); + + console.log(`โœ… Database updated: Discord ${discordId} โ†’ Tier ${tierLevel} (${status})`); + + // TODO: Trigger Discord role assignment via Arbiter + // This will be implemented in future task + + break; +} +``` + +--- + +### Step 6: Update Website Buttons + +**File:** Ghost CMS website (firefrostgaming.com) + +**BEFORE (old direct Stripe buttons):** +```html +Subscribe to Awakened +``` + +**AFTER (new OAuth flow buttons):** +```html + + + Subscribe to Awakened ($1) + + + + + Subscribe to Elemental (Fire) ($5/mo) + + + + + Subscribe to Elemental (Frost) ($5/mo) + + + + + Subscribe to Knight (Fire) ($10/mo) + + + + + Subscribe to Knight (Frost) ($10/mo) + + + + + Subscribe to Master (Fire) ($15/mo) + + + + + Subscribe to Master (Frost) ($15/mo) + + + + + Subscribe to Legend (Fire) ($20/mo) + + + + + Subscribe to Legend (Frost) ($20/mo) + + + + + Subscribe to Sovereign ($499) + +``` + +--- + +## ๐Ÿงช Testing the Flow + +### Test 1: Complete Checkout Flow + +1. Click subscribe button on website +2. Should redirect to Discord login +3. Log in with Discord +4. Should redirect to Stripe checkout +5. Complete test payment +6. Check database: + ```sql + SELECT id, discord_id, tier_level, status + FROM subscriptions + ORDER BY created_at DESC + LIMIT 5; + ``` +7. **EXPECTED:** New row with your Discord ID and correct tier_level + +### Test 2: Verify in Admin Panel + +1. Go to `https://discord-bot.firefrostgaming.com/admin/players` +2. **EXPECTED:** See your Discord username, avatar, and tier badge +3. **NOT:** See "N/A" or "Unlinked" + +### Test 3: Multiple Tiers + +1. Test purchasing different tiers (1, 5, 10) +2. Verify each creates correct database entry +3. Verify admin panel shows correct tier name + +--- + +## ๐Ÿšจ Troubleshooting + +### Issue: "Tier level is required" error + +**Cause:** Button missing `?tier=X` parameter + +**Fix:** Update website button URL to include tier number + +--- + +### Issue: Discord OAuth fails with "Invalid redirect_uri" + +**Cause:** Redirect URI not added to Discord Developer Portal + +**Fix:** +1. Go to Discord Developer Portal +2. Add `https://discord-bot.firefrostgaming.com/stripe/callback` +3. Save changes + +--- + +### Issue: Webhook receives no Discord ID + +**Cause:** `client_reference_id` not being set in session creation + +**Fix:** Verify callback route includes: +```javascript +client_reference_id: discordId +``` + +--- + +### Issue: Database shows NULL discord_id + +**Cause:** Webhook using old code that doesn't extract `client_reference_id` + +**Fix:** Update webhook handler per Step 5 + +--- + +## โœ… Success Criteria + +**You'll know it works when:** + +1. โœ… User clicks subscribe โ†’ redirects to Discord login +2. โœ… After Discord login โ†’ redirects to Stripe checkout +3. โœ… After payment โ†’ subscription in database has discord_id filled in +4. โœ… Admin panel Players page shows Discord username + avatar +5. โœ… No "N/A" or "Unlinked" entries for new subscriptions + +--- + +## ๐Ÿ“Š Database Schema Reference + +**subscriptions table:** +```sql +CREATE TABLE subscriptions ( + id SERIAL PRIMARY KEY, + stripe_subscription_id VARCHAR(255), + stripe_customer_id VARCHAR(255), + discord_id VARCHAR(255), -- This gets filled by OAuth flow! + tier_level INTEGER, + status VARCHAR(50), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +**stripe_products table:** +```sql +CREATE TABLE stripe_products ( + tier_level INTEGER PRIMARY KEY, + tier_name VARCHAR(255), + stripe_price_id VARCHAR(255), + billing_type VARCHAR(50) -- 'recurring' or 'one-time' +); +``` + +--- + +## ๐ŸŽฏ Next Steps After Implementation + +1. **Test with real Discord accounts** (use test mode Stripe cards) +2. **Verify admin panel shows linked subscriptions** +3. **Update Ghost CMS website buttons** to use new OAuth flow +4. **Remove old direct Stripe checkout links** +5. **Implement Discord role assignment** (future task - Arbiter will auto-assign roles based on tier_level) + +--- + +## ๐Ÿ”ฅ Why This Architecture Is Perfect + +**Stateless:** No cookies, no sessions, no CSRF tokens needed + +**Secure:** Discord OAuth validates user identity + +**Automatic:** Zero manual linking required + +**Scalable:** Works for 1 user or 10,000 users + +**Simple:** Only 2 new routes, minimal code changes + +**RV-Ready:** Runs completely automated while Michael travels + +--- + +## ๐Ÿ“ Files Modified + +1. `/opt/arbiter-3.0/.env` - Add DISCORD_CHECKOUT_REDIRECT_URI +2. `/opt/arbiter-3.0/src/routes/stripe.js` - Add /auth and /callback routes, update webhook +3. Ghost CMS website - Update subscribe button URLs +4. Discord Developer Portal - Add new redirect URI + +--- + +## ๐ŸŽ‰ Final Note + +**This is the LAST piece of the payment puzzle!** Once this is implemented and tested, Firefrost Gaming has a fully automated subscription system that: + +- Takes payments via Stripe โœ… +- Links Discord users automatically โœ… +- Shows subscribers in admin panel โœ… +- Tracks revenue in Financials โœ… +- Ready for Discord role auto-assignment (future task) ๐Ÿ”œ + +**SOFT LAUNCH READY!** ๐Ÿš€๐Ÿ”ฅโ„๏ธ + +--- + +*Credit: Gemini's "Stateless OAuth Bridge" architecture* +*Documented by: The Validator (Chronicler #57)* +*Date: April 3, 2026*