Introduces federation protection listener, expands federation commands, adds federation API endpoints to web server, and updates bot status to 'Protecting the Federation'. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: d254ee4b-e69e-44a5-b1be-b89c088a485a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/FUs0R2K Replit-Helium-Checkpoint-Created: true
1273 lines
37 KiB
JavaScript
1273 lines
37 KiB
JavaScript
const express = require('express');
|
|
const session = require('express-session');
|
|
const cookieParser = require('cookie-parser');
|
|
const cors = require('cors');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
function createWebServer(discordClient, supabase, options = {}) {
|
|
const app = express();
|
|
|
|
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
|
|
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
|
|
const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
|
|
// Production URL for OAuth
|
|
const BASE_URL = process.env.BASE_URL || 'https://bot.aethex.dev';
|
|
|
|
app.use(cors({
|
|
origin: true,
|
|
credentials: true
|
|
}));
|
|
app.use(cookieParser());
|
|
app.use(express.json());
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
app.use(session({
|
|
secret: SESSION_SECRET,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: process.env.NODE_ENV === 'production',
|
|
httpOnly: true,
|
|
maxAge: 7 * 24 * 60 * 60 * 1000
|
|
}
|
|
}));
|
|
|
|
app.use((req, res, next) => {
|
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
next();
|
|
});
|
|
|
|
app.use(express.static(path.join(__dirname, '../public')));
|
|
|
|
app.get('/auth/discord', (req, res) => {
|
|
if (!DISCORD_CLIENT_SECRET) {
|
|
return res.status(500).json({ error: 'Discord OAuth not configured. Please set DISCORD_CLIENT_SECRET.' });
|
|
}
|
|
|
|
const state = crypto.randomBytes(16).toString('hex');
|
|
req.session.oauthState = state;
|
|
|
|
const redirectUri = `${BASE_URL}/auth/callback`;
|
|
const scope = 'identify guilds';
|
|
|
|
const authUrl = `https://discord.com/api/oauth2/authorize?` +
|
|
`client_id=${DISCORD_CLIENT_ID}` +
|
|
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
`&response_type=code` +
|
|
`&scope=${encodeURIComponent(scope)}` +
|
|
`&state=${state}`;
|
|
|
|
res.redirect(authUrl);
|
|
});
|
|
|
|
app.get('/auth/callback', async (req, res) => {
|
|
const { code, state } = req.query;
|
|
|
|
if (!code) {
|
|
return res.redirect('/?error=no_code');
|
|
}
|
|
|
|
if (state !== req.session.oauthState) {
|
|
return res.redirect('/?error=invalid_state');
|
|
}
|
|
|
|
try {
|
|
const redirectUri = `${BASE_URL}/auth/callback`;
|
|
|
|
const tokenResponse = await fetch('https://discord.com/api/oauth2/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: new URLSearchParams({
|
|
client_id: DISCORD_CLIENT_ID,
|
|
client_secret: DISCORD_CLIENT_SECRET,
|
|
grant_type: 'authorization_code',
|
|
code,
|
|
redirect_uri: redirectUri
|
|
})
|
|
});
|
|
|
|
if (!tokenResponse.ok) {
|
|
console.error('Token exchange failed:', await tokenResponse.text());
|
|
return res.redirect('/?error=token_failed');
|
|
}
|
|
|
|
const tokens = await tokenResponse.json();
|
|
|
|
const userResponse = await fetch('https://discord.com/api/users/@me', {
|
|
headers: {
|
|
Authorization: `Bearer ${tokens.access_token}`
|
|
}
|
|
});
|
|
|
|
if (!userResponse.ok) {
|
|
return res.redirect('/?error=user_fetch_failed');
|
|
}
|
|
|
|
const user = await userResponse.json();
|
|
|
|
const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', {
|
|
headers: {
|
|
Authorization: `Bearer ${tokens.access_token}`
|
|
}
|
|
});
|
|
|
|
let guilds = [];
|
|
if (guildsResponse.ok) {
|
|
guilds = await guildsResponse.json();
|
|
}
|
|
|
|
req.session.user = {
|
|
id: user.id,
|
|
username: user.username,
|
|
discriminator: user.discriminator,
|
|
avatar: user.avatar,
|
|
globalName: user.global_name,
|
|
accessToken: tokens.access_token,
|
|
refreshToken: tokens.refresh_token,
|
|
guilds: guilds.map(g => ({
|
|
id: g.id,
|
|
name: g.name,
|
|
icon: g.icon,
|
|
owner: g.owner,
|
|
permissions: g.permissions,
|
|
isAdmin: (parseInt(g.permissions) & 0x20) === 0x20
|
|
}))
|
|
};
|
|
|
|
res.redirect('/dashboard');
|
|
|
|
} catch (error) {
|
|
console.error('OAuth callback error:', error);
|
|
res.redirect('/?error=auth_failed');
|
|
}
|
|
});
|
|
|
|
app.get('/auth/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.redirect('/');
|
|
});
|
|
|
|
app.get('/api/me', (req, res) => {
|
|
if (!req.session.user) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const user = req.session.user;
|
|
res.json({
|
|
id: user.id,
|
|
username: user.username,
|
|
discriminator: user.discriminator,
|
|
avatar: user.avatar,
|
|
globalName: user.globalName,
|
|
avatarUrl: user.avatar
|
|
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
|
|
: `https://cdn.discordapp.com/embed/avatars/${parseInt(user.discriminator || '0') % 5}.png`,
|
|
guilds: user.guilds.map(g => ({
|
|
...g,
|
|
icon: g.icon ? `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png` : null
|
|
}))
|
|
});
|
|
});
|
|
|
|
// Profile handler function
|
|
async function handleProfileRequest(req, res, userId) {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const targetUserId = userId || req.session.user?.id;
|
|
if (!targetUserId) {
|
|
return res.status(400).json({ error: 'User ID required' });
|
|
}
|
|
|
|
try {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id')
|
|
.eq('discord_id', targetUserId)
|
|
.maybeSingle();
|
|
|
|
if (!link) {
|
|
return res.json({ linked: false, message: 'Discord account not linked' });
|
|
}
|
|
|
|
const { data: profile } = await supabase
|
|
.from('user_profiles')
|
|
.select('*')
|
|
.eq('id', link.user_id)
|
|
.maybeSingle();
|
|
|
|
if (!profile) {
|
|
return res.json({ linked: true, profile: null });
|
|
}
|
|
|
|
res.json({
|
|
linked: true,
|
|
profile: {
|
|
id: profile.id,
|
|
username: profile.username,
|
|
xp: profile.xp || 0,
|
|
prestigeLevel: profile.prestige_level || 0,
|
|
totalXpEarned: profile.total_xp_earned || 0,
|
|
dailyStreak: profile.daily_streak || 0,
|
|
title: profile.title,
|
|
bio: profile.bio,
|
|
avatarUrl: profile.avatar_url,
|
|
createdAt: profile.created_at
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Profile fetch error:', error);
|
|
res.status(500).json({ error: 'Failed to fetch profile' });
|
|
}
|
|
}
|
|
|
|
// Route for current user's profile
|
|
app.get('/api/profile', (req, res) => handleProfileRequest(req, res, null));
|
|
|
|
// Route for specific user's profile
|
|
app.get('/api/profile/:userId', (req, res) => handleProfileRequest(req, res, req.params.userId));
|
|
|
|
app.get('/api/stats/:userId/:guildId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { userId, guildId } = req.params;
|
|
|
|
try {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id')
|
|
.eq('discord_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (!link) {
|
|
return res.json({ stats: null });
|
|
}
|
|
|
|
const { data: stats } = await supabase
|
|
.from('user_stats')
|
|
.select('*')
|
|
.eq('user_id', link.user_id)
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
res.json({ stats });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch stats' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/achievements/:guildId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
const userId = req.session.user?.id;
|
|
|
|
try {
|
|
const { data: achievements } = await supabase
|
|
.from('achievements')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.eq('hidden', false)
|
|
.order('created_at', { ascending: true });
|
|
|
|
let userAchievements = [];
|
|
if (userId) {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id')
|
|
.eq('discord_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (link) {
|
|
const { data } = await supabase
|
|
.from('user_achievements')
|
|
.select('achievement_id, earned_at')
|
|
.eq('user_id', link.user_id)
|
|
.eq('guild_id', guildId);
|
|
userAchievements = data || [];
|
|
}
|
|
}
|
|
|
|
res.json({ achievements, userAchievements });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch achievements' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/quests/:guildId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
const userId = req.session.user?.id;
|
|
|
|
try {
|
|
const now = new Date().toISOString();
|
|
const { data: quests } = await supabase
|
|
.from('quests')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.eq('active', true)
|
|
.or(`starts_at.is.null,starts_at.lte.${now}`)
|
|
.or(`expires_at.is.null,expires_at.gt.${now}`)
|
|
.order('created_at', { ascending: false });
|
|
|
|
let userQuests = [];
|
|
if (userId) {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id')
|
|
.eq('discord_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (link) {
|
|
const { data } = await supabase
|
|
.from('user_quests')
|
|
.select('*')
|
|
.eq('user_id', link.user_id)
|
|
.eq('guild_id', guildId);
|
|
userQuests = data || [];
|
|
}
|
|
}
|
|
|
|
res.json({ quests, userQuests });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch quests' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/shop/:guildId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
try {
|
|
const { data: items } = await supabase
|
|
.from('shop_items')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.eq('enabled', true)
|
|
.order('price', { ascending: true });
|
|
|
|
res.json({ items: items || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch shop items' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/inventory/:guildId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
try {
|
|
const { data: link } = await supabase
|
|
.from('discord_links')
|
|
.select('user_id')
|
|
.eq('discord_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (!link) {
|
|
return res.json({ inventory: [] });
|
|
}
|
|
|
|
const { data: inventory } = await supabase
|
|
.from('user_inventory')
|
|
.select(`
|
|
*,
|
|
shop_items (name, description, item_type, item_data)
|
|
`)
|
|
.eq('user_id', link.user_id)
|
|
.eq('guild_id', guildId);
|
|
|
|
res.json({ inventory: inventory || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch inventory' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/leaderboard/:guildId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
const { type = 'all', limit = 50 } = req.query;
|
|
|
|
try {
|
|
let tableName = 'leaderboard_alltime';
|
|
if (type === 'weekly') tableName = 'leaderboard_weekly';
|
|
else if (type === 'monthly') tableName = 'leaderboard_monthly';
|
|
|
|
const { data: leaderboard } = await supabase
|
|
.from(tableName)
|
|
.select(`
|
|
*,
|
|
user_profiles!inner (username, avatar_url, prestige_level)
|
|
`)
|
|
.eq('guild_id', guildId)
|
|
.order('xp', { ascending: false })
|
|
.limit(parseInt(limit));
|
|
|
|
res.json({ leaderboard: leaderboard || [], type });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch leaderboard' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/bot-stats', (req, res) => {
|
|
const stats = {
|
|
guilds: discordClient.guilds.cache.size,
|
|
users: discordClient.guilds.cache.reduce((acc, g) => acc + g.memberCount, 0),
|
|
commands: discordClient.commands?.size || 0,
|
|
uptime: Math.floor(process.uptime()),
|
|
status: discordClient.user ? 'online' : 'offline',
|
|
botName: discordClient.user?.username || 'AeThex',
|
|
botAvatar: discordClient.user?.displayAvatarURL() || null
|
|
};
|
|
res.json(stats);
|
|
});
|
|
|
|
app.get('/api/guild/:guildId/config', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access to this server' });
|
|
}
|
|
|
|
try {
|
|
const { data: xpConfig } = await supabase
|
|
.from('xp_config')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
const { data: serverConfig } = await supabase
|
|
.from('server_config')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
res.json({ xpConfig, serverConfig });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch config' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/guild/:guildId/xp-config', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access to this server' });
|
|
}
|
|
|
|
const {
|
|
message_xp,
|
|
message_cooldown,
|
|
reaction_xp_enabled,
|
|
reaction_xp_given,
|
|
reaction_xp_received,
|
|
reaction_cooldown,
|
|
level_curve,
|
|
xp_enabled,
|
|
levelup_channel_id,
|
|
levelup_message,
|
|
levelup_dm,
|
|
levelup_embed,
|
|
levelup_embed_color,
|
|
multiplier_roles,
|
|
bonus_channels
|
|
} = req.body;
|
|
|
|
try {
|
|
const { data: existing } = await supabase
|
|
.from('xp_config')
|
|
.select('guild_id')
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
const configData = {
|
|
guild_id: guildId,
|
|
message_xp: message_xp ?? 15,
|
|
message_cooldown: message_cooldown ?? 60,
|
|
reaction_xp_enabled: reaction_xp_enabled !== false,
|
|
reaction_xp_given: reaction_xp_given ?? 2,
|
|
reaction_xp_received: reaction_xp_received ?? 5,
|
|
reaction_cooldown: reaction_cooldown ?? 30,
|
|
level_curve: level_curve || 'normal',
|
|
xp_enabled: xp_enabled !== false,
|
|
levelup_channel_id: levelup_channel_id || null,
|
|
levelup_message: levelup_message || null,
|
|
levelup_dm: levelup_dm === true,
|
|
levelup_embed: levelup_embed !== false,
|
|
levelup_embed_color: levelup_embed_color || '#7c3aed',
|
|
multiplier_roles: multiplier_roles || [],
|
|
bonus_channels: bonus_channels || [],
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
|
|
if (existing) {
|
|
const { error } = await supabase
|
|
.from('xp_config')
|
|
.update(configData)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
} else {
|
|
const { error } = await supabase
|
|
.from('xp_config')
|
|
.insert(configData);
|
|
|
|
if (error) throw error;
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to save XP config:', error);
|
|
res.status(500).json({ error: 'Failed to save settings', details: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/guild/:guildId/quests', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
try {
|
|
const { data: quests } = await supabase
|
|
.from('quests')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
res.json({ quests: quests || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch quests' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/guild/:guildId/quests', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
const { name, description, quest_type, objective, target_value, xp_reward, duration_hours, repeatable, active } = req.body;
|
|
|
|
try {
|
|
const questData = {
|
|
guild_id: guildId,
|
|
name,
|
|
description: description || null,
|
|
quest_type: quest_type || 'daily',
|
|
objective: objective || 'messages',
|
|
target_value: target_value || 10,
|
|
xp_reward: xp_reward || 100,
|
|
repeatable: repeatable || false,
|
|
active: active !== false,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
if (duration_hours && duration_hours > 0) {
|
|
const expiry = new Date();
|
|
expiry.setHours(expiry.getHours() + duration_hours);
|
|
questData.expires_at = expiry.toISOString();
|
|
}
|
|
|
|
const { error } = await supabase.from('quests').insert(questData);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to create quest:', error);
|
|
res.status(500).json({ error: 'Failed to create quest' });
|
|
}
|
|
});
|
|
|
|
app.put('/api/guild/:guildId/quests/:questId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId, questId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
const { name, description, quest_type, objective, target_value, xp_reward, repeatable, active } = req.body;
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('quests')
|
|
.update({
|
|
name,
|
|
description: description || null,
|
|
quest_type,
|
|
objective,
|
|
target_value,
|
|
xp_reward,
|
|
repeatable: repeatable || false,
|
|
active
|
|
})
|
|
.eq('id', questId)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to update quest:', error);
|
|
res.status(500).json({ error: 'Failed to update quest' });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/guild/:guildId/quests/:questId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId, questId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('quests')
|
|
.delete()
|
|
.eq('id', questId)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to delete quest:', error);
|
|
res.status(500).json({ error: 'Failed to delete quest' });
|
|
}
|
|
});
|
|
|
|
// ============ ACHIEVEMENTS ADMIN API ============
|
|
|
|
app.get('/api/guild/:guildId/achievements', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
try {
|
|
const { data: achievements } = await supabase
|
|
.from('achievements')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
res.json({ achievements: achievements || [] });
|
|
} catch (error) {
|
|
console.error('Failed to fetch achievements:', error);
|
|
res.status(500).json({ error: 'Failed to fetch achievements' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/guild/:guildId/achievements', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
const { name, icon, description, trigger_type, trigger_value, reward_xp, reward_role_id, hidden } = req.body;
|
|
|
|
try {
|
|
const achievementData = {
|
|
guild_id: guildId,
|
|
name,
|
|
icon: icon || '🏆',
|
|
description: description || null,
|
|
trigger_type: trigger_type || 'level',
|
|
trigger_value: trigger_value || 1,
|
|
reward_xp: reward_xp || 0,
|
|
reward_role_id: reward_role_id || null,
|
|
hidden: hidden || false,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
const { error } = await supabase.from('achievements').insert(achievementData);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to create achievement:', error);
|
|
res.status(500).json({ error: 'Failed to create achievement' });
|
|
}
|
|
});
|
|
|
|
app.put('/api/guild/:guildId/achievements/:achievementId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId, achievementId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
const { name, icon, description, trigger_type, trigger_value, reward_xp, reward_role_id, hidden } = req.body;
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('achievements')
|
|
.update({
|
|
name,
|
|
icon: icon || '🏆',
|
|
description: description || null,
|
|
trigger_type,
|
|
trigger_value,
|
|
reward_xp: reward_xp || 0,
|
|
reward_role_id: reward_role_id || null,
|
|
hidden: hidden || false
|
|
})
|
|
.eq('id', achievementId)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to update achievement:', error);
|
|
res.status(500).json({ error: 'Failed to update achievement' });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/guild/:guildId/achievements/:achievementId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId, achievementId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('achievements')
|
|
.delete()
|
|
.eq('id', achievementId)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to delete achievement:', error);
|
|
res.status(500).json({ error: 'Failed to delete achievement' });
|
|
}
|
|
});
|
|
|
|
// ============ SHOP ADMIN API ============
|
|
|
|
app.get('/api/guild/:guildId/shop/admin', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
try {
|
|
const { data: items } = await supabase
|
|
.from('shop_items')
|
|
.select('*')
|
|
.eq('guild_id', guildId)
|
|
.order('created_at', { ascending: false });
|
|
|
|
res.json({ items: items || [] });
|
|
} catch (error) {
|
|
console.error('Failed to fetch shop items:', error);
|
|
res.status(500).json({ error: 'Failed to fetch shop items' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/guild/:guildId/shop', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
const { name, item_type, description, price, stock, level_required, prestige_required, enabled, category, item_data } = req.body;
|
|
|
|
try {
|
|
const itemData = {
|
|
guild_id: guildId,
|
|
name,
|
|
item_type: item_type || 'cosmetic',
|
|
description: description || null,
|
|
category: category || null,
|
|
price: price || 100,
|
|
stock: stock || null,
|
|
item_data: item_data || null,
|
|
level_required: level_required || 0,
|
|
prestige_required: prestige_required || 0,
|
|
enabled: enabled !== false,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
const { error } = await supabase.from('shop_items').insert(itemData);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to create shop item:', error);
|
|
res.status(500).json({ error: 'Failed to create shop item', details: error.message });
|
|
}
|
|
});
|
|
|
|
app.put('/api/guild/:guildId/shop/:itemId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId, itemId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
const { name, item_type, description, price, stock, level_required, prestige_required, enabled, category, item_data } = req.body;
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('shop_items')
|
|
.update({
|
|
name,
|
|
item_type: item_type || 'cosmetic',
|
|
description: description || null,
|
|
category: category || null,
|
|
price: price || 100,
|
|
stock: stock || null,
|
|
item_data: item_data || null,
|
|
level_required: level_required || 0,
|
|
prestige_required: prestige_required || 0,
|
|
enabled: enabled !== false,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', itemId)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to update shop item:', error);
|
|
res.status(500).json({ error: 'Failed to update shop item', details: error.message });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/guild/:guildId/shop/:itemId', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { guildId, itemId } = req.params;
|
|
|
|
const userGuild = req.session.user.guilds.find(g => g.id === guildId);
|
|
if (!userGuild || !userGuild.isAdmin) {
|
|
return res.status(403).json({ error: 'No admin access' });
|
|
}
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('shop_items')
|
|
.delete()
|
|
.eq('id', itemId)
|
|
.eq('guild_id', guildId);
|
|
|
|
if (error) throw error;
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to delete shop item:', error);
|
|
res.status(500).json({ error: 'Failed to delete shop item' });
|
|
}
|
|
});
|
|
|
|
// ============ FEDERATION API ============
|
|
|
|
app.get('/api/federation/stats', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
try {
|
|
const [servers, bans, applications] = await Promise.all([
|
|
supabase.from('federation_servers').select('id', { count: 'exact' }).eq('status', 'approved'),
|
|
supabase.from('federation_bans').select('id', { count: 'exact' }).eq('active', true),
|
|
supabase.from('federation_applications').select('id', { count: 'exact' }).eq('status', 'pending')
|
|
]);
|
|
|
|
res.json({
|
|
totalServers: servers.count || 0,
|
|
activeBans: bans.count || 0,
|
|
pendingApplications: applications.count || 0
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch stats' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/federation/bans', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { limit = 50, severity } = req.query;
|
|
|
|
try {
|
|
let query = supabase
|
|
.from('federation_bans')
|
|
.select('*')
|
|
.eq('active', true)
|
|
.order('created_at', { ascending: false })
|
|
.limit(parseInt(limit));
|
|
|
|
if (severity) {
|
|
query = query.eq('severity', severity);
|
|
}
|
|
|
|
const { data: bans } = await query;
|
|
res.json({ bans: bans || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch bans' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/federation/servers', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
try {
|
|
const { data: servers } = await supabase
|
|
.from('federation_servers')
|
|
.select('*')
|
|
.eq('status', 'approved')
|
|
.order('member_count', { ascending: false });
|
|
|
|
res.json({ servers: servers || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch servers' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/federation/applications', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
try {
|
|
const { data: applications } = await supabase
|
|
.from('federation_applications')
|
|
.select('*')
|
|
.order('created_at', { ascending: false });
|
|
|
|
res.json({ applications: applications || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch applications' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/federation/applications/:appId/approve', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { appId } = req.params;
|
|
|
|
try {
|
|
const { data: app } = await supabase
|
|
.from('federation_applications')
|
|
.select('*')
|
|
.eq('id', appId)
|
|
.maybeSingle();
|
|
|
|
if (!app) {
|
|
return res.status(404).json({ error: 'Application not found' });
|
|
}
|
|
|
|
await supabase.from('federation_applications').update({
|
|
status: 'approved',
|
|
reviewed_by: userId,
|
|
reviewed_at: new Date().toISOString()
|
|
}).eq('id', appId);
|
|
|
|
await supabase.from('federation_servers').insert({
|
|
guild_id: app.guild_id,
|
|
guild_name: app.guild_name,
|
|
guild_icon: app.guild_icon,
|
|
description: app.description,
|
|
category: app.category,
|
|
member_count: app.member_count,
|
|
owner_id: app.admin_id,
|
|
status: 'approved',
|
|
treaty_accepted: true,
|
|
treaty_accepted_at: new Date().toISOString()
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to approve application:', error);
|
|
res.status(500).json({ error: 'Failed to approve application' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/federation/applications/:appId/reject', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const userId = req.session.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Not authenticated' });
|
|
}
|
|
|
|
const { appId } = req.params;
|
|
const { reason } = req.body;
|
|
|
|
try {
|
|
await supabase.from('federation_applications').update({
|
|
status: 'rejected',
|
|
reviewed_by: userId,
|
|
reviewed_at: new Date().toISOString(),
|
|
rejection_reason: reason || 'No reason provided'
|
|
}).eq('id', appId);
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to reject application' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/federation/leaderboard', async (req, res) => {
|
|
if (!supabase) {
|
|
return res.status(503).json({ error: 'Database not available' });
|
|
}
|
|
|
|
const { limit = 50 } = req.query;
|
|
|
|
try {
|
|
const { data: leaders } = await supabase
|
|
.from('federation_reputation')
|
|
.select('*')
|
|
.order('reputation_score', { ascending: false })
|
|
.limit(parseInt(limit));
|
|
|
|
res.json({ leaderboard: leaders || [] });
|
|
} catch (error) {
|
|
res.status(500).json({ error: 'Failed to fetch leaderboard' });
|
|
}
|
|
});
|
|
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
bot: discordClient.user ? 'online' : 'offline',
|
|
uptime: process.uptime()
|
|
});
|
|
});
|
|
|
|
app.get('/federation', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../public/federation.html'));
|
|
});
|
|
|
|
app.get('/dashboard', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../public/dashboard.html'));
|
|
});
|
|
|
|
app.get('/leaderboard', (req, res) => {
|
|
res.redirect('/dashboard?page=leaderboard');
|
|
});
|
|
|
|
app.get('/features', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../public/features.html'));
|
|
});
|
|
|
|
app.get('/commands', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../public/commands.html'));
|
|
});
|
|
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../public/index.html'));
|
|
});
|
|
|
|
// Catch-all route for SPA - use middleware instead of wildcard
|
|
app.use((req, res, next) => {
|
|
if (req.method === 'GET' && !req.path.startsWith('/api/')) {
|
|
res.sendFile(path.join(__dirname, '../public/index.html'));
|
|
} else if (req.path.startsWith('/api/')) {
|
|
res.status(404).json({ error: 'API endpoint not found' });
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
module.exports = { createWebServer };
|