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": {
"@types/express": "^4.17.21",
"@types/monk": "^6.0.0",
"@types/ws": "^8.5.11",
"automod": "^0.1.0",
"dotenv": "^14.3.2",
"express": "^4.19.2",
"log75": "^2.2.0",
"monk": "^7.3.4",
"mongodb": "^6.8.0",
"redis": "^4.6.15",
"ulid": "^2.3.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 { logger } from '.';
export default (): IMonkManager => {
let dburl = getDBUrl();
let db = Monk(dburl);
let db: Db;
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;
};
} catch (error) {
logger.error('Failed to connect to MongoDB', error);
throw error;
}
}
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
function getDBUrl() {
let env = process.env;
function getDBUrl(): string {
const env = process.env;
if (env['DB_URL']) return env['DB_URL'];
if (!env['DB_HOST']) {
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'`);
throw 'Missing environment variables';
throw new Error('Missing environment variables');
}
// mongodb://username:password@hostname:port/dbname
let dburl = 'mongodb://';
if (env['DB_USERNAME']) dburl += env['DB_USERNAME'];
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'}`;
return dburl;
}

View file

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

View file

@ -2,9 +2,15 @@ import { app, db } from '../..';
import { Request, Response } from 'express';
import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth, unauthorized } from '../../utils';
import { botReq } from '../internal/ws';
import { FindOneResult } from 'monk';
import { Collection, Db, ObjectId } from 'mongodb';
import { ulid } from 'ulid';
let serversCollection: Collection;
export function initializeAutomodAPI(database: Db) {
serversCollection = database.collection('servers');
}
type AntispamRule = {
id: string;
max_msg: number;
@ -14,7 +20,7 @@ type AntispamRule = {
message: string | null;
}
app.get('/dash/server/:server/automod',requireAuth({ permission: 2 }) , async (req: Request, res: Response) => {
app.get('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (req: Request, res: Response) => {
const user = await isAuthenticated(req, res, true);
if (!user) return;
@ -31,11 +37,11 @@ app.get('/dash/server/:server/automod',requireAuth({ permission: 2 }) , async (r
const permissionLevel: 0|1|2|3 = response.perms;
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 = {
antispam: (serverConfig?.automodSettings?.spam as AntispamRule[]|undefined)
?.map(r => ({ // Removing unwanted fields from response
?.map(r => ({
action: r.action,
channels: r.channels,
id: r.id,
@ -57,28 +63,29 @@ app.patch('/dash/server/:server/automod/:ruleid', requireAuth({ permission: 2 })
const body = req.body;
if (!server || !ruleid) return badRequest(res);
const serverConfig: FindOneResult<any> = await db.get('servers').findOne({ id: server });
const antiSpamRules: AntispamRule[] = serverConfig.automodSettings?.spam ?? [];
const serverConfig = await serversCollection.findOne({ id: server });
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.' });
await db.get('servers').update({
id: server
}, {
const result = await serversCollection.updateOne(
{ id: server, "automodSettings.spam.id": ruleid },
{
$set: {
"automodSettings.spam.$[rulefilter]": {
"automodSettings.spam.$": {
...rule,
action: Number(body.action ?? rule.action),
channels: body.channels ?? rule.channels,
message: body.message ?? rule.message,
max_msg: body.max_msg ?? rule.max_msg,
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) => {
@ -109,9 +116,9 @@ app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (
const id = ulid();
await db.get('servers').update({
id: server,
}, {
const result = await serversCollection.updateOne(
{ id: server },
{
$push: {
"automodSettings.spam": {
id: id,
@ -121,9 +128,10 @@ app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async (
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) => {
@ -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' });
// todo: fix this shit idk if it works
let queryRes;
let result;
try {
queryRes = await db.get('servers').update({
id: server
}, {
result = await serversCollection.updateOne(
{ id: server },
{
$pull: {
"automodSettings.spam": { id: ruleid }
}
});
}
);
} catch(e) {
console.error(e);
res.status(500).send({ error: e });
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' });
});

View file

@ -2,14 +2,22 @@ import crypto from 'crypto';
import { app, SESSION_LIFETIME } from '..';
import { Request, Response } from 'express';
import { botReq } from './internal/ws';
import { db } from '..';
import { FindOneResult } from 'monk';
import { Collection, Db } from 'mongodb';
import { badRequest, isAuthenticated, requireAuth } from '../utils';
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 {
user: string;
}
class CompleteReqBody {
user: string;
nonce: string;
@ -24,14 +32,10 @@ app.post('/login/begin',
requireAuth({ noAuthOnly: true }),
async (req: Request, res: Response) => {
if (typeof await isAuthenticated(req) == 'string') return res.status(403).send({ error: 'You are already authenticated' });
const body = req.body as BeginReqBody;
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 });
});
@ -44,7 +48,7 @@ app.post('/login/complete',
(!body.nonce || typeof body.nonce != 'string') ||
(!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,
user: body.user,
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.confirmed) {
return res.status(400).send({ error: "This code is not yet valid." });
}
const sessionToken = crypto.randomBytes(48).toString('base64').replace(/=/g, '');
await Promise.all([
db.get('sessions').insert({
sessionsCollection.insertOne({
user: body.user.toUpperCase(),
token: sessionToken,
nonce: body.nonce,
invalid: false,
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 });

View file

@ -1,8 +1,13 @@
import { Request, Response } from "express";
import { FindOneResult } from "monk";
import { db } from ".";
import { Collection, Db } from "mongodb";
import { botReq } from "./routes/internal/ws";
let sessionsCollection: Collection;
export function initializeSessionAuthentication(db: Db) {
sessionsCollection = db.collection('sessions');
}
class Session {
user: string;
token: string;
@ -19,9 +24,7 @@ class Session {
async function isAuthenticated(req: Request, res?: Response, send401?: boolean): Promise<string|false> {
const user = req.header('x-auth-user');
const token = req.header('x-auth-token');
if (!user || !token) return false;
const info = await getSessionInfo(user, token);
if (res && send401 && !info.valid) {
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 }
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) {