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'); const { invalidateCooldownCache } = require('../utils/cooldownManager'); 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)); // Verification callback endpoint - called by aethex.dev when user verifies app.post('/api/verify-callback', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { discord_id, user_id, secret } = req.body; // Verify the callback secret (should match what aethex.dev uses) const expectedSecret = process.env.VERIFY_CALLBACK_SECRET; if (expectedSecret && secret !== expectedSecret) { return res.status(401).json({ error: 'Invalid callback secret' }); } if (!discord_id || !user_id) { return res.status(400).json({ error: 'Missing discord_id or user_id' }); } try { // Get server configs that have a verified role configured const { data: configs } = await supabase .from('server_config') .select('guild_id, verified_role_id') .not('verified_role_id', 'is', null); let rolesAssigned = 0; // Assign verified role in all guilds where the user is a member for (const config of configs || []) { if (!config.verified_role_id) continue; try { const guild = client.guilds.cache.get(config.guild_id); if (!guild) continue; const member = await guild.members.fetch(discord_id).catch(() => null); if (!member) continue; const role = guild.roles.cache.get(config.verified_role_id); if (!role) continue; await member.roles.add(role, 'Account verified via aethex.dev'); rolesAssigned++; console.log(`[Verify] Assigned verified role to ${member.user.tag} in ${guild.name}`); } catch (err) { console.error(`[Verify] Failed to assign role in guild ${config.guild_id}:`, err.message); } } // Clean up the verification code await supabase.from('discord_verifications').delete().eq('discord_id', discord_id); res.json({ success: true, rolesAssigned }); } catch (error) { console.error('Verify callback error:', error); res.status(500).json({ error: 'Failed to process verification callback' }); } }); // Verification status endpoint app.get('/api/verify-status/:discordId', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const { discordId } = req.params; try { const { data: link } = await supabase .from('discord_links') .select('user_id, linked_at') .eq('discord_id', discordId) .maybeSingle(); const { data: pending } = await supabase .from('discord_verifications') .select('verification_code, expires_at') .eq('discord_id', discordId) .maybeSingle(); res.json({ verified: !!link, userId: link?.user_id || null, linkedAt: link?.linked_at || null, pendingVerification: !!pending, pendingExpires: pending?.expires_at || null }); } catch (error) { console.error('Verify status error:', error); res.status(500).json({ error: 'Failed to fetch verification status' }); } }); // Verification success endpoint - called by aethex.dev when verification completes app.post('/api/verify-success', async (req, res) => { if (!supabase) { return res.status(503).json({ error: 'Database not available' }); } const webhookSecret = process.env.DISCORD_BOT_WEBHOOK_SECRET; const providedSecret = req.headers['x-webhook-secret'] || req.body.secret; if (webhookSecret && providedSecret !== webhookSecret) { console.log('[Verify-Success] Invalid webhook secret provided'); return res.status(401).json({ error: 'Invalid webhook secret' }); } const { discord_id, user_id, username } = req.body; if (!discord_id) { return res.status(400).json({ error: 'Missing discord_id' }); } console.log(`[Verify-Success] Received verification for Discord ID: ${discord_id}`); try { // Update or create the discord link if (user_id) { await supabase.from('discord_links').upsert({ discord_id, user_id, username: username || null, linked_at: new Date().toISOString() }, { onConflict: 'discord_id' }); } // Get server configs that have a verified role configured const { data: configs } = await supabase .from('server_config') .select('guild_id, verified_role_id') .not('verified_role_id', 'is', null); let rolesAssigned = 0; const assignedIn = []; // Assign verified role in all guilds where the user is a member for (const config of configs || []) { if (!config.verified_role_id) continue; try { const guild = client.guilds.cache.get(config.guild_id); if (!guild) continue; const member = await guild.members.fetch(discord_id).catch(() => null); if (!member) continue; const role = guild.roles.cache.get(config.verified_role_id); if (!role) continue; if (!member.roles.cache.has(role.id)) { await member.roles.add(role, 'Account verified via aethex.dev'); rolesAssigned++; assignedIn.push(guild.name); console.log(`[Verify-Success] Assigned verified role to ${member.user.tag} in ${guild.name}`); } } catch (err) { console.error(`[Verify-Success] Failed to assign role in guild ${config.guild_id}:`, err.message); } } // Clean up any pending verification codes await supabase.from('discord_verifications').delete().eq('discord_id', discord_id); res.json({ success: true, rolesAssigned, assignedIn, message: rolesAssigned > 0 ? `Verified role assigned in ${rolesAssigned} server(s)` : 'Verification recorded (user not found in any configured servers)' }); } catch (error) { console.error('[Verify-Success] Error:', error); res.status(500).json({ error: 'Failed to process verification' }); } }); 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', head: true }).eq('status', 'approved'), supabase.from('federation_bans').select('id', { count: 'exact', head: true }).eq('active', true), supabase.from('federation_applications').select('id', { count: 'exact', head: true }).eq('status', 'pending') ]); res.json({ totalServers: servers.count || 0, activeBans: bans.count || 0, pendingApplications: applications.count || 0 }); } catch (error) { console.error('[Federation Stats] Error:', 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, error } = await supabase .from('federation_servers') .select('*') .eq('status', 'approved') .order('member_count', { ascending: false }); res.json({ servers: servers || [] }); } catch (error) { console.error('[Federation Servers] Error:', 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' }); } }); app.get('/api/federation/guild/: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('*') .eq('guild_id', guildId) .maybeSingle(); if (!server) { return res.json({ member: false, message: 'This server is not a federation member' }); } const { getProgressToNextLevel, getTrustLevelInfo, TRUST_LEVELS } = require('../utils/trustLevels'); const trustLevelInfo = getTrustLevelInfo(server.trust_level || 'bronze'); const progression = getProgressToNextLevel(server); const guild = discordClient.guilds.cache.get(guildId); const memberCount = guild?.memberCount || server.member_count || 0; res.json({ member: true, server: { guild_id: server.guild_id, guild_name: server.guild_name, guild_icon: server.guild_icon, member_count: memberCount, tier: server.tier, status: server.status, joined_at: server.created_at, joined_federation_at: server.joined_federation_at || server.created_at }, trustLevel: { level: server.trust_level || 'bronze', name: trustLevelInfo.name, emoji: trustLevelInfo.emoji, color: trustLevelInfo.color, benefits: trustLevelInfo.benefits }, reputation: { score: server.reputation_score || 0, reports_submitted: server.reports_submitted || 0, false_positives: server.false_positives || 0, last_activity: server.last_activity }, progression: { nextLevel: progression.nextLevel, nextLevelInfo: progression.nextLevelInfo ? { name: progression.nextLevelInfo.name, emoji: progression.nextLevelInfo.emoji } : null, progress: progression.progress, allMet: progression.allMet } }); } catch (error) { console.error('Failed to fetch guild federation stats:', error); res.status(500).json({ error: 'Failed to fetch federation stats' }); } }); // ============ 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' }); } }); app.post('/api/guild/:guildId/moderation/bulk', async (req, res) => { 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 { action, userIds, reason, duration, deleteDays } = req.body; if (!action || !userIds || !Array.isArray(userIds)) { return res.status(400).json({ error: 'Invalid request' }); } if (userIds.length > 25) { return res.status(400).json({ error: 'Maximum 25 users per bulk action' }); } const validActions = ['ban', 'kick', 'timeout', 'warn', 'remove_timeout']; if (!validActions.includes(action)) { return res.status(400).json({ error: 'Invalid action' }); } const guild = discordClient.guilds.cache.get(guildId); if (!guild) { return res.status(404).json({ error: 'Guild not found' }); } const moderator = req.session.user; const results = { success: [], failed: [] }; const durationMap = { '5m': 5 * 60 * 1000, '10m': 10 * 60 * 1000, '30m': 30 * 60 * 1000, '1h': 60 * 60 * 1000, '6h': 6 * 60 * 60 * 1000, '12h': 12 * 60 * 60 * 1000, '1d': 24 * 60 * 60 * 1000, '3d': 3 * 24 * 60 * 60 * 1000, '1w': 7 * 24 * 60 * 60 * 1000 }; for (const targetId of userIds) { try { switch (action) { case 'ban': { await guild.members.ban(targetId, { reason: `[Dashboard Bulk] ${reason || 'No reason'} | By ${moderator.username}`, deleteMessageSeconds: (deleteDays || 0) * 24 * 60 * 60 }); if (supabase) { const user = await discordClient.users.fetch(targetId).catch(() => null); await supabase.from('mod_actions').insert({ guild_id: guildId, action: 'ban', user_id: targetId, user_tag: user?.tag || 'Unknown', moderator_id: moderator.id, moderator_tag: moderator.username, reason: `[Dashboard Bulk] ${reason || 'No reason'}` }).catch(() => {}); } results.success.push(targetId); break; } case 'kick': { const member = await guild.members.fetch(targetId).catch(() => null); if (!member) { results.failed.push({ id: targetId, reason: 'Not in server' }); continue; } if (!member.kickable) { results.failed.push({ id: targetId, reason: 'Not kickable' }); continue; } await member.kick(`[Dashboard Bulk] ${reason || 'No reason'} | By ${moderator.username}`); if (supabase) { await supabase.from('mod_actions').insert({ guild_id: guildId, action: 'kick', user_id: targetId, user_tag: member.user.tag, moderator_id: moderator.id, moderator_tag: moderator.username, reason: `[Dashboard Bulk] ${reason || 'No reason'}` }).catch(() => {}); } results.success.push(targetId); break; } case 'timeout': { const member = await guild.members.fetch(targetId).catch(() => null); if (!member) { results.failed.push({ id: targetId, reason: 'Not in server' }); continue; } if (!member.moderatable) { results.failed.push({ id: targetId, reason: 'Not moderatable' }); continue; } const timeoutMs = durationMap[duration] || durationMap['1h']; await member.timeout(timeoutMs, `[Dashboard Bulk] ${reason || 'No reason'} | By ${moderator.username}`); if (supabase) { await supabase.from('mod_actions').insert({ guild_id: guildId, action: 'timeout', user_id: targetId, user_tag: member.user.tag, moderator_id: moderator.id, moderator_tag: moderator.username, reason: `[Dashboard Bulk] ${reason || 'No reason'}`, duration: duration }).catch(() => {}); } results.success.push(targetId); break; } case 'warn': { const member = await guild.members.fetch(targetId).catch(() => null); if (!member) { results.failed.push({ id: targetId, reason: 'Not in server' }); continue; } if (supabase) { await supabase.from('warnings').insert({ guild_id: guildId, user_id: targetId, username: member.user.tag, moderator_id: moderator.id, moderator_tag: moderator.username, reason: `[Dashboard Bulk] ${reason || 'No reason'}` }); await supabase.from('mod_actions').insert({ guild_id: guildId, action: 'warn', user_id: targetId, user_tag: member.user.tag, moderator_id: moderator.id, moderator_tag: moderator.username, reason: `[Dashboard Bulk] ${reason || 'No reason'}` }).catch(() => {}); } results.success.push(targetId); break; } case 'remove_timeout': { const member = await guild.members.fetch(targetId).catch(() => null); if (!member) { results.failed.push({ id: targetId, reason: 'Not in server' }); continue; } if (!member.isCommunicationDisabled()) { results.failed.push({ id: targetId, reason: 'Not timed out' }); continue; } await member.timeout(null, `Timeout removed via Dashboard by ${moderator.username}`); if (supabase) { await supabase.from('mod_actions').insert({ guild_id: guildId, action: 'untimeout', user_id: targetId, user_tag: member.user.tag, moderator_id: moderator.id, moderator_tag: moderator.username, reason: 'Dashboard bulk timeout removal' }).catch(() => {}); } results.success.push(targetId); break; } } } catch (error) { results.failed.push({ id: targetId, reason: error.message?.substring(0, 50) || 'Unknown error' }); } } res.json(results); }); // 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' }); } }); // Coins API endpoints app.get('/api/guild/:guildId/coins', 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({ coins: 0, coinName: 'Coins' }); } const { data: stats } = await supabase .from('user_stats') .select('coins') .eq('user_id', link.user_id) .eq('guild_id', guildId) .maybeSingle(); const { data: config } = await supabase .from('xp_config') .select('coin_name') .eq('guild_id', guildId) .maybeSingle(); res.json({ coins: stats?.coins || 0, coinName: config?.coin_name || 'Coins' }); } catch (error) { console.error('Failed to fetch coins:', error); res.status(500).json({ error: 'Failed to load coins' }); } }); app.get('/api/guild/:guildId/coins/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' }); } try { const { data: config } = await supabase .from('xp_config') .select('coins_enabled, message_coins, daily_coins, coin_name') .eq('guild_id', guildId) .maybeSingle(); res.json({ config: config || { coins_enabled: true, message_coins: 1, daily_coins: 50, coin_name: 'Coins' } }); } catch (error) { console.error('Failed to fetch coin config:', error); res.status(500).json({ error: 'Failed to load coin config' }); } }); app.post('/api/guild/:guildId/coins/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' }); } const { coins_enabled, message_coins, daily_coins, coin_name } = req.body; // Validate inputs const validMessageCoins = Math.max(0, Math.min(100, parseInt(message_coins) || 1)); const validDailyCoins = Math.max(0, Math.min(1000, parseInt(daily_coins) || 50)); const validCoinName = (coin_name || 'Coins').toString().substring(0, 32); const validCoinsEnabled = coins_enabled === true; try { const { data: existing } = await supabase .from('xp_config') .select('guild_id') .eq('guild_id', guildId) .maybeSingle(); const coinData = { coins_enabled: validCoinsEnabled, message_coins: validMessageCoins, daily_coins: validDailyCoins, coin_name: validCoinName, updated_at: new Date().toISOString() }; if (existing) { const { error } = await supabase .from('xp_config') .update(coinData) .eq('guild_id', guildId); if (error) throw error; } else { const { error } = await supabase .from('xp_config') .insert({ guild_id: guildId, ...coinData }); if (error) throw error; } res.json({ success: true }); } catch (error) { console.error('Failed to save coin config:', error); res.status(500).json({ error: 'Failed to save coin settings' }); } }); app.get('/api/guild/:guildId/coins/leaderboard', async (req, res) => { if (!supabase) return res.status(503).json({ error: 'Database not available' }); const { guildId } = req.params; const { limit = 50 } = req.query; try { const { data: leaderboard } = await supabase .from('user_stats') .select('user_id, coins') .eq('guild_id', guildId) .gt('coins', 0) .order('coins', { ascending: false }) .limit(parseInt(limit)); const { data: config } = await supabase .from('xp_config') .select('coin_name') .eq('guild_id', guildId) .maybeSingle(); res.json({ leaderboard: leaderboard || [], coinName: config?.coin_name || 'Coins' }); } catch (error) { console.error('Failed to fetch coin leaderboard:', error); res.status(500).json({ error: 'Failed to load leaderboard' }); } }); // Activity Roles API endpoints app.get('/api/guild/:guildId/activity-roles', 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: roles } = await supabase .from('activity_roles') .select('*') .eq('guild_id', guildId) .order('milestone_type') .order('milestone_value', { ascending: true }); // Get Discord role names from the bot let guild = discordClient.guilds.cache.get(guildId); // Try to fetch guild if not in cache if (!guild) { try { guild = await discordClient.guilds.fetch(guildId); if (guild && !guild.roles.cache.size) { await guild.roles.fetch(); } } catch (fetchErr) { // Guild not accessible, continue with unknown role names console.log(`Guild ${guildId} not accessible for role names`); } } const rolesWithNames = (roles || []).map(r => { const discordRole = guild?.roles.cache.get(r.role_id); return { ...r, role_name: discordRole?.name || `Unknown (${r.role_id})`, role_color: discordRole?.hexColor || '#99aab5' }; }); res.json({ roles: rolesWithNames }); } catch (error) { console.error('Failed to fetch activity roles:', error); res.status(500).json({ error: 'Failed to load activity roles' }); } }); app.post('/api/guild/:guildId/activity-roles', 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 { role_id, milestone_type, milestone_value, stack_roles } = req.body; if (!role_id || !milestone_type || milestone_value === undefined || milestone_value === null) { return res.status(400).json({ error: 'Missing required fields' }); } const validTypes = ['messages', 'voice_hours', 'daily_streak', 'reactions_given', 'reactions_received', 'commands_used']; if (!validTypes.includes(milestone_type)) { return res.status(400).json({ error: 'Invalid milestone type' }); } const parsedValue = parseInt(milestone_value, 10); if (isNaN(parsedValue) || parsedValue < 1) { return res.status(400).json({ error: 'Milestone value must be a positive integer (1 or greater)' }); } try { const { error } = await supabase .from('activity_roles') .upsert({ guild_id: guildId, role_id, milestone_type, milestone_value: parsedValue, stack_roles: stack_roles !== false, created_at: new Date().toISOString() }, { onConflict: 'guild_id,role_id' }); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to add activity role:', error); res.status(500).json({ error: 'Failed to add activity role' }); } }); app.delete('/api/guild/:guildId/activity-roles/:roleId', 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, roleId } = 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('activity_roles') .delete() .eq('guild_id', guildId) .eq('role_id', roleId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to delete activity role:', error); res.status(500).json({ error: 'Failed to delete activity role' }); } }); app.get('/api/guild/:guildId/roles', async (req, res) => { 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 { let guild = discordClient.guilds.cache.get(guildId); // Try to fetch guild if not in cache if (!guild) { try { guild = await discordClient.guilds.fetch(guildId); } catch (fetchErr) { console.log(`Guild ${guildId} not accessible by bot`); return res.json({ roles: [], message: 'Bot is not in this server or cannot access it' }); } } // Ensure roles are cached if (!guild.roles.cache.size) { await guild.roles.fetch(); } const roles = guild.roles.cache .filter(r => !r.managed && r.id !== guild.id) .sort((a, b) => b.position - a.position) .map(r => ({ id: r.id, name: r.name, color: r.hexColor, position: r.position })); res.json({ roles }); } catch (error) { console.error('Failed to fetch roles:', error); res.status(500).json({ error: 'Failed to load roles' }); } }); // Command Cooldowns API endpoints const DEFAULT_COOLDOWNS = { work: 3600, daily: 72000, slots: 60, coinflip: 30, rep: 43200, trivia: 30, heist: 1800, duel: 300, gift: 60, trade: 60 }; app.get('/api/guild/:guildId/cooldowns', 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: customCooldowns } = await supabase .from('command_cooldowns') .select('*') .eq('guild_id', guildId) .order('command_name'); const customMap = {}; (customCooldowns || []).forEach(c => { customMap[c.command_name] = c.cooldown_seconds; }); const cooldowns = Object.entries(DEFAULT_COOLDOWNS).map(([cmd, defaultSec]) => ({ command: cmd, default_seconds: defaultSec, custom_seconds: customMap[cmd] ?? null, is_custom: customMap[cmd] !== undefined })); res.json({ cooldowns }); } catch (error) { console.error('Failed to fetch cooldowns:', error); res.status(500).json({ error: 'Failed to load cooldowns' }); } }); app.post('/api/guild/:guildId/cooldowns', 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 { command, seconds } = req.body; if (!command || !DEFAULT_COOLDOWNS.hasOwnProperty(command)) { return res.status(400).json({ error: 'Invalid command' }); } const parsedSeconds = parseInt(seconds, 10); if (isNaN(parsedSeconds) || parsedSeconds < 0 || parsedSeconds > 604800) { return res.status(400).json({ error: 'Seconds must be between 0 and 604800 (7 days)' }); } try { const { error } = await supabase .from('command_cooldowns') .upsert({ guild_id: guildId, command_name: command, cooldown_seconds: parsedSeconds, updated_at: new Date().toISOString() }, { onConflict: 'guild_id,command_name' }); if (error) throw error; invalidateCooldownCache(guildId, command); res.json({ success: true }); } catch (error) { console.error('Failed to set cooldown:', error); res.status(500).json({ error: 'Failed to save cooldown' }); } }); app.delete('/api/guild/:guildId/cooldowns/:command', 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, command } = 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('command_cooldowns') .delete() .eq('guild_id', guildId) .eq('command_name', command); if (error) throw error; invalidateCooldownCache(guildId, command); res.json({ success: true }); } catch (error) { console.error('Failed to reset cooldown:', error); res.status(500).json({ error: 'Failed to reset cooldown' }); } }); // ============================================================================= // BACKUP MANAGEMENT API // ============================================================================= app.get('/api/guild/:guildId/backups', async (req, res) => { const { guildId } = req.params; if (!supabase) { return res.status(503).json({ error: 'Database not connected' }); } try { const { data: backups, error } = await supabase .from('server_backups') .select('id, name, description, backup_type, created_at, created_by, roles_count, channels_count, size_bytes') .eq('guild_id', guildId) .order('created_at', { ascending: false }) .limit(50); if (error) throw error; res.json({ backups: backups || [] }); } catch (error) { console.error('Failed to get backups:', error); res.status(500).json({ error: 'Failed to fetch backups' }); } }); app.get('/api/guild/:guildId/backups/:backupId', async (req, res) => { const { guildId, backupId } = req.params; if (!supabase) { return res.status(503).json({ error: 'Database not connected' }); } try { const { data: backup, error } = await supabase .from('server_backups') .select('*') .eq('id', backupId) .eq('guild_id', guildId) .single(); if (error || !backup) { return res.status(404).json({ error: 'Backup not found' }); } res.json({ backup }); } catch (error) { console.error('Failed to get backup:', error); res.status(500).json({ error: 'Failed to fetch backup' }); } }); app.post('/api/guild/:guildId/backups', async (req, res) => { const { guildId } = req.params; const { name, description } = req.body; if (!supabase) { return res.status(503).json({ error: 'Database not connected' }); } try { const guild = discordClient.guilds.cache.get(guildId); if (!guild) { return res.status(404).json({ error: 'Guild not found' }); } const { createBackup } = require('../commands/backup'); const backupData = await createBackup(guild, supabase, guildId); const { data: backup, error } = await supabase.from('server_backups').insert({ guild_id: guildId, name: name || `Dashboard Backup - ${new Date().toLocaleDateString()}`, description: description || 'Created from dashboard', backup_type: 'manual', created_by: req.session?.user?.id || null, data: backupData, roles_count: backupData.roles?.length || 0, channels_count: backupData.channels?.length || 0, size_bytes: JSON.stringify(backupData).length }).select().single(); if (error) throw error; res.json({ success: true, backup }); } catch (error) { console.error('Failed to create backup:', error); res.status(500).json({ error: 'Failed to create backup' }); } }); app.delete('/api/guild/:guildId/backups/:backupId', async (req, res) => { const { guildId, backupId } = req.params; if (!supabase) { return res.status(503).json({ error: 'Database not connected' }); } try { const { error } = await supabase .from('server_backups') .delete() .eq('id', backupId) .eq('guild_id', guildId); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to delete backup:', error); res.status(500).json({ error: 'Failed to delete backup' }); } }); app.get('/api/guild/:guildId/backup-settings', async (req, res) => { const { guildId } = req.params; if (!supabase) { return res.status(503).json({ error: 'Database not connected' }); } try { const { data: settings } = await supabase .from('backup_settings') .select('*') .eq('guild_id', guildId) .single(); res.json({ settings: settings || { auto_enabled: false, interval_hours: 24, max_backups: 7 } }); } catch (error) { console.error('Failed to get backup settings:', error); res.status(500).json({ error: 'Failed to fetch backup settings' }); } }); app.post('/api/guild/:guildId/backup-settings', async (req, res) => { const { guildId } = req.params; const { auto_enabled, interval_hours, max_backups } = req.body; if (!supabase) { return res.status(503).json({ error: 'Database not connected' }); } try { const { error } = await supabase.from('backup_settings').upsert({ guild_id: guildId, auto_enabled: auto_enabled ?? false, interval_hours: interval_hours || 24, max_backups: max_backups || 7, updated_at: new Date().toISOString(), updated_by: req.session?.user?.id || null }); if (error) throw error; res.json({ success: true }); } catch (error) { console.error('Failed to update backup settings:', error); res.status(500).json({ error: 'Failed to update backup settings' }); } }); 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 };