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',