AeThex-Bot-Master/aethex-bot/server/webServer.js
sirpiglr be74f5f0bf Update web server and dashboard styling for improved user experience
Refactor bot.js to separate HTTP server logic and introduce Express web portal. Update dashboard.html with new styling, color themes, and font imports.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 17beead7-b08f-41f9-a6ea-f8e2ff20a502
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/b2aEQYO
Replit-Helium-Checkpoint-Created: true
2025-12-08 23:59:05 +00:00

515 lines
15 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');
const BASE_URL = process.env.REPLIT_DEV_DOMAIN
? `https://${process.env.REPLIT_DEV_DOMAIN}`
: process.env.BASE_URL || 'http://localhost:5000';
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.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('/', (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 };