diff --git a/api/bun.lockb b/api/bun.lockb index 33876fc..816e2d1 100755 Binary files a/api/bun.lockb and b/api/bun.lockb differ diff --git a/api/package.json b/api/package.json index 341c960..58b0d3b 100644 --- a/api/package.json +++ b/api/package.json @@ -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" diff --git a/api/src/db.ts b/api/src/db.ts index c8b38bd..0a7ff14 100644 --- a/api/src/db.ts +++ b/api/src/db.ts @@ -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); - return db; -}; +let db: Db; + +export default async function buildDBClient(): Promise { + 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; } diff --git a/api/src/middlewares/updateTokenExpiry.ts b/api/src/middlewares/updateTokenExpiry.ts index 35fe74f..6895f8d 100644 --- a/api/src/middlewares/updateTokenExpiry.ts +++ b/api/src/middlewares/updateTokenExpiry.ts @@ -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 = 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); + } }); diff --git a/api/src/routes/dash/server-automod.ts b/api/src/routes/dash/server-automod.ts index ff8bc83..acfbd66 100644 --- a/api/src/routes/dash/server-automod.ts +++ b/api/src/routes/dash/server-automod.ts @@ -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 = 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 = 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 - }, { - $set: { - "automodSettings.spam.$[rulefilter]": { - ...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 + const result = await serversCollection.updateOne( + { id: server, "automodSettings.spam.id": ruleid }, + { + $set: { + "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, + } + } } - }, { 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,21 +116,22 @@ app.post('/dash/server/:server/automod', requireAuth({ permission: 2 }), async ( const id = ulid(); - await db.get('servers').update({ - id: server, - }, { - $push: { - "automodSettings.spam": { - id: id, - max_msg: rule.max_msg ?? 5, - timeframe: rule.timeframe ?? 3, - action: rule.action ?? 0, - message: rule.message ?? null, + const result = await serversCollection.updateOne( + { id: server }, + { + $push: { + "automodSettings.spam": { + id: id, + max_msg: rule.max_msg ?? 5, + timeframe: rule.timeframe ?? 3, + action: rule.action ?? 0, + 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 - }, { - $pull: { - "automodSettings.spam": { id: ruleid } + 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' }); }); diff --git a/api/src/routes/login.ts b/api/src/routes/login.ts index ee7a68a..a943b40 100644 --- a/api/src/routes/login.ts +++ b/api/src/routes/login.ts @@ -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 }); }); @@ -43,8 +47,8 @@ app.post('/login/complete', if ((!body.user || typeof body.user != 'string') || (!body.nonce || typeof body.nonce != 'string') || (!body.code || typeof body.code != 'string')) return badRequest(res); - - const loginAttempt: FindOneResult = await db.get('pending_logins').findOne({ + + const loginAttempt = await pendingLoginsCollection.findOne({ code: body.code, user: body.user, nonce: body.nonce, @@ -52,24 +56,25 @@ app.post('/login/complete', invalid: false, }); - 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) { 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 }); diff --git a/api/src/utils.ts b/api/src/utils.ts index 1199a28..ef1a28c 100644 --- a/api/src/utils.ts +++ b/api/src/utils.ts @@ -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 { 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 { - const session: FindOneResult = await db.get('sessions').findOne({ user, token }); + const session = await sessionsCollection.findOne({ 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) {