This commit is contained in:
Declan Chidlow 2024-07-14 18:42:59 +08:00
parent bc7c15271e
commit 2c851516b2
12 changed files with 83 additions and 63 deletions

View file

@ -19,7 +19,7 @@ export default async function buildDBClient(): Promise<Db> {
}
}
const redis = Redis.createClient({ url: process.env.REDIS_URL });
const redis = Redis.createClient({ url: process.env['REDIS_URL'] });
export { redis };

View file

@ -5,7 +5,7 @@ import buildDBClient, { redis } from './db';
config();
const PORT = Number(process.env.API_PORT || 9000);
const PORT = Number(process.env['API_PORT'] || 9000);
const DEBUG = process.env.NODE_ENV != 'production';
const SESSION_LIFETIME = 1000 * 60 * 60 * 24 * 7;

View file

@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { Request, Response, NextFunction } from "express";
import { app } from "..";
app.use('*', (req: Request, res: Response, next: () => void) => {
app.use('*', (req: Request, res: Response, next: NextFunction) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-token');
res.header('Access-Control-Allow-Methods', '*');

View file

@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { Request } from "express";
import { app, logger } from "..";
app.use('*', (req: Request, res: Response, next: () => void) => {
app.use('*', (req: Request, next: () => void) => {
logger.debug(`${req.method} ${req.url}`);
next();
});

View file

@ -1,4 +1,4 @@
import { Request, Response } from "express";
import { Request, Response, NextFunction } from "express";
import { ulid } from "ulid";
import { app, logger } from "..";
import { redis } from "../db";
@ -14,34 +14,37 @@ class RateLimiter {
this.timeframe = limits.timeframe;
}
async execute(req: Request, res: Response, next: () => void) {
try {
const ip = req.ip;
const reqId = ulid();
// ratelimit:ip_address_base64:route_base64
const redisKey = `ratelimit:${Buffer.from(ip).toString('base64')}:${Buffer.from(this.route).toString('base64')}`;
const reqs = await redis.SCARD(redisKey);
if (reqs >= this.limit) {
logger.debug(`Ratelimiter: IP address exceeded ratelimit for ${this.route} [${this.limit}/${this.timeframe}]`);
res
.status(429)
.send({
error: 'You are being rate limited.',
limit: this.limit,
timeframe: this.timeframe,
});
} else {
next();
await redis.SADD(redisKey, reqId);
await redis.sendCommand([ 'EXPIREMEMBER', redisKey, reqId, this.timeframe.toString() ]);
middleware() {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const ip = req.ip;
const reqId = ulid();
// ratelimit:ip_address_base64:route_base64
const redisKey = `ratelimit:${Buffer.from(ip).toString('base64')}:${Buffer.from(this.route).toString('base64')}`;
const reqs = await redis.SCARD(redisKey);
if (reqs >= this.limit) {
logger.debug(`Ratelimiter: IP address exceeded ratelimit for ${this.route} [${this.limit}/${this.timeframe}]`);
res
.status(429)
.send({
error: 'You are being rate limited.',
limit: this.limit,
timeframe: this.timeframe,
});
} else {
await redis.SADD(redisKey, reqId);
await redis.sendCommand([ 'EXPIREMEMBER', redisKey, reqId, this.timeframe.toString() ]);
next();
}
} catch(e) {
console.error(e);
next(e);
}
} catch(e) { console.error(e) }
};
}
}
app.use('*', (...args) => (new RateLimiter('*', { limit: 20, timeframe: 1 })).execute(...args));
const globalRateLimiter = new RateLimiter('*', { limit: 20, timeframe: 1 });
app.use('*', globalRateLimiter.middleware());
export { RateLimiter }
export { RateLimiter };

View file

@ -1,4 +1,4 @@
import { Request, Response } from "express";
import { Request } from "express";
import { Collection, Db } from "mongodb";
import { app, SESSION_LIFETIME } from "..";
@ -8,7 +8,7 @@ export function initializeSessionsMiddleware(db: Db) {
sessionsCollection = db.collection('sessions');
}
app.use('*', async (req: Request, res: Response, next: () => void) => {
app.use('*', async (req: Request, next: () => void) => {
next();
const user = req.header('x-auth-user');
const token = req.header('x-auth-token');

View file

@ -1,8 +1,8 @@
import { app, db } from '../..';
import { app } from '../..';
import { Request, Response } from 'express';
import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth, unauthorized } from '../../utils';
import { botReq } from '../internal/ws';
import { Collection, Db, ObjectId } from 'mongodb';
import { Collection, Db } from 'mongodb';
import { ulid } from 'ulid';
let serversCollection: Collection;
@ -32,15 +32,15 @@ app.get('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (r
return res.status(response.statusCode ?? 500).send({ error: response.error });
}
if (!response.server) return res.status(404).send({ error: 'Server not found' });
if (!response['server']) return res.status(404).send({ error: 'Server not found' });
const permissionLevel: 0|1|2|3 = response.perms;
const permissionLevel: 0|1|2|3 = response['perms'];
if (permissionLevel < 1) return unauthorized(res, `Only moderators and bot managers may view this.`);
const serverConfig = await serversCollection.findOne({ id: server });
const result = {
antispam: (serverConfig?.automodSettings?.spam as AntispamRule[]|undefined)
antispam: (serverConfig?.['automodSettings']?.spam as AntispamRule[]|undefined)
?.map(r => ({
action: r.action,
channels: r.channels,
@ -64,7 +64,7 @@ app.patch('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 })
if (!server || !ruleid) return badRequest(res);
const serverConfig = await serversCollection.findOne({ id: server });
const antiSpamRules: AntispamRule[] = serverConfig?.automodSettings?.spam ?? [];
const antiSpamRules: AntispamRule[] = serverConfig?.['automodSettings']?.spam ?? [];
const rule = antiSpamRules.find(r => r.id == ruleid);
if (!rule) return res.status(404).send({ error: 'No rule with this ID could be found.' });
@ -100,7 +100,7 @@ app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (
return res.status(response.statusCode ?? 500).send({ error: response.error });
}
if (!response.server) return res.status(404).send({ error: 'Server not found' });
if (!response['server']) return res.status(404).send({ error: 'Server not found' });
let rule: any;
try {
@ -146,7 +146,7 @@ app.delete('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 }
return res.status(response.statusCode ?? 500).send({ error: response.error });
}
if (!response.server) return res.status(404).send({ error: 'Server not found' });
if (!response['server']) return res.status(404).send({ error: 'Server not found' });
let result;
try {

View file

@ -8,7 +8,7 @@ import { EventEmitter } from 'events';
import { logger } from "../..";
import server from '../../server';
if (!process.env.BOT_API_TOKEN) {
if (!process.env['BOT_API_TOKEN']) {
logger.error(`$BOT_API_TOKEN is not set. This token is `
+ `required for the bot to communicate with the API.`);
process.exit(1);

View file

@ -36,7 +36,7 @@ app.post('/login/begin',
if (!body.user || typeof body.user != 'string') return badRequest(res);
const r = await botReq('requestLogin', { user: body.user.toLowerCase() });
if (!r.success) return res.status(r.statusCode ?? 500).send(JSON.stringify({ error: r.error }, null, 4));
res.status(200).send({ success: true, nonce: r.nonce, code: r.code, uid: r.uid });
res.status(200).send({ success: true, nonce: r['nonce'], code: r['code'], uid: r['uid'] });
});
app.post('/login/complete',
@ -57,7 +57,7 @@ app.post('/login/complete',
});
if (!loginAttempt) return res.status(404).send({ error: 'The provided login info could not be found.' });
if (!loginAttempt.confirmed) {
if (!loginAttempt['confirmed']) {
return res.status(400).send({ error: "This code is not yet valid." });
}

View file

@ -1,6 +1,7 @@
import { app, db, logger } from '..';
import { Request, Response } from 'express';
import { Response } from 'express';
import { botReq } from './internal/ws';
import { WithId, Document, ObjectId } from 'mongodb';
let SERVER_COUNT = 0;
@ -8,7 +9,7 @@ const fetchStats = async () => {
try {
const res = await botReq('stats');
if (!res.success) return logger.warn(`Failed to fetch bot stats: ${res.statusCode} / ${res.error}`);
if (res.servers) SERVER_COUNT = Number(res.servers);
if (res['servers']) SERVER_COUNT = Number(res['servers']);
} catch(e) {
console.error(e);
}
@ -17,21 +18,37 @@ const fetchStats = async () => {
fetchStats();
setInterval(() => fetchStats(), 10000);
app.get('/stats', async (req: Request, res: Response) => {
app.get('/stats', async (res: Response) => {
res.send({
servers: SERVER_COUNT,
});
});
app.get('/stats/global_blacklist', async (req: Request, res: Response) => {
try {
const users = await db.get('users').find({ globalBlacklist: true });
app.get('/stats/global_blacklist', async (res: Response) => {
try {
const dbConnection = await db;
res.send({
total: users.length,
blacklist: users.map(u => ({ id: u.id?.toUpperCase(), reason: u.blacklistReason || null })),
});
} catch(e) {
console.error(''+e);
}
const users = await dbConnection.collection('users').find({ globalBlacklist: true }).toArray();
res.send({
total: users.length,
blacklist: users.map((u: WithId<Document>) => ({
id: getId(u._id),
reason: (u as any).blacklistReason || null
})),
});
} catch(e) {
console.error('Error fetching global blacklist:', e);
res.status(500).send({ error: 'Internal server error' });
}
});
function getId(id: string | ObjectId | undefined): string | null {
if (typeof id === 'string') {
return id.toUpperCase();
} else if (id instanceof ObjectId) {
return id.toHexString().toUpperCase();
} else {
return null;
}
}

View file

@ -66,10 +66,10 @@ function requireAuth(config: RequireAuthConfig): (req: Request, res: Response, n
if (config.permission != undefined) {
if (!auth) return unauthorized(res, 'Authentication required for this route');
const server_id = req.params.serverid || req.params.server;
const server_id = req.params['serverid'] || req.params['server'];
const levelRes = await getPermissionLevel(auth, server_id);
if (!levelRes.success) return res.status(500).send({ error: 'Unknown server or other error' });
if (levelRes.level < config.permission) return unauthorized(res, 'Your permission level is too low');
if (levelRes['level'] < config.permission) return unauthorized(res, 'Your permission level is too low');
}
next();

View file

@ -8,7 +8,7 @@
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,