De monk-ify

This commit is contained in:
Declan Chidlow 2024-07-13 20:58:10 +08:00
parent bfd2ea2094
commit 8d9bbc1893
7 changed files with 125 additions and 82 deletions

Binary file not shown.

View file

@ -10,13 +10,12 @@
}, },
"dependencies": { "dependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/monk": "^6.0.0",
"@types/ws": "^8.5.11", "@types/ws": "^8.5.11",
"automod": "^0.1.0", "automod": "^0.1.0",
"dotenv": "^14.3.2", "dotenv": "^14.3.2",
"express": "^4.19.2", "express": "^4.19.2",
"log75": "^2.2.0", "log75": "^2.2.0",
"monk": "^7.3.4", "mongodb": "^6.8.0",
"redis": "^4.6.15", "redis": "^4.6.15",
"ulid": "^2.3.0", "ulid": "^2.3.0",
"ws": "^8.18.0" "ws": "^8.18.0"

View file

@ -1,34 +1,44 @@
import Monk, { IMonkManager } from 'monk'; import { MongoClient, Db } from 'mongodb';
import Redis from 'redis'; import Redis from 'redis';
import { logger } from '.'; import { logger } from '.';
export default (): IMonkManager => { let db: Db;
let dburl = getDBUrl();
let db = Monk(dburl); export default async function buildDBClient(): Promise<Db> {
if (db) return db;
const url = getDBUrl();
const client = new MongoClient(url);
try {
await client.connect();
db = client.db();
logger.info('Connected successfully to MongoDB');
return db; return db;
}; } catch (error) {
logger.error('Failed to connect to MongoDB', error);
throw error;
}
}
const redis = Redis.createClient({ url: process.env.REDIS_URL }); const redis = Redis.createClient({ url: process.env.REDIS_URL });
export { redis } export { redis };
// Checks if all required env vars were supplied, and returns the mongo db URL // Checks if all required env vars were supplied, and returns the mongo db URL
function getDBUrl() { function getDBUrl(): string {
let env = process.env; const env = process.env;
if (env['DB_URL']) return env['DB_URL']; if (env['DB_URL']) return env['DB_URL'];
if (!env['DB_HOST']) { if (!env['DB_HOST']) {
logger.error(`Environment variable 'DB_HOST' not set, unable to connect to database`); logger.error(`Environment variable 'DB_HOST' not set, unable to connect to database`);
logger.error(`Specify either 'DB_URL' or 'DB_HOST', 'DB_USERNAME', 'DB_PASS' and 'DB_NAME'`); logger.error(`Specify either 'DB_URL' or 'DB_HOST', 'DB_USERNAME', 'DB_PASS' and 'DB_NAME'`);
throw 'Missing environment variables'; throw new Error('Missing environment variables');
} }
// mongodb://username:password@hostname:port/dbname // mongodb://username:password@hostname:port/dbname
let dburl = 'mongodb://'; let dburl = 'mongodb://';
if (env['DB_USERNAME']) dburl += env['DB_USERNAME']; if (env['DB_USERNAME']) dburl += env['DB_USERNAME'];
if (env['DB_PASS']) dburl += `:${env['DB_PASS']}`; if (env['DB_PASS']) dburl += `:${env['DB_PASS']}`;
dburl += `${process.env['DB_USERNAME'] ? '@' : ''}${env['DB_HOST']}`; // DB_HOST is assumed to contain the port dburl += `${env['DB_USERNAME'] ? '@' : ''}${env['DB_HOST']}`; // DB_HOST is assumed to contain the port
dburl += `/${env['DB_NAME'] ?? 'automod'}`; dburl += `/${env['DB_NAME'] ?? 'automod'}`;
return dburl; return dburl;
} }

View file

@ -1,19 +1,33 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { FindOneResult } from "monk"; import { Collection, Db } from "mongodb";
import { app, db, SESSION_LIFETIME } from ".."; import { app, SESSION_LIFETIME } from "..";
let sessionsCollection: Collection;
export function initializeSessionsMiddleware(db: Db) {
sessionsCollection = db.collection('sessions');
}
app.use('*', async (req: Request, res: Response, next: () => void) => { app.use('*', async (req: Request, res: Response, next: () => void) => {
next(); next();
const user = req.header('x-auth-user'); const user = req.header('x-auth-user');
const token = req.header('x-auth-token'); const token = req.header('x-auth-token');
if (!user || !token) return; if (!user || !token) return;
try { try {
const session: FindOneResult<any> = await db.get('sessions').findOne({ user, token, expires: { $gt: Date.now() } }); const session = await sessionsCollection.findOne({
if (session) { user,
await db.get('sessions').update({ _id: session._id }, { $set: { expires: Date.now() + SESSION_LIFETIME } }); token,
} expires: { $gt: new Date() }
} catch(e) { console.error(e) } });
if (session) {
await sessionsCollection.updateOne(
{ _id: session._id },
{ $set: { expires: new Date(Date.now() + SESSION_LIFETIME) } }
);
}
} catch(e) {
console.error(e);
}
}); });

View file

@ -2,9 +2,15 @@ import { app, db } from '../..';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth, unauthorized } from '../../utils'; import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth, unauthorized } from '../../utils';
import { botReq } from '../internal/ws'; import { botReq } from '../internal/ws';
import { FindOneResult } from 'monk'; import { Collection, Db, ObjectId } from 'mongodb';
import { ulid } from 'ulid'; import { ulid } from 'ulid';
let serversCollection: Collection;
export function initializeAutomodAPI(database: Db) {
serversCollection = database.collection('servers');
}
type AntispamRule = { type AntispamRule = {
id: string; id: string;
max_msg: number; max_msg: number;
@ -31,11 +37,11 @@ app.get('/dash/server/:server/automod',requireAuth({ permission: 2 }) , async (r
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.`); if (permissionLevel < 1) return unauthorized(res, `Only moderators and bot managers may view this.`);
const serverConfig: FindOneResult<any> = await db.get('servers').findOne({ id: server }); const serverConfig = await serversCollection.findOne({ id: server });
const result = { const result = {
antispam: (serverConfig?.automodSettings?.spam as AntispamRule[]|undefined) antispam: (serverConfig?.automodSettings?.spam as AntispamRule[]|undefined)
?.map(r => ({ // Removing unwanted fields from response ?.map(r => ({
action: r.action, action: r.action,
channels: r.channels, channels: r.channels,
id: r.id, id: r.id,
@ -57,28 +63,29 @@ app.patch('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 })
const body = req.body; const body = req.body;
if (!server || !ruleid) return badRequest(res); if (!server || !ruleid) return badRequest(res);
const serverConfig: FindOneResult<any> = await db.get('servers').findOne({ id: server }); 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); const rule = antiSpamRules.find(r => r.id == ruleid);
if (!rule) return res.status(404).send({ error: 'No rule with this ID could be found.' }); if (!rule) return res.status(404).send({ error: 'No rule with this ID could be found.' });
await db.get('servers').update({ const result = await serversCollection.updateOne(
id: server { id: server, "automodSettings.spam.id": ruleid },
}, { {
$set: { $set: {
"automodSettings.spam.$[rulefilter]": { "automodSettings.spam.$": {
...rule, ...rule,
action: Number(body.action ?? rule.action), action: Number(body.action ?? rule.action),
channels: body.channels ?? rule.channels, channels: body.channels ?? rule.channels,
message: body.message ?? rule.message, message: body.message ?? rule.message,
max_msg: body.max_msg ?? rule.max_msg, max_msg: body.max_msg ?? rule.max_msg,
timeframe: body.timeframe ?? rule.timeframe, timeframe: body.timeframe ?? rule.timeframe,
} as AntispamRule
} }
}, { arrayFilters: [ { "rulefilter.id": ruleid } ] }); }
}
);
return res.send({ success: true }); return res.send({ success: result.modifiedCount > 0 });
}); });
app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (req, res) => { app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (req, res) => {
@ -109,9 +116,9 @@ app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (
const id = ulid(); const id = ulid();
await db.get('servers').update({ const result = await serversCollection.updateOne(
id: server, { id: server },
}, { {
$push: { $push: {
"automodSettings.spam": { "automodSettings.spam": {
id: id, id: id,
@ -121,9 +128,10 @@ app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (
message: rule.message ?? null, message: rule.message ?? null,
} }
} }
}); }
);
res.status(200).send({ success: true, id: id }); res.status(200).send({ success: result.modifiedCount > 0, id: id });
}); });
app.delete('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 }), async (req, res) => { app.delete('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 }), async (req, res) => {
@ -140,22 +148,22 @@ app.delete('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 }
if (!response.server) return res.status(404).send({ error: 'Server not found' }); if (!response.server) return res.status(404).send({ error: 'Server not found' });
// todo: fix this shit idk if it works let result;
let queryRes;
try { try {
queryRes = await db.get('servers').update({ result = await serversCollection.updateOne(
id: server { id: server },
}, { {
$pull: { $pull: {
"automodSettings.spam": { id: ruleid } "automodSettings.spam": { id: ruleid }
} }
}); }
);
} catch(e) { } catch(e) {
console.error(e); console.error(e);
res.status(500).send({ error: e }); res.status(500).send({ error: e });
return; return;
} }
if (queryRes.nModified > 0) res.status(200).send({ success: true }); if (result.modifiedCount > 0) res.status(200).send({ success: true });
else res.status(404).send({ success: false, error: 'Rule not found' }); else res.status(404).send({ success: false, error: 'Rule not found' });
}); });

View file

@ -2,14 +2,22 @@ import crypto from 'crypto';
import { app, SESSION_LIFETIME } from '..'; import { app, SESSION_LIFETIME } from '..';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { botReq } from './internal/ws'; import { botReq } from './internal/ws';
import { db } from '..'; import { Collection, Db } from 'mongodb';
import { FindOneResult } from 'monk';
import { badRequest, isAuthenticated, requireAuth } from '../utils'; import { badRequest, isAuthenticated, requireAuth } from '../utils';
import { RateLimiter } from '../middlewares/ratelimit'; import { RateLimiter } from '../middlewares/ratelimit';
let pendingLoginsCollection: Collection;
let sessionsCollection: Collection;
export function initializeAuthAPI(database: Db) {
pendingLoginsCollection = database.collection('pending_logins');
sessionsCollection = database.collection('sessions');
}
class BeginReqBody { class BeginReqBody {
user: string; user: string;
} }
class CompleteReqBody { class CompleteReqBody {
user: string; user: string;
nonce: string; nonce: string;
@ -24,14 +32,10 @@ app.post('/login/begin',
requireAuth({ noAuthOnly: true }), requireAuth({ noAuthOnly: true }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
if (typeof await isAuthenticated(req) == 'string') return res.status(403).send({ error: 'You are already authenticated' }); if (typeof await isAuthenticated(req) == 'string') return res.status(403).send({ error: 'You are already authenticated' });
const body = req.body as BeginReqBody; const body = req.body as BeginReqBody;
if (!body.user || typeof body.user != 'string') return badRequest(res); if (!body.user || typeof body.user != 'string') return badRequest(res);
const r = await botReq('requestLogin', { user: body.user.toLowerCase() }); 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)); 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 });
}); });
@ -44,7 +48,7 @@ app.post('/login/complete',
(!body.nonce || typeof body.nonce != 'string') || (!body.nonce || typeof body.nonce != 'string') ||
(!body.code || typeof body.code != 'string')) return badRequest(res); (!body.code || typeof body.code != 'string')) return badRequest(res);
const loginAttempt: FindOneResult<any> = await db.get('pending_logins').findOne({ const loginAttempt = await pendingLoginsCollection.findOne({
code: body.code, code: body.code,
user: body.user, user: body.user,
nonce: body.nonce, nonce: body.nonce,
@ -53,23 +57,24 @@ app.post('/login/complete',
}); });
if (!loginAttempt) return res.status(404).send({ error: 'The provided login info could not be found.' }); 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." }); return res.status(400).send({ error: "This code is not yet valid." });
} }
const sessionToken = crypto.randomBytes(48).toString('base64').replace(/=/g, ''); const sessionToken = crypto.randomBytes(48).toString('base64').replace(/=/g, '');
await Promise.all([ await Promise.all([
db.get('sessions').insert({ sessionsCollection.insertOne({
user: body.user.toUpperCase(), user: body.user.toUpperCase(),
token: sessionToken, token: sessionToken,
nonce: body.nonce, nonce: body.nonce,
invalid: false, invalid: false,
expires: Date.now() + SESSION_LIFETIME, expires: Date.now() + SESSION_LIFETIME,
}), }),
db.get('pending_logins').update({ _id: loginAttempt._id }, { $set: { exchanged: true } }), pendingLoginsCollection.updateOne(
{ _id: loginAttempt._id },
{ $set: { exchanged: true } }
),
]); ]);
res.status(200).send({ success: true, user: body.user.toUpperCase(), token: sessionToken }); res.status(200).send({ success: true, user: body.user.toUpperCase(), token: sessionToken });

View file

@ -1,8 +1,13 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { FindOneResult } from "monk"; import { Collection, Db } from "mongodb";
import { db } from ".";
import { botReq } from "./routes/internal/ws"; import { botReq } from "./routes/internal/ws";
let sessionsCollection: Collection;
export function initializeSessionAuthentication(db: Db) {
sessionsCollection = db.collection('sessions');
}
class Session { class Session {
user: string; user: string;
token: string; token: string;
@ -19,9 +24,7 @@ class Session {
async function isAuthenticated(req: Request, res?: Response, send401?: boolean): Promise<string|false> { async function isAuthenticated(req: Request, res?: Response, send401?: boolean): Promise<string|false> {
const user = req.header('x-auth-user'); const user = req.header('x-auth-user');
const token = req.header('x-auth-token'); const token = req.header('x-auth-token');
if (!user || !token) return false; if (!user || !token) return false;
const info = await getSessionInfo(user, token); const info = await getSessionInfo(user, token);
if (res && send401 && !info.valid) { if (res && send401 && !info.valid) {
res.status(401).send({ error: 'Unauthorized' }); res.status(401).send({ error: 'Unauthorized' });
@ -32,9 +35,13 @@ async function isAuthenticated(req: Request, res?: Response, send401?: boolean):
type SessionInfo = { exists: boolean, valid: boolean, nonce?: string } type SessionInfo = { exists: boolean, valid: boolean, nonce?: string }
async function getSessionInfo(user: string, token: string): Promise<SessionInfo> { async function getSessionInfo(user: string, token: string): Promise<SessionInfo> {
const session: FindOneResult<Session> = await db.get('sessions').findOne({ user, token }); const session = await sessionsCollection.findOne<Session>({ user, token });
return { exists: !!session, valid: !!(session && !session.invalid && session.expires > Date.now()), nonce: session?.nonce } return {
exists: !!session,
valid: !!(session && !session.invalid && session.expires > Date.now()),
nonce: session?.nonce
}
} }
function badRequest(res: Response, infoText?: string) { function badRequest(res: Response, infoText?: string) {