summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--assets/locales/en/auth.json3
-rw-r--r--assets/schemas.json3
-rw-r--r--src/api/middlewares/TestClient.ts3
-rw-r--r--src/api/routes/auth/register.ts16
-rw-r--r--src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts176
-rw-r--r--src/api/routes/users/#id/profile.ts86
-rw-r--r--src/api/routes/users/@me/index.ts9
-rw-r--r--src/util/config/types/RegisterConfiguration.ts1
-rw-r--r--src/util/config/types/SecurityConfiguration.ts2
-rw-r--r--src/util/entities/Channel.ts12
-rw-r--r--src/util/schemas/UserModifySchema.ts1
-rw-r--r--src/util/util/InvisibleCharacters.ts9
-rw-r--r--src/util/util/MFA.ts5
13 files changed, 278 insertions, 48 deletions
diff --git a/assets/locales/en/auth.json b/assets/locales/en/auth.json
index a78d4d60..b6264a43 100644
--- a/assets/locales/en/auth.json
+++ b/assets/locales/en/auth.json
@@ -13,6 +13,7 @@
 		"EMAIL_ALREADY_REGISTERED": "Email is already registered",
 		"DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older",
 		"CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.",
-		"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another"
+		"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another",
+		"GUESTS_DISABLED": "Guest users are disabled"
 	}
 }
diff --git a/assets/schemas.json b/assets/schemas.json
index e3200800..05650a4e 100644
--- a/assets/schemas.json
+++ b/assets/schemas.json
@@ -1105,6 +1105,9 @@
 			},
 			"code": {
 				"type": "string"
+			},
+			"email": {
+				"type": "string"
 			}
 		},
 		"additionalProperties": false,
diff --git a/src/api/middlewares/TestClient.ts b/src/api/middlewares/TestClient.ts
index 2784c8ab..2c195994 100644
--- a/src/api/middlewares/TestClient.ts
+++ b/src/api/middlewares/TestClient.ts
@@ -45,6 +45,9 @@ export default function TestClient(app: Application) {
 				res.set(name, value);
 			});
 		} else {
+			if(req.params.file.endsWith(".map")) {
+				return res.status(404).send("Not found");	
+			}
 			console.log(`[TestClient] Downloading file not yet cached! Asset file: ${req.params.file}`);
 			response = await fetch(`https://discord.com/assets/${req.params.file}`, {
 				agent,
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index d3b5a59c..5cc28f7a 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -1,6 +1,6 @@
+import { getIpAdress, IPAnalysis, isProxy, route, verifyCaptcha } from "@fosscord/api";
+import { adjustEmail, Config, FieldErrors, generateToken, HTTPError, Invite, RegisterSchema, User } from "@fosscord/util";
 import { Request, Response, Router } from "express";
-import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, RegisterSchema } from "@fosscord/util";
-import { route, getIpAdress, IPAnalysis, isProxy, verifyCaptcha } from "@fosscord/api";
 
 let bcrypt: any;
 try {
@@ -9,7 +9,6 @@ try {
 	bcrypt = require("bcryptjs");
 	console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected.");
 }
-import { HTTPError } from "@fosscord/util";
 
 const router: Router = Router();
 
@@ -44,6 +43,12 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 		});
 	}
 
+	if (!register.allowGuests) {
+		throw FieldErrors({
+			email: { code: "GUESTS_DISABLED", message: req.t("auth:register.GUESTS_DISABLED") }
+		});
+	}
+
 	if (register.requireCaptcha && security.captcha.enabled) {
 		const { sitekey, service } = security.captcha;
 		if (!body.captcha_key) {
@@ -60,7 +65,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 				captcha_key: verify["error-codes"],
 				captcha_sitekey: sitekey,
 				captcha_service: service
-			})
+			});
 		}
 	}
 
@@ -111,7 +116,8 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 		});
 	}
 
