summary refs log tree commit diff
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/assets/schemas.json63
-rw-r--r--api/package.json3
-rw-r--r--api/scripts/globalSetup.js3
-rw-r--r--api/src/routes/auth/register.ts30
-rw-r--r--api/src/routes/channels/#channel_id/invites.ts6
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts6
-rw-r--r--api/src/routes/guilds/index.ts4
-rw-r--r--api/src/routes/guilds/templates/index.ts2
-rw-r--r--api/src/routes/users/@me/index.ts2
-rw-r--r--api/src/util/route.ts45
10 files changed, 88 insertions, 76 deletions
diff --git a/api/assets/schemas.json b/api/assets/schemas.json
index 9c34f968..e27087a9 100644
--- a/api/assets/schemas.json
+++ b/api/assets/schemas.json
@@ -26,8 +26,7 @@
                 "type": "string"
             },
             "date_of_birth": {
-                "type": "string",
-                "format": "date-time"
+                "type": "string"
             },
             "gift_code_sku_id": {
                 "type": "string"
@@ -713,22 +712,13 @@
         "type": "object",
         "properties": {
             "target_user_id": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "target_type": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "validate": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "max_age": {
                 "type": "integer"
@@ -2539,7 +2529,10 @@
                 "type": "string"
             },
             "icon": {
-                "type": "string"
+                "type": [
+                    "null",
+                    "string"
+                ]
             },
             "channels": {
                 "type": "array",
@@ -2551,10 +2544,7 @@
                 "type": "string"
             },
             "system_channel_id": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "rules_channel_id": {
                 "type": "string"
@@ -2820,10 +2810,7 @@
                 ]
             },
             "description": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "features": {
                 "type": "array",
@@ -2844,19 +2831,13 @@
                 "type": "integer"
             },
             "public_updates_channel_id": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "afk_timeout": {
                 "type": "integer"
             },
             "afk_channel_id": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "preferred_locale": {
                 "type": "string"
@@ -2869,16 +2850,16 @@
                 "type": "string"
             },
             "icon": {
-                "type": "string"
+                "type": [
+                    "null",
+                    "string"
+                ]
             },
             "guild_template_code": {
                 "type": "string"
             },
             "system_channel_id": {
-                "type": [
-                    "null",
-                    "string"
-                ]
+                "type": "string"
             },
             "rules_channel_id": {
                 "type": "string"
@@ -5718,7 +5699,10 @@
                 "type": "string"
             },
             "avatar": {
-                "type": "string"
+                "type": [
+                    "null",
+                    "string"
+                ]
             }
         },
         "additionalProperties": false,
