AeThex-Bot-Master/aethex-bot/server/webServer.js
sirpiglr 6183ca3e0e Add endpoint to register all bot servers to the federation
Create a POST endpoint `/api/federation/register-all` in `webServer.js` to upsert all servers the Discord client is connected to into the `federation_servers` table, using `guild_id` as the conflict target.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 31d9f5da-0c63-4f85-a2fa-50807ff47f17
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/2a77Jky
Replit-Helium-Checkpoint-Created: true
2025-12-13 18:16:42 +00:00

3143 lines
100 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');
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' });
}
});
// Admin endpoint to register all bot servers to federation
app.post('/api/federation/register-all', async (req, res) => {
if (!supabase) {
return res.status(503).json({ error: 'Database not available' });
}
try {
const guilds = discordClient.guilds.cache;
const results = [];
for (const [guildId, guild] of guilds) {
const serverData = {
guild_id: guildId,
guild_name: guild.name,
guild_icon: guild.iconURL({ size: 128 }),
owner_id: guild.ownerId,
member_count: guild.memberCount,
status: 'approved',
tier: 'free',
trust_level: 'bronze',
reputation_score: 0,
description: `Official AeThex server: ${guild.name}`,
joined_federation_at: new Date().toISOString()
};
const { error } = await supabase
.from('federation_servers')
.upsert(serverData, { onConflict: 'guild_id' });
results.push({
guild_id: guildId,
guild_name: guild.name,
success: !error,
error: error?.message
});
}
res.json({
success: true,
registered: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results
});
} catch (error) {
console.error('[Federation] Register all error:', error);
res.status(500).json({ error: 'Failed to register 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 };