-	if (register.dateOfBirth.required && !body.date_of_birth) {
+	// If no password is provided, this is a guest account
+	if (register.dateOfBirth.required && !body.date_of_birth && body.password) {
 		throw FieldErrors({
 			date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
 		});
diff --git a/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
index fdd775b7..f8c78bc5 100644
--- a/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
+++ b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
@@ -9,7 +9,7 @@ const skus = new Map([
 		[
 			{
 				id: "511651856145973248",
-				name: "Individual Premium Tier 2 Monthly (Legacy)",
+				name: "Individual Premium Tier 3 Monthly (Legacy)",
 				interval: 1,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -20,7 +20,7 @@ const skus = new Map([
 			},
 			{
 				id: "511651860671627264",
-				name: "Individiual Premium Tier 2 Yearly (Legacy)",
+				name: "Individiual Premium Tier 3 Yearly (Legacy)",
 				interval: 2,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -36,7 +36,7 @@ const skus = new Map([
 		[
 			{
 				id: "511651871736201216",
-				name: "Individual Premium Tier 1 Monthly",
+				name: "Individual Premium Tier 2 Monthly",
 				interval: 1,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -47,7 +47,7 @@ const skus = new Map([
 			},
 			{
 				id: "511651876987469824",
-				name: "Individual Premum Tier 1 Yearly",
+				name: "Individual Premum Tier 2 Yearly",
 				interval: 2,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -58,7 +58,7 @@ const skus = new Map([
 			},
 			{
 				id: "978380684370378761",
-				name: "Individual Premum Tier 0",
+				name: "Individual Premum Tier 1",
 				interval: 2,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -74,7 +74,7 @@ const skus = new Map([
 		[
 			{
 				id: "642251038925127690",
-				name: "Individual Premium Tier 2 Quarterly",
+				name: "Individual Premium Tier 3 Quarterly",
 				interval: 1,
 				interval_count: 3,
 				tax_inclusive: true,
@@ -85,7 +85,7 @@ const skus = new Map([
 			},
 			{
 				id: "511651880837840896",
-				name: "Individual Premium Tier 2 Monthly",
+				name: "Individual Premium Tier 3 Monthly",
 				interval: 1,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -96,7 +96,7 @@ const skus = new Map([
 			},
 			{
 				id: "511651885459963904",
-				name: "Individual Premium Tier 2 Yearly",
+				name: "Individual Premium Tier 3 Yearly",
 				interval: 2,
 				interval_count: 1,
 				tax_inclusive: true,
@@ -134,6 +134,166 @@ const skus = new Map([
 				price: 0,
 				price_tier: null
 			}
+		],
+	],
+	[
+		"978380684370378762",
+		[
+			[
+				{
+					"id": "978380692553465866",
+					"name": "Premium Tier 0 Monthly",
+					"interval": 1,
+					"interval_count": 1,
+					"tax_inclusive": true,
+					"sku_id": "978380684370378762",
+					"currency": "usd",
+					"price": 299,
+					"price_tier": null,
+					"prices": {
+						"0": {
+							"country_prices": {
+								"country_code": "US",
+								"prices": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							},
+							"payment_source_prices": {
+								"775487223059316758": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"736345864146255982": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"683074999590060249": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							}
+						},
+						"3": {
+							"country_prices": {
+								"country_code": "US",
+								"prices": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							},
+							"payment_source_prices": {
+								"775487223059316758": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"736345864146255982": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"683074999590060249": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							}
+						},
+						"4": {
+							"country_prices": {
+								"country_code": "US",
+								"prices": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							},
+							"payment_source_prices": {
+								"775487223059316758": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"736345864146255982": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"683074999590060249": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							}
+						},
+						"1": {
+							"country_prices": {
+								"country_code": "US",
+								"prices": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							},
+							"payment_source_prices": {
+								"775487223059316758": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"736345864146255982": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								],
+								"683074999590060249": [
+									{
+										"currency": "usd",
+										"amount": 0,
+										"exponent": 2
+									}
+								]
+							}
+						}
+					}
+				}
+			]
 		]
 	]
 ]);
diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
index 27717c79..766c9880 100644
--- a/src/api/routes/users/#id/profile.ts
+++ b/src/api/routes/users/#id/profile.ts
@@ -13,45 +13,81 @@ export interface UserProfileResponse {
 
 router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => {
 	if (req.params.id === "@me") req.params.id = req.user_id;
+
+	const { guild_id, with_mutual_guilds } = req.query;
+
 	const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] });
 
-	let mutual_guilds: object[] = [];
-	let premium_guild_since;
-	const requested_member = await Member.find({ where: { id: req.params.id } });
-	const self_member = await Member.find({ where: { id: req.user_id } });
+	var mutual_guilds: object[] = [];
+	var premium_guild_since;
+
+	if (with_mutual_guilds == "true") {
+		const requested_member = await Member.find({ where: { id: req.params.id } });
+		const self_member = await Member.find({ where: { id: req.user_id } });
 
-	for (const rmem of requested_member) {
-		if (rmem.premium_since) {
-			if (premium_guild_since) {
-				if (premium_guild_since > rmem.premium_since) {
+		for (const rmem of requested_member) {
+			if (rmem.premium_since) {
+				if (premium_guild_since) {
+					if (premium_guild_since > rmem.premium_since) {
+						premium_guild_since = rmem.premium_since;
+					}
+				} else {
 					premium_guild_since = rmem.premium_since;
 				}
-			} else {
-				premium_guild_since = rmem.premium_since;
 			}
-		}
-		for (const smem of self_member) {
-			if (smem.guild_id === rmem.guild_id) {
-				mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick });
+			for (const smem of self_member) {
+				if (smem.guild_id === rmem.guild_id) {
+					mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick });
+				}
 			}
 		}
 	}
+
+	const guild_member =
+		guild_id && typeof guild_id == "string"
+			? await Member.findOneOrFail({ where: { id: req.params.id, guild_id: guild_id }, relations: ["roles"] })
+			: undefined;
+
+	// TODO: make proper DTO's in util?
+
+	const userDto = {
+		username: user.username,
+		discriminator: user.discriminator,
+		id: user.id,
+		public_flags: user.public_flags,
+		avatar: user.avatar,
+		accent_color: user.accent_color,
+		banner: user.banner,
+		bio: req.user_bot ? null : user.bio,
+		bot: user.bot
+	};
+
+	const guildMemberDto = guild_member
+		? {
+				avatar: user.avatar, // TODO
+				banner: user.banner, // TODO
+				bio: req.user_bot ? null : user.bio, // TODO
+				communication_disabled_until: null, // TODO
+				deaf: guild_member.deaf,
+				flags: user.flags,
+				is_pending: guild_member.pending,
+				pending: guild_member.pending, // why is this here twice, discord?
+				joined_at: guild_member.joined_at,
+				mute: guild_member.mute,
+				nick: guild_member.nick,
+				premium_since: guild_member.premium_since,
+				roles: guild_member.roles.map((x) => x.id).filter((id) => id != guild_id),
+				user: userDto
+		  }
+		: undefined;
+
 	res.json({
 		connected_accounts: user.connected_accounts,
 		premium_guild_since: premium_guild_since, // TODO
 		premium_since: user.premium_since, // TODO
 		mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
-		user: {
-			username: user.username,
-			discriminator: user.discriminator,
-			id: user.id,
-			public_flags: user.public_flags,
-			avatar: user.avatar,
-			accent_color: user.accent_color,
-			banner: user.banner,
-			bio: req.user_bot ? null : user.bio,
-			bot: user.bot
-		}
+		user: userDto,
+		guild_member: guildMemberDto
 	});
 });
 
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index fcb0a9df..563300dc 100644
--- a/src/api/routes/users/@me/index.ts
+++ b/src/api/routes/users/@me/index.ts
@@ -1,5 +1,7 @@
 import { route } from "@fosscord/api";
 import {
+	adjustEmail,
+	Config,
 	emitEvent,
 	FieldErrors,
 	generateToken,
@@ -45,6 +47,13 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res:
 		}
 	}
 
+	if (body.email) {
+		body.email = adjustEmail(body.email);
+		if (!body.email && Config.get().register.email.required)
+			throw FieldErrors({ email: { message: req.t("auth:register.EMAIL_INVALID"), code: "EMAIL_INVALID" } });
+		if (!body.password) throw FieldErrors({ password: { message: req.t("auth:register.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+	}
+
 	if (body.new_password) {
 		if (!body.password && !user.email) {
 			throw FieldErrors({
diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts
index 939605a6..68946272 100644
--- a/src/util/config/types/RegisterConfiguration.ts
+++ b/src/util/config/types/RegisterConfiguration.ts
@@ -9,6 +9,7 @@ export class RegisterConfiguration {
 	disabled: boolean = false;
 	requireCaptcha: boolean = true;
 	requireInvite: boolean = false;
+	allowGuests: boolean = true;
 	guestsRequireInvite: boolean = true;
 	allowNewRegistration: boolean = true;
 	allowMultipleAccounts: boolean = true;
diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts
index 98c04c99..a2cebbd3 100644
--- a/src/util/config/types/SecurityConfiguration.ts
+++ b/src/util/config/types/SecurityConfiguration.ts
@@ -14,4 +14,6 @@ export class SecurityConfiguration {
 	// CF-Connecting-IP for cloudflare
 	forwadedFor: string | null = null;
 	ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9";
+	mfaBackupCodeCount: number = 10;
+	mfaBackupCodeBytes: number = 4;
 }
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index 23fc6544..b17fdba0 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -181,10 +181,16 @@ export class Channel extends BaseClass {
 				for (let character of InvisibleCharacters)
 					if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403);
 
-				if (channel.name.match(/\-\-+/g)) throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403);
+				// Categories and voice skip these checks on discord.com
+				const skipChecksTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_VOICE];
+				if ((channel.type && !skipChecksTypes.includes(channel.type)) || guild.features.includes("IRC_LIKE_CHANNEL_NAMES")) {
+					if (channel.name.includes(" ")) throw new HTTPError("Channel name cannot include invalid characters", 403);
 
-				if (channel.name.charAt(0) === "-" || channel.name.charAt(channel.name.length - 1) === "-")
-					throw new HTTPError("Channel name cannot start/end with dash.", 403);
+					if (channel.name.match(/\-\-+/g)) throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403);
+
+					if (channel.name.charAt(0) === "-" || channel.name.charAt(channel.name.length - 1) === "-")
+						throw new HTTPError("Channel name cannot start/end with dash.", 403);
+				} else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com
 			}
 
 			if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
diff --git a/src/util/schemas/UserModifySchema.ts b/src/util/schemas/UserModifySchema.ts
index d69f83f4..622497d9 100644
--- a/src/util/schemas/UserModifySchema.ts
+++ b/src/util/schemas/UserModifySchema.ts
@@ -15,4 +15,5 @@ export interface UserModifySchema {
 	password?: string;
 	new_password?: string;
 	code?: string;
+	email?: string;
 }
diff --git a/src/util/util/InvisibleCharacters.ts b/src/util/util/InvisibleCharacters.ts
index 4c809e48..9bffec58 100644
--- a/src/util/util/InvisibleCharacters.ts
+++ b/src/util/util/InvisibleCharacters.ts
@@ -1,9 +1,10 @@
 // List from https://invisible-characters.com/
 export const InvisibleCharacters = [
 	"\u{9}", //Tab
-	"\u{20}", //Space
+	"\u{c}", //Form feed
+	//'\u{20}',			//Space	//categories can have spaces in them
 	"\u{ad}", //Soft hyphen
-	"\u{34f}", //Combining grapheme joiner
+	// '\u{34f}',		//Combining grapheme joiner
 	"\u{61c}", //Arabic letter mark
 	"\u{115f}", //Hangul choseong filler
 	"\u{1160}", //Hangul jungseong filler
@@ -23,12 +24,12 @@ export const InvisibleCharacters = [
 	"\u{200a}", //Hair space
 	"\u{200b}", //Zero width space
 	"\u{200c}", //Zero width non-joiner
-	"\u{200d}", //Zero width joiner
+	// '\u{200d}',		//Zero width joiner
 	"\u{200e}", //Left-to-right mark
 	"\u{200f}", //Right-to-left mark
 	"\u{202f}", //Narrow no-break space
 	"\u{205f}", //Medium mathematical space
-	"\u{2060}", //Word joiner
+	// '\u{2060}',		//Word joiner -- WJ is required in some languages that don't use spaces to split words
 	"\u{2061}", //Function application
 	"\u{2062}", //Invisible times
 	"\u{2063}", //Invisible separator
diff --git a/src/util/util/MFA.ts b/src/util/util/MFA.ts
index a2afcad6..b9af6d23 100644
--- a/src/util/util/MFA.ts
+++ b/src/util/util/MFA.ts
@@ -1,12 +1,13 @@
 import crypto from "crypto";
+import { Config } from ".";
 import { BackupCode } from "../entities/BackupCodes";
 
 export function generateMfaBackupCodes(user_id: string) {
 	let backup_codes: BackupCode[] = [];
-	for (let i = 0; i < 10; i++) {
+	for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) {
 		const code = BackupCode.create({
 			user: { id: user_id },
-			code: crypto.randomBytes(4).toString("hex"), // 8 characters
+			code: crypto.randomBytes(Config.get().security.mfaBackupCodeBytes).toString("hex"), // 8 characters
 			consumed: false,
 			expired: false
 		});