implement changes to commands and templates
This commit is contained in:
+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({
|
||||
|
||||
Reference in New Issue
Block a user