@@ -6241,10 +6225,7 @@
                 "type": "string"
             },
             "accent_color": {
-                "type": [
-                    "null",
-                    "integer"
-                ]
+                "type": "integer"
             },
             "banner": {
                 "type": [
diff --git a/api/package.json b/api/package.json
index d93a6269..ad959e57 100644
--- a/api/package.json
+++ b/api/package.json
@@ -5,7 +5,8 @@
 	"main": "dist/Server.js",
 	"types": "dist/Server.d.ts",
 	"scripts": {
-		"test": "npm run build && jest --coverage --verbose --forceExit ./tests",
+		"test:only": "node -r ./scripts/tsconfig-paths-bootstrap.js node_modules/.bin/jest --coverage --verbose --forceExit ./tests",
+		"test": "npm run build && npm run test:only",
 		"test:watch": "jest --watch",
 		"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start",
 		"build": "npx tsc -b .",
diff --git a/api/scripts/globalSetup.js b/api/scripts/globalSetup.js
index 76cd8e0d..98e70fb9 100644
--- a/api/scripts/globalSetup.js
+++ b/api/scripts/globalSetup.js
@@ -1,10 +1,11 @@
 const fs = require("fs");
+const path = require("path");
 const { FosscordServer } = require("../dist/Server");
 const Server = new FosscordServer({ port: 3001 });
 global.server = Server;
 module.exports = async () => {
 	try {
-		fs.unlinkSync(`${__dirname}/../database.db`);
+		fs.unlinkSync(path.join(__dirname, "..", "database.db"));
 	} catch {}
 	return await Server.start();
 };
diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts
index 33f089b2..efe91625 100644
--- a/api/src/routes/auth/register.ts
+++ b/api/src/routes/auth/register.ts
@@ -27,13 +27,16 @@ export interface RegisterSchema {
 	email?: string;
 	fingerprint?: string;
 	invite?: string;
+	/**
+	 * @TJS-type string
+	 */
 	date_of_birth?: Date; // "2000-04-03"
 	gift_code_sku_id?: string;
 	captcha_key?: string;
 }
 
 router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
-	const {
+	let {
 		email,
 		username,
 		password,
@@ -61,14 +64,11 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 	// TODO: gift_code_sku_id?
 	// TODO: check password strength
 
-	// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
-	let adjusted_email = adjustEmail(email);
-
-	// adjusted_password will be the hash of the password
-	let adjusted_password = "";
+	// email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
+	email = adjustEmail(email);
 
 	// trim special uf8 control characters -> Backspace, Newline, ...
-	let adjusted_username = trimSpecial(username);
+	username = trimSpecial(username);
 
 	// discriminator will be randomly generated
 	let discriminator = "";
@@ -96,10 +96,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 
 	if (email) {
 		// replace all dots and chars after +, if its a gmail.com email
-		if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } });
+		if (!email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } });
 
 		// check if there is already an account with this email
-		const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {});
+		const exists = await User.findOneOrFail({ email: email }).catch((e) => {});
 
 		if (exists) {
 			throw FieldErrors({
@@ -122,6 +122,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 	} else if (register.dateOfBirth.minimum) {
 		const minimum = new Date();
 		minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
+		date_of_birth = new Date(date_of_birth);
 
 		// higher is younger
 		if (date_of_birth > minimum) {
@@ -162,7 +163,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 	}
 
 	// the salt is saved in the password refer to bcrypt docs
-	adjusted_password = await bcrypt.hash(password, 12);
+	password = await bcrypt.hash(password, 12);
 
 	let exists;
 	// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
@@ -171,7 +172,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 	// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
 	for (let tries = 0; tries < 5; tries++) {
 		discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
-		exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] });
+		exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
 		if (!exists) break;
 	}
 
