diff options
-rw-r--r-- | package-lock.json | 50 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/util/Config.ts | 307 |
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"); |