From 150ec1707230cc59432fa63f86cbf2baa8c67c25 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Wed, 10 Dec 2025 02:08:27 +0000 Subject: [PATCH] Add pricing plans and integrate Stripe for server upgrades Add new styling and HTML structure for pricing cards to federation.html, and configure Stripe webhook handling in webServer.js to manage subscription events and update server tiers in the database. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 2926813b-ebbf-4fd8-aa3c-58eb0dbcf182 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/0iqMcgO Replit-Helium-Checkpoint-Created: true --- .replit | 4 + aethex-bot/public/federation.html | 208 ++++++++++++++++++++++++++++++ aethex-bot/server/webServer.js | 202 +++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+) diff --git a/.replit b/.replit index 9585c0f..c1e5885 100644 --- a/.replit +++ b/.replit @@ -21,6 +21,10 @@ externalPort = 80 localPort = 8080 externalPort = 8080 +[[ports]] +localPort = 39693 +externalPort = 3000 + [workflows] runButton = "Project" diff --git a/aethex-bot/public/federation.html b/aethex-bot/public/federation.html index 14d3c1a..f67d376 100644 --- a/aethex-bot/public/federation.html +++ b/aethex-bot/public/federation.html @@ -327,6 +327,86 @@ color: var(--muted); } + .pricing-section { + margin-bottom: 3rem; + } + + .pricing-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + } + + .pricing-card { + background: var(--card); + border: 1px solid var(--card-border); + border-radius: 16px; + padding: 2rem; + text-align: center; + transition: all 0.2s; + } + + .pricing-card:hover { + border-color: var(--card-border-hover); + transform: translateY(-4px); + } + + .pricing-card.featured { + border-color: var(--primary); + background: linear-gradient(180deg, rgba(99, 102, 241, 0.1) 0%, var(--card) 100%); + } + + .pricing-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + background: var(--primary); + color: white; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + margin-bottom: 1rem; + } + + .pricing-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; } + .pricing-price { font-size: 2.5rem; font-weight: 700; color: var(--primary-light); } + .pricing-period { font-size: 0.9rem; color: var(--muted); } + .pricing-desc { color: var(--muted); margin: 1rem 0; font-size: 0.9rem; } + + .pricing-features { + list-style: none; + text-align: left; + margin: 1.5rem 0; + } + + .pricing-features li { + padding: 0.5rem 0; + color: var(--muted); + display: flex; + align-items: center; + gap: 0.5rem; + } + + .pricing-features li::before { + content: '✓'; + color: var(--success); + font-weight: 700; + } + + .select-wrapper { + margin-bottom: 1rem; + } + + .select-wrapper select { + width: 100%; + padding: 0.75rem; + border-radius: 8px; + background: var(--secondary); + border: 1px solid var(--border); + color: var(--foreground); + font-family: inherit; + font-size: 0.9rem; + } + @media (max-width: 768px) { .stats-grid { grid-template-columns: 1fr; } .nav-links { display: none; } @@ -364,6 +444,64 @@ +
+

Upgrade Your Protection

+
+
+
Free
+
$0
+
forever
+
Basic protection for all federation members
+
    +
  • Critical threat auto-bans
  • +
  • Server directory listing
  • +
  • Reputation tracking
  • +
  • Community support
  • +
+ +
+ + + +
+
Featured Slot
+
$200
+
per week
+
Promote your server across the entire federation
+
    +
  • Featured in all member servers
  • +
  • Cross-server promotion
  • +
  • Boost your member count
  • +
  • Priority directory placement
  • +
+
+ +
+ +
+
+
+
@@ -615,11 +753,81 @@ } } + async function loadUserGuilds() { + try { + const res = await fetch('/api/user'); + if (!res.ok) return; + + const data = await res.json(); + if (!data.user?.guilds) return; + + const adminGuilds = data.user.guilds.filter(g => g.isAdmin); + + const premiumSelect = document.getElementById('premiumGuildSelect'); + const featuredSelect = document.getElementById('featuredGuildSelect'); + + adminGuilds.forEach(g => { + const option1 = new Option(g.name, g.id); + const option2 = new Option(g.name, g.id); + premiumSelect.appendChild(option1); + featuredSelect.appendChild(option2); + }); + } catch (e) { + console.error('Failed to load user guilds:', e); + } + } + + async function upgradePlan(planType) { + const selectId = planType === 'premium' ? 'premiumGuildSelect' : 'featuredGuildSelect'; + const guildId = document.getElementById(selectId).value; + + if (!guildId) { + alert('Please select a server first'); + return; + } + + try { + const res = await fetch('/api/stripe/create-checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId, planType }) + }); + + if (!res.ok) { + const error = await res.json(); + if (res.status === 401) { + alert('Please log in first using Discord'); + window.location.href = '/auth/discord'; + return; + } + throw new Error(error.error || 'Failed to create checkout'); + } + + const data = await res.json(); + if (data.url) { + window.location.href = data.url; + } + } catch (e) { + alert('Failed to start checkout: ' + e.message); + } + } + + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('success') === 'true') { + const plan = urlParams.get('plan'); + alert(`Successfully upgraded to ${plan === 'premium' ? 'Premium' : 'Featured Slot'}! Your server is now protected.`); + window.history.replaceState({}, '', '/federation'); + } + if (urlParams.get('canceled') === 'true') { + window.history.replaceState({}, '', '/federation'); + } + loadStats(); loadServers(); loadBans(); loadApplications(); loadLeaderboard(); + loadUserGuilds(); diff --git a/aethex-bot/server/webServer.js b/aethex-bot/server/webServer.js index f799337..6f90c7b 100644 --- a/aethex-bot/server/webServer.js +++ b/aethex-bot/server/webServer.js @@ -4,10 +4,14 @@ const cookieParser = require('cookie-parser'); const cors = require('cors'); const path = require('path'); const crypto = require('crypto'); +const Stripe = require('stripe'); function createWebServer(discordClient, supabase, options = {}) { const app = express(); + const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY) : null; + const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET; + const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'); @@ -19,6 +23,97 @@ function createWebServer(discordClient, supabase, options = {}) { credentials: true })); app.use(cookieParser()); + + // Stripe webhook must be registered BEFORE express.json() to access raw body + app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => { + if (!stripe || !STRIPE_WEBHOOK_SECRET) { + return res.status(503).json({ error: 'Stripe webhook not configured' }); + } + + const sig = req.headers['stripe-signature']; + let event; + + try { + event = stripe.webhooks.constructEvent(req.body, sig, STRIPE_WEBHOOK_SECRET); + } catch (err) { + console.error('Webhook signature verification failed:', err.message); + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + try { + switch (event.type) { + case 'checkout.session.completed': { + const session = event.data.object; + const { guild_id, plan_type } = session.metadata; + + if (plan_type === 'premium') { + await supabase.from('federation_servers').update({ + tier: 'premium', + subscription_id: session.subscription, + subscription_status: 'active', + updated_at: new Date().toISOString() + }).eq('guild_id', guild_id); + + console.log(`[Stripe] Guild ${guild_id} upgraded to premium`); + } else if (plan_type === 'featured') { + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); + + await supabase.from('federation_featured').upsert({ + guild_id: guild_id, + subscription_id: session.subscription, + active: true, + expires_at: expiresAt.toISOString(), + created_at: new Date().toISOString() + }, { onConflict: 'guild_id' }); + + console.log(`[Stripe] Guild ${guild_id} purchased featured slot`); + } + break; + } + + case 'customer.subscription.updated': { + const subscription = event.data.object; + const status = subscription.status; + + if (status === 'active') { + await supabase.from('federation_servers').update({ + subscription_status: 'active' + }).eq('subscription_id', subscription.id); + } else if (status === 'past_due' || status === 'unpaid') { + await supabase.from('federation_servers').update({ + subscription_status: status + }).eq('subscription_id', subscription.id); + } + break; + } + + case 'customer.subscription.deleted': { + const subscription = event.data.object; + + await supabase.from('federation_servers').update({ + tier: 'free', + subscription_id: null, + subscription_status: null, + updated_at: new Date().toISOString() + }).eq('subscription_id', subscription.id); + + await supabase.from('federation_featured').update({ + active: false + }).eq('subscription_id', subscription.id); + + console.log(`[Stripe] Subscription ${subscription.id} canceled`); + break; + } + } + + res.json({ received: true }); + } catch (error) { + console.error('Webhook processing error:', error); + res.status(500).json({ error: 'Webhook processing failed' }); + } + }); + app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -1224,6 +1319,113 @@ function createWebServer(discordClient, supabase, options = {}) { } }); + // ============ STRIPE PAYMENT API ============ + + app.post('/api/stripe/create-checkout', async (req, res) => { + if (!stripe) { + return res.status(503).json({ error: 'Stripe not configured' }); + } + + const userId = req.session.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { guildId, planType } = req.body; + + if (!guildId || !planType) { + return res.status(400).json({ error: 'Missing guildId or planType' }); + } + + const userGuild = req.session.user.guilds?.find(g => g.id === guildId); + if (!userGuild || !userGuild.isAdmin) { + return res.status(403).json({ error: 'Must be server admin to purchase' }); + } + + try { + const prices = { + premium: { + amount: 5000, + name: 'Federation Premium', + description: 'Full protection from all ban severity levels - auto-kick for all threats', + recurring: { interval: 'month' } + }, + featured: { + amount: 20000, + name: 'Featured Server Slot', + description: 'Cross-promotion across all federation servers for 1 week', + recurring: { interval: 'week' } + } + }; + + const plan = prices[planType]; + if (!plan) { + return res.status(400).json({ error: 'Invalid plan type' }); + } + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ['card'], + mode: 'subscription', + line_items: [{ + price_data: { + currency: 'usd', + product_data: { + name: plan.name, + description: plan.description + }, + unit_amount: plan.amount, + recurring: plan.recurring + }, + quantity: 1 + }], + metadata: { + guild_id: guildId, + user_id: userId, + plan_type: planType + }, + success_url: `${BASE_URL}/federation?success=true&plan=${planType}`, + cancel_url: `${BASE_URL}/federation?canceled=true` + }); + + res.json({ url: session.url }); + } catch (error) { + console.error('Stripe checkout error:', error); + res.status(500).json({ error: 'Failed to create checkout session' }); + } + }); + + app.get('/api/federation/subscription/:guildId', async (req, res) => { + if (!supabase) { + return res.status(503).json({ error: 'Database not available' }); + } + + const { guildId } = req.params; + + try { + const { data: server } = await supabase + .from('federation_servers') + .select('tier, subscription_status') + .eq('guild_id', guildId) + .maybeSingle(); + + const { data: featured } = await supabase + .from('federation_featured') + .select('active, expires_at') + .eq('guild_id', guildId) + .eq('active', true) + .maybeSingle(); + + res.json({ + tier: server?.tier || 'free', + subscriptionStatus: server?.subscription_status || null, + featured: featured?.active || false, + featuredExpiresAt: featured?.expires_at || null + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch subscription' }); + } + }); + app.get('/health', (req, res) => { res.json({ status: 'ok',