implement changes to commands and templates
This commit is contained in:
@@ -90,3 +90,15 @@ Example document stored in the `discord.guild_permissions` collection:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Configuration**
|
||||
Set secrets via environment variables, or copy `config.example.json` to `config.json` and fill in your own values.
|
||||
|
||||
Recommended env vars:
|
||||
- `DISCORD_BOT_TOKEN`
|
||||
- `DISCORD_CLIENT_ID`
|
||||
- `DISCORD_CLIENT_SECRET`
|
||||
- `DISCORD_REDIRECT_URI`
|
||||
- `MONGODB_URI`
|
||||
|
||||
@@ -14,6 +14,12 @@ module.exports = {
|
||||
async execute(interaction) {
|
||||
const dbClient = await connectToDatabase();
|
||||
const message = interaction.options.getString('message');
|
||||
if (message.length > 500) {
|
||||
return interaction.reply({
|
||||
content: 'Message must be 500 characters or fewer.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
const db = dbClient.db('discord');
|
||||
const collection = db.collection('test');
|
||||
await collection.insertOne({ userId: interaction.user.id, message: message });
|
||||
|
||||
@@ -40,22 +40,21 @@ module.exports = {
|
||||
}
|
||||
|
||||
const senderId = interaction.user.id;
|
||||
const debitResult = await collection.findOneAndUpdate(
|
||||
{ userID: senderId, balance: { $gte: amount } },
|
||||
{ $inc: { balance: -amount } },
|
||||
{ returnDocument: 'after' }
|
||||
);
|
||||
|
||||
if (!debitResult.value) {
|
||||
const senderData = await collection.findOne({ userID: senderId });
|
||||
const senderBalance = senderData?.balance ?? 0;
|
||||
|
||||
if (senderBalance < amount) {
|
||||
return interaction.reply({
|
||||
content: `You only have **${senderBalance}** coins available.`,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
await collection.updateOne(
|
||||
{ userID: senderId },
|
||||
{ $inc: { balance: -amount } },
|
||||
{ upsert: true }
|
||||
);
|
||||
|
||||
await collection.updateOne(
|
||||
{ userID: target.id },
|
||||
{ $inc: { balance: amount } },
|
||||
|
||||
@@ -18,6 +18,12 @@ module.exports = {
|
||||
.setDescription('The reason of the kick')),
|
||||
category: 'Moderation',
|
||||
async execute(interaction) {
|
||||
if (!interaction.guild) {
|
||||
return interaction.reply({
|
||||
content: 'This command can only be used inside a server.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
const user = interaction.options.getUser('target');
|
||||
const reason = interaction.options.getString('reason') || 'No reason provided';
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ module.exports = {
|
||||
.setDescription('The reason of the mute')),
|
||||
category: 'Moderation',
|
||||
async execute(interaction) {
|
||||
if (!interaction.guild) {
|
||||
return interaction.reply({
|
||||
content: 'This command can only be used inside a server.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
const target = interaction.options.getUser('target');
|
||||
const duration = interaction.options.getInteger('duration');
|
||||
const reason = interaction.options.getString('reason') || 'No reason provided';
|
||||
|
||||
@@ -48,6 +48,9 @@ module.exports = {
|
||||
.setRequired(true))),
|
||||
category: 'Moderation',
|
||||
async execute(interaction) {
|
||||
if (!interaction.guild) {
|
||||
return interaction.reply({ content: 'This command can only be used inside a server.', ephemeral: true });
|
||||
}
|
||||
const target = interaction.options.getUser('target');
|
||||
const role = interaction.options.getRole('role');
|
||||
const action = interaction.options.getSubcommand();
|
||||
|
||||
@@ -18,6 +18,8 @@ module.exports = {
|
||||
option
|
||||
.setName('durationms')
|
||||
.setDescription('Duration of the temporary ban (in milliseconds).')
|
||||
.setMinValue(60_000)
|
||||
.setMaxValue(2_592_000_000)
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option
|
||||
@@ -31,23 +33,41 @@ module.exports = {
|
||||
.setMinValue(0).setMaxValue(7)),
|
||||
category: 'Moderation',
|
||||
async execute(interaction) {
|
||||
if (!interaction.guild) {
|
||||
return interaction.reply({
|
||||
content: 'This command can only be used inside a server.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
const user = interaction.options.getUser('target');
|
||||
const reason = interaction.options.getString('reason');
|
||||
const durationms = interaction.options.getInteger('durationms');
|
||||
const deleteMessages = interaction.options.getInteger('delete-messages');
|
||||
|
||||
// If you want to parse time like "1d", "10m", etc., use ms package:
|
||||
let banUntil;
|
||||
try {
|
||||
banUntil = Date.now() + durationms;
|
||||
} catch (e) {
|
||||
// If parsing fails, handle error
|
||||
if (user.bot) {
|
||||
return interaction.reply({
|
||||
content: 'Invalid duration format. Please use something like "10m" or "1d".',
|
||||
ephemeral: true
|
||||
content: 'You cannot temp-ban a bot account.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (user.id === interaction.user.id) {
|
||||
return interaction.reply({
|
||||
content: 'You cannot temp-ban yourself.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
// If you want to parse time like "1d", "10m", etc., use ms package:
|
||||
if (!Number.isFinite(durationms) || durationms <= 0) {
|
||||
return interaction.reply({
|
||||
content: 'Invalid duration. Provide a positive number of milliseconds.',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const banUntil = Date.now() + durationms;
|
||||
|
||||
|
||||
// A function to send ephemeral messages
|
||||
async function sendEphemeral(message) {
|
||||
|
||||
@@ -4,6 +4,8 @@ const { getGuildSettings } = require('../../services/guildSettings');
|
||||
const { recordModerationAction } = require('../../services/moderationRecords');
|
||||
const { connectToDatabase } = require('../../db');
|
||||
|
||||
const MAX_REASON_LENGTH = 500;
|
||||
|
||||
module.exports = {
|
||||
permissionGroup: 'moderation',
|
||||
data: new SlashCommandBuilder()
|
||||
@@ -31,6 +33,12 @@ module.exports = {
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
if (reason.length > MAX_REASON_LENGTH) {
|
||||
return interaction.reply({
|
||||
content: `Reason must be ${MAX_REASON_LENGTH} characters or fewer.`,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const { warnings, strikeCount } = await addWarning({
|
||||
guildId: interaction.guildId,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"token": "YOUR_DISCORD_BOT_TOKEN",
|
||||
"clientId": "YOUR_DISCORD_CLIENT_ID",
|
||||
"clientSecret": "YOUR_DISCORD_CLIENT_SECRET",
|
||||
"mongodb": "mongodb://127.0.0.1:27017/botdb",
|
||||
"redirectUri": "http://localhost:3000/auth/discord/callback",
|
||||
"dashboardInvitePermissions": "0",
|
||||
"dashboardSessionTtlMs": "86400000",
|
||||
"dashboardClientUrl": "http://localhost:3000",
|
||||
"trustProxy": false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, 'config.json');
|
||||
let cachedConfig = null;
|
||||
|
||||
const loadConfig = () => {
|
||||
if (cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
cachedConfig = {};
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
cachedConfig = parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch (error) {
|
||||
console.error('Failed to parse config.json', { message: error.message });
|
||||
cachedConfig = {};
|
||||
}
|
||||
|
||||
return cachedConfig;
|
||||
};
|
||||
|
||||
const getConfig = () => loadConfig();
|
||||
|
||||
const getConfigValue = (envKey, ...configKeys) => {
|
||||
const envValue = process.env[envKey];
|
||||
if (typeof envValue === 'string' && envValue.trim() !== '') {
|
||||
return envValue.trim();
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
for (const key of configKeys) {
|
||||
const value = config?.[key];
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
CONFIG_PATH,
|
||||
getConfig,
|
||||
getConfigValue,
|
||||
};
|
||||
+135
-36
@@ -5,39 +5,40 @@ const axios = require('axios');
|
||||
const express = require('express');
|
||||
const {addWarning, getWarnings, clearWarnings} = require('./services/warnings');
|
||||
const {getGuildPermissions, updateGuildPermissions} = require('./services/permissions');
|
||||
const { CONFIG_PATH, getConfig, getConfigValue } = require('./config');
|
||||
const DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
const DEFAULT_SESSION_CLEANUP_MS = 60 * 60 * 1000;
|
||||
const MAX_REASON_LENGTH = 500;
|
||||
const MAX_MEMBER_QUERY_LENGTH = 64;
|
||||
const JSON_BODY_LIMIT = '50kb';
|
||||
const PERMISSION_BITS = {
|
||||
ADMINISTRATOR: 0x8n,
|
||||
MANAGE_GUILD: 0x20n,
|
||||
};
|
||||
|
||||
const loadConfig = () => {
|
||||
const configPath = path.join(__dirname, 'config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
return require(configPath);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const config = getConfig();
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const DISCORD_CLIENT_ID =
|
||||
process.env.DISCORD_CLIENT_ID ||
|
||||
config.discordClientId ||
|
||||
config.clientId ||
|
||||
config.appId;
|
||||
const DISCORD_CLIENT_SECRET =
|
||||
process.env.DISCORD_CLIENT_SECRET ||
|
||||
config.discordClientSecret ||
|
||||
config.clientSecret;
|
||||
const DISCORD_REDIRECT_URI =
|
||||
process.env.DISCORD_REDIRECT_URI ||
|
||||
config.discordRedirectUri ||
|
||||
config.redirectUri;
|
||||
const DISCORD_BOT_TOKEN =
|
||||
process.env.DISCORD_BOT_TOKEN ||
|
||||
config.discordBotToken ||
|
||||
config.token;
|
||||
const DISCORD_CLIENT_ID = getConfigValue(
|
||||
'DISCORD_CLIENT_ID',
|
||||
'discordClientId',
|
||||
'clientId',
|
||||
'appId',
|
||||
);
|
||||
const DISCORD_CLIENT_SECRET = getConfigValue(
|
||||
'DISCORD_CLIENT_SECRET',
|
||||
'discordClientSecret',
|
||||
'clientSecret',
|
||||
);
|
||||
const DISCORD_REDIRECT_URI = getConfigValue(
|
||||
'DISCORD_REDIRECT_URI',
|
||||
'discordRedirectUri',
|
||||
'redirectUri',
|
||||
);
|
||||
const DISCORD_BOT_TOKEN = getConfigValue(
|
||||
'DISCORD_BOT_TOKEN',
|
||||
'discordBotToken',
|
||||
'token',
|
||||
);
|
||||
const DASHBOARD_INVITE_PERMISSIONS = Number.parseInt(
|
||||
process.env.DASHBOARD_INVITE_PERMISSIONS || config.dashboardInvitePermissions || '0',
|
||||
10,
|
||||
@@ -50,13 +51,17 @@ const SESSION_TTL_MS = Number.parseInt(
|
||||
process.env.DASHBOARD_SESSION_TTL_MS || config.dashboardSessionTtlMs,
|
||||
10,
|
||||
);
|
||||
const TRUST_PROXY =
|
||||
process.env.DASHBOARD_TRUST_PROXY === 'true' ||
|
||||
process.env.DASHBOARD_TRUST_PROXY === '1' ||
|
||||
Boolean(config.trustProxy);
|
||||
|
||||
console.log('[oauth config]', {
|
||||
DISCORD_CLIENT_ID_present: Boolean(DISCORD_CLIENT_ID && String(DISCORD_CLIENT_ID).trim()),
|
||||
DISCORD_CLIENT_SECRET_present: Boolean(DISCORD_CLIENT_SECRET && String(DISCORD_CLIENT_SECRET).trim()),
|
||||
DISCORD_REDIRECT_URI: DISCORD_REDIRECT_URI || null,
|
||||
configFilePath: path.join(__dirname, 'config.json'),
|
||||
configFileExists: fs.existsSync(path.join(__dirname, 'config.json')),
|
||||
configFilePath: CONFIG_PATH,
|
||||
configFileExists: fs.existsSync(CONFIG_PATH),
|
||||
});
|
||||
|
||||
const DISCORD_API_BASE = 'https://discord.com/api';
|
||||
@@ -72,6 +77,61 @@ const botGuildCache = {
|
||||
fetchedAt: 0,
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('Unhandled promise rejection:', reason);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
});
|
||||
|
||||
const cleanupExpiredSessions = () => {
|
||||
const now = Date.now();
|
||||
for (const [sessionId, session] of sessionStore.entries()) {
|
||||
if (!session?.expiresAt || session.expiresAt <= now) {
|
||||
sessionStore.delete(sessionId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createRateLimiter = ({ windowMs, max, keyFn }) => {
|
||||
const hits = new Map();
|
||||
|
||||
const prune = () => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of hits.entries()) {
|
||||
if (!entry || entry.resetAt <= now) {
|
||||
hits.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (req, res, next) => {
|
||||
const key = keyFn(req);
|
||||
const now = Date.now();
|
||||
const entry = hits.get(key);
|
||||
if (!entry || entry.resetAt <= now) {
|
||||
hits.set(key, { count: 1, resetAt: now + windowMs });
|
||||
return next();
|
||||
}
|
||||
if (entry.count >= max) {
|
||||
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
|
||||
res.set('Retry-After', String(retryAfter));
|
||||
return res.status(429).json({ error: 'RATE_LIMITED', retryAfterSeconds: retryAfter });
|
||||
}
|
||||
entry.count += 1;
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
const ensureCsrf = (req, res, next) => {
|
||||
const token = req.headers['x-csrf-token'];
|
||||
if (!token || token !== req.session?.csrfToken) {
|
||||
return res.status(403).json({ error: 'CSRF_INVALID', message: 'CSRF token missing or invalid.' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
|
||||
const parseCookies = (headerValue) => {
|
||||
const cookies = {};
|
||||
if (!headerValue) {
|
||||
@@ -115,13 +175,15 @@ const clearCookie = (res, name) => {
|
||||
const createSession = (payload) => {
|
||||
const sessionId = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = Date.now() + (Number.isNaN(SESSION_TTL_MS) ? DEFAULT_SESSION_TTL_MS : SESSION_TTL_MS);
|
||||
const csrfToken = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
sessionStore.set(sessionId, {
|
||||
...payload,
|
||||
expiresAt,
|
||||
csrfToken,
|
||||
});
|
||||
|
||||
return {sessionId, expiresAt};
|
||||
return {sessionId, expiresAt, csrfToken};
|
||||
};
|
||||
|
||||
const getSession = (sessionId) => {
|
||||
@@ -409,10 +471,34 @@ const ensureManageableGuild = async (req, res, next) => {
|
||||
const createDashboardApp = () => {
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.disable('x-powered-by');
|
||||
if (TRUST_PROXY) {
|
||||
app.set('trust proxy', 1);
|
||||
}
|
||||
app.use((req, res, next) => {
|
||||
res.set('X-Content-Type-Options', 'nosniff');
|
||||
res.set('X-Frame-Options', 'DENY');
|
||||
res.set('Referrer-Policy', 'no-referrer');
|
||||
res.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
||||
return next();
|
||||
});
|
||||
app.use(express.json({ limit: JSON_BODY_LIMIT }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(createRateLimiter({
|
||||
windowMs: 60_000,
|
||||
max: 120,
|
||||
keyFn: (req) => req.ip || 'unknown',
|
||||
}));
|
||||
|
||||
app.get('/auth/discord', (req, res) => {
|
||||
const authLimiter = createRateLimiter({
|
||||
windowMs: 60_000,
|
||||
max: 20,
|
||||
keyFn: (req) => req.ip || 'unknown',
|
||||
});
|
||||
|
||||
setInterval(cleanupExpiredSessions, DEFAULT_SESSION_CLEANUP_MS).unref();
|
||||
|
||||
app.get('/auth/discord', authLimiter, (req, res) => {
|
||||
if (!ensureOAuthConfig()) {
|
||||
return res.status(500).json({error: 'Missing Discord OAuth configuration'});
|
||||
}
|
||||
@@ -428,7 +514,7 @@ const createDashboardApp = () => {
|
||||
return res.redirect(buildAuthorizationUrl(state));
|
||||
});
|
||||
|
||||
app.get('/auth/discord/callback', async (req, res) => {
|
||||
app.get('/auth/discord/callback', authLimiter, async (req, res) => {
|
||||
if (!ensureOAuthConfig()) {
|
||||
return res.status(500).json({error: 'Missing Discord OAuth configuration'});
|
||||
}
|
||||
@@ -467,7 +553,7 @@ const createDashboardApp = () => {
|
||||
return res.redirect('/dashboard.html');
|
||||
}
|
||||
|
||||
return res.status(200).json({user});
|
||||
return res.status(200).json({user, csrfToken: session.csrfToken});
|
||||
} catch (error) {
|
||||
console.error('Discord OAuth failed', {
|
||||
message: error.message,
|
||||
@@ -482,7 +568,7 @@ const createDashboardApp = () => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/auth/logout', ensureSession, (req, res) => {
|
||||
app.post('/auth/logout', authLimiter, ensureSession, ensureCsrf, (req, res) => {
|
||||
destroySession(req.sessionId);
|
||||
clearCookie(res, 'dashboard_session');
|
||||
return res.status(204).send();
|
||||
@@ -492,6 +578,7 @@ const createDashboardApp = () => {
|
||||
return res.json({
|
||||
user: req.session.user,
|
||||
expiresAt: req.session.expiresAt,
|
||||
csrfToken: req.session.csrfToken,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -554,8 +641,12 @@ const createDashboardApp = () => {
|
||||
'/api/guilds/:guildId/permissions',
|
||||
ensureSession,
|
||||
ensureManageableGuild,
|
||||
ensureCsrf,
|
||||
async (req, res) => {
|
||||
try {
|
||||
if (!req.body || typeof req.body !== 'object') {
|
||||
return res.status(400).json({ error: 'Invalid permissions payload' });
|
||||
}
|
||||
const permissions = await updateGuildPermissions({
|
||||
guildId: req.params.guildId,
|
||||
permissions: req.body || {},
|
||||
@@ -605,6 +696,9 @@ const createDashboardApp = () => {
|
||||
if (query.length < 2) {
|
||||
return res.json({members: []});
|
||||
}
|
||||
if (query.length > MAX_MEMBER_QUERY_LENGTH) {
|
||||
return res.status(400).json({ error: 'Query too long' });
|
||||
}
|
||||
try {
|
||||
const members = await fetchDiscordGuildMembersSearch(req.params.guildId, query);
|
||||
const normalized = (members || [])
|
||||
@@ -679,17 +773,21 @@ const createDashboardApp = () => {
|
||||
'/api/guilds/:guildId/warnings/:userId',
|
||||
ensureSession,
|
||||
ensureManageableGuild,
|
||||
ensureCsrf,
|
||||
async (req, res) => {
|
||||
const {reason} = req.body || {};
|
||||
if (!reason) {
|
||||
if (typeof reason !== 'string' || reason.trim().length === 0) {
|
||||
return res.status(400).json({error: 'Reason is required'});
|
||||
}
|
||||
if (reason.length > MAX_REASON_LENGTH) {
|
||||
return res.status(400).json({error: `Reason must be ${MAX_REASON_LENGTH} characters or fewer`});
|
||||
}
|
||||
try {
|
||||
const result = await addWarning({
|
||||
guildId: req.params.guildId,
|
||||
userId: req.params.userId,
|
||||
moderatorId: req.session.user?.id,
|
||||
reason,
|
||||
reason: reason.trim(),
|
||||
});
|
||||
const warnings = (result.warnings || []).map((warning) => ({
|
||||
...warning,
|
||||
@@ -718,6 +816,7 @@ const createDashboardApp = () => {
|
||||
'/api/guilds/:guildId/warnings/:userId',
|
||||
ensureSession,
|
||||
ensureManageableGuild,
|
||||
ensureCsrf,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const warnings = await clearWarnings({
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
const {MongoClient, ServerApiVersion} = require('mongodb');
|
||||
const {mongodb} = require("./config.json");
|
||||
const { MongoClient, ServerApiVersion } = require('mongodb');
|
||||
const { getConfigValue } = require('./config');
|
||||
|
||||
let dbClient;
|
||||
|
||||
async function connectToDatabase() {
|
||||
if (!dbClient) {
|
||||
dbClient = new MongoClient(mongodb, {
|
||||
const mongodbUri = getConfigValue('MONGODB_URI', 'mongodb', 'mongodbUri');
|
||||
if (!mongodbUri) {
|
||||
throw new Error('Missing MongoDB connection string. Set MONGODB_URI or config.json mongodb.');
|
||||
}
|
||||
|
||||
dbClient = new MongoClient(mongodbUri, {
|
||||
serverApi: {
|
||||
version: ServerApiVersion.v1,
|
||||
strict: true,
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
const path = require('node:path');
|
||||
const { Client, Collection, GatewayIntentBits } = require('discord.js');
|
||||
const { token } = require('./config.json');
|
||||
const { getConfigValue } = require('./config');
|
||||
const { loadCommandModules } = require('./loaders/commands');
|
||||
const { loadEventModules } = require('./loaders/events');
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('Unhandled promise rejection:', reason);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
});
|
||||
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
client.commands = new Collection();
|
||||
const commandsPath = path.join(__dirname, 'commands');
|
||||
@@ -26,4 +34,10 @@ for (const { event } of eventModules) {
|
||||
}
|
||||
}
|
||||
|
||||
const token = getConfigValue('DISCORD_BOT_TOKEN', 'token', 'discordBotToken');
|
||||
if (!token) {
|
||||
console.error('Missing Discord bot token. Set DISCORD_BOT_TOKEN or config.json token.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
client.login(token);
|
||||
|
||||
+20
-1
@@ -14,11 +14,21 @@ const createFetchError = async (response) => {
|
||||
return error;
|
||||
};
|
||||
|
||||
let csrfToken = null;
|
||||
|
||||
const setCsrfToken = (token) => {
|
||||
csrfToken = typeof token === 'string' ? token : null;
|
||||
};
|
||||
|
||||
const getCsrfToken = () => csrfToken;
|
||||
|
||||
const fetchJson = async (url, options = {}) => {
|
||||
const method = (options.method || 'GET').toUpperCase();
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(csrfToken && method !== 'GET' && method !== 'HEAD' ? {'X-CSRF-Token': csrfToken} : {}),
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options,
|
||||
@@ -110,6 +120,7 @@ const initIndexPage = async () => {
|
||||
|
||||
try {
|
||||
const data = await fetchJson('/api/me');
|
||||
setCsrfToken(data.csrfToken);
|
||||
sessionStatus.textContent = `Signed in as ${formatUserDisplay(data.user)}.`;
|
||||
loginButton.hidden = true;
|
||||
dashboardButton.hidden = false;
|
||||
@@ -131,7 +142,13 @@ const initIndexPage = async () => {
|
||||
|
||||
logoutButton?.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/auth/logout', {method: 'POST', credentials: 'include'});
|
||||
await fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...(csrfToken ? {'X-CSRF-Token': csrfToken} : {}),
|
||||
},
|
||||
});
|
||||
sessionStatus.textContent = 'Logged out successfully.';
|
||||
loginButton.hidden = false;
|
||||
dashboardButton.hidden = true;
|
||||
@@ -151,6 +168,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
window.dashboardApi = {
|
||||
fetchJson,
|
||||
setCsrfToken,
|
||||
getCsrfToken,
|
||||
showToast,
|
||||
setErrorBanner,
|
||||
clearErrorBanner,
|
||||
|
||||
+8
-1
@@ -580,6 +580,7 @@ const updateBotStatusControls = () => {
|
||||
const loadSession = async () => {
|
||||
const data = await fetchDashboardJson('/api/me');
|
||||
elements.userDisplay.textContent = dashboardApi.formatUserDisplay(data.user);
|
||||
dashboardApi.setCsrfToken(data.csrfToken);
|
||||
if (data.expiresAt) {
|
||||
const expires = new Date(data.expiresAt);
|
||||
elements.sessionExpiry.textContent = `Session expires ${expires.toLocaleString()}`;
|
||||
@@ -993,7 +994,13 @@ const initDashboard = async () => {
|
||||
}
|
||||
|
||||
elements.logoutButton.addEventListener('click', async () => {
|
||||
await fetch('/auth/logout', {method: 'POST', credentials: 'include'});
|
||||
await fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
...(dashboardApi.getCsrfToken() ? {'X-CSRF-Token': dashboardApi.getCsrfToken()} : {}),
|
||||
},
|
||||
});
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user