@@ -190,7 +191,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 
 	const user = await new User({
 		created_at: new Date(),
-		username: adjusted_username,
+		username: username,
 		discriminator,
 		id: Snowflake.generate(),
 		bot: false,
@@ -204,12 +205,12 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 		verified: false,
 		disabled: false,
 		deleted: false,
-		email: adjusted_email,
+		email: email,
 		nsfw_allowed: true, // TODO: depending on age
 		public_flags: "0",
 		flags: "0", // TODO: generate
 		data: {
-			hash: adjusted_password,
+			hash: password,
 			valid_tokens_since: new Date()
 		},
 		settings: { ...defaultSettings, locale: req.language || "en-US" },
@@ -220,6 +221,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 });
 
 export function adjustEmail(email: string): string | undefined {
+	if (!email) return email;
 	// body parser already checked if it is a valid email
 	const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
 	// @ts-ignore
diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts
index 71612e31..22420983 100644
--- a/api/src/routes/channels/#channel_id/invites.ts
+++ b/api/src/routes/channels/#channel_id/invites.ts
@@ -8,9 +8,9 @@ import { isTextChannel } from "./messages";
 const router: Router = Router();
 
 export interface InviteCreateSchema {
-	target_user_id?: string | null;
-	target_type?: string | null;
-	validate?: string | null; // ? what is this
+	target_user_id?: string;
+	target_type?: string;
+	validate?: string; // ? what is this
 	max_age?: number;
 	max_uses?: number;
 	temporary?: boolean;
diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 7e4bf28a..63000b84 100644
--- a/api/src/routes/guilds/#guild_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -11,15 +11,15 @@ const router = Router();
 export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
 	banner?: string | null;
 	splash?: string | null;
-	description?: string | null;
+	description?: string;
 	features?: string[];
 	verification_level?: number;
 	default_message_notifications?: number;
 	system_channel_flags?: number;
 	explicit_content_filter?: number;
-	public_updates_channel_id?: string | null;
+	public_updates_channel_id?: string;
 	afk_timeout?: number;
-	afk_channel_id?: string | null;
+	afk_channel_id?: string;
 	preferred_locale?: string;
 }
 
diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index 2334bb9c..2e68d953 100644
--- a/api/src/routes/guilds/index.ts
+++ b/api/src/routes/guilds/index.ts
@@ -12,10 +12,10 @@ export interface GuildCreateSchema {
 	 */
 	name: string;
 	region?: string;
-	icon?: string;
+	icon?: string | null;
 	channels?: ChannelModifySchema[];
 	guild_template_code?: string;
-	system_channel_id?: string | null;
+	system_channel_id?: string;
 	rules_channel_id?: string;
 }
 
diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts
index eb3867c8..b5e243e9 100644
--- a/api/src/routes/guilds/templates/index.ts
+++ b/api/src/routes/guilds/templates/index.ts
@@ -6,7 +6,7 @@ import { DiscordApiErrors } from "@fosscord/util";
 
 export interface GuildTemplateCreateSchema {
 	name: string;
-	avatar?: string;
+	avatar?: string | null;
 }
 
 router.get("/:code", route({}), async (req: Request, res: Response) => {
diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts
index c0002d79..da2f3348 100644
--- a/api/src/routes/users/@me/index.ts
+++ b/api/src/routes/users/@me/index.ts
@@ -16,7 +16,7 @@ export interface UserModifySchema {
 	 * @maxLength 1024
 	 */
 	bio?: string;
-	accent_color?: number | null;
+	accent_color?: number;
 	banner?: string | null;
 	password?: string;
 	new_password?: string;
diff --git a/api/src/util/route.ts b/api/src/util/route.ts
index 6cd8f622..678ca64c 100644
--- a/api/src/util/route.ts
+++ b/api/src/util/route.ts
@@ -43,10 +43,37 @@ export interface RouteOptions {
 	};
 }
 
+// Normalizer is introduced to workaround https://github.com/ajv-validator/ajv/issues/1287
+// this removes null values as ajv doesn't treat them as undefined
+// normalizeBody allows to handle circular structures without issues
+// taken from https://github.com/serverless/serverless/blob/master/lib/classes/ConfigSchemaHandler/index.js#L30 (MIT license)
+const normalizeBody = (body: any = {}) => {
+	const normalizedObjectsSet = new WeakSet();
+	const normalizeObject = (object: any) => {
+		if (normalizedObjectsSet.has(object)) return;
+		normalizedObjectsSet.add(object);
+		if (Array.isArray(object)) {
+			for (const [index, value] of object.entries()) {
+				if (typeof value === "object") normalizeObject(value);
+			}
+		} else {
+			for (const [key, value] of Object.entries(object)) {
+				if (value == null) {
+					if (key === "icon" || key === "avatar" || key === "banner" || key === "splash") continue;
+					delete object[key];
+				} else if (typeof value === "object") {
+					normalizeObject(value);
+				}
+			}
+		}
+	};
+	normalizeObject(body);
+	return body;
+};
+
 export function route(opts: RouteOptions) {
-	var validate: AnyValidateFunction<any>;
+	var validate: AnyValidateFunction<any> | undefined;
 	if (opts.body) {
-		// @ts-ignore
 		validate = ajv.getSchema(opts.body);
 		if (!validate) throw new Error(`Body schema ${opts.body} not found`);
 	}
@@ -60,14 +87,14 @@ export function route(opts: RouteOptions) {
 			if (!permission.has(required)) {
 				throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string);
 			}
+		}
 
-			if (validate) {
-				const valid = validate(req.body);
-				if (!valid) {
-					const fields: Record<string, { code?: string; message: string }> = {};
-					validate.errors?.forEach((x) => (fields[x.instancePath] = { code: x.keyword, message: x.message || "" }));
-					throw FieldErrors(fields);
-				}
+		if (validate) {
+			const valid = validate(normalizeBody(req.body));
+			if (!valid) {
+				const fields: Record<string, { code?: string; message: string }> = {};
+				validate.errors?.forEach((x) => (fields[x.instancePath.slice(1)] = { code: x.keyword, message: x.message || "" }));
+				throw FieldErrors(fields);
 			}
 		}
 		next();