AeThex-Bot-Master/aethex-bot/server/webServer.js
sirpiglr 76cad00dd9 Enhance bot with federation protection and new command features
Introduces federation protection listener, expands federation commands, adds federation API endpoints to web server, and updates bot status to 'Protecting the Federation'.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: d254ee4b-e69e-44a5-b1be-b89c088a485a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/FUs0R2K
Replit-Helium-Checkpoint-Created: true
2025-12-10 01:41:25 +00:00

1273 lines
37 KiB
JavaScript

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');
function createWebServer(discordClient, supabase, options = {}) {
const app = express();
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());
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' });
}
});
app.get('/health', (req, res) => {
res.json({
status: 'ok',
bot: discordClient.user ? 'online' : 'offline',
uptime: process.uptime()
});
});
app.get('/federation', (req, res) => {
res.sendFile(path.join(__dirname, '../public/federation.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 };