diff options
author | TheArcaneBrony <myrainbowdash949@gmail.com> | 2022-09-20 16:43:35 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-20 16:43:35 +0200 |
commit | 313959557df08b03ae07a85c6faafa9817a9f696 (patch) | |
tree | 8e3c1e5159f0b1ee0eba3c9f69a15d7894430726 | |
parent | Merge pull request #885 from fosscord/dev/Maddy/fix/genSchemas (diff) | |
parent | Courtesy: run prettier (diff) | |
download | server-313959557df08b03ae07a85c6faafa9817a9f696.tar.xz |
Merge pull request #891 from fosscord/dev/improve-security
Improved security: one-time registration token support, register and message ratelimit
56 files changed, 824 insertions, 137 deletions
diff --git a/.gitignore b/.gitignore index a582a2f3..8d2feb42 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ yarn.lock dbconf.json migrations.db + +package-lock.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..036ed868 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +.DS_STORE +db/ +dist/ +node_modules +tsconfig.tsbuildinfo +*.log +*.log.ansi +bundle/depclean.* +*.tmp +tmp/ +assets/cache/ +*.generated +.yarn/ +yarn.lock \ No newline at end of file diff --git a/assets/locales/en/auth.json b/assets/locales/en/auth.json index b6264a43..dc4358f1 100644 --- a/assets/locales/en/auth.json +++ b/assets/locales/en/auth.json @@ -14,6 +14,11 @@ "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", - "GUESTS_DISABLED": "Guest users are disabled" + "GUESTS_DISABLED": "Guest users are disabled", + "TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later", + "IP_BLOCKED": "Your IP is blocked from registration" + }, + "generic": { + "MISSING_AUTH_HEADER": "Missing Authorization Header" } } diff --git a/assets/locales/en/common.json b/assets/locales/en/common.json index 8bb9c042..edcafa1d 100644 --- a/assets/locales/en/common.json +++ b/assets/locales/en/common.json @@ -14,5 +14,36 @@ "EMAIL_TYPE_INVALID_EMAIL": "Not a well-formed email address", "DATE_TYPE_PARSE": "Could not parse {{date}}. Should be ISO8601", "BASE_TYPE_BAD_LENGTH": "Must be between {{length}} in length" + }, + "body": { + "INVALID_BODY": "Invalid Body", + "INVALID_REQUEST_SIGNATURE": "Invalid request signature", + "MISSING_FILE": "File missing", + "INVALID_FILE_TYPE": "Invalid file type" + }, + "notfound": { + "CHANNEL": "This channel doesn't exist", + "USER": "User not found", + "ROLE": "Role not found", + "REACTION": "Reaction not found", + "FILE": "File not found" + }, + "toomany": { + "CHANNEL": "Too many channels", + "USER": "Too many users", + "ROLE": "Too many roles", + "REACTION": "Too many reactions", + "FILE": "Too many files", + "MESSAGE": "Too many messages" + }, + "relationship": { + "ALREADY_BLOCKED": "You already blocked the user", + "NOT_FRIENDS": "You are not friends with the user", + "ALREADY_FRIENDS": "You are already friends with the user", + "ALREADY_SENT": "You already sent a friend request", + "ADD_SELF": "You can't add yourself as a friend", + "REMOVE_SELF": "You can't remove yourself as a friend", + "UNBLOCK": "Unblock the user before sending a friend request", + "BLOCKED": "The user blocked you" } } diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 6d063953..00c2e5e6 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -53,7 +53,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc }) ) return next(); - if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401)); + if (!req.headers.authorization) return next(new HTTPError("Missing authorization header!", 401)); try { const { jwtSecret } = Config.get().security; diff --git a/src/api/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts index 36d89da7..b7c99638 100644 --- a/src/api/middlewares/BodyParser.ts +++ b/src/api/middlewares/BodyParser.ts @@ -11,7 +11,7 @@ export function BodyParser(opts?: OptionsJson) { jsonParser(req, res, (err) => { if (err) { // TODO: different errors for body parser (request size limit, wrong body type, invalid body, ...) - return next(new HTTPError("Invalid Body", 400)); + return next(new HTTPError(req.t("common:body.INVALID_BODY"), 400)); } next(); }); diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts index dc93dcef..b19f9f7e 100644 --- a/src/api/middlewares/RateLimit.ts +++ b/src/api/middlewares/RateLimit.ts @@ -1,5 +1,4 @@ -import { getIpAdress } from "@fosscord/api"; -import { Config, getRights, listenEvent } from "@fosscord/util"; +import { Config, getIpAdress, getRights, listenEvent } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts new file mode 100644 index 00000000..6f7f8630 --- /dev/null +++ b/src/api/routes/auth/generate-registration-tokens.ts @@ -0,0 +1,27 @@ +import { route } from "@fosscord/api"; +import { Config, random, ValidRegistrationToken } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); +export default router; + +router.get("/", route({ right: "OPERATOR" }), async (req: Request, res: Response) => { + let count = (req.query.count as unknown as number) ?? 1; + let tokens: string[] = []; + let dbtokens: ValidRegistrationToken[] = []; + for (let i = 0; i < count; i++) { + let token = random((req.query.length as unknown as number) ?? 255); + let vrt = new ValidRegistrationToken(); + vrt.token = token; + dbtokens.push(vrt); + if (req.query.include_url == "true") token = `${Config.get().general.publicUrl}/register?token=${token}`; + tokens.push(token); + } + await ValidRegistrationToken.save(dbtokens, { chunk: 1000, reload: false, transaction: false }); + + if (req.query.plain == "true") { + if (count == 1) res.send(tokens[0]); + else res.send(tokens.join("\n")); + } else if (count == 1) res.json({ token: tokens[0] }); + else res.json({ tokens }); +}); diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts index b8caf579..5ccd7e85 100644 --- a/src/api/routes/auth/location-metadata.ts +++ b/src/api/routes/auth/location-metadata.ts @@ -1,4 +1,5 @@ -import { getIpAdress, IPAnalysis, route } from "@fosscord/api"; +import { route } from "@fosscord/api"; +import { getIpAdress, IPAnalysis } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 045b86eb..4c882c14 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -1,5 +1,5 @@ -import { getIpAdress, route, verifyCaptcha } from "@fosscord/api"; -import { adjustEmail, Config, FieldErrors, generateToken, LoginSchema, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { adjustEmail, Config, FieldErrors, generateToken, getIpAdress, LoginSchema, User, verifyCaptcha } from "@fosscord/util"; import crypto from "crypto"; import { Request, Response, Router } from "express"; diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts new file mode 100644 index 00000000..7e36ae9a --- /dev/null +++ b/src/api/routes/auth/logout.ts @@ -0,0 +1,16 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); +export default router; + +router.post("/", route({}), async (req: Request, res: Response) => { + if (req.body.provider != null || req.body.voip_provider != null) { + console.log(`[LOGOUT]: provider or voip provider not null!`, req.body); + } else { + delete req.body.provider; + delete req.body.voip_provider; + if (Object.keys(req.body).length != 0) console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body); + } + res.status(204).send(); +}); diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 5cc28f7a..50f89522 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -1,6 +1,22 @@ -import { getIpAdress, IPAnalysis, isProxy, route, verifyCaptcha } from "@fosscord/api"; -import { adjustEmail, Config, FieldErrors, generateToken, HTTPError, Invite, RegisterSchema, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { + adjustEmail, + Config, + FieldErrors, + generateToken, + getIpAdress, + HTTPError, + Invite, + IPAnalysis, + isProxy, + RegisterSchema, + User, + ValidRegistrationToken, + verifyCaptcha +} from "@fosscord/util"; import { Request, Response, Router } from "express"; +import { red, yellow } from "picocolors"; +import { LessThan, MoreThan } from "typeorm"; let bcrypt: any; try { @@ -14,17 +30,28 @@ const router: Router = Router(); router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { const body = req.body as RegisterSchema; - const { register, security } = Config.get(); + const { register, security, limits } = Config.get(); const ip = getIpAdress(req); // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick let email = adjustEmail(body.email); - // check if registration is allowed - if (!register.allowNewRegistration) { - throw FieldErrors({ - email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } - }); + //check if referrer starts with any valid registration token + //!! bypasses captcha and registration disabling !!// + let validToken = false; + if (req.get("Referrer") && req.get("Referrer")?.includes("token=")) { + let token = req.get("Referrer")?.split("token=")[1].split("&")[0]; + if (token) { + await ValidRegistrationToken.delete({ expires_at: LessThan(new Date()) }); + let registrationToken = await ValidRegistrationToken.findOne({ where: { token: token, expires_at: MoreThan(new Date()) } }); + if (registrationToken) { + console.log(yellow(`[REGISTER] Registration token ${token} used for registration!`)); + await ValidRegistrationToken.delete(token); + validToken = true; + } else { + console.log(yellow(`[REGISTER] Invalid registration token ${token} used for registration by ${ip}!`)); + } + } } // check if the user agreed to the Terms of Service @@ -34,22 +61,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } - if (register.disabled) { - throw FieldErrors({ - email: { - code: "DISABLED", - message: "registration is disabled on this instance" - } - }); - } - - if (!register.allowGuests) { - throw FieldErrors({ - email: { code: "GUESTS_DISABLED", message: req.t("auth:register.GUESTS_DISABLED") } - }); - } - - if (register.requireCaptcha && security.captcha.enabled) { + if (register.requireCaptcha && security.captcha.enabled && !validToken) { const { sitekey, service } = security.captcha; if (!body.captcha_key) { return res?.status(400).json({ @@ -69,24 +81,24 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re } } - if (!register.allowMultipleAccounts) { - // TODO: check if fingerprint was eligible generated - const exists = await User.findOne({ where: { fingerprints: body.fingerprint }, select: ["id"] }); - - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } - }); - } + // check if registration is allowed + if (!register.allowNewRegistration && !validToken) { + throw FieldErrors({ + email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } + }); } if (register.blockProxies) { - if (isProxy(await IPAnalysis(ip))) { - console.log(`proxy ${ip} blocked from registration`); - throw new HTTPError("Your IP is blocked from registration"); + let data; + try { + data = await IPAnalysis(ip); + } catch (e: any) { + console.warn(red(`[REGISTER]: Failed to analyze IP ${ip}: failed to contact api.ipdata.co!`), e.message); + } + + if (data && isProxy(data)) { + console.log(yellow(`[REGISTER] Proxy ${ip} blocked from registration!`)); + throw new HTTPError(req.t("auth:register.IP_BLOCKED")); } } @@ -94,15 +106,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re // TODO: check password strength if (email) { - // replace all dots and chars after +, if its a gmail.com 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.findOne({ where: { email: email } }); - if (exists) { + if (exists && !register.disabled) { throw FieldErrors({ email: { code: "EMAIL_ALREADY_REGISTERED", @@ -153,6 +160,46 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } + if ( + !validToken && + limits.absoluteRate.register.enabled && + (await await User.count({ where: { created_at: MoreThan(new Date(Date.now() - limits.absoluteRate.register.window)) } })) >= + limits.absoluteRate.register.limit + ) { + console.log( + yellow( + `[REGISTER] Global register rate limit exceeded for ${getIpAdress(req)}: ${ + process.env.LOG_SENSITIVE ? req.body.email : "<email redacted>" + }, ${req.body.username}, ${req.body.invite ?? "No invite given"}` + ) + ); + let oldest = await User.findOne({ + where: { created_at: MoreThan(new Date(Date.now() - limits.absoluteRate.register.window)) }, + order: { created_at: "ASC" } + }); + if (!oldest) { + console.warn( + red( + `[REGISTER/WARN] Global rate limits exceeded, but no oldest user found. This should not happen. Did you misconfigure the limits?` + ) + ); + } else { + let retryAfterSec = Math.ceil( + (oldest!.created_at.getTime() - new Date(Date.now() - limits.absoluteRate.register.window).getTime()) / 1000 + ); + return res + .status(429) + .set("X-RateLimit-Limit", `${limits.absoluteRate.register.limit}`) + .set("X-RateLimit-Remaining", "0") + .set("X-RateLimit-Reset", `${(oldest!.created_at.getTime() + limits.absoluteRate.register.window) / 1000}`) + .set("X-RateLimit-Reset-After", `${retryAfterSec}`) + .set("X-RateLimit-Global", `true`) + .set("Retry-After", `${retryAfterSec}`) + .set("X-RateLimit-Bucket", `register`) + .send({ message: req.t("auth:register.TOO_MANY_REGISTRATIONS"), retry_after: retryAfterSec, global: true }); + } + } + const user = await User.register({ ...body, req }); if (body.invite) { diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index 3a1d2666..5fb8f5df 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -15,7 +15,7 @@ router.post( isTextChannel(channel.type); if (!channel.guild_id) { - throw new HTTPError("This channel doesn't exist", 404); + throw new HTTPError(req.t("common:notfound.CHANNEL"), 404); } const { guild_id } = channel; @@ -46,7 +46,7 @@ router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, r const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); if (!channel.guild_id) { - throw new HTTPError("This channel doesn't exist", 404); + throw new HTTPError(req.t("common:notfound.CHANNEL"), 404); } const { guild_id } = channel; diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index 44de5c45..f13f5f94 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -63,7 +63,7 @@ router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: R const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); - if (!already_added) throw new HTTPError("Reaction not found", 404); + if (!already_added) throw new HTTPError(req.t("common:notfound.REACTION"), 404); message.reactions.remove(already_added); await Promise.all([ @@ -89,7 +89,7 @@ router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); - if (!reaction) throw new HTTPError("Reaction not found", 404); + if (!reaction) throw new HTTPError(req.t("common:notfound.REACTION"), 404); const users = await User.find({ where: { @@ -163,7 +163,7 @@ router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) } const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); - if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404); + if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError(req.t("common:notfound.REACTION"), 404); already_added.count--; diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 5fdcb6f9..87c689ec 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -6,17 +6,21 @@ import { Config, DmChannelDTO, emitEvent, + getIpAdress, getPermission, + getRights, HTTPError, Member, Message, MessageCreateEvent, MessageCreateSchema, + Rights, Snowflake, uploadFile } from "@fosscord/util"; import { Request, Response, Router } from "express"; import multer from "multer"; +import { red, yellow } from "picocolors"; import { FindManyOptions, LessThan, MoreThan } from "typeorm"; import { URL } from "url"; @@ -53,7 +57,7 @@ export function isTextChannel(type: ChannelType): boolean { router.get("/", async (req: Request, res: Response) => { const channel_id = req.params.channel_id; const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - if (!channel) throw new HTTPError("Channel not found", 404); + if (!channel) throw new HTTPError(req.t("common:notfound.CHANNEL"), 404); isTextChannel(channel.type); const around = req.query.around ? `${req.query.around}` : undefined; @@ -159,6 +163,46 @@ router.post( if (!channel.isWritable()) { throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400); } + var limits = Config.get().limits; + + if ( + !(await getRights(req.user_id)).has(Rights.FLAGS.BYPASS_RATE_LIMITS) && + limits.absoluteRate.sendMessage.enabled && + (await Message.count({ + where: { channel_id, timestamp: MoreThan(new Date(Date.now() - limits.absoluteRate.sendMessage.window)) } + })) >= limits.absoluteRate.sendMessage.limit + ) { + console.log( + yellow( + `[MESSAGE] Global register rate limit exceeded for ${getIpAdress(req)}: ${channel_id}, ${req.user_id}, ${body.content}` + ) + ); + let oldest = await Message.findOne({ + where: { channel_id, timestamp: MoreThan(new Date(Date.now() - limits.absoluteRate.sendMessage.window)) }, + order: { timestamp: "ASC" } + }); + if (!oldest) { + console.warn( + red( + `[MESSAGE/WARN] Global rate limits exceeded, but no oldest message found. This should not happen. Did you misconfigure the limits?` + ) + ); + } else { + let retryAfterSec = Math.ceil( + (oldest!.timestamp.getTime() - new Date(Date.now() - limits.absoluteRate.sendMessage.window).getTime()) / 1000 + ); + return res + .status(429) + .set("X-RateLimit-Limit", `${limits.absoluteRate.sendMessage.limit}`) + .set("X-RateLimit-Remaining", "0") + .set("X-RateLimit-Reset", `${(oldest!.timestamp.getTime() + limits.absoluteRate.sendMessage.window) / 1000}`) + .set("X-RateLimit-Reset-After", `${retryAfterSec}`) + .set("X-RateLimit-Global", `false`) + .set("Retry-After", `${retryAfterSec}`) + .set("X-RateLimit-Bucket", `chnl_${channel_id}`) + .send({ message: req.t("common:toomany.MESSAGE"), retry_after: retryAfterSec, global: false }); + } + } const files = (req.files as Express.Multer.File[]) ?? []; for (let currFile of files) { diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts index bd462ea6..6b888c80 100644 --- a/src/api/routes/channels/#channel_id/permissions.ts +++ b/src/api/routes/channels/#channel_id/permissions.ts @@ -21,12 +21,12 @@ router.put( const body = req.body as ChannelPermissionOverwriteSchema; let channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - if (!channel.guild_id) throw new HTTPError("Channel not found", 404); + if (!channel.guild_id) throw new HTTPError(req.t("common:notfound.CHANNEL"), 404); if (body.type === 0) { - if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError("role not found", 404); + if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError(req.t("common:notfound.ROLE"), 404); } else if (body.type === 1) { - if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError("user not found", 404); + if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError(req.t("common:notfound.USER"), 404); } else throw new HTTPError("type not supported", 501); // @ts-ignore @@ -60,7 +60,7 @@ router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (re const { channel_id, overwrite_id } = req.params; const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); - if (!channel.guild_id) throw new HTTPError("Channel not found", 404); + if (!channel.guild_id) throw new HTTPError(req.t("common:notfound.CHANNEL"), 404); channel.permission_overwrites = channel.permission_overwrites!.filter((x) => x.id === overwrite_id); diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 38dcb869..14e14c8b 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -23,7 +23,7 @@ router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOO let { avatar, name } = req.body as { name: string; avatar?: string }; name = trimSpecial(name); - if (name === "clyde") throw new HTTPError("Invalid name", 400); + if (name.toLowerCase() === "clyde") throw new HTTPError("Invalid name", 400); // TODO: save webhook in database and send response res.json(new Webhook()); diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 4600b4cb..4963d36a 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -1,10 +1,11 @@ -import { getIpAdress, route } from "@fosscord/api"; +import { route } from "@fosscord/api"; import { Ban, BanModeratorSchema, BanRegistrySchema, DiscordApiErrors, emitEvent, + getIpAdress, GuildBanAddEvent, GuildBanRemoveEvent, HTTPError, diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index aa57ec65..4a5f5eca 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -1,5 +1,5 @@ -import { getIpAdress, getVoiceRegions, route } from "@fosscord/api"; -import { Guild } from "@fosscord/util"; +import { getVoiceRegions, route } from "@fosscord/api"; +import { getIpAdress, Guild } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts index 15741780..06dade89 100644 --- a/src/api/routes/guilds/#guild_id/stickers.ts +++ b/src/api/routes/guilds/#guild_id/stickers.ts @@ -37,7 +37,7 @@ router.post( bodyParser, route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }), async (req: Request, res: Response) => { - if (!req.file) throw new HTTPError("missing file"); + if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE")); const { guild_id } = req.params; const body = req.body as ModifyGuildStickerSchema; diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts index 448ee033..af116760 100644 --- a/src/api/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts @@ -1,5 +1,5 @@ -import { generateCode, route } from "@fosscord/api"; -import { Guild, HTTPError, OrmUtils, Template } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { generateCode, Guild, HTTPError, OrmUtils, Template } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts index 368fe46e..66cc456f 100644 --- a/src/api/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts @@ -1,5 +1,5 @@ -import { random, route } from "@fosscord/api"; -import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions, random } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts index 8267c142..6383f3f3 100644 --- a/src/api/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts @@ -69,7 +69,7 @@ router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, router.delete("/:id", route({}), async (req: Request, res: Response) => { const { id } = req.params; - if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend"); + if (id === req.user_id) throw new HTTPError(req.t("common:relationship.REMOVE_SELF")); const user = await User.findOneOrFail({ where: { id: req.user_id }, select: userProjection, relations: ["relationships"] }); const friend = await User.findOneOrFail({ where: { id: id }, select: userProjection, relations: ["relationships"] }); @@ -77,7 +77,7 @@ router.delete("/:id", route({}), async (req: Request, res: Response) => { const relationship = user.relationships.find((x) => x.to_id === id); const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id); - if (!relationship) throw new HTTPError("You are not friends with the user", 404); + if (!relationship) throw new HTTPError(req.t("common:relationship.NOT_FRIENDS")); if (relationship?.type === RelationshipType.blocked) { // unblock user @@ -118,7 +118,7 @@ export default router; async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) { const id = friend.id; - if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); + if (id === req.user_id) throw new HTTPError(req.t("common:relationship.ADD_SELF")); const user = await User.findOneOrFail({ where: { id: req.user_id }, @@ -132,7 +132,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ // TODO: you can add infinitely many blocked users (should this be prevented?) if (type === RelationshipType.blocked) { if (relationship) { - if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user"); + if (relationship.type === RelationshipType.blocked) throw new HTTPError(req.t("common:relationship.ALREADY_BLOCKED")); relationship.type = RelationshipType.blocked; await relationship.save(); } else { @@ -178,17 +178,18 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ }); if (friendRequest) { - if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); - if (friendRequest.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user"); + //TODO: shouldn't this be failed silently? + if (friendRequest.type === RelationshipType.blocked) throw new HTTPError(req.t("common:relationship.BLOCKED")); + if (friendRequest.type === RelationshipType.friends) throw new HTTPError(req.t("common:relationship.ALREADY_FRIENDS")); // accept friend request incoming_relationship = friendRequest as any; //TODO: checkme, any cast incoming_relationship.type = RelationshipType.friends; } if (relationship) { - if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request"); - if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request"); - if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user"); + if (relationship.type === RelationshipType.outgoing) throw new HTTPError(req.t("common:relationship.ALREADY_SENT")); + if (relationship.type === RelationshipType.blocked) throw new HTTPError(req.t("common:relationship.UNBLOCK")); + if (relationship.type === RelationshipType.friends) throw new HTTPError(req.t("common:relationship.ALREADY_FRIENDS")); outgoing_relationship = relationship as any; //TODO: checkme, any cast outgoing_relationship.type = RelationshipType.friends; } diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts index eacdcf11..1b5541e7 100644 --- a/src/api/routes/voice/regions.ts +++ b/src/api/routes/voice/regions.ts @@ -1,4 +1,5 @@ -import { getIpAdress, getVoiceRegions, route } from "@fosscord/api"; +import { getVoiceRegions, route } from "@fosscord/api"; +import { getIpAdress } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts index 4d60eb91..98d28ff0 100644 --- a/src/api/util/handlers/Voice.ts +++ b/src/api/util/handlers/Voice.ts @@ -1,5 +1,4 @@ -import { Config } from "@fosscord/util"; -import { distanceBetweenLocations, IPAnalysis } from "../utility/ipAddress"; +import { Config, distanceBetweenLocations, IPAnalysis } from "@fosscord/util"; export async function getVoiceRegions(ipAddress: string, vip: boolean) { const regions = Config.get().regions; diff --git a/src/api/util/index.ts b/src/api/util/index.ts index d06860cd..46cbd5ba 100644 --- a/src/api/util/index.ts +++ b/src/api/util/index.ts @@ -2,9 +2,3 @@ export * from "./entities/AssetCacheItem"; export * from "./handlers/Message"; export * from "./handlers/route"; export * from "./handlers/Voice"; -export * from "./utility/Base64"; -export * from "./utility/captcha"; -export * from "./utility/ipAddress"; -export * from "./utility/passwordStrength"; -export * from "./utility/RandomInviteID"; -export * from "./utility/String"; diff --git a/src/api/util/utility/String.ts b/src/api/util/utility/String.ts deleted file mode 100644 index a2e491e4..00000000 --- a/src/api/util/utility/String.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FieldErrors } from "@fosscord/util"; -import { Request } from "express"; -import { ntob } from "./Base64"; - -export function checkLength(str: string, min: number, max: number, key: string, req: Request) { - if (str.length < min || str.length > max) { - throw FieldErrors({ - [key]: { - code: "BASE_TYPE_BAD_LENGTH", - message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` }) - } - }); - } -} - -export function generateCode() { - return ntob(Date.now() + Math.randomIntBetween(0, 10000)); -} diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index 013f03d8..530d3a8b 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -10,8 +10,9 @@ const router = Router(); const SANITIZED_CONTENT_TYPE = ["text/html", "text/mhtml", "multipart/related", "application/xhtml+xml"]; router.post("/:channel_id", multer.single("file"), async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); - if (!req.file) throw new HTTPError("file missing"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); + if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE")); const { buffer, mimetype, size, originalname, fieldname } = req.file; const { channel_id } = req.params; @@ -49,7 +50,7 @@ router.get("/:channel_id/:id/:filename", async (req: Request, res: Response) => const { channel_id, id, filename } = req.params; const file = await storage.get(`attachments/${channel_id}/${id}/${filename}`); - if (!file) throw new HTTPError("File not found"); + if (!file) throw new HTTPError(req.t("common:notfound.FILE")); const type = await FileType.fromBuffer(file); let content_type = type?.mime || "application/octet-stream"; @@ -64,7 +65,8 @@ router.get("/:channel_id/:id/:filename", async (req: Request, res: Response) => }); router.delete("/:channel_id/:id/:filename", async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); const { channel_id, id, filename } = req.params; const path = `attachments/${channel_id}/${id}/${filename}`; diff --git a/src/cdn/routes/avatars.ts b/src/cdn/routes/avatars.ts index fa26938f..2fa88987 100644 --- a/src/cdn/routes/avatars.ts +++ b/src/cdn/routes/avatars.ts @@ -17,8 +17,9 @@ const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES]; const router = Router(); router.post("/:user_id", multer.single("file"), async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); - if (!req.file) throw new HTTPError("Missing file"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); + if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE")); const { buffer, mimetype, size, originalname, fieldname } = req.file; const { user_id } = req.params; @@ -47,7 +48,7 @@ router.get("/:user_id", async (req: Request, res: Response) => { const path = `avatars/${user_id}`; const file = await storage.get(path); - if (!file) throw new HTTPError("not found", 404); + if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404); const type = await FileType.fromBuffer(file); res.set("Content-Type", type?.mime); @@ -62,7 +63,7 @@ router.get("/:user_id/:hash", async (req: Request, res: Response) => { const path = `avatars/${user_id}/${hash}`; const file = await storage.get(path); - if (!file) throw new HTTPError("not found", 404); + if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404); const type = await FileType.fromBuffer(file); res.set("Content-Type", type?.mime); @@ -72,7 +73,8 @@ router.get("/:user_id/:hash", async (req: Request, res: Response) => { }); router.delete("/:user_id/:id", async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); const { user_id, id } = req.params; const path = `avatars/${user_id}/${id}`; diff --git a/src/cdn/routes/external.ts b/src/cdn/routes/external.ts index 7ccf9b8a..43c4e505 100644 --- a/src/cdn/routes/external.ts +++ b/src/cdn/routes/external.ts @@ -19,7 +19,8 @@ const DEFAULT_FETCH_OPTIONS: any = { }; router.post("/", async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); if (!req.body) throw new HTTPError("Invalid Body"); @@ -44,7 +45,7 @@ router.get("/:id", async (req: Request, res: Response) => { const { id } = req.params; const file = await storage.get(`/external/${id}`); - if (!file) throw new HTTPError("File not found"); + if (!file) throw new HTTPError(req.t("common:notfound.FILE")); const result = await FileType.fromBuffer(file); res.set("Content-Type", result?.mime); diff --git a/src/cdn/routes/guild-profiles.ts b/src/cdn/routes/guild-profiles.ts index 32c05ad9..4ae492ea 100644 --- a/src/cdn/routes/guild-profiles.ts +++ b/src/cdn/routes/guild-profiles.ts @@ -17,8 +17,9 @@ const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES]; const router = Router(); router.post("/", multer.single("file"), async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); - if (!req.file) throw new HTTPError("Missing file"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); + if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE")); const { buffer, mimetype, size, originalname, fieldname } = req.file; const { guild_id, user_id } = req.params; @@ -47,7 +48,7 @@ router.get("/", async (req: Request, res: Response) => { const path = `guilds/${guild_id}/users/${user_id}/avatars`; const file = await storage.get(path); - if (!file) throw new HTTPError("not found", 404); + if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404); const type = await FileType.fromBuffer(file); res.set("Content-Type", type?.mime); @@ -62,7 +63,7 @@ router.get("/:hash", async (req: Request, res: Response) => { const path = `guilds/${guild_id}/users/${user_id}/avatars/${hash}`; const file = await storage.get(path); - if (!file) throw new HTTPError("not found", 404); + if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404); const type = await FileType.fromBuffer(file); res.set("Content-Type", type?.mime); @@ -72,7 +73,8 @@ router.get("/:hash", async (req: Request, res: Response) => { }); router.delete("/:id", async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); const { guild_id, user_id, id } = req.params; const path = `guilds/${guild_id}/users/${user_id}/avatars/${id}`; diff --git a/src/cdn/routes/role-icons.ts b/src/cdn/routes/role-icons.ts index 768e194f..b0946eaa 100644 --- a/src/cdn/routes/role-icons.ts +++ b/src/cdn/routes/role-icons.ts @@ -17,8 +17,9 @@ const ALLOWED_MIME_TYPES = [...STATIC_MIME_TYPES]; const router = Router(); router.post("/:role_id", multer.single("file"), async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); - if (!req.file) throw new HTTPError("Missing file"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); + if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE")); const { buffer, mimetype, size, originalname, fieldname } = req.file; const { role_id } = req.params; @@ -46,7 +47,7 @@ router.get("/:role_id", async (req: Request, res: Response) => { const path = `role-icons/${role_id}`; const file = await storage.get(path); - if (!file) throw new HTTPError("not found", 404); + if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404); const type = await FileType.fromBuffer(file); res.set("Content-Type", type?.mime); @@ -61,7 +62,7 @@ router.get("/:role_id/:hash", async (req: Request, res: Response) => { const path = `role-icons/${role_id}/${hash}`; const file = await storage.get(path); - if (!file) throw new HTTPError("not found", 404); + if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404); const type = await FileType.fromBuffer(file); res.set("Content-Type", type?.mime); @@ -71,7 +72,8 @@ router.get("/:role_id/:hash", async (req: Request, res: Response) => { }); router.delete("/:role_id/:id", async (req: Request, res: Response) => { - if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + if (req.headers.signature !== Config.get().security.requestSignature) + throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE")); const { role_id, id } = req.params; const path = `role-icons/${role_id}/${id}`; diff --git a/src/util/config/types/GeneralConfiguration.ts b/src/util/config/types/GeneralConfiguration.ts index 5cb8df89..6d030645 100644 --- a/src/util/config/types/GeneralConfiguration.ts +++ b/src/util/config/types/GeneralConfiguration.ts @@ -3,6 +3,7 @@ import { Snowflake } from "../../util"; export class GeneralConfiguration { instanceName: string = "Fosscord Instance"; instanceDescription: string | null = "This is a Fosscord instance made in the pre-release days"; + publicUrl: string = "http://localhost:3001"; frontPage: string | null = null; tosPage: string | null = null; correspondenceEmail: string | null = "noreply@localhost.local"; diff --git a/src/util/config/types/LimitConfigurations.ts b/src/util/config/types/LimitConfigurations.ts index a3a52cf5..105fd1d6 100644 --- a/src/util/config/types/LimitConfigurations.ts +++ b/src/util/config/types/LimitConfigurations.ts @@ -1,4 +1,4 @@ -import { ChannelLimits, GuildLimits, MessageLimits, RateLimits, UserLimits } from "."; +import { ChannelLimits, GlobalRateLimits, GuildLimits, MessageLimits, RateLimits, UserLimits } from "."; export class LimitsConfiguration { user: UserLimits = new UserLimits(); @@ -6,4 +6,5 @@ export class LimitsConfiguration { message: MessageLimits = new MessageLimits(); channel: ChannelLimits = new ChannelLimits(); rate: RateLimits = new RateLimits(); + absoluteRate: GlobalRateLimits = new GlobalRateLimits(); } diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts index 68946272..caeab123 100644 --- a/src/util/config/types/RegisterConfiguration.ts +++ b/src/util/config/types/RegisterConfiguration.ts @@ -12,7 +12,6 @@ export class RegisterConfiguration { allowGuests: boolean = true; guestsRequireInvite: boolean = true; allowNewRegistration: boolean = true; - allowMultipleAccounts: boolean = true; blockProxies: boolean = true; incrementingDiscriminators: boolean = false; // random otherwise defaultRights: string = "0"; diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index 5a3d5aa6..229587c3 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -17,4 +17,5 @@ export class SecurityConfiguration { mfaBackupCodeCount: number = 10; mfaBackupCodeBytes: number = 4; statsWorldReadable: boolean = true; + defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week } diff --git a/src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts b/src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts new file mode 100644 index 00000000..00526fb4 --- /dev/null +++ b/src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts @@ -0,0 +1,10 @@ +export class GlobalRateLimits { + register: GlobalRateLimit = { limit: 25, window: 60 * 60 * 1000, enabled: true }; + sendMessage: GlobalRateLimit = { limit: 120, window: 60 * 1000, enabled: true }; +} + +export class GlobalRateLimit { + limit: number = 100; + window: number = 60 * 60 * 1000; + enabled: boolean = true; +} diff --git a/src/util/config/types/subconfigurations/limits/index.ts b/src/util/config/types/subconfigurations/limits/index.ts index a4911542..d2d24ccb 100644 --- a/src/util/config/types/subconfigurations/limits/index.ts +++ b/src/util/config/types/subconfigurations/limits/index.ts @@ -1,4 +1,5 @@ export * from "./ChannelLimits"; +export * from "./GlobalRateLimits"; export * from "./GuildLimits"; export * from "./MessageLimits"; export * from "./RateLimits"; diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts index 8392f415..c0ea3dec 100644 --- a/src/util/entities/Attachment.ts +++ b/src/util/entities/Attachment.ts @@ -1,6 +1,6 @@ import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { URL } from "url"; -import { deleteFile } from "../util/cdn"; +import { deleteFile } from "../util/CDN"; import { BaseClass } from "./BaseClass"; @Entity("attachments") diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts index f6ba85d7..151fcc59 100644 --- a/src/util/entities/Invite.ts +++ b/src/util/entities/Invite.ts @@ -1,4 +1,4 @@ -import { random } from "@fosscord/api"; +import { random } from "@fosscord/util"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm"; import { BaseClassWithoutId } from "./BaseClass"; import { Channel } from "./Channel"; diff --git a/src/util/entities/ValidRegistrationTokens.ts b/src/util/entities/ValidRegistrationTokens.ts new file mode 100644 index 00000000..5d0747b8 --- /dev/null +++ b/src/util/entities/ValidRegistrationTokens.ts @@ -0,0 +1,12 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm"; +import { Config } from ".."; + +@Entity("valid_registration_tokens") +export class ValidRegistrationToken extends BaseEntity { + @PrimaryColumn() + token: string; + @Column() + created_at: Date = new Date(); + @Column() + expires_at: Date = new Date(Date.now() + Config.get().security.defaultRegistrationTokenExpiration); +} diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index 2b91c2ba..5342d951 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -29,5 +29,6 @@ export * from "./TeamMember"; export * from "./Template"; export * from "./User"; export * from "./UserSettings"; +export * from "./ValidRegistrationTokens"; export * from "./VoiceState"; export * from "./Webhook"; diff --git a/src/util/migrations/mariadb/1663440589234-registration_tokens.ts b/src/util/migrations/mariadb/1663440589234-registration_tokens.ts new file mode 100644 index 00000000..364f8668 --- /dev/null +++ b/src/util/migrations/mariadb/1663440589234-registration_tokens.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class registrationTokens1663440589234 implements MigrationInterface { + name = "registrationTokens1663440589234"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + CREATE TABLE \`valid_registration_tokens\` ( + \`id\` varchar(255) NOT NULL, + \`token\` varchar(255) NOT NULL, + \`created_at\` datetime NOT NULL, + \`expires_at\` datetime NOT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP COLUMN \`notes\` + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`users\` + ADD \`notes\` text NOT NULL + `); + await queryRunner.query(` + DROP TABLE \`valid_registration_tokens\` + `); + } +} diff --git a/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts b/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts new file mode 100644 index 00000000..58743d07 --- /dev/null +++ b/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class dropIdForRegistrationTokens1663448562034 implements MigrationInterface { + name = "dropIdForRegistrationTokens1663448562034"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` DROP PRIMARY KEY + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` DROP COLUMN \`id\` + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` + ADD PRIMARY KEY (\`token\`) + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` DROP PRIMARY KEY + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` + ADD \`id\` varchar(255) NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` + ADD PRIMARY KEY (\`id\`) + `); + } +} diff --git a/src/util/migrations/postgres/1663440587650-registration_tokens.ts b/src/util/migrations/postgres/1663440587650-registration_tokens.ts new file mode 100644 index 00000000..d5f602b8 --- /dev/null +++ b/src/util/migrations/postgres/1663440587650-registration_tokens.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class registrationTokens1663440587650 implements MigrationInterface { + name = "registrationTokens1663440587650"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "id" character varying NOT NULL, + "token" character varying NOT NULL, + "created_at" TIMESTAMP NOT NULL, + "expires_at" TIMESTAMP NOT NULL, + CONSTRAINT "PK_aac42a46cd46369450217de1c8a" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + ALTER TABLE "members" + ALTER COLUMN "bio" DROP DEFAULT + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "members" + ALTER COLUMN "bio" + SET DEFAULT '' + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + } +} diff --git a/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts b/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts new file mode 100644 index 00000000..4dc8c6ba --- /dev/null +++ b/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class dropIdForRegistrationTokens1663448561249 implements MigrationInterface { + name = "dropIdForRegistrationTokens1663448561249"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" DROP CONSTRAINT "PK_aac42a46cd46369450217de1c8a" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" DROP COLUMN "id" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + ADD CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" PRIMARY KEY ("token") + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" DROP CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + ADD "id" character varying NOT NULL + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + ADD CONSTRAINT "PK_aac42a46cd46369450217de1c8a" PRIMARY KEY ("id") + `); + } +} diff --git a/src/util/migrations/sqlite/1663440585960-registration_tokens.ts b/src/util/migrations/sqlite/1663440585960-registration_tokens.ts new file mode 100644 index 00000000..520977c7 --- /dev/null +++ b/src/util/migrations/sqlite/1663440585960-registration_tokens.ts @@ -0,0 +1,245 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class registrationTokens1663440585960 implements MigrationInterface { + name = "registrationTokens1663440585960"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "id" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + CREATE TABLE "temporary_users" ( + "id" varchar PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "discriminator" varchar NOT NULL, + "avatar" varchar, + "accent_color" integer, + "banner" varchar, + "phone" varchar, + "desktop" boolean NOT NULL, + "mobile" boolean NOT NULL, + "premium" boolean NOT NULL, + "premium_type" integer NOT NULL, + "bot" boolean NOT NULL, + "bio" varchar, + "system" boolean NOT NULL, + "nsfw_allowed" boolean NOT NULL, + "mfa_enabled" boolean, + "totp_secret" varchar, + "totp_last_ticket" varchar, + "created_at" datetime NOT NULL, + "premium_since" datetime, + "verified" boolean NOT NULL, + "disabled" boolean NOT NULL, + "deleted" boolean NOT NULL, + "email" varchar, + "flags" varchar NOT NULL, + "public_flags" integer NOT NULL, + "rights" bigint NOT NULL, + "data" text NOT NULL, + "fingerprints" text NOT NULL, + "extended_settings" text NOT NULL, + "settingsId" varchar, + CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"), + CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_users"( + "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + ) + SELECT "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + FROM "users" + `); + await queryRunner.query(` + DROP TABLE "users" + `); + await queryRunner.query(` + ALTER TABLE "temporary_users" + RENAME TO "users" + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "users" + RENAME TO "temporary_users" + `); + await queryRunner.query(` + CREATE TABLE "users" ( + "id" varchar PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "discriminator" varchar NOT NULL, + "avatar" varchar, + "accent_color" integer, + "banner" varchar, + "phone" varchar, + "desktop" boolean NOT NULL, + "mobile" boolean NOT NULL, + "premium" boolean NOT NULL, + "premium_type" integer NOT NULL, + "bot" boolean NOT NULL, + "bio" varchar, + "system" boolean NOT NULL, + "nsfw_allowed" boolean NOT NULL, + "mfa_enabled" boolean, + "totp_secret" varchar, + "totp_last_ticket" varchar, + "created_at" datetime NOT NULL, + "premium_since" datetime, + "verified" boolean NOT NULL, + "disabled" boolean NOT NULL, + "deleted" boolean NOT NULL, + "email" varchar, + "flags" varchar NOT NULL, + "public_flags" integer NOT NULL, + "rights" bigint NOT NULL, + "data" text NOT NULL, + "fingerprints" text NOT NULL, + "extended_settings" text NOT NULL, + "notes" text NOT NULL, + "settingsId" varchar, + CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"), + CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "users"( + "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + ) + SELECT "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + FROM "temporary_users" + `); + await queryRunner.query(` + DROP TABLE "temporary_users" + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + } +} diff --git a/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts b/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts new file mode 100644 index 00000000..f99f2348 --- /dev/null +++ b/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts @@ -0,0 +1,96 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class dropIdForRegistrationTokens1663448560501 implements MigrationInterface { + name = "dropIdForRegistrationTokens1663448560501"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + CREATE TABLE "temporary_valid_registration_tokens" ( + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + await queryRunner.query(` + ALTER TABLE "temporary_valid_registration_tokens" + RENAME TO "valid_registration_tokens" + `); + await queryRunner.query(` + CREATE TABLE "temporary_valid_registration_tokens" ( + "token" varchar PRIMARY KEY NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + await queryRunner.query(` + ALTER TABLE "temporary_valid_registration_tokens" + RENAME TO "valid_registration_tokens" + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + RENAME TO "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + RENAME TO "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "id" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "temporary_valid_registration_tokens" + `); + } +} diff --git a/src/api/util/utility/Base64.ts b/src/util/util/Base64.ts index 46cff77a..46cff77a 100644 --- a/src/api/util/utility/Base64.ts +++ b/src/util/util/Base64.ts diff --git a/src/util/util/cdn.ts b/src/util/util/CDN.ts index 5573b848..5573b848 100644 --- a/src/util/util/cdn.ts +++ b/src/util/util/CDN.ts diff --git a/src/api/util/utility/captcha.ts b/src/util/util/Captcha.ts index 02983f3f..02983f3f 100644 --- a/src/api/util/utility/captcha.ts +++ b/src/util/util/Captcha.ts diff --git a/src/api/util/utility/ipAddress.ts b/src/util/util/IPAddress.ts index c96feb9e..c96feb9e 100644 --- a/src/api/util/utility/ipAddress.ts +++ b/src/util/util/IPAddress.ts diff --git a/src/api/util/utility/passwordStrength.ts b/src/util/util/PasswordStrength.ts index ff83d3df..ff83d3df 100644 --- a/src/api/util/utility/passwordStrength.ts +++ b/src/util/util/PasswordStrength.ts diff --git a/src/api/util/utility/RandomInviteID.ts b/src/util/util/RandomInviteID.ts index feebfd3d..49302916 100644 --- a/src/api/util/utility/RandomInviteID.ts +++ b/src/util/util/RandomInviteID.ts @@ -1,13 +1,13 @@ import { Snowflake } from "@fosscord/util"; +import crypto from "crypto"; -export function random(length = 6) { +export function random(length = 6, chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") { // Declare all characters - let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // Pick characers randomly let str = ""; for (let i = 0; i < length; i++) { - str += chars.charAt(Math.floor(Math.random() * chars.length)); + str += chars.charAt(Math.floor(crypto.randomInt(chars.length))); } return str; diff --git a/src/util/util/String.ts b/src/util/util/String.ts index 55f11e8d..b14cd257 100644 --- a/src/util/util/String.ts +++ b/src/util/util/String.ts @@ -1,5 +1,23 @@ +import { FieldErrors } from "@fosscord/util"; +import { Request } from "express"; +import { ntob } from "./Base64"; import { SPECIAL_CHAR } from "./Regex"; +export function checkLength(str: string, min: number, max: number, key: string, req: Request) { + if (str.length < min || str.length > max) { + throw FieldErrors({ + [key]: { + code: "BASE_TYPE_BAD_LENGTH", + message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` }) + } + }); + } +} + +export function generateCode() { + return ntob(Date.now() + Math.randomIntBetween(0, 10000)); +} + export function trimSpecial(str?: string): string { // @ts-ignore if (!str) return; diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 11f0b72a..e10dc563 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -1,8 +1,9 @@ export * from "./ApiError"; export * from "./Array"; export * from "./BitField"; +export * from "./Captcha"; //export * from "./Categories"; -export * from "./cdn"; +export * from "./CDN"; export * from "./Config"; export * from "./Constants"; export * from "./Database"; @@ -14,9 +15,11 @@ export * from "./imports/index"; export * from "./imports/OrmUtils"; export * from "./Intents"; export * from "./InvisibleCharacters"; +export * from "./IPAddress"; export * from "./MessageFlags"; export * from "./Permissions"; export * from "./RabbitMQ"; +export * from "./RandomInviteID"; export * from "./Regex"; export * from "./Rights"; export * from "./Snowflake"; |