summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json50
-rw-r--r--package.json2
-rw-r--r--src/util/Config.ts307
3 files changed, 235 insertions, 124 deletions
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");