Merge branch 'master' of https://gitlab.libraryofcode.us/engineering/cloudservices-rewrite
commit
0dbb6c698c
|
@ -36,6 +36,8 @@
|
||||||
"camelcase": "off",
|
"camelcase": "off",
|
||||||
"indent": "warn",
|
"indent": "warn",
|
||||||
"object-curly-newline": "off",
|
"object-curly-newline": "off",
|
||||||
"import/prefer-default-export": "off"
|
"import/prefer-default-export": "off",
|
||||||
|
"no-useless-constructor": "off",
|
||||||
|
"@typescript-eslint/no-useless-constructor": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,6 +18,7 @@
|
||||||
"eris-pagination": "bsian03/eris-pagination",
|
"eris-pagination": "bsian03/eris-pagination",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
|
"helmet": "^3.21.2",
|
||||||
"ioredis": "^4.14.1",
|
"ioredis": "^4.14.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"moment-precise-range-plugin": "^1.3.0",
|
"moment-precise-range-plugin": "^1.3.0",
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.2",
|
"@types/express": "^4.17.2",
|
||||||
"@types/fs-extra": "^8.0.0",
|
"@types/fs-extra": "^8.0.0",
|
||||||
|
"@types/helmet": "^0.0.45",
|
||||||
"@types/ioredis": "^4.0.18",
|
"@types/ioredis": "^4.0.18",
|
||||||
"@types/mongoose": "^5.5.20",
|
"@types/mongoose": "^5.5.20",
|
||||||
"@types/nodemailer": "^6.2.1",
|
"@types/nodemailer": "^6.2.1",
|
||||||
|
|
|
@ -38,6 +38,10 @@ export default class Security {
|
||||||
return `${salt}:${encrypted}`;
|
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<null | AccountInterface> {
|
public async checkBearer(bearer: string): Promise<null | AccountInterface> {
|
||||||
const decipher = crypto.createDecipheriv('aes-256-gcm', this.keys.key, this.keys.iv);
|
const decipher = crypto.createDecipheriv('aes-256-gcm', this.keys.key, this.keys.iv);
|
||||||
try {
|
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 {
|
public extractBearer(req: Request): string {
|
||||||
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
|
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
|
||||||
return req.headers.authorization.split(' ')[1];
|
return req.headers.authorization.split(' ')[1];
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
/* eslint-disable no-useless-return */
|
/* eslint-disable no-useless-return */
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
|
import helmet from 'helmet';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { Client } from '..';
|
import { Client } from '..';
|
||||||
import { Security } from '.';
|
import { Security } from '.';
|
||||||
import { Route } from '../class';
|
import { Collection, Route } from '../class';
|
||||||
|
|
||||||
export default class Server {
|
export default class Server {
|
||||||
public routes: Map<string, Route>;
|
public routes: Collection<Route>
|
||||||
|
|
||||||
public client: Client;
|
public client: Client;
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ export default class Server {
|
||||||
|
|
||||||
constructor(client: Client, options?: { port: number }) {
|
constructor(client: Client, options?: { port: number }) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.routes = new Map();
|
this.routes = new Collection();
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.security = new Security(this.client);
|
this.security = new Security(this.client);
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
@ -34,7 +35,13 @@ export default class Server {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const route = new (require(`${__dirname}/routes/${routeFile}`).default)(this);
|
const route = new (require(`${__dirname}/routes/${routeFile}`).default)(this);
|
||||||
|
if (route.conf.deprecated === true) {
|
||||||
|
route.deprecated();
|
||||||
|
} else if (route.conf.maintenance === true) {
|
||||||
|
route.maintenance();
|
||||||
|
} else {
|
||||||
route.bind();
|
route.bind();
|
||||||
|
}
|
||||||
this.routes.set(route.conf.path, route);
|
this.routes.set(route.conf.path, route);
|
||||||
this.app.use(route.conf.path, route.router);
|
this.app.use(route.conf.path, route.router);
|
||||||
this.client.signale.success(`Successfully loaded route ${route.conf.path}`);
|
this.client.signale.success(`Successfully loaded route ${route.conf.path}`);
|
||||||
|
@ -45,6 +52,16 @@ export default class Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
private connect(): void {
|
private connect(): void {
|
||||||
|
this.app.set('trust proxy', 'loopback');
|
||||||
|
this.app.use(helmet({
|
||||||
|
hsts: false,
|
||||||
|
hidePoweredBy: false,
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
this.app.use(bodyParser.json());
|
this.app.use(bodyParser.json());
|
||||||
this.app.listen(this.options.port, () => {
|
this.app.listen(this.options.port, () => {
|
||||||
this.client.signale.success(`API Server listening on port ${this.options.port}`);
|
this.client.signale.success(`API Server listening on port ${this.options.port}`);
|
||||||
|
|
|
@ -10,13 +10,11 @@ export default class Account extends Route {
|
||||||
|
|
||||||
public bind() {
|
public bind() {
|
||||||
this.router.use(async (req, res, next) => {
|
this.router.use(async (req, res, next) => {
|
||||||
const account = await this.server.security.checkBearer(this.server.security.extractBearer(req));
|
await this.authorize(req, res, next);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.get('/', async (req: Req, res) => {
|
this.router.get('/', async (req: Req, res) => {
|
||||||
|
try {
|
||||||
const acc: any = {};
|
const acc: any = {};
|
||||||
acc.username = req.account.username;
|
acc.username = req.account.username;
|
||||||
acc.userID = req.account.userID;
|
acc.userID = req.account.userID;
|
||||||
|
@ -27,9 +25,13 @@ export default class Account extends Route {
|
||||||
acc.createdBy = req.account.createdBy;
|
acc.createdBy = req.account.createdBy;
|
||||||
acc.permissions = req.account.permissions;
|
acc.permissions = req.account.permissions;
|
||||||
res.status(200).json({ code: this.constants.codes.SUCCESS, message: acc });
|
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) => {
|
this.router.get('/moderations/:id?', async (req: Req, res) => {
|
||||||
|
try {
|
||||||
const moderations = await this.server.client.db.Moderation.find({ username: req.account.username });
|
const moderations = await this.server.client.db.Moderation.find({ username: req.account.username });
|
||||||
if (!moderations.length) res.sendStatus(204);
|
if (!moderations.length) res.sendStatus(204);
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
|
@ -38,6 +40,18 @@ export default class Account extends Route {
|
||||||
} else {
|
} else {
|
||||||
res.status(200).json({ code: this.constants.codes.SUCCESS, message: moderations });
|
res.status(200).json({ code: this.constants.codes.SUCCESS, message: moderations });
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router.get('/storage', async (req: Req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await this.server.client.redis.get(`storage-${req.account.username}`) ? Number(await this.server.client.redis.get(`storage-${req.account.username}`)) : null;
|
||||||
|
res.status(200).json({ code: this.constants.codes.SUCCESS, message: data });
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error, res);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* eslint-disable consistent-return */
|
||||||
|
import { Server } from '..';
|
||||||
|
import { Route } from '../../class';
|
||||||
|
|
||||||
|
export default class FileSystem extends Route {
|
||||||
|
constructor(server: Server) {
|
||||||
|
super(server, { path: '/fs', deprecated: false, maintenance: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public bind() {
|
||||||
|
this.router.use(async (req, res, next) => {
|
||||||
|
await this.authorize(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.router.get('/:');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import os from 'os';
|
||||||
|
import { Server } from '..';
|
||||||
|
import { Route } from '../../class';
|
||||||
|
|
||||||
|
export default class Root extends Route {
|
||||||
|
constructor(server: Server) {
|
||||||
|
super(server, { path: '/', deprecated: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
public bind() {
|
||||||
|
this.router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const date = new Date();
|
||||||
|
date.setSeconds(-process.uptime());
|
||||||
|
const accounts = await this.server.client.db.Account.find();
|
||||||
|
const administrators = accounts.filter((account) => account.root === true).length;
|
||||||
|
const response = {
|
||||||
|
nodeVersion: process.version,
|
||||||
|
uptime: process.uptime(),
|
||||||
|
server: {
|
||||||
|
users: accounts.length,
|
||||||
|
administrators,
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
uptime: os.uptime(),
|
||||||
|
loadAverage: os.loadavg(),
|
||||||
|
cpuModel: os.cpus()[0].model,
|
||||||
|
cpuClock: os.cpus()[0].speed / 1000,
|
||||||
|
cpuCores: os.cpus().length,
|
||||||
|
hostname: os.hostname(),
|
||||||
|
ipv4: os.networkInterfaces().eth0.filter((r) => r.family === 'IPv4')[0].address,
|
||||||
|
ipv6: os.networkInterfaces().eth0.filter((r) => r.family === 'IPv6')[0].address,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(200).json({ code: this.constants.codes.SUCCESS, message: response });
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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';
|
import { Server } from '../api';
|
||||||
|
|
||||||
export default class Route {
|
export default class Route {
|
||||||
|
@ -8,7 +9,7 @@ export default class Route {
|
||||||
|
|
||||||
public conf: { path: string, deprecated?: boolean };
|
public conf: { path: string, deprecated?: boolean };
|
||||||
|
|
||||||
constructor(server: Server, conf: { path: string, deprecated?: boolean }) {
|
constructor(server: Server, conf: { path: string, deprecated?: boolean, maintenance?: boolean }) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.router = router();
|
this.router = router();
|
||||||
this.conf = conf;
|
this.conf = conf;
|
||||||
|
@ -16,6 +17,47 @@ export default class Route {
|
||||||
|
|
||||||
public bind() {}
|
public bind() {}
|
||||||
|
|
||||||
|
public deprecated(): void {
|
||||||
|
this.router.all('*', (_req, res) => {
|
||||||
|
res.status(501).json({ code: this.constants.codes.DEPRECATED, message: this.constants.messages.DEPRECATED });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public maintenance(): void {
|
||||||
|
this.router.all('*', (_req, res) => {
|
||||||
|
res.status(503).json({ code: this.constants.codes.MAINTENANCE_OR_UNAVAILABLE, message: this.constants.messages.MAINTENANCE_OR_UNAVAILABLE });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function checks for the presense of a Bearer token with Security.extractBearer(),
|
||||||
|
* then it will attempt to validate it with Security.checkBearer().
|
||||||
|
* If it can authenticate the request, it'll add a custom property on Request called
|
||||||
|
* `account`, which will hold an the bearer token's account owner. The account is of the
|
||||||
|
* type `AccountInterface`.
|
||||||
|
* @param req The Request object from Express.
|
||||||
|
* @param res The Response object from Express.
|
||||||
|
* @param next The NextFunction from Express.
|
||||||
|
* @example Security.authorize(req, res, next);
|
||||||
|
*/
|
||||||
|
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: this.constants.messages.UNAUTHORIZED });
|
||||||
|
Object.defineProperty(req, 'account', { value: account, writable: true, enumerable: true, configurable: true });
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function calls Util.handleError() internally, however it also sends a generic
|
||||||
|
* response to the user.
|
||||||
|
* @param error The Error object.
|
||||||
|
* @param res The Response object from Express.
|
||||||
|
*/
|
||||||
|
public handleError(error: Error, res: Response): void {
|
||||||
|
this.server.client.util.handleError(error);
|
||||||
|
res.status(500).json({ code: this.constants.codes.SERVER_ERROR, message: this.constants.messages.SERVER_ERROR });
|
||||||
|
}
|
||||||
|
|
||||||
get constants() {
|
get constants() {
|
||||||
return {
|
return {
|
||||||
codes: {
|
codes: {
|
||||||
|
@ -26,7 +68,16 @@ export default class Route {
|
||||||
ACCOUNT_NOT_FOUND: 1041,
|
ACCOUNT_NOT_FOUND: 1041,
|
||||||
CLIENT_ERROR: 1044,
|
CLIENT_ERROR: 1044,
|
||||||
SERVER_ERROR: 105,
|
SERVER_ERROR: 105,
|
||||||
UNKNOWN_SERVER_ERROR: 1051,
|
DEPRECATED: 1051,
|
||||||
|
MAINTENANCE_OR_UNAVAILABLE: 1053,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
UNAUTHORIZED: ['CREDENTIALS_INVALID', 'The credentials you supplied are invalid.'],
|
||||||
|
PERMISSION_DENIED: ['PERMISSION_DENIED', 'You do not have valid credentials to access this resource.'],
|
||||||
|
NOT_FOUND: ['NOT_FOUND', 'The resource you requested cannot be located.'],
|
||||||
|
SERVER_ERROR: ['INTERNAL_ERROR', 'An internal error has occurred, Engineers have been notified.'],
|
||||||
|
DEPRECATED: ['ENDPOINT_OR_RESOURCE_DEPRECATED', 'The endpoint or resource you\'re trying to access has been deprecated.'],
|
||||||
|
MAINTENANCE_OR_UNAVAILABLE: ['SERVICE_UNAVAILABLE', 'The endpoint or resource you\'re trying to access is either in maintenance or is not available.'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,8 +43,6 @@ export default class Util {
|
||||||
*/
|
*/
|
||||||
public resolveCommand(command: string, args?: string[], message?: Message): Promise<{cmd: Command, args: string[] }> {
|
public resolveCommand(command: string, args?: string[], message?: Message): Promise<{cmd: Command, args: string[] }> {
|
||||||
try {
|
try {
|
||||||
this.client.signale.info(command);
|
|
||||||
this.client.signale.info(args);
|
|
||||||
let resolvedCommand: Command;
|
let resolvedCommand: Command;
|
||||||
|
|
||||||
if (this.client.commands.has(command)) resolvedCommand = this.client.commands.get(command);
|
if (this.client.commands.has(command)) resolvedCommand = this.client.commands.get(command);
|
||||||
|
@ -117,7 +115,7 @@ export default class Util {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public splitFields(fields: {name: string, value: string, inline?: boolean}[]): {name: string, value: string, inline?: boolean}[][] {
|
public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const array: {name: string, value: string, inline?: boolean}[][] = [[]];
|
const array: {name: string, value: string, inline?: boolean}[][] = [[]];
|
||||||
while (fields.length) {
|
while (fields.length) {
|
||||||
|
@ -145,7 +143,7 @@ export default class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async createHash(password: string) {
|
public async createHash(password: string): Promise<string> {
|
||||||
const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`);
|
const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`);
|
||||||
return hashed;
|
return hashed;
|
||||||
}
|
}
|
||||||
|
@ -225,7 +223,7 @@ export default class Util {
|
||||||
const expiration = { date, processed };
|
const expiration = { date, processed };
|
||||||
|
|
||||||
logInput.expiration = expiration;
|
logInput.expiration = expiration;
|
||||||
const log = await new this.client.db.Moderation(logInput);
|
const log = new this.client.db.Moderation(logInput);
|
||||||
await log.save();
|
await log.save();
|
||||||
|
|
||||||
let embedTitle: string;
|
let embedTitle: string;
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default class CWG_Create extends Command {
|
||||||
<b>Port:</b> ${domain.port}<br>
|
<b>Port:</b> ${domain.port}<br>
|
||||||
<b>Certificate Issuer:</b> ${cert.issuer.organizationName}<br>
|
<b>Certificate Issuer:</b> ${cert.issuer.organizationName}<br>
|
||||||
<b>Certificate Subject:</b> ${cert.subject.commonName}<br>
|
<b>Certificate Subject:</b> ${cert.subject.commonName}<br>
|
||||||
<b>Responsible Engineer:</b> ${message.author.username}#${message.author.discriminator}<br>
|
<b>Responsible Engineer:</b> ${message.author.username}#${message.author.discriminator}<br><br>
|
||||||
|
|
||||||
If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.<br>
|
If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.<br>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue