diff --git a/package.json b/package.json index a6f520d..ad00d84 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "eris-pagination": "bsian03/eris-pagination", "express": "^4.17.1", "fs-extra": "^8.1.0", + "helmet": "^3.21.2", "ioredis": "^4.14.1", "moment": "^2.24.0", "moment-precise-range-plugin": "^1.3.0", @@ -29,6 +30,7 @@ "devDependencies": { "@types/express": "^4.17.2", "@types/fs-extra": "^8.0.0", + "@types/helmet": "^0.0.45", "@types/ioredis": "^4.0.18", "@types/mongoose": "^5.5.20", "@types/nodemailer": "^6.2.1", diff --git a/src/api/Security.ts b/src/api/Security.ts index e5ec505..bd478a5 100644 --- a/src/api/Security.ts +++ b/src/api/Security.ts @@ -38,6 +38,10 @@ export default class Security { return `${salt}:${encrypted}`; } + /** + * If the bearer token is valid, will return the Account, else will return null. + * @param bearer The bearer token provided. + */ public async checkBearer(bearer: string): Promise { const decipher = crypto.createDecipheriv('aes-256-gcm', this.keys.key, this.keys.iv); try { @@ -57,6 +61,10 @@ export default class Security { } } + /** + * Returns the Bearer token, searches in headers and query. + * @param req The Request object from Express. + */ public extractBearer(req: Request): string { if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { return req.headers.authorization.split(' ')[1]; diff --git a/src/api/Server.ts b/src/api/Server.ts index bc36325..81c5055 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -1,6 +1,7 @@ /* eslint-disable no-useless-return */ import express from 'express'; import bodyParser from 'body-parser'; +import helmet from 'helmet'; import fs from 'fs-extra'; import { Client } from '..'; import { Security } from '.'; @@ -34,7 +35,11 @@ export default class Server { try { // eslint-disable-next-line new-cap const route = new (require(`${__dirname}/routes/${routeFile}`).default)(this); - route.bind(); + if (route.conf.deprecated === true) { + route.deprecated(); + } else { + route.bind(); + } this.routes.set(route.conf.path, route); this.app.use(route.conf.path, route.router); this.client.signale.success(`Successfully loaded route ${route.conf.path}`); @@ -45,6 +50,12 @@ export default class Server { } private connect(): void { + this.app.set('trust proxy', 'loopback'); + this.app.use(helmet({ + hsts: false, + hidePoweredBy: false, + contentSecurityPolicy: true, + })); this.app.use(bodyParser.json()); this.app.listen(this.options.port, () => { this.client.signale.success(`API Server listening on port ${this.options.port}`); diff --git a/src/api/routes/Account.ts b/src/api/routes/Account.ts index 284f3d4..6240f3d 100644 --- a/src/api/routes/Account.ts +++ b/src/api/routes/Account.ts @@ -10,33 +10,38 @@ export default class Account extends Route { public bind() { this.router.use(async (req, res, next) => { - const account = await this.server.security.checkBearer(this.server.security.extractBearer(req)); - if (!account) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: 'BEARER_TOKEN_INVALID' }); - Object.defineProperty(req, 'account', { value: account, writable: true, enumerable: true, configurable: true }); - next(); + await this.authorize(req, res, next); }); this.router.get('/', async (req: Req, res) => { - const acc: any = {}; - acc.username = req.account.username; - acc.userID = req.account.userID; - acc.email = req.account.emailAddress; - acc.locked = req.account.locked; - acc.root = req.account.root; - acc.createdAt = req.account.createdAt; - acc.createdBy = req.account.createdBy; - acc.permissions = req.account.permissions; - res.status(200).json({ code: this.constants.codes.SUCCESS, message: acc }); + try { + const acc: any = {}; + acc.username = req.account.username; + acc.userID = req.account.userID; + acc.email = req.account.emailAddress; + acc.locked = req.account.locked; + acc.root = req.account.root; + acc.createdAt = req.account.createdAt; + acc.createdBy = req.account.createdBy; + acc.permissions = req.account.permissions; + res.status(200).json({ code: this.constants.codes.SUCCESS, message: acc }); + } catch (error) { + this.handleError(error, res); + } }); this.router.get('/moderations/:id?', async (req: Req, res) => { - const moderations = await this.server.client.db.Moderation.find({ username: req.account.username }); - if (!moderations.length) res.sendStatus(204); - if (req.params.id) { - const filtered = moderations.filter((moderation) => moderation.logID === req.params.id); - res.status(200).json({ code: this.constants.codes.SUCCESS, message: { filtered } }); - } else { - res.status(200).json({ code: this.constants.codes.SUCCESS, message: moderations }); + try { + const moderations = await this.server.client.db.Moderation.find({ username: req.account.username }); + if (!moderations.length) res.sendStatus(204); + if (req.params.id) { + const filtered = moderations.filter((moderation) => moderation.logID === req.params.id); + res.status(200).json({ code: this.constants.codes.SUCCESS, message: { filtered } }); + } else { + res.status(200).json({ code: this.constants.codes.SUCCESS, message: moderations }); + } + } catch (error) { + this.handleError(error, res); } }); } diff --git a/src/class/Route.ts b/src/class/Route.ts index 1fbc728..5488924 100644 --- a/src/class/Route.ts +++ b/src/class/Route.ts @@ -1,4 +1,5 @@ -import { Router as router } from 'express'; +/* eslint-disable consistent-return */ +import { Request, Response, NextFunction, Router as router } from 'express'; import { Server } from '../api'; export default class Route { @@ -16,6 +17,24 @@ export default class Route { public bind() {} + public deprecated() { + this.router.all('*', (_req, res) => { + res.status(501).json({ code: this.constants.codes.DEPRECATED, message: 'This endpoint is deprecated.' }); + }); + } + + public async authorize(req: Request, res: Response, next: NextFunction) { + const account = await this.server.security.checkBearer(this.server.security.extractBearer(req)); + if (!account) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: 'BEARER_TOKEN_INVALID' }); + Object.defineProperty(req, 'account', { value: account, writable: true, enumerable: true, configurable: true }); + next(); + } + + public handleError(error: Error, res: Response) { + this.server.client.util.handleError(error); + res.status(500).json({ code: this.constants.codes.SERVER_ERROR, message: 'An internal error has occurred, Engineers have been notified.' }); + } + get constants() { return { codes: { @@ -26,7 +45,7 @@ export default class Route { ACCOUNT_NOT_FOUND: 1041, CLIENT_ERROR: 1044, SERVER_ERROR: 105, - UNKNOWN_SERVER_ERROR: 1051, + DEPRECATED: 1051, }, }; }