diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 0d3146324..560d1ec31 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -3,7 +3,7 @@ import * as UserDAL from "../../dal/user"; import MonkeyError from "../../utils/error"; import Logger from "../../utils/logger"; import { MonkeyResponse } from "../../utils/monkey-response"; -import { getDiscordUser } from "../../utils/discord"; +import * as DiscordUtils from "../../utils/discord"; import { buildAgentLog, sanitizeString } from "../../utils/misc"; import * as George from "../../tasks/george"; import admin from "firebase-admin"; @@ -14,6 +14,8 @@ import { deleteConfig } from "../../dal/config"; import { verify } from "../../utils/captcha"; import * as LeaderboardsDAL from "../../dal/leaderboards"; import { purgeUserFromDailyLeaderboards } from "../../utils/daily-leaderboards"; +import { randomBytes } from "crypto"; +import * as RedisClient from "../../init/redis"; async function verifyCaptcha(captcha: string): Promise { if (!(await verify(captcha))) { @@ -210,21 +212,52 @@ export async function getUser( return new MonkeyResponse("User data retrieved", userData); } +export async function getOauthLink( + req: MonkeyTypes.Request +): Promise { + const connection = RedisClient.getConnection(); + if (!connection) { + throw new MonkeyError(500, "Redis connection not found"); + } + + const { uid } = req.ctx.decodedToken; + const token = randomBytes(10).toString("hex"); + + //add the token uid pair to reids + await connection.setex(`discordoauth:${uid}`, 60, token); + + //build the url + const url = DiscordUtils.getOauthLink(); + + //return + return new MonkeyResponse("Discord oauth link generated", { + url: `${url}&state=${token}`, + }); +} + export async function linkDiscord( req: MonkeyTypes.Request ): Promise { + const connection = RedisClient.getConnection(); + if (!connection) { + throw new MonkeyError(500, "Redis connection not found"); + } const { uid } = req.ctx.decodedToken; - const { tokenType, accessToken } = req.body; + const { tokenType, accessToken, state } = req.body; + + const redisToken = await connection.getdel(`discordoauth:${uid}`); + + if (!redisToken || redisToken !== state) { + throw new MonkeyError(403, "Invalid user token"); + } const userInfo = await UserDAL.getUser(uid, "link discord"); if (userInfo.banned) { throw new MonkeyError(403, "Banned accounts cannot link with Discord"); } - const { id: discordId, avatar: discordAvatar } = await getDiscordUser( - tokenType, - accessToken - ); + const { id: discordId, avatar: discordAvatar } = + await DiscordUtils.getDiscordUser(tokenType, accessToken); if (userInfo.discordId) { await UserDAL.linkDiscord(uid, userInfo.discordId, discordAvatar); diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts index c3750e1b4..f349e55d3 100644 --- a/backend/src/api/routes/users.ts +++ b/backend/src/api/routes/users.ts @@ -343,6 +343,14 @@ const requireDiscordIntegrationEnabled = validateConfiguration({ invalidMessage: "Discord integration is not available at this time", }); +router.get( + "/discord/oauth", + requireDiscordIntegrationEnabled, + authenticateRequest(), + RateLimit.userDiscordLink, + asyncHandler(UserController.getOauthLink) +); + router.post( "/discord/link", requireDiscordIntegrationEnabled, @@ -352,6 +360,7 @@ router.post( body: { tokenType: joi.string().required(), accessToken: joi.string().required(), + state: joi.string().length(20).required(), }, }), asyncHandler(UserController.linkDiscord) diff --git a/backend/src/utils/discord.ts b/backend/src/utils/discord.ts index 090e0d9d4..187e8a440 100644 --- a/backend/src/utils/discord.ts +++ b/backend/src/utils/discord.ts @@ -32,3 +32,11 @@ export async function getDiscordUser( return (await response.json()) as DiscordUser; } + +export function getOauthLink(): string { + return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${ + process.env.MODE === "dev" + ? `http%3A%2F%2Flocalhost%3A3000%2Fverify` + : `https%3A%2F%2Fmonkeytype.com%2Fverify` + }&response_type=token&scope=identify`; +} diff --git a/frontend/src/ts/ape/endpoints/users.ts b/frontend/src/ts/ape/endpoints/users.ts index 175ca0054..6e48b23c9 100644 --- a/frontend/src/ts/ape/endpoints/users.ts +++ b/frontend/src/ts/ape/endpoints/users.ts @@ -153,9 +153,17 @@ export default class Users { return await this.httpClient.post(`${BASE_PATH}/customThemes`, { payload }); } - async linkDiscord(tokenType: string, accessToken: string): Ape.EndpointData { + async getOauthLink(): Ape.EndpointData { + return await this.httpClient.get(`${BASE_PATH}/discord/oauth`); + } + + async linkDiscord( + tokenType: string, + accessToken: string, + state: string + ): Ape.EndpointData { return await this.httpClient.post(`${BASE_PATH}/discord/link`, { - payload: { tokenType, accessToken }, + payload: { tokenType, accessToken, state }, }); } diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 4e40d3197..17b5d5883 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -15,6 +15,7 @@ import * as ApeKeysPopup from "../popups/ape-keys-popup"; import * as CookiePopup from "../popups/cookie-popup"; import Page from "./page"; import { Auth } from "../firebase"; +import Ape from "../ape"; interface SettingsGroups { [key: string]: SettingsGroup; @@ -1104,6 +1105,15 @@ $(".pageSettings .section.autoSwitchThemeInputs").on( } ); +$(".pageSettings .section.discordIntegration .getLinkAndGoToOauth").on( + "click", + () => { + Ape.users.getOauthLink().then((res) => { + window.open(res.data.url, "_self"); + }); + } +); + let configEventDisabled = false; export function setEventDisabled(value: boolean): void { configEventDisabled = value; diff --git a/frontend/src/ts/ui.ts b/frontend/src/ts/ui.ts index 762b38c88..ee80b7e1c 100644 --- a/frontend/src/ts/ui.ts +++ b/frontend/src/ts/ui.ts @@ -47,16 +47,6 @@ if (window.location.hostname === "localhost") { $("body").append( `
local
local
` ); - $(".pageSettings .discordIntegration .buttons a").attr( - "href", - "https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fverify&response_type=token&scope=identify" - ); - $( - ".pageSettings .discordIntegration .info #discordButtonGroup #updateDiscordAvatarButton" - ).attr( - "href", - "https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fverify&response_type=token&scope=identify" - ); } //stop space scrolling diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index a9d82f178..601752c61 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -19,10 +19,10 @@ export async function linkDiscord(hashOverride: string): Promise { history.replaceState(null, "", "/"); const accessToken = fragment.get("access_token") as string; const tokenType = fragment.get("token_type") as string; + const state = fragment.get("state") as string; Loader.show(); - - const response = await Ape.users.linkDiscord(tokenType, accessToken); + const response = await Ape.users.linkDiscord(tokenType, accessToken, state); Loader.hide(); if (response.status !== 200) { diff --git a/frontend/static/html/pages/settings.html b/frontend/static/html/pages/settings.html index bbdb85115..e0a623f23 100644 --- a/frontend/static/html/pages/settings.html +++ b/frontend/static/html/pages/settings.html @@ -53,13 +53,7 @@ give you a role.
- - Link with Discord - +
Link with Discord