AeThex-Bot-Master/aethex-bot/server/webServer.js
sirpiglr 363737efb6 Add secure authentication and service endpoints for Nexus integration
Implement Nexus API client and secure endpoints for token management, text scrubbing, IP checking, and activity logging, requiring AeThex session authentication.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 4b61d3bd-c210-44e9-90e0-2e6834788822
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/wAz691s
Replit-Helium-Checkpoint-Created: true
2025-12-14 03:59:45 +00:00

3298 lines
106 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');
const { NexusClient } = require('../utils/nexusClient');
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'));
});
// ============================================
// NEXUS Security API Integration Routes
// ============================================
const nexusClient = new NexusClient();
// Middleware: Require AeThex session authentication
const requireAethexAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({ error: 'AeThex authentication required. Please log in via Discord OAuth.' });
}
next();
};
// Middleware: Require admin permissions (MANAGE_GUILD permission)
const requireAdmin = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const guildId = req.query.guildId || req.body.guildId || req.params.guildId;
if (guildId) {
const guild = req.session.user.guilds?.find(g => g.id === guildId);
if (!guild || !guild.isAdmin) {
return res.status(403).json({ error: 'Admin permissions required for this guild' });
}
}
next();
};
// Authentication: Get token (requires AeThex session - for authenticated users only)
app.post('/api/nexus/token', requireAethexAuth, async (req, res) => {
try {
const result = await nexusClient.getToken(req.body);
res.json(result);
} catch (error) {
console.error('[NEXUS] Token error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Authentication: Refresh token (requires AeThex session)
app.post('/api/nexus/refresh', requireAethexAuth, async (req, res) => {
try {
const { refresh_token } = req.body;
if (!refresh_token) {
return res.status(400).json({ error: 'refresh_token required' });
}
const result = await nexusClient.refreshToken(refresh_token);
res.json(result);
} catch (error) {
console.error('[NEXUS] Refresh error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Sentinel: Scrub text (requires AeThex session + NEXUS bearer token)
app.post('/api/nexus/scrub', requireAethexAuth, async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'NEXUS Bearer token required' });
}
const token = authHeader.split(' ')[1];
try {
const { text } = req.body;
if (!text) {
return res.status(400).json({ error: 'text required' });
}
const result = await nexusClient.scrubText(text, token);
res.json(result);
} catch (error) {
console.error('[NEXUS] Scrub error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Sentinel: Check self IP (requires AeThex session + NEXUS bearer token)
app.get('/api/nexus/check-self', requireAethexAuth, async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'NEXUS Bearer token required' });
}
const token = authHeader.split(' ')[1];
try {
const result = await nexusClient.checkSelfIP(token);
res.json(result);
} catch (error) {
console.error('[NEXUS] Check-self error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Sentinel: Log activity (requires AeThex session + NEXUS bearer token)
app.post('/api/nexus/activity/log', requireAethexAuth, async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'NEXUS Bearer token required' });
}
const token = authHeader.split(' ')[1];
try {
const result = await nexusClient.logActivity(req.body, token);
res.json(result);
} catch (error) {
console.error('[NEXUS] Activity log error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Sentinel: Check IP blacklist (requires admin permissions + NEXUS bearer token)
app.get('/api/nexus/blacklist/check/:ip', requireAdmin, async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'NEXUS Bearer token required' });
}
const token = authHeader.split(' ')[1];
try {
const result = await nexusClient.checkIP(req.params.ip, token);
res.json(result);
} catch (error) {
console.error('[NEXUS] Blacklist check error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Sentinel: Get user snapshot (requires admin permissions + NEXUS bearer token)
app.get('/api/nexus/user-snapshot/:userId', requireAdmin, async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'NEXUS Bearer token required' });
}
const token = authHeader.split(' ')[1];
try {
const result = await nexusClient.getUserSnapshot(req.params.userId, token);
res.json(result);
} catch (error) {
console.error('[NEXUS] User snapshot error:', error.message);
res.status(400).json({ error: error.message });
}
});
// Public: Get NEXUS public stats (no auth required - public data only)
app.get('/api/nexus/public-stats', async (req, res) => {
try {
const result = await nexusClient.getPublicStats();
res.json(result);
} catch (error) {
console.error('[NEXUS] Public stats error:', error.message);
res.status(400).json({ error: error.message });
}
});
// 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 };