Files
ECS-FullStack/backend/index.js

467 lines
16 KiB
JavaScript

require('dotenv').config({ path: __dirname + '/.env' });
const express = require('express');
const cors = require('cors');
const app = express();
const port = process.env.PORT || 3001;
const host = process.env.HOST || '0.0.0.0';
const corsOrigin = process.env.CORS_ORIGIN || null; // e.g. 'http://example.com' or '*' or 'http://127.0.0.1:3000'
// Convenience base URLs (override if you want fully-qualified URLs)
const BACKEND_BASE = process.env.BACKEND_BASE || `http://${host}:${port}`;
const FRONTEND_BASE = process.env.FRONTEND_BASE || 'http://localhost:3000';
if (corsOrigin) {
app.use(cors({ origin: corsOrigin }));
} else {
app.use(cors());
}
app.use(express.json());
const axios = require('axios');
const crypto = require('crypto');
// Invite token helpers: short-lived HMAC-signed token so frontend can authorize invite deletes
const INVITE_TOKEN_TTL_MS = 5 * 60 * 1000; // 5 minutes
const inviteTokenSecret = process.env.INVITE_TOKEN_SECRET || process.env.ENCRYPTION_KEY || 'fallback-invite-secret';
function generateInviteToken(guildId) {
const payload = JSON.stringify({ gid: guildId, iat: Date.now() });
const payloadB64 = Buffer.from(payload).toString('base64url');
const h = crypto.createHmac('sha256', inviteTokenSecret).update(payloadB64).digest('base64url');
return `${payloadB64}.${h}`;
}
function verifyInviteToken(token) {
try {
if (!token) return null;
const parts = token.split('.');
if (parts.length !== 2) return null;
const [payloadB64, sig] = parts;
const expected = crypto.createHmac('sha256', inviteTokenSecret).update(payloadB64).digest('base64url');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return null;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'));
if (!payload || !payload.gid || !payload.iat) return null;
if (Date.now() - payload.iat > INVITE_TOKEN_TTL_MS) return null;
return payload;
} catch (e) {
return null;
}
}
app.get('/auth/discord', (req, res) => {
const redirectUri = `${BACKEND_BASE}/auth/discord/callback`;
const url = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify%20guilds`;
res.redirect(url);
});
// Provide a short-lived invite token for frontend actions (delete). Not a replacement for proper auth
app.get('/api/servers/:guildId/invite-token', (req, res) => {
const { guildId } = req.params;
try {
const token = generateInviteToken(guildId);
res.json({ token });
} catch (err) {
res.status(500).json({ success: false, message: 'Failed to generate token' });
}
});
app.get('/auth/discord/callback', async (req, res) => {
const code = req.query.code;
if (!code) {
return res.status(400).send('No code provided');
}
try {
const params = new URLSearchParams();
params.append('client_id', process.env.DISCORD_CLIENT_ID);
params.append('client_secret', process.env.DISCORD_CLIENT_SECRET);
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', `${BACKEND_BASE}/auth/discord/callback`);
const response = await axios.post('https://discord.com/api/oauth2/token', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const { access_token } = response.data;
const userResponse = await axios.get('https://discord.com/api/users/@me', {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
const guildsResponse = await axios.get('https://discord.com/api/users/@me/guilds', {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
const adminGuilds = guildsResponse.data.filter(guild => (guild.permissions & 0x8) === 0x8);
const user = userResponse.data;
const db = readDb();
user.theme = db.users && db.users[user.id] ? db.users[user.id].theme : 'light';
const guilds = adminGuilds;
res.redirect(`${FRONTEND_BASE}/dashboard?user=${encodeURIComponent(JSON.stringify(user))}&guilds=${encodeURIComponent(JSON.stringify(guilds))}`);
} catch (error) {
console.error('Error during Discord OAuth2 callback:', error);
res.status(500).send('Internal Server Error');
}
});
const { readDb, writeDb } = require('./db');
app.get('/api/servers/:guildId/settings', (req, res) => {
const { guildId } = req.params;
const db = readDb();
const settings = db[guildId] || { pingCommand: false };
res.json(settings);
});
app.post('/api/servers/:guildId/settings', (req, res) => {
const { guildId } = req.params;
const newSettings = req.body || {};
const db = readDb();
if (!db[guildId]) db[guildId] = {};
// Merge incoming settings with existing settings to avoid overwriting unrelated keys
db[guildId] = { ...db[guildId], ...newSettings };
writeDb(db);
res.json({ success: true });
});
// Toggle a single command for a guild (preserves other toggles)
app.post('/api/servers/:guildId/commands/:cmdName/toggle', (req, res) => {
const { guildId, cmdName } = req.params;
const { enabled } = req.body; // boolean
const protectedCommands = ['help', 'manage-commands'];
if (protectedCommands.includes(cmdName)) {
return res.status(403).json({ success: false, message: 'This command is locked and cannot be toggled.' });
}
const db = readDb();
if (!db[guildId]) db[guildId] = {};
if (!db[guildId].commandToggles) db[guildId].commandToggles = {};
if (typeof enabled === 'boolean') {
db[guildId].commandToggles[cmdName] = enabled;
writeDb(db);
return res.json({ success: true, cmdName, enabled });
}
return res.status(400).json({ success: false, message: 'Missing or invalid "enabled" boolean in request body' });
});
app.get('/api/servers/:guildId/bot-status', (req, res) => {
const { guildId } = req.params;
const guild = bot.client.guilds.cache.get(guildId);
if (guild) {
res.json({ isBotInServer: true });
} else {
res.json({ isBotInServer: false });
}
});
app.get('/api/client-id', (req, res) => {
res.json({ clientId: process.env.DISCORD_CLIENT_ID });
});
app.post('/api/user/theme', (req, res) => {
const { userId, theme } = req.body;
const db = readDb();
if (!db.users) {
db.users = {};
}
if (!db.users[userId]) {
db.users[userId] = {};
}
db.users[userId].theme = theme;
writeDb(db);
res.json({ success: true });
});
app.post('/api/servers/:guildId/leave', async (req, res) => {
const { guildId } = req.params;
try {
const guild = await bot.client.guilds.fetch(guildId);
if (guild) {
await guild.leave();
res.json({ success: true });
} else {
res.status(404).json({ success: false, message: 'Bot is not in the specified server' });
}
} catch (error) {
console.error('Error leaving server:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.get('/api/servers/:guildId/channels', async (req, res) => {
const { guildId } = req.params;
const guild = bot.client.guilds.cache.get(guildId);
if (!guild) {
return res.json([]);
}
try {
const channels = await guild.channels.fetch();
const textChannels = channels.filter(channel => channel.type === 0).map(channel => ({ id: channel.id, name: channel.name }));
res.json(textChannels);
} catch (error) {
console.error('Error fetching channels:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.get('/api/servers/:guildId/welcome-leave-settings', (req, res) => {
const { guildId } = req.params;
const db = readDb();
const settings = db[guildId] || {};
const welcomeLeaveSettings = {
welcome: {
enabled: settings.welcomeEnabled || false,
channel: settings.welcomeChannel || '',
message: settings.welcomeMessage || 'Welcome to the server, {user}!',
customMessage: settings.welcomeCustomMessage || '',
},
leave: {
enabled: settings.leaveEnabled || false,
channel: settings.leaveChannel || '',
message: settings.leaveMessage || '{user} has left the server.',
customMessage: settings.leaveCustomMessage || '',
},
};
res.json(welcomeLeaveSettings);
});
app.post('/api/servers/:guildId/welcome-leave-settings', (req, res) => {
const { guildId } = req.params;
const newSettings = req.body;
const db = readDb();
if (!db[guildId]) {
db[guildId] = {};
}
db[guildId].welcomeEnabled = newSettings.welcome.enabled;
db[guildId].welcomeChannel = newSettings.welcome.channel;
db[guildId].welcomeMessage = newSettings.welcome.message;
db[guildId].welcomeCustomMessage = newSettings.welcome.customMessage;
db[guildId].leaveEnabled = newSettings.leave.enabled;
db[guildId].leaveChannel = newSettings.leave.channel;
db[guildId].leaveMessage = newSettings.leave.message;
db[guildId].leaveCustomMessage = newSettings.leave.customMessage;
writeDb(db);
res.json({ success: true });
});
app.get('/api/servers/:guildId/roles', async (req, res) => {
const { guildId } = req.params;
const guild = bot.client.guilds.cache.get(guildId);
if (!guild) {
return res.json([]);
}
try {
const rolesCollection = await guild.roles.fetch();
// Exclude @everyone (role.id === guild.id), exclude managed roles, and only include roles below the bot's highest role
const botHighest = guild.members.me.roles.highest.position;
const manageable = rolesCollection
.filter(role => role.id !== guild.id && !role.managed && role.position < botHighest)
.sort((a, b) => b.position - a.position)
.map(role => ({ id: role.id, name: role.name, color: role.hexColor }));
res.json(manageable);
} catch (error) {
console.error('Error fetching roles:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.get('/api/servers/:guildId/autorole-settings', (req, res) => {
const { guildId } = req.params;
const db = readDb();
const settings = db[guildId] || {};
const autoroleSettings = settings.autorole || { enabled: false, roleId: '' };
res.json(autoroleSettings);
});
app.post('/api/servers/:guildId/autorole-settings', (req, res) => {
const { guildId } = req.params;
const { enabled, roleId } = req.body;
const db = readDb();
if (!db[guildId]) {
db[guildId] = {};
}
db[guildId].autorole = { enabled, roleId };
writeDb(db);
res.json({ success: true });
});
app.get('/', (req, res) => {
res.send('Hello from the backend!');
});
// Return list of bot commands and per-guild enabled/disabled status
app.get('/api/servers/:guildId/commands', (req, res) => {
try {
const { guildId } = req.params;
const db = readDb();
const guildSettings = db[guildId] || {};
const toggles = guildSettings.commandToggles || {};
const protectedCommands = ['manage-commands', 'help'];
const commands = Array.from(bot.client.commands.values()).map(cmd => {
const isLocked = protectedCommands.includes(cmd.name);
const isEnabled = isLocked ? true : (toggles[cmd.name] !== false && cmd.enabled !== false);
return {
name: cmd.name,
description: cmd.description || 'No description.',
enabled: isEnabled,
locked: isLocked,
hasSlashBuilder: !!cmd.builder,
};
});
res.json(commands);
} catch (error) {
console.error('Error returning commands:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
// INVITES: create, list, delete
app.get('/api/servers/:guildId/invites', async (req, res) => {
try {
const { guildId } = req.params;
const db = readDb();
const saved = (db[guildId] && db[guildId].invites) ? db[guildId].invites : [];
// try to enrich with live data where possible
const guild = bot.client.guilds.cache.get(guildId);
let liveInvites = [];
if (guild) {
try {
const fetched = await guild.invites.fetch();
liveInvites = Array.from(fetched.values());
} catch (e) {
// ignore fetch errors
}
}
const combined = saved.map(inv => {
const live = liveInvites.find(li => li.code === inv.code);
return {
...inv,
uses: live ? live.uses : inv.uses || 0,
maxUses: inv.maxUses || (live ? live.maxUses : 0),
maxAge: inv.maxAge || (live ? live.maxAge : 0),
};
});
res.json(combined);
} catch (error) {
console.error('Error listing invites:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.post('/api/servers/:guildId/invites', async (req, res) => {
try {
const { guildId } = req.params;
const { channelId, maxAge, maxUses, temporary } = req.body || {};
const guild = bot.client.guilds.cache.get(guildId);
if (!guild) return res.status(404).json({ success: false, message: 'Guild not found' });
let channel = null;
if (channelId) {
try { channel = await guild.channels.fetch(channelId); } catch (e) { channel = null; }
}
if (!channel) {
// fall back to first text channel
const channels = await guild.channels.fetch();
channel = channels.find(c => c.type === 0) || channels.first();
}
if (!channel) return res.status(400).json({ success: false, message: 'No channel available to create invite' });
const inviteOptions = {
maxAge: typeof maxAge === 'number' ? maxAge : 0,
maxUses: typeof maxUses === 'number' ? maxUses : 0,
temporary: !!temporary,
unique: true,
};
const invite = await channel.createInvite(inviteOptions);
const db = readDb();
if (!db[guildId]) db[guildId] = {};
if (!db[guildId].invites) db[guildId].invites = [];
const item = {
code: invite.code,
url: invite.url,
channelId: channel.id,
createdAt: new Date().toISOString(),
maxUses: invite.maxUses || inviteOptions.maxUses || 0,
maxAge: invite.maxAge || inviteOptions.maxAge || 0,
temporary: !!invite.temporary,
};
db[guildId].invites.push(item);
writeDb(db);
res.json({ success: true, invite: item });
} catch (error) {
console.error('Error creating invite:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
app.delete('/api/servers/:guildId/invites/:code', async (req, res) => {
// Require a short-lived invite token issued by /api/servers/:guildId/invite-token
const providedToken = req.headers['x-invite-token'];
const payload = verifyInviteToken(providedToken);
if (!payload || payload.gid !== req.params.guildId) {
return res.status(401).json({ success: false, message: 'Unauthorized: missing or invalid invite token' });
}
try {
const { guildId, code } = req.params;
const db = readDb();
const guild = bot.client.guilds.cache.get(guildId);
// Try to delete on Discord if possible
if (guild) {
try {
// fetch invites and delete matching code
const fetched = await guild.invites.fetch();
const inv = fetched.find(i => i.code === code);
if (inv) await inv.delete();
} catch (e) {
// ignore
}
}
if (db[guildId] && db[guildId].invites) {
db[guildId].invites = db[guildId].invites.filter(i => i.code !== code);
writeDb(db);
}
res.json({ success: true });
} catch (error) {
console.error('Error deleting invite:', error);
res.status(500).json({ success: false, message: 'Internal Server Error' });
}
});
const bot = require('../discord-bot');
bot.login();
app.listen(port, host, () => {
console.log(`Server is running on ${host}:${port}`);
});