AeThex-Bot-Master/aethex-bot/server/webServer.js
sirpiglr 84d7720819 Add moderation and analytics pages to the dashboard interface
Implement new API endpoints for moderation statistics, warnings, bans, and activity feed data in webServer.js, and add corresponding sections and navigation to dashboard.html.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 72415ade-753e-448a-a127-8d4fee249942
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/bakeZwZ
Replit-Helium-Checkpoint-Created: true
2025-12-12 23:26:55 +00:00

1863 lines
58 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');
const Stripe = require('stripe');
function createWebServer(discordClient, supabase, options = {}) {
const app = express();
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY) : null;
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
// Production URL for OAuth
const BASE_URL = process.env.BASE_URL || 'https://bot.aethex.dev';
app.use(cors({
origin: true,
credentials: true
}));
app.use(cookieParser());
// Stripe webhook must be registered BEFORE express.json() to access raw body
app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
if (!stripe || !STRIPE_WEBHOOK_SECRET) {
return res.status(503).json({ error: 'Stripe webhook not configured' });
}
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
const { guild_id, plan_type } = session.metadata;
if (plan_type === 'premium') {
await supabase.from('federation_servers').update({
tier: 'premium',
subscription_id: session.subscription,
subscription_status: 'active',
updated_at: new Date().toISOString()
}).eq('guild_id', guild_id);
console.log(`[Stripe] Guild ${guild_id} upgraded to premium`);
} else if (plan_type === 'featured') {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
await supabase.from('federation_featured').upsert({
guild_id: guild_id,
subscription_id: session.subscription,
active: true,
expires_at: expiresAt.toISOString(),
created_at: new Date().toISOString()
}, { onConflict: 'guild_id' });
console.log(`[Stripe] Guild ${guild_id} purchased featured slot`);
}
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object;
const status = subscription.status;
if (status === 'active') {
await supabase.from('federation_servers').update({
subscription_status: 'active'
}).eq('subscription_id', subscription.id);
} else if (status === 'past_due' || status === 'unpaid') {
await supabase.from('federation_servers').update({
subscription_status: status
}).eq('subscription_id', subscription.id);
}
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
await supabase.from('federation_servers').update({
tier: 'free',
subscription_id: null,
subscription_status: null,
updated_at: new Date().toISOString()
}).eq('subscription_id', subscription.id);
await supabase.from('federation_featured').update({
active: false
}).eq('subscription_id', subscription.id);
console.log(`[Stripe] Subscription ${subscription.id} canceled`);
break;
}
}
res.json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000
}
}));
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
app.use(express.static(path.join(__dirname, '../public')));
app.get('/auth/discord', (req, res) => {
if (!DISCORD_CLIENT_SECRET) {
return res.status(500).json({ error: 'Discord OAuth not configured. Please set DISCORD_CLIENT_SECRET.' });
}
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const redirectUri = `${BASE_URL}/auth/callback`;
const scope = 'identify guilds';
const authUrl = `https://discord.com/api/oauth2/authorize?` +
`client_id=${DISCORD_CLIENT_ID}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&response_type=code` +
`&scope=${encodeURIComponent(scope)}` +
`&state=${state}`;
res.redirect(authUrl);
});
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (!code) {
return res.redirect('/?error=no_code');
}
if (state !== req.session.oauthState) {
return res.redirect('/?error=invalid_state');
}
try {
const redirectUri = `${BASE_URL}/auth/callback`;
const tokenResponse = await fetch('https://discord.com/api/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
client_id: DISCORD_CLIENT_ID,
client_secret: DISCORD_CLIENT_SECRET,
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri
})
});
if (!tokenResponse.ok) {
console.error('Token exchange failed:', await tokenResponse.text());
return res.redirect('/?error=token_failed');
}
const tokens = await tokenResponse.json();
const userResponse = await fetch('https://discord.com/api/users/@me', {
headers: {
Authorization: `Bearer ${tokens.access_token}`
}
});
if (!userResponse.ok) {
return res.redirect('/?error=user_fetch_failed');
}
const user = await userResponse.json();
const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', {
headers: {
Authorization: `Bearer ${tokens.access_token}`
}
});
let guilds = [];
if (guildsResponse.ok) {
guilds = await guildsResponse.json();
}
req.session.user = {
id: user.id,
username: user.username,
discriminator: user.discriminator,
avatar: user.avatar,
globalName: user.global_name,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
guilds: guilds.map(g => ({
id: g.id,
name: g.name,
icon: g.icon,
owner: g.owner,
permissions: g.permissions,
isAdmin: (parseInt(g.permissions) & 0x20) === 0x20
}))
};
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth callback error:', error);
res.redirect('/?error=auth_failed');
}
});
app.get('/auth/logout', (req, res) => {
req.session.destroy();
res.redirect('/');
});
app.get('/api/me', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const user = req.session.user;
res.json({
id: user.id,
username: user.username,
discriminator: user.discriminator,
avatar: user.avatar,
globalName: user.globalName,
avatarUrl: user.avatar
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
: `https://cdn.discordapp.com/embed/avatars/${parseInt(user.discriminator || '0') % 5}.png`,
guilds: user.guilds.map(g => ({
...g,
icon: g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : null
}))
});
});
// Profile handler function
async function handleProfileRequest(req, res, userId) {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const targetUserId = userId || req.session.user?.id;
if (!targetUserId) {
return res.status(400).json({ error: 'User ID required' });
}
try {
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
.eq('discord_id', targetUserId)
.maybeSingle();
if (!link) {
return res.json({ linked: false, message: 'Discord account not linked' });
}
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', link.user_id)
.maybeSingle();
if (!profile) {
return res.json({ linked: true, profile: null });
}
res.json({
linked: true,
profile: {
id: profile.id,
username: profile.username,
xp: profile.xp || 0,
prestigeLevel: profile.prestige_level || 0,
totalXpEarned: profile.total_xp_earned || 0,
dailyStreak: profile.daily_streak || 0,
title: profile.title,
bio: profile.bio,
avatarUrl: profile.avatar_url,
createdAt: profile.created_at
}
});
} catch (error) {
console.error('Profile fetch error:', error);
res.status(500).json({ error: 'Failed to fetch profile' });
}
}
// Route for current user's profile
app.get('/api/profile', (req, res) => handleProfileRequest(req, res, null));
// Route for specific user's profile
app.get('/api/profile/:userId', (req, res) => handleProfileRequest(req, res, req.params.userId));
app.get('/api/stats/:userId/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { userId, guildId } = req.params;
try {
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
.eq('discord_id', userId)
.maybeSingle();
if (!link) {
return res.json({ stats: null });
}
const { data: stats } = await supabase
.from('user_stats')
.select('*')
.eq('user_id', link.user_id)
.eq('guild_id', guildId)
.maybeSingle();
res.json({ stats });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch stats' });
}
});
app.get('/api/achievements/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { guildId } = req.params;
const userId = req.session.user?.id;
try {
const { data: achievements } = await supabase
.from('achievements')
.select('*')
.eq('guild_id', guildId)
.eq('hidden', false)
.order('created_at', { ascending: true });
let userAchievements = [];
if (userId) {
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
.eq('discord_id', userId)
.maybeSingle();
if (link) {
const { data } = await supabase
.from('user_achievements')
.select('achievement_id, earned_at')
.eq('user_id', link.user_id)
.eq('guild_id', guildId);
userAchievements = data || [];
}
}
res.json({ achievements, userAchievements });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch achievements' });
}
});
app.get('/api/quests/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { guildId } = req.params;
const userId = req.session.user?.id;
try {
const now = new Date().toISOString();
const { data: quests } = await supabase
.from('quests')
.select('*')
.eq('guild_id', guildId)
.eq('active', true)
.or(`starts_at.is.null,starts_at.lte.${now}`)
.or(`expires_at.is.null,expires_at.gt.${now}`)
.order('created_at', { ascending: false });
let userQuests = [];
if (userId) {
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
.eq('discord_id', userId)
.maybeSingle();
if (link) {
const { data } = await supabase
.from('user_quests')
.select('*')
.eq('user_id', link.user_id)
.eq('guild_id', guildId);
userQuests = data || [];
}
}
res.json({ quests, userQuests });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch quests' });
}
});
app.get('/api/shop/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { guildId } = req.params;
try {
const { data: items } = await supabase
.from('shop_items')
.select('*')
.eq('guild_id', guildId)
.eq('enabled', true)
.order('price', { ascending: true });
res.json({ items: items || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch shop items' });
}
});
app.get('/api/inventory/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
try {
const { data: link } = await supabase
.from('discord_links')
.select('user_id')
.eq('discord_id', userId)
.maybeSingle();
if (!link) {
return res.json({ inventory: [] });
}
const { data: inventory } = await supabase
.from('user_inventory')
.select(`
*,
shop_items (name, description, item_type, item_data)
`)
.eq('user_id', link.user_id)
.eq('guild_id', guildId);
res.json({ inventory: inventory || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch inventory' });
}
});
app.get('/api/leaderboard/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { guildId } = req.params;
const { type = 'all', limit = 50 } = req.query;
try {
let tableName = 'leaderboard_alltime';
if (type === 'weekly') tableName = 'leaderboard_weekly';
else if (type === 'monthly') tableName = 'leaderboard_monthly';
const { data: leaderboard } = await supabase
.from(tableName)
.select(`
*,
user_profiles!inner (username, avatar_url, prestige_level)
`)
.eq('guild_id', guildId)
.order('xp', { ascending: false })
.limit(parseInt(limit));
res.json({ leaderboard: leaderboard || [], type });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch leaderboard' });
}
});
app.get('/api/bot-stats', (req, res) => {
const stats = {
guilds: discordClient.guilds.cache.size,
users: discordClient.guilds.cache.reduce((acc, g) => acc + g.memberCount, 0),
commands: discordClient.commands?.size || 0,
uptime: Math.floor(process.uptime()),
status: discordClient.user ? 'online' : 'offline',
botName: discordClient.user?.username || 'AeThex',
botAvatar: discordClient.user?.displayAvatarURL() || null
};
res.json(stats);
});
app.get('/api/guild/:guildId/config', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access to this server' });
}
try {
const { data: xpConfig } = await supabase
.from('xp_config')
.select('*')
.eq('guild_id', guildId)
.maybeSingle();
const { data: serverConfig } = await supabase
.from('server_config')
.select('*')
.eq('guild_id', guildId)
.maybeSingle();
res.json({ xpConfig, serverConfig });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch config' });
}
});
app.post('/api/guild/:guildId/xp-config', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access to this server' });
}
const {
message_xp,
message_cooldown,
reaction_xp_enabled,
reaction_xp_given,
reaction_xp_received,
reaction_cooldown,
level_curve,
xp_enabled,
levelup_channel_id,
levelup_message,
levelup_dm,
levelup_embed,
levelup_embed_color,
multiplier_roles,
bonus_channels
} = req.body;
try {
const { data: existing } = await supabase
.from('xp_config')
.select('guild_id')
.eq('guild_id', guildId)
.maybeSingle();
const configData = {
guild_id: guildId,
message_xp: message_xp ?? 15,
message_cooldown: message_cooldown ?? 60,
reaction_xp_enabled: reaction_xp_enabled !== false,
reaction_xp_given: reaction_xp_given ?? 2,
reaction_xp_received: reaction_xp_received ?? 5,
reaction_cooldown: reaction_cooldown ?? 30,
level_curve: level_curve || 'normal',
xp_enabled: xp_enabled !== false,
levelup_channel_id: levelup_channel_id || null,
levelup_message: levelup_message || null,
levelup_dm: levelup_dm === true,
levelup_embed: levelup_embed !== false,
levelup_embed_color: levelup_embed_color || '#7c3aed',
multiplier_roles: multiplier_roles || [],
bonus_channels: bonus_channels || [],
updated_at: new Date().toISOString()
};
if (existing) {
const { error } = await supabase
.from('xp_config')
.update(configData)
.eq('guild_id', guildId);
if (error) throw error;
} else {
const { error } = await supabase
.from('xp_config')
.insert(configData);
if (error) throw error;
}
res.json({ success: true });
} catch (error) {
console.error('Failed to save XP config:', error);
res.status(500).json({ error: 'Failed to save settings', details: error.message });
}
});
app.get('/api/guild/:guildId/quests', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
try {
const { data: quests } = await supabase
.from('quests')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false });
res.json({ quests: quests || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch quests' });
}
});
app.post('/api/guild/:guildId/quests', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
const { name, description, quest_type, objective, target_value, xp_reward, duration_hours, repeatable, active } = req.body;
try {
const questData = {
guild_id: guildId,
name,
description: description || null,
quest_type: quest_type || 'daily',
objective: objective || 'messages',
target_value: target_value || 10,
xp_reward: xp_reward || 100,
repeatable: repeatable || false,
active: active !== false,
created_at: new Date().toISOString()
};
if (duration_hours && duration_hours > 0) {
const expiry = new Date();
expiry.setHours(expiry.getHours() + duration_hours);
questData.expires_at = expiry.toISOString();
}
const { error } = await supabase.from('quests').insert(questData);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to create quest:', error);
res.status(500).json({ error: 'Failed to create quest' });
}
});
app.put('/api/guild/:guildId/quests/:questId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, questId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
const { name, description, quest_type, objective, target_value, xp_reward, repeatable, active } = req.body;
try {
const { error } = await supabase
.from('quests')
.update({
name,
description: description || null,
quest_type,
objective,
target_value,
xp_reward,
repeatable: repeatable || false,
active
})
.eq('id', questId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to update quest:', error);
res.status(500).json({ error: 'Failed to update quest' });
}
});
app.delete('/api/guild/:guildId/quests/:questId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, questId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
try {
const { error } = await supabase
.from('quests')
.delete()
.eq('id', questId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to delete quest:', error);
res.status(500).json({ error: 'Failed to delete quest' });
}
});
// ============ ACHIEVEMENTS ADMIN API ============
app.get('/api/guild/:guildId/achievements', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
try {
const { data: achievements } = await supabase
.from('achievements')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false });
res.json({ achievements: achievements || [] });
} catch (error) {
console.error('Failed to fetch achievements:', error);
res.status(500).json({ error: 'Failed to fetch achievements' });
}
});
app.post('/api/guild/:guildId/achievements', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
const { name, icon, description, trigger_type, trigger_value, reward_xp, reward_role_id, hidden } = req.body;
try {
const achievementData = {
guild_id: guildId,
name,
icon: icon || '🏆',
description: description || null,
trigger_type: trigger_type || 'level',
trigger_value: trigger_value || 1,
reward_xp: reward_xp || 0,
reward_role_id: reward_role_id || null,
hidden: hidden || false,
created_at: new Date().toISOString()
};
const { error } = await supabase.from('achievements').insert(achievementData);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to create achievement:', error);
res.status(500).json({ error: 'Failed to create achievement' });
}
});
app.put('/api/guild/:guildId/achievements/:achievementId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, achievementId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
const { name, icon, description, trigger_type, trigger_value, reward_xp, reward_role_id, hidden } = req.body;
try {
const { error } = await supabase
.from('achievements')
.update({
name,
icon: icon || '🏆',
description: description || null,
trigger_type,
trigger_value,
reward_xp: reward_xp || 0,
reward_role_id: reward_role_id || null,
hidden: hidden || false
})
.eq('id', achievementId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to update achievement:', error);
res.status(500).json({ error: 'Failed to update achievement' });
}
});
app.delete('/api/guild/:guildId/achievements/:achievementId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, achievementId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
try {
const { error } = await supabase
.from('achievements')
.delete()
.eq('id', achievementId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to delete achievement:', error);
res.status(500).json({ error: 'Failed to delete achievement' });
}
});
// ============ SHOP ADMIN API ============
app.get('/api/guild/:guildId/shop/admin', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
try {
const { data: items } = await supabase
.from('shop_items')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false });
res.json({ items: items || [] });
} catch (error) {
console.error('Failed to fetch shop items:', error);
res.status(500).json({ error: 'Failed to fetch shop items' });
}
});
app.post('/api/guild/:guildId/shop', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
const { name, item_type, description, price, stock, level_required, prestige_required, enabled, category, item_data } = req.body;
try {
const itemData = {
guild_id: guildId,
name,
item_type: item_type || 'cosmetic',
description: description || null,
category: category || null,
price: price || 100,
stock: stock || null,
item_data: item_data || null,
level_required: level_required || 0,
prestige_required: prestige_required || 0,
enabled: enabled !== false,
created_at: new Date().toISOString()
};
const { error } = await supabase.from('shop_items').insert(itemData);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to create shop item:', error);
res.status(500).json({ error: 'Failed to create shop item', details: error.message });
}
});
app.put('/api/guild/:guildId/shop/:itemId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, itemId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
const { name, item_type, description, price, stock, level_required, prestige_required, enabled, category, item_data } = req.body;
try {
const { error } = await supabase
.from('shop_items')
.update({
name,
item_type: item_type || 'cosmetic',
description: description || null,
category: category || null,
price: price || 100,
stock: stock || null,
item_data: item_data || null,
level_required: level_required || 0,
prestige_required: prestige_required || 0,
enabled: enabled !== false,
updated_at: new Date().toISOString()
})
.eq('id', itemId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to update shop item:', error);
res.status(500).json({ error: 'Failed to update shop item', details: error.message });
}
});
app.delete('/api/guild/:guildId/shop/:itemId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, itemId } = req.params;
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'No admin access' });
}
try {
const { error } = await supabase
.from('shop_items')
.delete()
.eq('id', itemId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to delete shop item:', error);
res.status(500).json({ error: 'Failed to delete shop item' });
}
});
// ============ FEDERATION API ============
app.get('/api/federation/stats', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
try {
const [servers, bans, applications] = await Promise.all([
supabase.from('federation_servers').select('id', { count: 'exact' }).eq('status', 'approved'),
supabase.from('federation_bans').select('id', { count: 'exact' }).eq('active', true),
supabase.from('federation_applications').select('id', { count: 'exact' }).eq('status', 'pending')
]);
res.json({
totalServers: servers.count || 0,
activeBans: bans.count || 0,
pendingApplications: applications.count || 0
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch stats' });
}
});
app.get('/api/federation/bans', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { limit = 50, severity } = req.query;
try {
let query = supabase
.from('federation_bans')
.select('*')
.eq('active', true)
.order('created_at', { ascending: false })
.limit(parseInt(limit));
if (severity) {
query = query.eq('severity', severity);
}
const { data: bans } = await query;
res.json({ bans: bans || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch bans' });
}
});
app.get('/api/federation/servers', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
try {
const { data: servers } = await supabase
.from('federation_servers')
.select('*')
.eq('status', 'approved')
.order('member_count', { ascending: false });
res.json({ servers: servers || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch servers' });
}
});
app.get('/api/federation/applications', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const { data: applications } = await supabase
.from('federation_applications')
.select('*')
.order('created_at', { ascending: false });
res.json({ applications: applications || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch applications' });
}
});
app.post('/api/federation/applications/:appId/approve', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { appId } = req.params;
try {
const { data: app } = await supabase
.from('federation_applications')
.select('*')
.eq('id', appId)
.maybeSingle();
if (!app) {
return res.status(404).json({ error: 'Application not found' });
}
await supabase.from('federation_applications').update({
status: 'approved',
reviewed_by: userId,
reviewed_at: new Date().toISOString()
}).eq('id', appId);
await supabase.from('federation_servers').insert({
guild_id: app.guild_id,
guild_name: app.guild_name,
guild_icon: app.guild_icon,
description: app.description,
category: app.category,
member_count: app.member_count,
owner_id: app.admin_id,
status: 'approved',
treaty_accepted: true,
treaty_accepted_at: new Date().toISOString()
});
res.json({ success: true });
} catch (error) {
console.error('Failed to approve application:', error);
res.status(500).json({ error: 'Failed to approve application' });
}
});
app.post('/api/federation/applications/:appId/reject', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { appId } = req.params;
const { reason } = req.body;
try {
await supabase.from('federation_applications').update({
status: 'rejected',
reviewed_by: userId,
reviewed_at: new Date().toISOString(),
rejection_reason: reason || 'No reason provided'
}).eq('id', appId);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Failed to reject application' });
}
});
app.get('/api/federation/leaderboard', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { limit = 50 } = req.query;
try {
const { data: leaders } = await supabase
.from('federation_reputation')
.select('*')
.order('reputation_score', { ascending: false })
.limit(parseInt(limit));
res.json({ leaderboard: leaders || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch leaderboard' });
}
});
// ============ STRIPE PAYMENT API ============
app.post('/api/stripe/create-checkout', async (req, res) => {
if (!stripe) {
return res.status(503).json({ error: 'Stripe not configured' });
}
const userId = req.session.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { guildId, planType } = req.body;
if (!guildId || !planType) {
return res.status(400).json({ error: 'Missing guildId or planType' });
}
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild || !userGuild.isAdmin) {
return res.status(403).json({ error: 'Must be server admin to purchase' });
}
try {
const prices = {
premium: {
amount: 5000,
name: 'Federation Premium',
description: 'Full protection from all ban severity levels - auto-kick for all threats',
recurring: { interval: 'month' }
},
featured: {
amount: 20000,
name: 'Featured Server Slot',
description: 'Cross-promotion across all federation servers for 1 week',
recurring: { interval: 'week' }
}
};
const plan = prices[planType];
if (!plan) {
return res.status(400).json({ error: 'Invalid plan type' });
}
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'subscription',
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: plan.name,
description: plan.description
},
unit_amount: plan.amount,
recurring: plan.recurring
},
quantity: 1
}],
metadata: {
guild_id: guildId,
user_id: userId,
plan_type: planType
},
success_url: `${BASE_URL}/federation?success=true&plan=${planType}`,
cancel_url: `${BASE_URL}/federation?canceled=true`
});
res.json({ url: session.url });
} catch (error) {
console.error('Stripe checkout error:', error);
res.status(500).json({ error: 'Failed to create checkout session' });
}
});
app.get('/api/federation/subscription/:guildId', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
const { guildId } = req.params;
try {
const { data: server } = await supabase
.from('federation_servers')
.select('tier, subscription_status')
.eq('guild_id', guildId)
.maybeSingle();
const { data: featured } = await supabase
.from('federation_featured')
.select('active, expires_at')
.eq('guild_id', guildId)
.eq('active', true)
.maybeSingle();
res.json({
tier: server?.tier || 'free',
subscriptionStatus: server?.subscription_status || null,
featured: featured?.active || false,
featuredExpiresAt: featured?.expires_at || null
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch subscription' });
}
});
// Moderation API Endpoints
app.get('/api/guild/:guildId/moderation/stats', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const [warningsRes, bansRes, actionsRes] = await Promise.all([
supabase.from('warnings').select('*', { count: 'exact', head: true }).eq('guild_id', guildId),
supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).eq('action_type', 'ban'),
supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString())
]);
res.json({
totalWarnings: warningsRes.count || 0,
activeBans: bansRes.count || 0,
recentActions: actionsRes.count || 0
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch stats' });
}
});
app.get('/api/guild/:guildId/moderation/warnings', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data: warnings } = await supabase
.from('warnings')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false })
.limit(100);
res.json({ warnings: warnings || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch warnings' });
}
});
app.delete('/api/guild/:guildId/moderation/warnings/:warningId', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId, warningId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { error } = await supabase
.from('warnings')
.delete()
.eq('id', warningId)
.eq('guild_id', guildId);
if (error) throw error;
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Failed to delete warning' });
}
});
app.get('/api/guild/:guildId/moderation/bans', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data: bans } = await supabase
.from('mod_actions')
.select('*')
.eq('guild_id', guildId)
.eq('action_type', 'ban')
.order('created_at', { ascending: false })
.limit(100);
res.json({ bans: bans || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch bans' });
}
});
app.get('/api/guild/:guildId/moderation/activity', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data: activity } = await supabase
.from('mod_actions')
.select('*')
.eq('guild_id', guildId)
.order('created_at', { ascending: false })
.limit(50);
res.json({ activity: activity || [] });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch activity' });
}
});
app.get('/api/guild/:guildId/moderation/search', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const { q } = req.query;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
if (!q) return res.json({ results: [] });
try {
const { data: warnings } = await supabase
.from('warnings')
.select('user_id, username')
.eq('guild_id', guildId)
.or(`user_id.eq.${q},username.ilike.%${q}%`)
.limit(20);
const userMap = new Map();
(warnings || []).forEach(w => {
if (!userMap.has(w.user_id)) {
userMap.set(w.user_id, { user_id: w.user_id, username: w.username, warnings_count: 0, is_banned: false });
}
userMap.get(w.user_id).warnings_count++;
});
const { data: bans } = await supabase
.from('mod_actions')
.select('target_id')
.eq('guild_id', guildId)
.eq('action_type', 'ban');
const bannedIds = new Set((bans || []).map(b => b.target_id));
userMap.forEach(u => { u.is_banned = bannedIds.has(u.user_id); });
res.json({ results: Array.from(userMap.values()) });
} catch (error) {
res.status(500).json({ error: 'Search failed' });
}
});
// Analytics API Endpoints
app.get('/api/guild/:guildId/analytics/stats', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const today = new Date();
today.setHours(0, 0, 0, 0);
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
try {
const [messagesRes, activeRes, xpRes, commandsRes] = await Promise.all([
supabase.from('message_logs').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', today.toISOString()),
supabase.from('user_stats').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('last_active', weekAgo.toISOString()),
supabase.from('xp_logs').select('xp_amount').eq('guild_id', guildId).gte('created_at', today.toISOString()),
supabase.from('command_logs').select('*', { count: 'exact', head: true }).eq('guild_id', guildId).gte('created_at', today.toISOString())
]);
const xpToday = (xpRes.data || []).reduce((sum, r) => sum + (r.xp_amount || 0), 0);
res.json({
messagesToday: messagesRes.count || 0,
activeUsers: activeRes.count || 0,
xpToday,
commandsUsed: commandsRes.count || 0
});
} catch (error) {
res.json({ messagesToday: 0, activeUsers: 0, xpToday: 0, commandsUsed: 0 });
}
});
app.get('/api/guild/:guildId/analytics/activity', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const days = [];
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
date.setHours(0, 0, 0, 0);
const nextDate = new Date(date);
nextDate.setDate(nextDate.getDate() + 1);
const { count } = await supabase
.from('message_logs')
.select('*', { count: 'exact', head: true })
.eq('guild_id', guildId)
.gte('created_at', date.toISOString())
.lt('created_at', nextDate.toISOString());
days.push({
label: dayNames[date.getDay()],
messages: count || 0
});
}
res.json({ days });
} catch (error) {
res.json({ days: [] });
}
});
app.get('/api/guild/:guildId/analytics/top-earners', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
const { data } = await supabase
.from('xp_logs')
.select('user_id, xp_amount')
.eq('guild_id', guildId)
.gte('created_at', today.toISOString());
const userXp = new Map();
(data || []).forEach(r => {
userXp.set(r.user_id, (userXp.get(r.user_id) || 0) + (r.xp_amount || 0));
});
const earners = Array.from(userXp.entries())
.map(([user_id, xp_earned]) => ({ user_id, xp_earned }))
.sort((a, b) => b.xp_earned - a.xp_earned)
.slice(0, 10);
res.json({ earners });
} catch (error) {
res.json({ earners: [] });
}
});
app.get('/api/guild/:guildId/analytics/top-channels', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
const today = new Date();
today.setHours(0, 0, 0, 0);
try {
const { data } = await supabase
.from('message_logs')
.select('channel_id, channel_name')
.eq('guild_id', guildId)
.gte('created_at', today.toISOString());
const channelCounts = new Map();
(data || []).forEach(r => {
const key = r.channel_id;
if (!channelCounts.has(key)) {
channelCounts.set(key, { channel_id: r.channel_id, channel_name: r.channel_name, message_count: 0 });
}
channelCounts.get(key).message_count++;
});
const channels = Array.from(channelCounts.values())
.sort((a, b) => b.message_count - a.message_count)
.slice(0, 10);
res.json({ channels });
} catch (error) {
res.json({ channels: [] });
}
});
app.get('/api/guild/:guildId/analytics/commands', async (req, res) => {
if (!supabase) return res.status(503).json({ error: 'Database not available' });
const userId = req.session.user?.id;
if (!userId) return res.status(401).json({ error: 'Not authenticated' });
const { guildId } = req.params;
const userGuild = req.session.user.guilds?.find(g => g.id === guildId);
if (!userGuild?.isAdmin) return res.status(403).json({ error: 'No admin access' });
try {
const { data } = await supabase
.from('command_logs')
.select('command_name')
.eq('guild_id', guildId);
const commandCounts = new Map();
(data || []).forEach(r => {
commandCounts.set(r.command_name, (commandCounts.get(r.command_name) || 0) + 1);
});
const commands = Array.from(commandCounts.entries())
.map(([command_name, use_count]) => ({ command_name, use_count }))
.sort((a, b) => b.use_count - a.use_count)
.slice(0, 10);
res.json({ commands });
} catch (error) {
res.json({ commands: [] });
}
});
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 };