diff --git a/src/routes/api/v8/auth/register.ts b/src/routes/api/v8/auth/register.ts index 9a4e81fa..d9e2839a 100644 --- a/src/routes/api/v8/auth/register.ts +++ b/src/routes/api/v8/auth/register.ts @@ -1,4 +1,224 @@ -import { Router } from "express"; +import { Request, Response, Router } from "express"; +import Config from "../../../../util/Config"; +import db from "../../../../util/Database"; +import bcrypt from "bcrypt"; +import { check, Email, EMAIL_REGEX, FieldErrors } from "../../../../util/instanceOf"; +import { Snowflake } from "../../../../util/Snowflake"; +import "missing-native-js-functions"; +import { User } from "../../../../models/User"; +import { generateToken } from "./login"; const router: Router = Router(); +router.post( + "/", + check({ + username: String, + password: String, + consent: Boolean, + $email: Email, + $fingerprint: String, + $invite: String, + $date_of_birth: String, // "2000-04-03" + $gift_code_sku_id: String, + $captcha_key: String, + }), + async (req: Request, res: Response) => { + const { + email, + username, + password, + consent, + fingerprint, + invite, + date_of_birth, + gift_code_sku_id, // ? what is this + captcha_key, + } = req.body; + // TODO: automatically join invite + // TODO: gift_code_sku_id? + + let adjusted_email: string = email; + let adjusted_password: string = password; + let adjusted_username: string = username; + const { register } = Config.get(); + + if (!register.allowNewRegistration) { + throw FieldErrors({ + email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") }, + }); + } + + if (!consent) { + throw FieldErrors({ + consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") }, + }); + } + + if (register.requireInvite && !invite) { + throw FieldErrors({ + email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") }, + }); + } + + if (email) { + const parts = email.match(EMAIL_REGEX); + const domain = parts[5]; + const user = parts[1]; + + if (domain === "gmail.com") { + // replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator + adjusted_email = user.replace(/[.]|(\+.*)/g, ""); + } + + const exists = await db.data.users({ email: adjusted_email }).get(); + if (exists) { + throw FieldErrors({ + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t("auth.register.EMAIL_ALREADY_REGISTERED"), + }, + }); + } + } else if (register.email.required) { + throw FieldErrors({ + email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }, + }); + } + + if (register.dateOfBirth.required && !date_of_birth) { + throw FieldErrors({ + date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }, + }); + } else if (register.dateOfBirth.minimum) { + const minimum = new Date(); + minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); + + // higher is younger + if (date_of_birth > minimum) { + throw FieldErrors({ + date_of_birth: { + code: "DATE_OF_BIRTH_UNDERAGE", + message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE"), + }, + }); + } + } + + if (!register.allowMultipleAccounts) { + // TODO: check if fingerprint was eligible generated + const exists = await db.data.users({ fingerprint }).get(); + if (exists) { + throw FieldErrors({ + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t("auth:register.EMAIL_ALREADY_REGISTERED"), + }, + }); + } + } + + if (register.requireCaptcha) { + if (!captcha_key) { + const { sitekey, service } = Config.get().security.captcha; + return res + .status(400) + .json({ captcha_key: ["captcha-required"], captcha_sitekey: sitekey, captcha_service: service }); + } + + // TODO: check captcha + } + + adjusted_password = await bcrypt.hash(password, 12); + adjusted_username = username.replace(/[]/g, ""); + var discriminator = ""; + let exists; + // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database? + for (let tries = 5; tries >= 0; tries--) { + discriminator = Math.randomBetween(1, 9999).toString().padStart(4, "0"); + exists = await db.data.users({ discriminator, username: adjusted_username }).get({ id: true }); + if (!exists) break; + } + + if (exists) { + throw FieldErrors({ + username: { + code: "USERNAME_TOO_MANY_USERS", + message: req.t("auth:register.USERNAME_TOO_MANY_USERS"), + }, + }); + } + + const user: User = { + id: Snowflake.generate(), + created_at: Date.now(), + username: adjusted_username, + discriminator, + avatar: null, + bot: false, + system: false, + mfa_enabled: false, + verified: false, + email: adjusted_email, + flags: 0n, // TODO: generate default flags + hash: adjusted_password, + valid_tokens_since: Date.now(), + user_settings: { + afk_timeout: 300, + allow_accessibility_detection: true, + animate_emoji: true, + animate_stickers: 0, + contact_sync_enabled: false, + convert_emoticons: false, + custom_status: { + emoji_id: null, + emoji_name: null, + expires_at: null, + text: null, + }, + default_guilds_restricted: false, + detect_platform_accounts: true, + developer_mode: false, + disable_games_tab: false, + enable_tts_command: true, + explicit_content_filter: 0, + friend_source_flags: { all: true }, + gif_auto_play: true, + guild_folders: [], + guild_positions: [], + inline_attachment_media: true, + inline_embed_media: true, + locale: req.language, + message_display_compact: false, + native_phone_integration_enabled: true, + render_embeds: true, + render_reactions: true, + restricted_guilds: [], + show_current_game: true, + status: "offline", + stream_notifications_enabled: true, + theme: "dark", + timezone_offset: 0, + // timezone_offset: // TODO: timezone from request + }, + }; + + await db.data.users.push(user); + + const token = generateToken(user.id); + + return res.json({ token }); + } +); + export default router; + +/** + * POST /auth/register + * @argument { "fingerprint":"805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", "email":"qo8etzvaf@gmail.com", "username":"qp39gr98", "password":"wtp9gep9gw", "invite":null, "consent":true, "date_of_birth":"2000-04-04", "gift_code_sku_id":null, "captcha_key":null} + * + * Field Error + * @returns { "code": 50035, "errors": { "consent": { "_errors": [{ "code": "CONSENT_REQUIRED", "message": "You must agree to Discord's Terms of Service and Privacy Policy." }]}}, "message": "Invalid Form Body"} + * + * Success 201: + * @returns {token: "OMITTED"} + */