diff options
23 files changed, 205 insertions, 122 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 812888a3..9e41b453 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { checkToken, Config, Rights } from "@spacebar/util"; +import { checkToken, Rights } from "@spacebar/util"; import * as Sentry from "@sentry/node"; import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts index cb4f8180..b3ca1e9e 100644 --- a/src/api/routes/auth/reset.ts +++ b/src/api/routes/auth/reset.ts @@ -19,7 +19,6 @@ import { route } from "@spacebar/api"; import { checkToken, - Config, Email, FieldErrors, generateToken, diff --git a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts index 0d432c2b..d44cf314 100644 --- a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts +++ b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts @@ -22,7 +22,7 @@ const router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { // TODO: - const { connection_name, connection_id } = req.params; + // const { connection_name, connection_id } = req.params; res.sendStatus(204); }); diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts index 9031f3c8..789a7878 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts @@ -23,9 +23,9 @@ import { ConnectionStore, DiscordApiErrors, FieldErrors, + RefreshableConnection, } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import RefreshableConnection from "../../../../../../../util/connections/RefreshableConnection"; const router = Router(); // TODO: this route is only used for spotify, twitch, and youtube. (battlenet seems to be able to PUT, maybe others also) diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 7edc2e92..4fdfccb1 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { BattleNetSettings } from "./BattleNetSettings"; interface BattleNetConnectionUser { @@ -33,10 +33,10 @@ interface BattleNetConnectionUser { battletag: string; } -interface BattleNetErrorResponse { - error: string; - error_description: string; -} +// interface BattleNetErrorResponse { +// error: string; +// error_description: string; +// } export default class BattleNetConnection extends Connection { public readonly id = "battlenet"; @@ -47,17 +47,21 @@ export default class BattleNetConnection extends Connection { settings: BattleNetSettings = new BattleNetSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( - this.id, - this.settings, - ) as BattleNetSettings; + const settings = + ConnectionLoader.getConnectionConfig<BattleNetSettings>( + this.id, + this.settings, + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -85,8 +89,8 @@ export default class BattleNetConnection extends Connection { new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, redirect_uri: this.getRedirectUri(), }), ) @@ -115,8 +119,11 @@ export default class BattleNetConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); diff --git a/src/connections/Discord/index.ts b/src/connections/Discord/index.ts index 76de33be..731086f1 100644 --- a/src/connections/Discord/index.ts +++ b/src/connections/Discord/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { DiscordSettings } from "./DiscordSettings"; interface UserResponse { @@ -43,10 +43,13 @@ export default class DiscordConnection extends Connection { settings: DiscordSettings = new DiscordSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<DiscordSettings>( this.id, this.settings, - ) as DiscordSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { @@ -54,7 +57,7 @@ export default class DiscordConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("state", state); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("response_type", "code"); // controls whether, on repeated authorizations, the consent screen is shown @@ -82,8 +85,8 @@ export default class DiscordConnection extends Connection { }) .body( new URLSearchParams({ - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, grant_type: "authorization_code", code: code, redirect_uri: this.getRedirectUri(), @@ -114,8 +117,11 @@ export default class DiscordConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); diff --git a/src/connections/EpicGames/index.ts b/src/connections/EpicGames/index.ts index bd7c7eef..e5b2d336 100644 --- a/src/connections/EpicGames/index.ts +++ b/src/connections/EpicGames/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { EpicGamesSettings } from "./EpicGamesSettings"; export interface UserResponse { @@ -53,17 +53,21 @@ export default class EpicGamesConnection extends Connection { settings: EpicGamesSettings = new EpicGamesSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( - this.id, - this.settings, - ) as EpicGamesSettings; + const settings = + ConnectionLoader.getConnectionConfig<EpicGamesSettings>( + this.id, + this.settings, + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -127,8 +131,11 @@ export default class EpicGamesConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo[0].accountId); diff --git a/src/connections/Facebook/index.ts b/src/connections/Facebook/index.ts index 6ce722dd..2bf26f34 100644 --- a/src/connections/Facebook/index.ts +++ b/src/connections/Facebook/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { FacebookSettings } from "./FacebookSettings"; export interface FacebookErrorResponse { @@ -52,17 +52,20 @@ export default class FacebookConnection extends Connection { settings: FacebookSettings = new FacebookSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<FacebookSettings>( this.id, this.settings, - ) as FacebookSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("state", state); url.searchParams.append("response_type", "code"); @@ -73,8 +76,11 @@ export default class FacebookConnection extends Connection { getTokenUrl(code: string): string { const url = new URL(this.tokenUrl); - url.searchParams.append("client_id", this.settings.clientId!); - url.searchParams.append("client_secret", this.settings.clientSecret!); + url.searchParams.append("client_id", this.settings.clientId as string); + url.searchParams.append( + "client_secret", + this.settings.clientSecret as string, + ); url.searchParams.append("code", code); url.searchParams.append("redirect_uri", this.getRedirectUri()); return url.toString(); @@ -118,8 +124,11 @@ export default class FacebookConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index a675873f..25e5f89f 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { GitHubSettings } from "./GitHubSettings"; interface UserResponse { @@ -42,17 +42,20 @@ export default class GitHubConnection extends Connection { settings: GitHubSettings = new GitHubSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<GitHubSettings>( this.id, this.settings, - ) as GitHubSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -61,8 +64,11 @@ export default class GitHubConnection extends Connection { getTokenUrl(code: string): string { const url = new URL(this.tokenUrl); - url.searchParams.append("client_id", this.settings.clientId!); - url.searchParams.append("client_secret", this.settings.clientSecret!); + url.searchParams.append("client_id", this.settings.clientId as string); + url.searchParams.append( + "client_secret", + this.settings.clientSecret as string, + ); url.searchParams.append("code", code); return url.toString(); } @@ -105,8 +111,11 @@ export default class GitHubConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); diff --git a/src/connections/Reddit/index.ts b/src/connections/Reddit/index.ts index 191c6452..149cce02 100644 --- a/src/connections/Reddit/index.ts +++ b/src/connections/Reddit/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { RedditSettings } from "./RedditSettings"; export interface UserResponse { @@ -54,17 +54,20 @@ export default class RedditConnection extends Connection { settings: RedditSettings = new RedditSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<RedditSettings>( this.id, this.settings, - ) as RedditSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -124,8 +127,11 @@ export default class RedditConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts index 61b17366..ece404d8 100644 --- a/src/connections/Spotify/index.ts +++ b/src/connections/Spotify/index.ts @@ -22,9 +22,9 @@ import { ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, + RefreshableConnection, } from "@spacebar/util"; import wretch from "wretch"; -import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { SpotifySettings } from "./SpotifySettings"; export interface UserResponse { @@ -63,17 +63,20 @@ export default class SpotifyConnection extends RefreshableConnection { * So to prevent spamming the spotify api we disable the ability to refresh. */ this.refreshEnabled = false; - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<SpotifySettings>( this.id, this.settings, - ) as SpotifySettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -98,7 +101,9 @@ export default class SpotifyConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( @@ -129,7 +134,9 @@ export default class SpotifyConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( @@ -169,8 +176,11 @@ export default class SpotifyConnection extends RefreshableConnection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); diff --git a/src/connections/Twitch/index.ts b/src/connections/Twitch/index.ts index 6d679aa4..9a6cea35 100644 --- a/src/connections/Twitch/index.ts +++ b/src/connections/Twitch/index.ts @@ -22,9 +22,9 @@ import { ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, + RefreshableConnection, } from "@spacebar/util"; import wretch from "wretch"; -import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { TwitchSettings } from "./TwitchSettings"; interface TwitchConnectionUserResponse { @@ -55,17 +55,20 @@ export default class TwitchConnection extends RefreshableConnection { settings: TwitchSettings = new TwitchSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<TwitchSettings>( this.id, this.settings, - ) as TwitchSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -94,8 +97,8 @@ export default class TwitchConnection extends RefreshableConnection { new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, redirect_uri: this.getRedirectUri(), }), ) @@ -124,8 +127,8 @@ export default class TwitchConnection extends RefreshableConnection { .body( new URLSearchParams({ grant_type: "refresh_token", - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, refresh_token: refresh_token, }), ) @@ -148,7 +151,7 @@ export default class TwitchConnection extends RefreshableConnection { return wretch(url.toString()) .headers({ Authorization: `Bearer ${token}`, - "Client-Id": this.settings.clientId!, + "Client-Id": this.settings.clientId as string, }) .get() .json<TwitchConnectionUserResponse>() @@ -161,8 +164,11 @@ export default class TwitchConnection extends RefreshableConnection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.data[0].id); diff --git a/src/connections/Twitter/index.ts b/src/connections/Twitter/index.ts index aa48ca12..62fd7da1 100644 --- a/src/connections/Twitter/index.ts +++ b/src/connections/Twitter/index.ts @@ -22,9 +22,9 @@ import { ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, + RefreshableConnection, } from "@spacebar/util"; import wretch from "wretch"; -import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { TwitterSettings } from "./TwitterSettings"; interface TwitterUserResponse { @@ -40,10 +40,10 @@ interface TwitterUserResponse { }; } -interface TwitterErrorResponse { - error: string; - error_description: string; -} +// interface TwitterErrorResponse { +// error: string; +// error_description: string; +// } export default class TwitterConnection extends RefreshableConnection { public readonly id = "twitter"; @@ -55,17 +55,20 @@ export default class TwitterConnection extends RefreshableConnection { settings: TwitterSettings = new TwitterSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<TwitterSettings>( this.id, this.settings, - ) as TwitterSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -92,14 +95,16 @@ export default class TwitterConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, + client_id: this.settings.clientId as string, redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), @@ -126,14 +131,16 @@ export default class TwitterConnection extends RefreshableConnection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( new URLSearchParams({ grant_type: "refresh_token", refresh_token, - client_id: this.settings.clientId!, + client_id: this.settings.clientId as string, redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), @@ -163,8 +170,11 @@ export default class TwitterConnection extends RefreshableConnection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.data.id); diff --git a/src/connections/Xbox/index.ts b/src/connections/Xbox/index.ts index c592fd0b..935ff7ab 100644 --- a/src/connections/Xbox/index.ts +++ b/src/connections/Xbox/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { XboxSettings } from "./XboxSettings"; interface XboxUserResponse { @@ -44,10 +44,10 @@ interface XboxUserResponse { }; } -interface XboxErrorResponse { - error: string; - error_description: string; -} +// interface XboxErrorResponse { +// error: string; +// error_description: string; +// } export default class XboxConnection extends Connection { public readonly id = "xbox"; @@ -62,17 +62,20 @@ export default class XboxConnection extends Connection { settings: XboxSettings = new XboxSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<XboxSettings>( this.id, this.settings, - ) as XboxSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -124,14 +127,16 @@ export default class XboxConnection extends Connection { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from( - `${this.settings.clientId!}:${this.settings.clientSecret!}`, + `${this.settings.clientId as string}:${ + this.settings.clientSecret as string + }`, ).toString("base64")}`, }) .body( new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, + client_id: this.settings.clientId as string, redirect_uri: this.getRedirectUri(), scope: this.scopes.join(" "), }), @@ -174,8 +179,11 @@ export default class XboxConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userToken = await this.getUserToken(tokenData.access_token); const userInfo = await this.getUser(userToken); diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts index f3a43fcc..844803cf 100644 --- a/src/connections/Youtube/index.ts +++ b/src/connections/Youtube/index.ts @@ -19,12 +19,12 @@ import { ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, + Connection, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@spacebar/util"; import wretch from "wretch"; -import Connection from "../../util/connections/Connection"; import { YoutubeSettings } from "./YoutubeSettings"; interface YouTubeConnectionChannelListResult { @@ -62,17 +62,20 @@ export default class YoutubeConnection extends Connection { settings: YoutubeSettings = new YoutubeSettings(); init(): void { - this.settings = ConnectionLoader.getConnectionConfig( + const settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>( this.id, this.settings, - ) as YoutubeSettings; + ); + + if (settings.enabled && (!settings.clientId || !settings.clientSecret)) + throw new Error(`Invalid settings for connection ${this.id}`); } getAuthorizationUrl(userId: string): string { const state = this.createState(userId); const url = new URL(this.authorizeUrl); - url.searchParams.append("client_id", this.settings.clientId!); + url.searchParams.append("client_id", this.settings.clientId as string); url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -101,8 +104,8 @@ export default class YoutubeConnection extends Connection { new URLSearchParams({ grant_type: "authorization_code", code: code, - client_id: this.settings.clientId!, - client_secret: this.settings.clientSecret!, + client_id: this.settings.clientId as string, + client_secret: this.settings.clientSecret as string, redirect_uri: this.getRedirectUri(), }), ) @@ -131,8 +134,11 @@ export default class YoutubeConnection extends Connection { async handleCallback( params: ConnectionCallbackSchema, ): Promise<ConnectedAccount | null> { - const userId = this.getUserId(params.state); - const tokenData = await this.exchangeCode(params.state, params.code!); + const { state, code } = params; + if (!code) throw new Error("No code provided"); + + const userId = this.getUserId(state); + const tokenData = await this.exchangeCode(state, code); const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.items[0].id); diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index becee589..5bdebd47 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -24,7 +24,7 @@ import { Config, DiscordApiErrors } from "../util"; /** * A connection that can be used to connect to an external service. */ -export default abstract class Connection { +export abstract class Connection { id: string; settings: { enabled: boolean }; states: Map<string, string> = new Map(); diff --git a/src/util/connections/ConnectionLoader.ts b/src/util/connections/ConnectionLoader.ts index 28f1a202..e9dc6973 100644 --- a/src/util/connections/ConnectionLoader.ts +++ b/src/util/connections/ConnectionLoader.ts @@ -16,9 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import { Connection } from "@spacebar/util"; import fs from "fs"; import path from "path"; -import Connection from "./Connection"; import { ConnectionConfig } from "./ConnectionConfig"; import { ConnectionStore } from "./ConnectionStore"; @@ -48,8 +48,7 @@ export class ConnectionLoader { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static getConnectionConfig(id: string, defaults?: any): any { + public static getConnectionConfig<T>(id: string, defaults?: unknown): T { let cfg = ConnectionConfig.get()[id]; if (defaults) { if (cfg) cfg = Object.assign({}, defaults, cfg); @@ -70,8 +69,7 @@ export class ConnectionLoader { public static async setConnectionConfig( id: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: Partial<any>, + config: Partial<unknown>, ): Promise<void> { if (!config) console.warn(`[Connections/WARN] ${id} tried to set config=null!`); diff --git a/src/util/connections/ConnectionStore.ts b/src/util/connections/ConnectionStore.ts index 39abfea6..95e54fd9 100644 --- a/src/util/connections/ConnectionStore.ts +++ b/src/util/connections/ConnectionStore.ts @@ -16,8 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import Connection from "./Connection"; -import RefreshableConnection from "./RefreshableConnection"; +import { Connection } from "./Connection"; +import { RefreshableConnection } from "./RefreshableConnection"; export class ConnectionStore { public static connections: Map<string, Connection | RefreshableConnection> = diff --git a/src/util/connections/RefreshableConnection.ts b/src/util/connections/RefreshableConnection.ts index fd93adfa..88ad8dab 100644 --- a/src/util/connections/RefreshableConnection.ts +++ b/src/util/connections/RefreshableConnection.ts @@ -18,13 +18,14 @@ import { ConnectedAccount } from "../entities"; import { ConnectedAccountCommonOAuthTokenResponse } from "../interfaces"; -import Connection from "./Connection"; +import { Connection } from "./Connection"; /** * A connection that can refresh its token. */ -export default abstract class RefreshableConnection extends Connection { +export abstract class RefreshableConnection extends Connection { refreshEnabled = true; + /** * Refreshes the token for a connected account. * @param connectedAccount The connected account to refresh diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts index 0a3604d5..f9efd980 100644 --- a/src/util/dtos/ConnectedAccountDTO.ts +++ b/src/util/dtos/ConnectedAccountDTO.ts @@ -30,7 +30,7 @@ export class ConnectedAccountDTO { verified?: boolean; visibility?: number; integrations?: string[]; - metadata_?: any; + metadata_?: unknown; metadata_visibility?: number; two_way_link?: boolean; diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts index 5dd21250..6e089de1 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts @@ -66,6 +66,7 @@ export class ConnectedAccount extends BaseClass { integrations?: string[] = []; @Column({ type: "simple-json", name: "metadata", nullable: true }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata_?: any; @Column() diff --git a/src/util/schemas/ConnectedAccountSchema.ts b/src/util/schemas/ConnectedAccountSchema.ts index fe808a35..5fd05b71 100644 --- a/src/util/schemas/ConnectedAccountSchema.ts +++ b/src/util/schemas/ConnectedAccountSchema.ts @@ -30,7 +30,7 @@ export interface ConnectedAccountSchema { verified?: boolean; visibility?: number; integrations?: string[]; - metadata_?: any; + metadata_?: unknown; metadata_visibility?: number; two_way_link?: boolean; } diff --git a/src/util/schemas/ConnectionCallbackSchema.ts b/src/util/schemas/ConnectionCallbackSchema.ts index eb86c087..b66bfe20 100644 --- a/src/util/schemas/ConnectionCallbackSchema.ts +++ b/src/util/schemas/ConnectionCallbackSchema.ts @@ -21,5 +21,5 @@ export interface ConnectionCallbackSchema { state: string; insecure: boolean; friend_sync: boolean; - openid_params?: any; // TODO: types + openid_params?: unknown; // TODO: types } |