summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/api/middlewares/Authentication.ts2
-rw-r--r--src/api/routes/auth/reset.ts1
-rw-r--r--src/api/routes/connections/#connection_name/#connection_id/refresh.ts2
-rw-r--r--src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts2
-rw-r--r--src/connections/BattleNet/index.ts35
-rw-r--r--src/connections/Discord/index.ts22
-rw-r--r--src/connections/EpicGames/index.ts23
-rw-r--r--src/connections/Facebook/index.ts25
-rw-r--r--src/connections/GitHub/index.ts25
-rw-r--r--src/connections/Reddit/index.ts18
-rw-r--r--src/connections/Spotify/index.ts26
-rw-r--r--src/connections/Twitch/index.ts28
-rw-r--r--src/connections/Twitter/index.ts38
-rw-r--r--src/connections/Xbox/index.ts32
-rw-r--r--src/connections/Youtube/index.ts22
-rw-r--r--src/util/connections/Connection.ts2
-rw-r--r--src/util/connections/ConnectionLoader.ts8
-rw-r--r--src/util/connections/ConnectionStore.ts4
-rw-r--r--src/util/connections/RefreshableConnection.ts5
-rw-r--r--src/util/dtos/ConnectedAccountDTO.ts2
-rw-r--r--src/util/entities/ConnectedAccount.ts1
-rw-r--r--src/util/schemas/ConnectedAccountSchema.ts2
-rw-r--r--src/util/schemas/ConnectionCallbackSchema.ts2
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
 }