🐛 fix body parse treating null not as undefined (except for icons/avatars)
This commit is contained in:
parent
4003fef534
commit
859fdd679b
@ -26,8 +26,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"date_of_birth": {
|
"date_of_birth": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"format": "date-time"
|
|
||||||
},
|
},
|
||||||
"gift_code_sku_id": {
|
"gift_code_sku_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -713,22 +712,13 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"target_user_id": {
|
"target_user_id": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"target_type": {
|
"target_type": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"validate": {
|
"validate": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"max_age": {
|
"max_age": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@ -2539,7 +2529,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "string"
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -2551,10 +2544,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"system_channel_id": {
|
"system_channel_id": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"rules_channel_id": {
|
"rules_channel_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -2820,10 +2810,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -2844,19 +2831,13 @@
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"public_updates_channel_id": {
|
"public_updates_channel_id": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"afk_timeout": {
|
"afk_timeout": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"afk_channel_id": {
|
"afk_channel_id": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"preferred_locale": {
|
"preferred_locale": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -2869,16 +2850,16 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "string"
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"guild_template_code": {
|
"guild_template_code": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"system_channel_id": {
|
"system_channel_id": {
|
||||||
"type": [
|
"type": "string"
|
||||||
"null",
|
|
||||||
"string"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"rules_channel_id": {
|
"rules_channel_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -5718,7 +5699,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"type": "string"
|
"type": [
|
||||||
|
"null",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@ -6241,10 +6225,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"accent_color": {
|
"accent_color": {
|
||||||
"type": [
|
"type": "integer"
|
||||||
"null",
|
|
||||||
"integer"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"banner": {
|
"banner": {
|
||||||
"type": [
|
"type": [
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
"main": "dist/Server.js",
|
"main": "dist/Server.js",
|
||||||
"types": "dist/Server.d.ts",
|
"types": "dist/Server.d.ts",
|
||||||
"scripts": {
|
"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",
|
"test:watch": "jest --watch",
|
||||||
"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start",
|
"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start",
|
||||||
"build": "npx tsc -b .",
|
"build": "npx tsc -b .",
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const { FosscordServer } = require("../dist/Server");
|
const { FosscordServer } = require("../dist/Server");
|
||||||
const Server = new FosscordServer({ port: 3001 });
|
const Server = new FosscordServer({ port: 3001 });
|
||||||
global.server = Server;
|
global.server = Server;
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(`${__dirname}/../database.db`);
|
fs.unlinkSync(path.join(__dirname, "..", "database.db"));
|
||||||
} catch {}
|
} catch {}
|
||||||
return await Server.start();
|
return await Server.start();
|
||||||
};
|
};
|
||||||
|
@ -27,13 +27,16 @@ export interface RegisterSchema {
|
|||||||
email?: string;
|
email?: string;
|
||||||
fingerprint?: string;
|
fingerprint?: string;
|
||||||
invite?: string;
|
invite?: string;
|
||||||
|
/**
|
||||||
|
* @TJS-type string
|
||||||
|
*/
|
||||||
date_of_birth?: Date; // "2000-04-03"
|
date_of_birth?: Date; // "2000-04-03"
|
||||||
gift_code_sku_id?: string;
|
gift_code_sku_id?: string;
|
||||||
captcha_key?: string;
|
captcha_key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
|
router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
|
||||||
const {
|
let {
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@ -61,14 +64,11 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
|
|||||||
// TODO: gift_code_sku_id?
|
// TODO: gift_code_sku_id?
|
||||||
// TODO: check password strength
|
// TODO: check password strength
|
||||||
|
|
||||||
// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
|
// email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
|
||||||
let adjusted_email = adjustEmail(email);
|
email = adjustEmail(email);
|
||||||
|
|
||||||
// adjusted_password will be the hash of the password
|
|
||||||
let adjusted_password = "";
|
|
||||||
|
|
||||||
// trim special uf8 control characters -> Backspace, Newline, ...
|
// trim special uf8 control characters -> Backspace, Newline, ...
|
||||||
let adjusted_username = trimSpecial(username);
|
username = trimSpecial(username);
|
||||||
|
|
||||||
// discriminator will be randomly generated
|
// discriminator will be randomly generated
|
||||||
let discriminator = "";
|
let discriminator = "";
|
||||||
@ -96,10 +96,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
|
|||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
// replace all dots and chars after +, if its a gmail.com 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
|
// 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) {
|
if (exists) {
|
||||||
throw FieldErrors({
|
throw FieldErrors({
|
||||||
@ -122,6 +122,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
|
|||||||
} else if (register.dateOfBirth.minimum) {
|
} else if (register.dateOfBirth.minimum) {
|
||||||
const minimum = new Date();
|
const minimum = new Date();
|
||||||
minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
|
minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
|
||||||
|
date_of_birth = new Date(date_of_birth);
|
||||||
|
|
||||||
// higher is younger
|
// higher is younger
|
||||||
if (date_of_birth > minimum) {
|
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
|
// 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;
|
let exists;
|
||||||
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already 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?
|
// 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++) {
|
for (let tries = 0; tries < 5; tries++) {
|
||||||
discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
|
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;
|
if (!exists) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +191,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
|
|||||||
|
|
||||||
const user = await new User({
|
const user = await new User({
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
username: adjusted_username,
|
username: username,
|
||||||
discriminator,
|
discriminator,
|
||||||
id: Snowflake.generate(),
|
id: Snowflake.generate(),
|
||||||
bot: false,
|
bot: false,
|
||||||
@ -204,12 +205,12 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
|
|||||||
verified: false,
|
verified: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
email: adjusted_email,
|
email: email,
|
||||||
nsfw_allowed: true, // TODO: depending on age
|
nsfw_allowed: true, // TODO: depending on age
|
||||||
public_flags: "0",
|
public_flags: "0",
|
||||||
flags: "0", // TODO: generate
|
flags: "0", // TODO: generate
|
||||||
data: {
|
data: {
|
||||||
hash: adjusted_password,
|
hash: password,
|
||||||
valid_tokens_since: new Date()
|
valid_tokens_since: new Date()
|
||||||
},
|
},
|
||||||
settings: { ...defaultSettings, locale: req.language || "en-US" },
|
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 {
|
export function adjustEmail(email: string): string | undefined {
|
||||||
|
if (!email) return email;
|
||||||
// body parser already checked if it is a valid email
|
// body parser already checked if it is a valid email
|
||||||
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
|
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -8,9 +8,9 @@ import { isTextChannel } from "./messages";
|
|||||||
const router: Router = Router();
|
const router: Router = Router();
|
||||||
|
|
||||||
export interface InviteCreateSchema {
|
export interface InviteCreateSchema {
|
||||||
target_user_id?: string | null;
|
target_user_id?: string;
|
||||||
target_type?: string | null;
|
target_type?: string;
|
||||||
validate?: string | null; // ? what is this
|
validate?: string; // ? what is this
|
||||||
max_age?: number;
|
max_age?: number;
|
||||||
max_uses?: number;
|
max_uses?: number;
|
||||||
temporary?: boolean;
|
temporary?: boolean;
|
||||||
|
@ -11,15 +11,15 @@ const router = Router();
|
|||||||
export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
|
export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
|
||||||
banner?: string | null;
|
banner?: string | null;
|
||||||
splash?: string | null;
|
splash?: string | null;
|
||||||
description?: string | null;
|
description?: string;
|
||||||
features?: string[];
|
features?: string[];
|
||||||
verification_level?: number;
|
verification_level?: number;
|
||||||
default_message_notifications?: number;
|
default_message_notifications?: number;
|
||||||
system_channel_flags?: number;
|
system_channel_flags?: number;
|
||||||
explicit_content_filter?: number;
|
explicit_content_filter?: number;
|
||||||
public_updates_channel_id?: string | null;
|
public_updates_channel_id?: string;
|
||||||
afk_timeout?: number;
|
afk_timeout?: number;
|
||||||
afk_channel_id?: string | null;
|
afk_channel_id?: string;
|
||||||
preferred_locale?: string;
|
preferred_locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,10 +12,10 @@ export interface GuildCreateSchema {
|
|||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
icon?: string;
|
icon?: string | null;
|
||||||
channels?: ChannelModifySchema[];
|
channels?: ChannelModifySchema[];
|
||||||
guild_template_code?: string;
|
guild_template_code?: string;
|
||||||
system_channel_id?: string | null;
|
system_channel_id?: string;
|
||||||
rules_channel_id?: string;
|
rules_channel_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { DiscordApiErrors } from "@fosscord/util";
|
|||||||
|
|
||||||
export interface GuildTemplateCreateSchema {
|
export interface GuildTemplateCreateSchema {
|
||||||
name: string;
|
name: string;
|
||||||
avatar?: string;
|
avatar?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get("/:code", route({}), async (req: Request, res: Response) => {
|
router.get("/:code", route({}), async (req: Request, res: Response) => {
|
||||||
|
@ -16,7 +16,7 @@ export interface UserModifySchema {
|
|||||||
* @maxLength 1024
|
* @maxLength 1024
|
||||||
*/
|
*/
|
||||||
bio?: string;
|
bio?: string;
|
||||||
accent_color?: number | null;
|
accent_color?: number;
|
||||||
banner?: string | null;
|
banner?: string | null;
|
||||||
password?: string;
|
password?: string;
|
||||||
new_password?: string;
|
new_password?: string;
|
||||||
|
@ -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) {
|
export function route(opts: RouteOptions) {
|
||||||
var validate: AnyValidateFunction<any>;
|
var validate: AnyValidateFunction<any> | undefined;
|
||||||
if (opts.body) {
|
if (opts.body) {
|
||||||
// @ts-ignore
|
|
||||||
validate = ajv.getSchema(opts.body);
|
validate = ajv.getSchema(opts.body);
|
||||||
if (!validate) throw new Error(`Body schema ${opts.body} not found`);
|
if (!validate) throw new Error(`Body schema ${opts.body} not found`);
|
||||||
}
|
}
|
||||||
@ -60,14 +87,14 @@ export function route(opts: RouteOptions) {
|
|||||||
if (!permission.has(required)) {
|
if (!permission.has(required)) {
|
||||||
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string);
|
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (validate) {
|
if (validate) {
|
||||||
const valid = validate(req.body);
|
const valid = validate(normalizeBody(req.body));
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
const fields: Record<string, { code?: string; message: string }> = {};
|
const fields: Record<string, { code?: string; message: string }> = {};
|
||||||
validate.errors?.forEach((x) => (fields[x.instancePath] = { code: x.keyword, message: x.message || "" }));
|
validate.errors?.forEach((x) => (fields[x.instancePath.slice(1)] = { code: x.keyword, message: x.message || "" }));
|
||||||
throw FieldErrors(fields);
|
throw FieldErrors(fields);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user