From 2c851516b277630c1c9d1b82fbdf1b17fc8bae29 Mon Sep 17 00:00:00 2001 From: Declan Chidlow Date: Sun, 14 Jul 2024 18:42:59 +0800 Subject: [PATCH] I forget --- api/src/db.ts | 2 +- api/src/index.ts | 2 +- api/src/middlewares/cors.ts | 4 +- api/src/middlewares/log.ts | 4 +- api/src/middlewares/ratelimit.ts | 57 +++++++++++++----------- api/src/middlewares/updateTokenExpiry.ts | 4 +- api/src/routes/dash/server-automod.ts | 16 +++---- api/src/routes/internal/ws.ts | 2 +- api/src/routes/login.ts | 4 +- api/src/routes/stats.ts | 45 +++++++++++++------ api/src/utils.ts | 4 +- api/tsconfig.json | 2 +- 12 files changed, 83 insertions(+), 63 deletions(-) diff --git a/api/src/db.ts b/api/src/db.ts index 0a7ff14..4a3bcae 100644 --- a/api/src/db.ts +++ b/api/src/db.ts @@ -19,7 +19,7 @@ export default async function buildDBClient(): Promise { } } -const redis = Redis.createClient({ url: process.env.REDIS_URL }); +const redis = Redis.createClient({ url: process.env['REDIS_URL'] }); export { redis }; diff --git a/api/src/index.ts b/api/src/index.ts index 304049e..4244dfc 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -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; diff --git a/api/src/middlewares/cors.ts b/api/src/middlewares/cors.ts index 13972ab..c9a5871 100644 --- a/api/src/middlewares/cors.ts +++ b/api/src/middlewares/cors.ts @@ -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', '*'); diff --git a/api/src/middlewares/log.ts b/api/src/middlewares/log.ts index 6d912c1..c55811e 100644 --- a/api/src/middlewares/log.ts +++ b/api/src/middlewares/log.ts @@ -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(); }); diff --git a/api/src/middlewares/ratelimit.ts b/api/src/middlewares/ratelimit.ts index 357cd04..4bd8d83 100644 --- a/api/src/middlewares/ratelimit.ts +++ b/api/src/middlewares/ratelimit.ts @@ -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 }; diff --git a/api/src/middlewares/updateTokenExpiry.ts b/api/src/middlewares/updateTokenExpiry.ts index 6895f8d..d6e8daf 100644 --- a/api/src/middlewares/updateTokenExpiry.ts +++ b/api/src/middlewares/updateTokenExpiry.ts @@ -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'); diff --git a/api/src/routes/dash/server-automod.ts b/api/src/routes/dash/server-automod.ts index acfbd66..0406658 100644 --- a/api/src/routes/dash/server-automod.ts +++ b/api/src/routes/dash/server-automod.ts @@ -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 { diff --git a/api/src/routes/internal/ws.ts b/api/src/routes/internal/ws.ts index b236163..5d16ac8 100644 --- a/api/src/routes/internal/ws.ts +++ b/api/src/routes/internal/ws.ts @@ -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); diff --git a/api/src/routes/login.ts b/api/src/routes/login.ts index a943b40..526a2b1 100644 --- a/api/src/routes/login.ts +++ b/api/src/routes/login.ts @@ -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." }); } diff --git a/api/src/routes/stats.ts b/api/src/routes/stats.ts index 427378e..7fd0a64 100644 --- a/api/src/routes/stats.ts +++ b/api/src/routes/stats.ts @@ -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 }); - - res.send({ - total: users.length, - blacklist: users.map(u => ({ id: u.id?.toUpperCase(), reason: u.blacklistReason || null })), - }); - } catch(e) { - console.error(''+e); - } +app.get('/stats/global_blacklist', async (res: Response) => { + try { + const dbConnection = await db; + + const users = await dbConnection.collection('users').find({ globalBlacklist: true }).toArray(); + + res.send({ + total: users.length, + blacklist: users.map((u: WithId) => ({ + 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; + } +} diff --git a/api/src/utils.ts b/api/src/utils.ts index ef1a28c..dbaedbe 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -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(); diff --git a/api/tsconfig.json b/api/tsconfig.json index 1ac58a3..6b5fc52 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -8,7 +8,7 @@ "jsx": "react-jsx", "allowJs": true, // Bundler mode - "moduleResolution": "bundler", + "moduleResolution": "node", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true,