diff options
-rw-r--r-- | assets/locales/en/auth.json | 3 | ||||
-rw-r--r-- | assets/schemas.json | 3 | ||||
-rw-r--r-- | src/api/middlewares/TestClient.ts | 3 | ||||
-rw-r--r-- | src/api/routes/auth/register.ts | 16 | ||||
-rw-r--r-- | src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts | 176 | ||||
-rw-r--r-- | src/api/routes/users/#id/profile.ts | 86 | ||||
-rw-r--r-- | src/api/routes/users/@me/index.ts | 9 | ||||
-rw-r--r-- | src/util/config/types/RegisterConfiguration.ts | 1 | ||||
-rw-r--r-- | src/util/config/types/SecurityConfiguration.ts | 2 | ||||
-rw-r--r-- | src/util/entities/Channel.ts | 12 | ||||
-rw-r--r-- | src/util/schemas/UserModifySchema.ts | 1 | ||||
-rw-r--r-- | src/util/util/InvisibleCharacters.ts | 9 | ||||
-rw-r--r-- | src/util/util/MFA.ts | 5 |
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 }); |