diff --git a/package-lock.json b/package-lock.json
index 137f56b1..7cb096d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
"env-paths": "^2.2.1",
"jsonwebtoken": "^8.5.1",
"missing-native-js-functions": "^1.2.2",
- "mongodb": "^3.6.6",
+ "mongodb": "^3.6.8",
"mongoose": "^5.12.3",
"mongoose-autopopulate": "^0.12.3",
"typescript": "^4.1.3"
@@ -284,14 +284,14 @@
"integrity": "sha512-kNdwKWXh1hM8RdNqW2BIHsqD6fYN9RV27M+0uQF1pGF1yLKVc+xIv1VB8WEN1HxQ22N8Rj9sdEezOX2yBpsMZA=="
},
"node_modules/mongodb": {
- "version": "3.6.6",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.6.tgz",
- "integrity": "sha512-WlirMiuV1UPbej5JeCMqE93JRfZ/ZzqE7nJTwP85XzjAF4rRSeq2bGCb1cjfoHLOF06+HxADaPGqT0g3SbVT1w==",
+ "version": "3.6.8",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.8.tgz",
+ "integrity": "sha512-sDjJvI73WjON1vapcbyBD3Ao9/VN3TKYY8/QX9EPbs22KaCSrQ5rXo5ZZd44tWJ3wl3FlnrFZ+KyUtNH6+1ZPQ==",
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
- "optional-require": "^1.0.2",
+ "optional-require": "^1.0.3",
"safe-buffer": "^5.1.2"
},
"engines": {
@@ -299,6 +299,26 @@
},
"optionalDependencies": {
"saslprep": "^1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws4": {
+ "optional": true
+ },
+ "bson-ext": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "mongodb-extjson": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ }
}
},
"node_modules/mongoose": {
@@ -398,9 +418,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/optional-require": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.2.tgz",
- "integrity": "sha512-HZubVd6IfHsbnpdNF/ICaSAzBUEW1TievpkjY3tB4Jnk8L7+pJ3conPzUt3Mn/6OZx9uzTDOHYPGA8/AxYHBOg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
+ "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==",
"engines": {
"node": ">=4"
}
@@ -785,14 +805,14 @@
"integrity": "sha512-kNdwKWXh1hM8RdNqW2BIHsqD6fYN9RV27M+0uQF1pGF1yLKVc+xIv1VB8WEN1HxQ22N8Rj9sdEezOX2yBpsMZA=="
},
"mongodb": {
- "version": "3.6.6",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.6.tgz",
- "integrity": "sha512-WlirMiuV1UPbej5JeCMqE93JRfZ/ZzqE7nJTwP85XzjAF4rRSeq2bGCb1cjfoHLOF06+HxADaPGqT0g3SbVT1w==",
+ "version": "3.6.8",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.8.tgz",
+ "integrity": "sha512-sDjJvI73WjON1vapcbyBD3Ao9/VN3TKYY8/QX9EPbs22KaCSrQ5rXo5ZZd44tWJ3wl3FlnrFZ+KyUtNH6+1ZPQ==",
"requires": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
- "optional-require": "^1.0.2",
+ "optional-require": "^1.0.3",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
}
@@ -884,9 +904,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"optional-require": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.2.tgz",
- "integrity": "sha512-HZubVd6IfHsbnpdNF/ICaSAzBUEW1TievpkjY3tB4Jnk8L7+pJ3conPzUt3Mn/6OZx9uzTDOHYPGA8/AxYHBOg=="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz",
+ "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA=="
},
"process-nextick-args": {
"version": "2.0.1",
diff --git a/package.json b/package.json
index 7729e45f..24500399 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"env-paths": "^2.2.1",
"jsonwebtoken": "^8.5.1",
"missing-native-js-functions": "^1.2.2",
- "mongodb": "^3.6.6",
+ "mongodb": "^3.6.8",
"mongoose": "^5.12.3",
"mongoose-autopopulate": "^0.12.3",
"typescript": "^4.1.3"
diff --git a/src/util/Config.ts b/src/util/Config.ts
index 588f8dea..a8a610fa 100644
--- a/src/util/Config.ts
+++ b/src/util/Config.ts
@@ -1,113 +1,204 @@
+import { Schema, model, Types, Document } from "mongoose";
import "missing-native-js-functions";
-import envPaths from "env-paths";
-import path from "path";
-import { JSONSchemaType, ValidateFunction } from "ajv"
-import fs from 'fs'
-import dotProp from "dot-prop";
-
-interface Options<T> {
- path: string;
- schemaValidator: ValidateFunction;
- schema: JSONSchemaType<T>;
+import db, { MongooseCache } from "./Database";
+import { Snowflake } from "./Snowflake";
+import crypto from "crypto";
+
+var Config = new MongooseCache(db.collection("config"), [], { onlyEvents: false });
+
+export default {
+ init: async function init(defaultOpts: any = DefaultOptions) {
+ await Config.init();
+ return this.set(Config.data.merge(defaultOpts));
+ },
+ get: function get() {
+ return <DefaultOptions>Config.data;
+ },
+ set: function set(val: any) {
+ return db.collection("config").updateOne({}, { $set: val }, { upsert: true });
+ },
+};
+
+export interface RateLimitOptions {
+ count: number;
+ timespan: number;
}
-type Deserialize<T> = (text: string) => T;
-
-export function getConfigPathForFile(name: string, configFileName: string, extension: string): string {
- const configEnvPath = envPaths(name, { suffix: "" }).config;
- const configPath = path.resolve(configEnvPath, `${configFileName}${extension}`)
- return configPath
+export interface DefaultOptions {
+ gateway: {
+ endpoint: string;
+ };
+ general: {
+ instance_id: string;
+ };
+ permissions: {
+ user: {
+ createGuilds: boolean;
+ };
+ };
+ limits: {
+ user: {
+ maxGuilds: number;
+ maxUsername: number;
+ maxFriends: number;
+ };
+ guild: {
+ maxRoles: number;
+ maxMembers: number;
+ maxChannels: number;
+ maxChannelsInCategory: number;
+ hideOfflineMember: number;
+ };
+ message: {
+ maxCharacters: number;
+ maxTTSCharacters: number;
+ maxReactions: number;
+ maxAttachmentSize: number;
+ maxBulkDelete: number;
+ };
+ channel: {
+ maxPins: number;
+ maxTopic: number;
+ };
+ rate: {
+ ip: {
+ enabled: boolean;
+ count: number;
+ timespan: number;
+ };
+ routes: {
+ auth?: {
+ login?: RateLimitOptions;
+ register?: RateLimitOptions;
+ };
+ // TODO: rate limit configuration for all routes
+ };
+ };
+ };
+ security: {
+ jwtSecret: string;
+ forwadedFor: string | null; // header to get the real user ip address
+ captcha: {
+ enabled: boolean;
+ service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
+ sitekey: string | null;
+ secret: string | null;
+ };
+ };
+ login: {
+ requireCaptcha: boolean;
+ };
+ register: {
+ email: {
+ necessary: boolean;
+ allowlist: boolean;
+ blocklist: boolean;
+ domains: string[];
+ };
+ dateOfBirth: {
+ necessary: boolean;
+ minimum: number; // in years
+ };
+ requireCaptcha: boolean;
+ requireInvite: boolean;
+ allowNewRegistration: boolean;
+ allowMultipleAccounts: boolean;
+ password: {
+ minLength: number;
+ minNumbers: number;
+ minUpperCase: number;
+ minSymbols: number;
+ };
+ };
}
-class Store<T extends Record<string, any> = Record<string, unknown>> implements Iterable<[keyof T, T[keyof T]]>{
- readonly path: string;
- readonly validator: ValidateFunction;
- readonly schema: string;
-
- constructor(path: string, validator: ValidateFunction, schema: JSONSchemaType<T>) {
- this.validator = validator;
- if (fs.existsSync(path)) {
- this.path = path
- } else {
- this._ensureDirectory()
- }
- }
-
- private readonly _deserialize: Deserialize<T> = value => JSON.parse(value);
-
- private _ensureDirectory(): void {
- fs.mkdirSync(path.dirname(this.path), { recursive: true })
- }
-
- protected _validate(data: T | unknown): void {
- const valid = this.validator(data);
- if (valid || !this.validator.errors) {
- return;
- }
-
- const errors = this.validator.errors.map(({ instancePath, message = '' }) => `\`${instancePath.slice(1)}\` ${message}`);
- throw new Error("The configuration schema was violated!: " + errors.join('; '))
-
- }
-
- *[Symbol.iterator](): IterableIterator<[keyof T, T[keyof T]]> {
- for (const [key, value] of Object.entries(this.store)) {
- yield [key, value]
- }
- }
-
- public get store(): T {
- try {
- const data = fs.readFileSync(this.path).toString();
- const deserializedData = this._deserialize(data);
- this._validate(deserializedData);
- return Object.assign(Object.create(null), deserializedData);
- } catch (error) {
- if (error == 'ENOENT') {
- this._ensureDirectory();
- throw new Error("Critical, config store does not exist, the base directory has been created, copy the necessary config files to the directory");
- }
-
- throw error;
- }
-
- }
-}
-
-class Config<T extends Record<string, any> = Record<string, unknown>> extends Store<T> implements Iterable<[keyof T, T[keyof T]]> {
- constructor(options: Readonly<Partial<Options<T>>>) {
- super(options.path!, options.schemaValidator!, options.schema!);
-
- this._validate(this.store);
-
- }
-
- public get<Key extends keyof T>(key: Key): T[Key];
- public get<Key extends keyof T>(key: Key, defaultValue: Required<T>[Key]): Required<T>[Key];
- public get<Key extends string, Value = unknown>(key: Exclude<Key, keyof T>, defaultValue?: Value): Value;
- public get(key: string, defaultValue?: unknown): unknown {
- return this._get(key, defaultValue);
- }
-
- public getAll(): T {
- return this.store;
- }
-
- private _has<Key extends keyof T>(key: Key | string): boolean {
- return dotProp.has(this.store, key as string);
- }
-
- private _get<Key extends keyof T>(key: Key): T[Key] | undefined;
- private _get<Key extends keyof T, Default = unknown>(key: Key, defaultValue: Default): T[Key] | Default;
- private _get<Key extends keyof T, Default = unknown>(key: Key | string, defaultValue?: Default): Default | undefined {
- if (!this._has(key)) {
- throw new Error("Tried to acess a non existant property in the config");
- }
-
- return dotProp.get<T[Key] | undefined>(this.store, key as string, defaultValue as T[Key]);
- }
-
-}
-
-export default Config;
-
+export const DefaultOptions: DefaultOptions = {
+ gateway: {
+ endpoint: "ws://localhost:3001",
+ },
+ general: {
+ instance_id: Snowflake.generate(),
+ },
+ permissions: {
+ user: {
+ createGuilds: true,
+ },
+ },
+ limits: {
+ user: {
+ maxGuilds: 100,
+ maxUsername: 32,
+ maxFriends: 1000,
+ },
+ guild: {
+ maxRoles: 250,
+ maxMembers: 250000,
+ maxChannels: 500,
+ maxChannelsInCategory: 50,
+ hideOfflineMember: 1000,
+ },
+ message: {
+ maxCharacters: 2000,
+ maxTTSCharacters: 200,
+ maxReactions: 20,
+ maxAttachmentSize: 8388608,
+ maxBulkDelete: 100,
+ },
+ channel: {
+ maxPins: 50,
+ maxTopic: 1024,
+ },
+ rate: {
+ ip: {
+ enabled: true,
+ count: 1000,
+ timespan: 1000 * 60 * 10,
+ },
+ routes: {},
+ },
+ },
+ security: {
+ jwtSecret: crypto.randomBytes(256).toString("base64"),
+ forwadedFor: null,
+ // forwadedFor: "X-Forwarded-For" // nginx/reverse proxy
+ // forwadedFor: "CF-Connecting-IP" // cloudflare:
+ captcha: {
+ enabled: false,
+ service: null,
+ sitekey: null,
+ secret: null,
+ },
+ },
+ login: {
+ requireCaptcha: false,
+ },
+ register: {
+ email: {
+ necessary: true,
+ allowlist: false,
+ blocklist: true,
+ domains: [], // TODO: efficiently save domain blocklist in database
+ // domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
+ },
+ dateOfBirth: {
+ necessary: true,
+ minimum: 13,
+ },
+ requireInvite: false,
+ requireCaptcha: true,
+ allowNewRegistration: true,
+ allowMultipleAccounts: true,
+ password: {
+ minLength: 8,
+ minNumbers: 2,
+ minUpperCase: 2,
+ minSymbols: 0,
+ },
+ },
+};
+
+export const ConfigSchema = new Schema(Object);
+
+export interface DefaultOptionsDocument extends DefaultOptions, Document {}
+
+export const ConfigModel = model<DefaultOptionsDocument>("Config", ConfigSchema, "config");
|