const express = require('express'); const session = require('express-session'); 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'); // Production URL for OAuth const BASE_URL = process.env.BASE_URL || 'https://bot.aethex.dev'; app.use(cors({ origin: true, 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 })); app.use(session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 } })); app.use((req, res, next) => { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); next(); }); app.use(express.static(path.join(__dirname, '../public'))); app.get('/auth/discord', (req, res) => { if (!DISCORD_CLIENT_SECRET) { return res.status(500).json({ error: 'Discord OAuth not configured. Please set DISCORD_CLIENT_SECRET.' }); } const state = crypto.randomBytes(16).toString('hex'); req.session.oauthState = state; const redirectUri = `${BASE_URL}/auth/callback`; const scope = 'identify guilds'; const authUrl = `https://discord.com/api/oauth2/authorize?` + `client_id=${DISCORD_CLIENT_ID}` + `&redirect_uri=${encodeURIComponent(redirectUri)}` + `&response_type=code` + `&scope=${encodeURIComponent(scope)}` + `&state=${state}`; res.redirect(authUrl); }); app.get('/auth/callback', async (req, res) => { const { code, state } = req.query; if (!code) { return res.redirect('/?error=no_code'); } if (state !== req.session.oauthState) { return res.redirect('/?error=invalid_state'); } try { const redirectUri = `${BASE_URL}/auth/callback`; const tokenResponse = await fetch('https://discord.com/api/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: DISCORD_CLIENT_ID, client_secret: DISCORD_CLIENT_SECRET, grant_type: 'authorization_code', code, redirect_uri: redirectUri }) }); if (!tokenResponse.ok) { console.error('Token exchange failed:', await tokenResponse.text()); return res.redirect('/?error=token_failed'); } const tokens = await tokenResponse.json(); const userResponse = await fetch('https://discord.com/api/users/@me', { headers: { Authorization: `Bearer ${tokens.access_token}` } }); if (!userResponse.ok) { return res.redirect('/?error=user_fetch_failed'); } const user = await userResponse.json(); const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', { headers: { Authorization: `Bearer ${tokens.access_token}` } }); let guilds = []; if (guildsResponse.ok) { guilds = await guildsResponse.json(); } req.session.user = { id: user.id, username: user.username, discriminator: user.discriminator, avatar: user.avatar, globalName: user.global_name, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, guilds: guilds.map(g => ({ id: g.id, name: g.name, icon: g.icon, owner: g.owner, permissions: g.permissions, isAdmin: (parseInt(g.permissions) & 0x20) === 0x20 })) }; res.redirect('/dashboard'); } catch (error) { console.error('OAuth callback error:', error); res.redirect('/?error=auth_failed'); } }); app.get('/auth/logout', (req, res) => { req.session.destroy(); res.redirect('/'); }); app.get('/api/me', (req, res) => { if (!req.session.user) { return res.status(401).json({ error: 'Not authenticated' }); } const user = req.session.user; res.json({ id: user.id, username: user.username, discriminator: user.discriminator, avatar: user.avatar, globalName: user.globalName, avatarUrl: user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png` : `https://cdn.discordapp.com/embed/avatars/${parseInt(user.discriminator || '0') % 5}.png`, guilds: user.guilds.map(g => ({ ...g, icon: g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : null })) }); }); // Profile handler function async function handleProfileRequest(req, res, userId) { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const targetUserId = userId || req.session.user?.id; if (!targetUserId) { return res.status(400).json({ error: 'User ID required' }); } try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', targetUserId) .maybeSingle(); if (!link) { return res.json({ linked: false, message: 'Discord account not linked' }); } const { data: profile } = await supabase .from('user_profiles') .select('*') .eq('id', link.user_id) .maybeSingle(); if (!profile) { return res.json({ linked: true, profile: null }); } res.json({ linked: true, profile: { id: profile.id, username: profile.username, xp: profile.xp || 0, prestigeLevel: profile.prestige_level || 0, totalXpEarned: profile.total_xp_earned || 0, dailyStreak: profile.daily_streak || 0, title: profile.title, bio: profile.bio, avatarUrl: profile.avatar_url, createdAt: profile.created_at } }); } catch (error) { console.error('Profile fetch error:', error); res.status(500).json({ error: 'Failed to fetch profile' }); } } // Route for current user's profile app.get('/api/profile', (req, res) => handleProfileRequest(req, res, null)); // Route for specific user's profile app.get('/api/profile/:userId', (req, res) => handleProfileRequest(req, res, req.params.userId)); app.get('/api/stats/:userId/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { userId, guildId } = req.params; try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', userId) .maybeSingle(); if (!link) { return res.json({ stats: null }); } const { data: stats } = await supabase .from('user_stats') .select('*') .eq('user_id', link.user_id) .eq('guild_id', guildId) .maybeSingle(); res.json({ stats }); } catch (error) { res.status(500).json({ error: 'Failed to fetch stats' }); } }); app.get('/api/achievements/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { guildId } = req.params; const userId = req.session.user?.id; try { const { data: achievements } = await supabase .from('achievements') .select('*') .eq('guild_id', guildId) .eq('hidden', false) .order('created_at', { ascending: true }); let userAchievements = []; if (userId) { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', userId) .maybeSingle(); if (link) { const { data } = await supabase .from('user_achievements') .select('achievement_id, earned_at') .eq('user_id', link.user_id) .eq('guild_id', guildId); userAchievements = data || []; } } res.json({ achievements, userAchievements }); } catch (error) { res.status(500).json({ error: 'Failed to fetch achievements' }); } }); app.get('/api/quests/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { guildId } = req.params; const userId = req.session.user?.id; try { const now = new Date().toISOString(); const { data: quests } = await supabase .from('quests') .select('*') .eq('guild_id', guildId) .eq('active', true) .or(`starts_at.is.null,starts_at.lte.${now}`) .or(`expires_at.is.null,expires_at.gt.${now}`) .order('created_at', { ascending: false }); let userQuests = []; if (userId) { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', userId) .maybeSingle(); if (link) { const { data } = await supabase .from('user_quests') .select('*') .eq('user_id', link.user_id) .eq('guild_id', guildId); userQuests = data || []; } } res.json({ quests, userQuests }); } catch (error) { res.status(500).json({ error: 'Failed to fetch quests' }); } }); app.get('/api/shop/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { guildId } = req.params; try { const { data: items } = await supabase .from('shop_items') .select('*') .eq('guild_id', guildId) .eq('enabled', true) .order('price', { ascending: true }); res.json({ items: items || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch shop items' }); } }); app.get('/api/inventory/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', userId) .maybeSingle(); if (!link) { return res.json({ inventory: [] }); } const { data: inventory } = await supabase .from('user_inventory') .select(` *, shop_items (name, description, item_type, item_data) `) .eq('user_id', link.user_id) .eq('guild_id', guildId); res.json({ inventory: inventory || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch inventory' }); } }); app.get('/api/leaderboard/:guildId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { guildId } = req.params; const { type = 'all', limit = 50 } = req.query; try { let tableName = 'leaderboard_alltime'; if (type === 'weekly') tableName = 'leaderboard_weekly'; else if (type === 'monthly') tableName = 'leaderboard_monthly'; const { data: leaderboard } = await supabase .from(tableName) .select(` *, user_profiles!inner (username, avatar_url, prestige_level) `) .eq('guild_id', guildId) .order('xp', { ascending: false }) .limit(parseInt(limit)); res.json({ leaderboard: leaderboard || [], type }); } catch (error) { res.status(500).json({ error: 'Failed to fetch leaderboard' }); } }); app.get('/api/bot-stats', (req, res) => { const stats = { guilds: discordClient.guilds.cache.size, users: discordClient.guilds.cache.reduce((acc, g) => acc + g.memberCount, 0), commands: discordClient.commands?.size || 0, uptime: Math.floor(process.uptime()), status: discordClient.user ? 'online' : 'offline', botName: discordClient.user?.username || 'AeThex', botAvatar: discordClient.user?.displayAvatarURL() || null }; res.json(stats); }); app.get('/api/guild/:guildId/config', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access to this server' }); } try { const { data: xpConfig } = await supabase .from('xp_config') .select('*') .eq('guild_id', guildId) .maybeSingle(); const { data: serverConfig } = await supabase .from('server_config') .select('*') .eq('guild_id', guildId) .maybeSingle(); res.json({ xpConfig, serverConfig }); } catch (error) { res.status(500).json({ error: 'Failed to fetch config' }); } }); app.post('/api/guild/:guildId/xp-config', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access to this server' }); } const { message_xp, message_cooldown, reaction_xp_enabled, reaction_xp_given, reaction_xp_received, reaction_cooldown, level_curve, xp_enabled, levelup_channel_id, levelup_message, levelup_dm, levelup_embed, levelup_embed_color, multiplier_roles, bonus_channels } = req.body; try { const { data: existing } = await supabase .from('xp_config') .select('guild_id') .eq('guild_id', guildId) .maybeSingle(); const configData = { guild_id: guildId, message_xp: message_xp ?? 15, message_cooldown: message_cooldown ?? 60, reaction_xp_enabled: reaction_xp_enabled !== false, reaction_xp_given: reaction_xp_given ?? 2, reaction_xp_received: reaction_xp_received ?? 5, reaction_cooldown: reaction_cooldown ?? 30, level_curve: level_curve || 'normal', xp_enabled: xp_enabled !== false, levelup_channel_id: levelup_channel_id || null, levelup_message: levelup_message || null, levelup_dm: levelup_dm === true, levelup_embed: levelup_embed !== false, levelup_embed_color: levelup_embed_color || '#7c3aed', multiplier_roles: multiplier_roles || [], bonus_channels: bonus_channels || [], updated_at: new Date().toISOString() }; if (existing) { const { error } = await supabase .from('xp_config') .update(configData) .eq('guild_id', guildId); if (error) throw error; } else { const { error } = await supabase .from('xp_config') .insert(configData); if (error) throw error; } res.json({ success: true }); } catch (error) { console.error('Failed to save XP config:', error); res.status(500).json({ error: 'Failed to save settings', details: error.message }); } }); app.get('/api/guild/:guildId/quests', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } try { const { data: quests } = await supabase .from('quests') .select('*') .eq('guild_id', guildId) .order('created_at', { ascending: false }); res.json({ quests: quests || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch quests' }); } }); app.post('/api/guild/:guildId/quests', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } const { name, description, quest_type, objective, target_value, xp_reward, duration_hours, repeatable, active } = req.body; try { const questData = { guild_id: guildId, name, description: description || null, quest_type: quest_type || 'daily', objective: objective || 'messages', target_value: target_value || 10, xp_reward: xp_reward || 100, repeatable: repeatable || false, active: active !== false, created_at: new Date().toISOString() }; if (duration_hours && duration_hours > 0) { const expiry = new Date(); expiry.setHours(expiry.getHours() + duration_hours); questData.expires_at = expiry.toISOString(); } const { error } = await supabase.from('quests').insert(questData); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to create quest:', error); res.status(500).json({ error: 'Failed to create quest' }); } }); app.put('/api/guild/:guildId/quests/:questId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId, questId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } const { name, description, quest_type, objective, target_value, xp_reward, repeatable, active } = req.body; try { const { error } = await supabase .from('quests') .update({ name, description: description || null, quest_type, objective, target_value, xp_reward, repeatable: repeatable || false, active }) .eq('id', questId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to update quest:', error); res.status(500).json({ error: 'Failed to update quest' }); } }); app.delete('/api/guild/:guildId/quests/:questId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId, questId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } try { const { error } = await supabase .from('quests') .delete() .eq('id', questId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to delete quest:', error); res.status(500).json({ error: 'Failed to delete quest' }); } }); // ============ ACHIEVEMENTS ADMIN API ============ app.get('/api/guild/:guildId/achievements', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } try { const { data: achievements } = await supabase .from('achievements') .select('*') .eq('guild_id', guildId) .order('created_at', { ascending: false }); res.json({ achievements: achievements || [] }); } catch (error) { console.error('Failed to fetch achievements:', error); res.status(500).json({ error: 'Failed to fetch achievements' }); } }); app.post('/api/guild/:guildId/achievements', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } const { name, icon, description, trigger_type, trigger_value, reward_xp, reward_role_id, hidden } = req.body; try { const achievementData = { guild_id: guildId, name, icon: icon || '🏆', description: description || null, trigger_type: trigger_type || 'level', trigger_value: trigger_value || 1, reward_xp: reward_xp || 0, reward_role_id: reward_role_id || null, hidden: hidden || false, created_at: new Date().toISOString() }; const { error } = await supabase.from('achievements').insert(achievementData); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to create achievement:', error); res.status(500).json({ error: 'Failed to create achievement' }); } }); app.put('/api/guild/:guildId/achievements/:achievementId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId, achievementId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } const { name, icon, description, trigger_type, trigger_value, reward_xp, reward_role_id, hidden } = req.body; try { const { error } = await supabase .from('achievements') .update({ name, icon: icon || '🏆', description: description || null, trigger_type, trigger_value, reward_xp: reward_xp || 0, reward_role_id: reward_role_id || null, hidden: hidden || false }) .eq('id', achievementId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to update achievement:', error); res.status(500).json({ error: 'Failed to update achievement' }); } }); app.delete('/api/guild/:guildId/achievements/:achievementId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId, achievementId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } try { const { error } = await supabase .from('achievements') .delete() .eq('id', achievementId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to delete achievement:', error); res.status(500).json({ error: 'Failed to delete achievement' }); } }); // ============ SHOP ADMIN API ============ app.get('/api/guild/:guildId/shop/admin', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } try { const { data: items } = await supabase .from('shop_items') .select('*') .eq('guild_id', guildId) .order('created_at', { ascending: false }); res.json({ items: items || [] }); } catch (error) { console.error('Failed to fetch shop items:', error); res.status(500).json({ error: 'Failed to fetch shop items' }); } }); app.post('/api/guild/:guildId/shop', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } const { name, item_type, description, price, stock, level_required, prestige_required, enabled, category, item_data } = req.body; try { const itemData = { guild_id: guildId, name, item_type: item_type || 'cosmetic', description: description || null, category: category || null, price: price || 100, stock: stock || null, item_data: item_data || null, level_required: level_required || 0, prestige_required: prestige_required || 0, enabled: enabled !== false, created_at: new Date().toISOString() }; const { error } = await supabase.from('shop_items').insert(itemData); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to create shop item:', error); res.status(500).json({ error: 'Failed to create shop item', details: error.message }); } }); app.put('/api/guild/:guildId/shop/:itemId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId, itemId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } const { name, item_type, description, price, stock, level_required, prestige_required, enabled, category, item_data } = req.body; try { const { error } = await supabase .from('shop_items') .update({ name, item_type: item_type || 'cosmetic', description: description || null, category: category || null, price: price || 100, stock: stock || null, item_data: item_data || null, level_required: level_required || 0, prestige_required: prestige_required || 0, enabled: enabled !== false, updated_at: new Date().toISOString() }) .eq('id', itemId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to update shop item:', error); res.status(500).json({ error: 'Failed to update shop item', details: error.message }); } }); app.delete('/api/guild/:guildId/shop/:itemId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { guildId, itemId } = req.params; const userGuild = req.session.user.guilds.find(g => g.id === guildId); if (!userGuild || !userGuild.isAdmin) { return res.status(403).json({ error: 'No admin access' }); } try { const { error } = await supabase .from('shop_items') .delete() .eq('id', itemId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to delete shop item:', error); res.status(500).json({ error: 'Failed to delete shop item' }); } }); // ============ FEDERATION API ============ app.get('/api/federation/stats', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } try { const [servers, bans, applications] = await Promise.all([ supabase.from('federation_servers').select('id', { count: 'exact' }).eq('status', 'approved'), supabase.from('federation_bans').select('id', { count: 'exact' }).eq('active', true), supabase.from('federation_applications').select('id', { count: 'exact' }).eq('status', 'pending') ]); res.json({ totalServers: servers.count || 0, activeBans: bans.count || 0, pendingApplications: applications.count || 0 }); } catch (error) { res.status(500).json({ error: 'Failed to fetch stats' }); } }); app.get('/api/federation/bans', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { limit = 50, severity } = req.query; try { let query = supabase .from('federation_bans') .select('*') .eq('active', true) .order('created_at', { ascending: false }) .limit(parseInt(limit)); if (severity) { query = query.eq('severity', severity); } const { data: bans } = await query; res.json({ bans: bans || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch bans' }); } }); app.get('/api/federation/servers', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } try { const { data: servers } = await supabase .from('federation_servers') .select('*') .eq('status', 'approved') .order('member_count', { ascending: false }); res.json({ servers: servers || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch servers' }); } }); app.get('/api/federation/applications', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } try { const { data: applications } = await supabase .from('federation_applications') .select('*') .order('created_at', { ascending: false }); res.json({ applications: applications || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch applications' }); } }); app.post('/api/federation/applications/:appId/approve', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { appId } = req.params; try { const { data: app } = await supabase .from('federation_applications') .select('*') .eq('id', appId) .maybeSingle(); if (!app) { return res.status(404).json({ error: 'Application not found' }); } await supabase.from('federation_applications').update({ status: 'approved', reviewed_by: userId, reviewed_at: new Date().toISOString() }).eq('id', appId); await supabase.from('federation_servers').insert({ guild_id: app.guild_id, guild_name: app.guild_name, guild_icon: app.guild_icon, description: app.description, category: app.category, member_count: app.member_count, owner_id: app.admin_id, status: 'approved', treaty_accepted: true, treaty_accepted_at: new Date().toISOString() }); res.json({ success: true }); } catch (error) { console.error('Failed to approve application:', error); res.status(500).json({ error: 'Failed to approve application' }); } }); app.post('/api/federation/applications/:appId/reject', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const userId = req.session.user?.id; if (!userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { appId } = req.params; const { reason } = req.body; try { await supabase.from('federation_applications').update({ status: 'rejected', reviewed_by: userId, reviewed_at: new Date().toISOString(), rejection_reason: reason || 'No reason provided' }).eq('id', appId); res.json({ success: true }); } catch (error) { res.status(500).json({ error: 'Failed to reject application' }); } }); app.get('/api/federation/leaderboard', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { limit = 50 } = req.query; try { const { data: leaders } = await supabase .from('federation_reputation') .select('*') .order('reputation_score', { ascending: false }) .limit(parseInt(limit)); res.json({ leaderboard: leaders || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch leaderboard' }); } }); // ============ 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', bot: discordClient.user ? 'online' : 'offline', uptime: process.uptime() }); }); app.get('/api/bot-stats', async (req, res) => { try { const guilds = discordClient.guilds?.cache?.size || 0; let users = 0; if (discordClient.guilds?.cache) { discordClient.guilds.cache.forEach(guild => { users += guild.memberCount || 0; }); } let linkedProfiles = 0; if (supabase) { const { count } = await supabase .from('discord_links') .select('*', { count: 'exact', head: true }); linkedProfiles = count || 0; } res.json({ guilds, users, linkedProfiles, commands: 66, uptime: process.uptime() }); } catch (error) { res.json({ guilds: 0, users: 0, linkedProfiles: 0, commands: 66 }); } }); app.get('/federation', (req, res) => { res.sendFile(path.join(__dirname, '../public/federation.html')); }); app.get('/pricing', (req, res) => { res.sendFile(path.join(__dirname, '../public/pricing.html')); }); app.get('/dashboard', (req, res) => { res.sendFile(path.join(__dirname, '../public/dashboard.html')); }); app.get('/leaderboard', (req, res) => { res.redirect('/dashboard?page=leaderboard'); }); app.get('/features', (req, res) => { res.sendFile(path.join(__dirname, '../public/features.html')); }); app.get('/commands', (req, res) => { res.sendFile(path.join(__dirname, '../public/commands.html')); }); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); // Catch-all route for SPA - use middleware instead of wildcard app.use((req, res, next) => { if (req.method === 'GET' && !req.path.startsWith('/api/')) { res.sendFile(path.join(__dirname, '../public/index.html')); } else if (req.path.startsWith('/api/')) { res.status(404).json({ error: 'API endpoint not found' }); } else { next(); } }); return app; } module.exports = { createWebServer };