De monk-ify
This commit is contained in:
parent
bfd2ea2094
commit
8d9bbc1893
7 changed files with 125 additions and 82 deletions
BIN
api/bun.lockb
BIN
api/bun.lockb
Binary file not shown.
|
@ -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"
|
||||||
|
|
|
@ -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> {
|
||||||
return 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 });
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
expires: { $gt: new Date() }
|
||||||
|
});
|
||||||
|
|
||||||
if (session) {
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -14,7 +20,7 @@ type AntispamRule = {
|
||||||
message: string | null;
|
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);
|
const user = await isAuthenticated(req, res, true);
|
||||||
if (!user) return;
|
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;
|
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,21 +116,22 @@ 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,
|
||||||
max_msg: rule.max_msg ?? 5,
|
max_msg: rule.max_msg ?? 5,
|
||||||
timeframe: rule.timeframe ?? 3,
|
timeframe: rule.timeframe ?? 3,
|
||||||
action: rule.action ?? 0,
|
action: rule.action ?? 0,
|
||||||
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' });
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,8 +47,8 @@ app.post('/login/complete',
|
||||||
if ((!body.user || typeof body.user != 'string') ||
|
if ((!body.user || typeof body.user != 'string') ||
|
||||||
(!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,
|
||||||
|
@ -52,24 +56,25 @@ app.post('/login/complete',
|
||||||
invalid: false,
|
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) {
|
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 });
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue