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 <claude@firefrostgaming.com>
This commit is contained in:
Claude (Chronicler #58)
2026-04-04 00:18:27 +00:00
parent ce5ff2097a
commit d9f033aa02

View File

@@ -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
<a href="https://buy.stripe.com/..." class="btn">Subscribe to Awakened</a>
```
**AFTER (new OAuth flow buttons):**
```html
<!-- Awakened - Tier 1 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=1" class="btn">
Subscribe to Awakened ($1)
</a>
<!-- Elemental Fire - Tier 2 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=2" class="btn">
Subscribe to Elemental (Fire) ($5/mo)
</a>
<!-- Elemental Frost - Tier 3 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=3" class="btn">
Subscribe to Elemental (Frost) ($5/mo)
</a>
<!-- Knight Fire - Tier 4 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=4" class="btn">
Subscribe to Knight (Fire) ($10/mo)
</a>
<!-- Knight Frost - Tier 5 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=5" class="btn">
Subscribe to Knight (Frost) ($10/mo)
</a>
<!-- Master Fire - Tier 6 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=6" class="btn">
Subscribe to Master (Fire) ($15/mo)
</a>
<!-- Master Frost - Tier 7 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=7" class="btn">
Subscribe to Master (Frost) ($15/mo)
</a>
<!-- Legend Fire - Tier 8 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=8" class="btn">
Subscribe to Legend (Fire) ($20/mo)
</a>
<!-- Legend Frost - Tier 9 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=9" class="btn">
Subscribe to Legend (Frost) ($20/mo)
</a>
<!-- Sovereign - Tier 10 -->
<a href="https://discord-bot.firefrostgaming.com/stripe/auth?tier=10" class="btn btn-sovereign">
Subscribe to Sovereign ($499)
</a>
```
---
## 🧪 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*