Introduces new API endpoints for XP configuration and quests, along with frontend updates in dashboard.html for styling and field name consistency. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 4b20cda7-6144-4154-8c0b-880fe406cb59 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/xfdSNeM Replit-Helium-Checkpoint-Created: true
1062 lines
31 KiB
JavaScript
1062 lines
31 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
|
|
});
|
|
});
|
|
|
|
// 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('available', 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, value)
|
|
`)
|
|
.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_min,
|
|
message_xp_max,
|
|
message_cooldown,
|
|
reaction_xp,
|
|
voice_xp_per_minute,
|
|
daily_xp,
|
|
xp_base,
|
|
levelup_channel_id,
|
|
levelup_enabled,
|
|
levelup_dm,
|
|
xp_multiplier,
|
|
weekend_multiplier
|
|
} = req.body;
|
|
|
|
try {
|
|
const { data: existing } = await supabase
|
|
.from('xp_config')
|
|
.select('id')
|
|
.eq('guild_id', guildId)
|
|
.maybeSingle();
|
|
|
|
const configData = {
|
|
guild_id: guildId,
|
|
message_xp_min: message_xp_min ?? 15,
|
|
message_xp_max: message_xp_max ?? 25,
|
|
message_cooldown: message_cooldown ?? 60,
|
|
reaction_xp: reaction_xp ?? 5,
|
|
voice_xp_per_minute: voice_xp_per_minute ?? 2,
|
|
daily_xp: daily_xp ?? 100,
|
|
xp_base: xp_base ?? 100,
|
|
levelup_channel_id: levelup_channel_id || null,
|
|
levelup_enabled: levelup_enabled !== false,
|
|
levelup_dm: levelup_dm === true,
|
|
xp_multiplier: xp_multiplier ?? 1.0,
|
|
weekend_multiplier: weekend_multiplier ?? 1.5,
|
|
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 {
|
|
configData.created_at = new Date().toISOString();
|
|
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' });
|
|
}
|
|
});
|
|
|
|
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, available } = req.body;
|
|
|
|
try {
|
|
const itemData = {
|
|
guild_id: guildId,
|
|
name,
|
|
item_type: item_type || 'cosmetic',
|
|
description: description || null,
|
|
price: price || 100,
|
|
stock: stock || null,
|
|
level_required: level_required || 0,
|
|
prestige_required: prestige_required || 0,
|
|
available: available !== 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' });
|
|
}
|
|
});
|
|
|
|
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, available } = req.body;
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('shop_items')
|
|
.update({
|
|
name,
|
|
item_type: item_type || 'cosmetic',
|
|
description: description || null,
|
|
price: price || 100,
|
|
stock: stock || null,
|
|
level_required: level_required || 0,
|
|
prestige_required: prestige_required || 0,
|
|
available: available !== false
|
|
})
|
|
.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' });
|
|
}
|
|
});
|
|
|
|
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' });
|
|
}
|
|
});
|
|
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
bot: discordClient.user ? 'online' : 'offline',
|
|
uptime: process.uptime()
|
|
});
|
|
});
|
|
|
|
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('/', (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 };
|