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' }); } }); // Moderation API Endpoints app.get('/api/guild/:guildId/moderation/stats', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const [warningsRes, bansRes, actionsRes] = await Promise.all([ supabase.from('warnings').select('*', { count: 'exact', head: true }).eq('guild_id', guildId), supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).eq('action_type', 'ban'), supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()) ]); res.json({ totalWarnings: warningsRes.count || 0, activeBans: bansRes.count || 0, recentActions: actionsRes.count || 0 }); } catch (error) { res.status(500).json({ error: 'Failed to fetch stats' }); } }); app.get('/api/guild/:guildId/moderation/warnings', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const { data: warnings } = await supabase .from('warnings') .select('*') .eq('guild_id', guildId) .order('created_at', { ascending: false }) .limit(100); res.json({ warnings: warnings || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch warnings' }); } }); app.delete('/api/guild/:guildId/moderation/warnings/:warningId', 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, warningId } = req.params; const userGuild = req.session.user.guilds?.find(g => g.id === guildId); if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const { error } = await supabase .from('warnings') .delete() .eq('id', warningId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { res.status(500).json({ error: 'Failed to delete warning' }); } }); app.get('/api/guild/:guildId/moderation/bans', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const { data: bans } = await supabase .from('mod_actions') .select('*') .eq('guild_id', guildId) .eq('action_type', 'ban') .order('created_at', { ascending: false }) .limit(100); res.json({ bans: bans || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch bans' }); } }); app.get('/api/guild/:guildId/moderation/activity', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const { data: activity } = await supabase .from('mod_actions') .select('*') .eq('guild_id', guildId) .order('created_at', { ascending: false }) .limit(50); res.json({ activity: activity || [] }); } catch (error) { res.status(500).json({ error: 'Failed to fetch activity' }); } }); app.get('/api/guild/:guildId/moderation/search', 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 { q } = req.query; const userGuild = req.session.user.guilds?.find(g => g.id === guildId); if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' }); if (!q) return res.json({ results: [] }); try { const { data: warnings } = await supabase .from('warnings') .select('user_id, username') .eq('guild_id', guildId) .or(`user_id.eq.${q},username.ilike.%${q}%`) .limit(20); const userMap = new Map(); (warnings || []).forEach(w => { if (!userMap.has(w.user_id)) { userMap.set(w.user_id, { user_id: w.user_id, username: w.username, warnings_count: 0, is_banned: false }); } userMap.get(w.user_id).warnings_count++; }); const { data: bans } = await supabase .from('mod_actions') .select('target_id') .eq('guild_id', guildId) .eq('action_type', 'ban'); const bannedIds = new Set((bans || []).map(b => b.target_id)); userMap.forEach(u => { u.is_banned = bannedIds.has(u.user_id); }); res.json({ results: Array.from(userMap.values()) }); } catch (error) { res.status(500).json({ error: 'Search failed' }); } }); // Analytics API Endpoints app.get('/api/guild/:guildId/analytics/stats', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); const today = new Date(); today.setHours(0, 0, 0, 0); const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); try { const [messagesRes, activeRes, xpRes, commandsRes] = await Promise.all([ supabase.from('message_logs').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', today.toISOString()), supabase.from('user_stats').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('last_active', weekAgo.toISOString()), supabase.from('xp_logs').select('xp_amount').eq('guild_id', guildId).gte('created_at', today.toISOString()), supabase.from('command_logs').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', today.toISOString()) ]); const xpToday = (xpRes.data || []).reduce((sum, r) => sum + (r.xp_amount || 0), 0); res.json({ messagesToday: messagesRes.count || 0, activeUsers: activeRes.count || 0, xpToday, commandsUsed: commandsRes.count || 0 }); } catch (error) { res.json({ messagesToday: 0, activeUsers: 0, xpToday: 0, commandsUsed: 0 }); } }); app.get('/api/guild/:guildId/analytics/activity', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const days = []; const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); date.setHours(0, 0, 0, 0); const nextDate = new Date(date); nextDate.setDate(nextDate.getDate() + 1); const { count } = await supabase .from('message_logs') .select('*', { count: 'exact', head: true }) .eq('guild_id', guildId) .gte('created_at', date.toISOString()) .lt('created_at', nextDate.toISOString()); days.push({ label: dayNames[date.getDay()], messages: count || 0 }); } res.json({ days }); } catch (error) { res.json({ days: [] }); } }); app.get('/api/guild/:guildId/analytics/top-earners', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); const today = new Date(); today.setHours(0, 0, 0, 0); try { const { data } = await supabase .from('xp_logs') .select('user_id, xp_amount') .eq('guild_id', guildId) .gte('created_at', today.toISOString()); const userXp = new Map(); (data || []).forEach(r => { userXp.set(r.user_id, (userXp.get(r.user_id) || 0) + (r.xp_amount || 0)); }); const earners = Array.from(userXp.entries()) .map(([user_id, xp_earned]) => ({ user_id, xp_earned })) .sort((a, b) => b.xp_earned - a.xp_earned) .slice(0, 10); res.json({ earners }); } catch (error) { res.json({ earners: [] }); } }); app.get('/api/guild/:guildId/analytics/top-channels', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); const today = new Date(); today.setHours(0, 0, 0, 0); try { const { data } = await supabase .from('message_logs') .select('channel_id, channel_name') .eq('guild_id', guildId) .gte('created_at', today.toISOString()); const channelCounts = new Map(); (data || []).forEach(r => { const key = r.channel_id; if (!channelCounts.has(key)) { channelCounts.set(key, { channel_id: r.channel_id, channel_name: r.channel_name, message_count: 0 }); } channelCounts.get(key).message_count++; }); const channels = Array.from(channelCounts.values()) .sort((a, b) => b.message_count - a.message_count) .slice(0, 10); res.json({ channels }); } catch (error) { res.json({ channels: [] }); } }); app.get('/api/guild/:guildId/analytics/commands', 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?.isAdmin) return res.status(403).json({ error: 'No admin access' }); try { const { data } = await supabase .from('command_logs') .select('command_name') .eq('guild_id', guildId); const commandCounts = new Map(); (data || []).forEach(r => { commandCounts.set(r.command_name, (commandCounts.get(r.command_name) || 0) + 1); }); const commands = Array.from(commandCounts.entries()) .map(([command_name, use_count]) => ({ command_name, use_count })) .sort((a, b) => b.use_count - a.use_count) .slice(0, 10); res.json({ commands }); } catch (error) { res.json({ commands: [] }); } }); // Titles API endpoints app.get('/api/guild/:guildId/titles', 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 { // Get user's link const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', userId) .maybeSingle(); if (!link) { return res.json({ titles: [], activeTitle: null }); } // Get user's inventory items that are titles const { data: inventory } = await supabase .from('user_inventory') .select(` id, item_id, purchased_at, is_active, shop_items!inner (id, name, description, item_type) `) .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('shop_items.item_type', 'title'); const titles = (inventory || []).map(inv => ({ id: inv.id, item_id: inv.item_id, name: inv.shop_items?.name || 'Unknown Title', description: inv.shop_items?.description || '', purchased_at: inv.purchased_at, is_active: inv.is_active === true })); const activeTitle = titles.find(t => t.is_active) || null; res.json({ titles, activeTitle }); } catch (error) { console.error('Failed to fetch titles:', error); res.status(500).json({ error: 'Failed to load titles', titles: [], activeTitle: null }); } }); app.post('/api/guild/:guildId/titles/active', 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 { title_id } = req.body; if (!title_id) { return res.status(400).json({ error: 'Title ID required' }); } try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', userId) .maybeSingle(); if (!link) { return res.status(400).json({ error: 'Account not linked' }); } // Get all title-type inventory items and clear their active status const { data: titleItems } = await supabase .from('user_inventory') .select('id, shop_items!inner(item_type)') .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('shop_items.item_type', 'title'); if (titleItems && titleItems.length > 0) { const titleIds = titleItems.map(t => t.id); await supabase .from('user_inventory') .update({ is_active: false }) .in('id', titleIds); } // Set the selected title as active const { error } = await supabase .from('user_inventory') .update({ is_active: true }) .eq('id', title_id) .eq('user_id', link.user_id) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to set active title:', error); res.status(500).json({ error: 'Failed to set title' }); } }); app.delete('/api/guild/:guildId/titles/active', 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.status(400).json({ error: 'Account not linked' }); } // Get all title-type inventory items and clear their active status const { data: titleItems } = await supabase .from('user_inventory') .select('id, shop_items!inner(item_type)') .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('shop_items.item_type', 'title'); if (titleItems && titleItems.length > 0) { const titleIds = titleItems.map(t => t.id); await supabase .from('user_inventory') .update({ is_active: false }) .in('id', titleIds); } res.json({ success: true }); } catch (error) { console.error('Failed to clear active title:', error); res.status(500).json({ error: 'Failed to clear title' }); } }); 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 };