From 21bfda32e452c05b8906bf318df7415d6cd5acd0 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 22 Dec 2022 10:05:51 -0500 Subject: add connections --- src/util/connections/Connection.ts | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/util/connections/Connection.ts (limited to 'src/util/connections/Connection.ts') diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts new file mode 100644 index 00000000..02104d39 --- /dev/null +++ b/src/util/connections/Connection.ts @@ -0,0 +1,72 @@ +import crypto from "crypto"; +import { ConnectedAccount } from "../entities"; +import { OrmUtils } from "../imports"; +import { ConnectionCallbackSchema } from "../schemas"; +import { DiscordApiErrors } from "../util"; + +export default abstract class Connection { + id: string; + settings: { enabled: boolean }; + states: Map = new Map(); + + abstract init(): void; + + /** + * Generates an authorization url for the connection. + * @param args + */ + abstract getAuthorizationUrl(userId: string): string; + + /** + * Processes the callback + * @param args Callback arguments + */ + abstract handleCallback(params: ConnectionCallbackSchema): Promise; + + /** + * Gets a user id from state + * @param state the state to get the user id from + * @returns the user id associated with the state + */ + getUserId(state: string): string { + if (!this.states.has(state)) throw DiscordApiErrors.INVALID_OAUTH_STATE; + return this.states.get(state) as string; + } + + /** + * Generates a state + * @param user_id The user id to generate a state for. + * @returns a new state + */ + createState(userId: string): string { + const state = crypto.randomBytes(16).toString("hex"); + this.states.set(state, userId); + + return state; + } + + /** + * Takes a state and checks if it is valid, and deletes it. + * @param state The state to check. + */ + validateState(state: string): void { + if (!this.states.has(state)) throw DiscordApiErrors.INVALID_OAUTH_STATE; + this.states.delete(state); + } + + async createConnection(data: any): Promise { + const ca = OrmUtils.mergeDeep(new ConnectedAccount(), data); + await ca.save(); + } + + async hasConnection(userId: string, externalId: string): Promise { + const existing = await ConnectedAccount.findOne({ + where: { + user_id: userId, + external_id: externalId, + }, + }); + + return !!existing; + } +} -- cgit 1.5.1 From 6a52e65e27d514273a4210dadafbee9692f23354 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 22 Dec 2022 10:47:32 -0500 Subject: adding connection now works --- src/api/Server.ts | 5 +++++ .../connections/#connection_name/authorize.ts | 2 +- .../connections/#connection_name/callback.ts | 2 +- src/connections/BattleNet/index.ts | 7 +------ src/connections/GitHub/index.ts | 7 +------ src/util/connections/Connection.ts | 4 ++-- src/util/connections/ConnectionConfig.ts | 1 - src/util/connections/ConnectionLoader.ts | 7 ++++--- src/util/dtos/ConnectedAccountDTO.ts | 18 ++++++++-------- src/util/entities/ConnectedAccount.ts | 24 +++++++++++----------- src/util/index.ts | 1 + src/util/schemas/ConnectedAccountSchema.ts | 16 +++++++++++++++ src/util/schemas/index.ts | 1 + 13 files changed, 54 insertions(+), 41 deletions(-) create mode 100644 src/util/schemas/ConnectedAccountSchema.ts (limited to 'src/util/connections/Connection.ts') diff --git a/src/api/Server.ts b/src/api/Server.ts index 49229494..dc3b66ef 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -25,6 +25,8 @@ import { registerRoutes, Sentry, WebAuthn, + ConnectionConfig, + ConnectionLoader } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { Server, ServerOptions } from "lambert-server"; @@ -64,6 +66,7 @@ export class FosscordServer extends Server { await Config.init(); await initEvent(); await Email.init(); + await ConnectionConfig.init(); await initInstance(); await Sentry.init(this.app); WebAuthn.init(); @@ -130,6 +133,8 @@ export class FosscordServer extends Server { Sentry.errorHandler(this.app); + ConnectionLoader.loadConnections(); + if (logRequests) console.log( red( diff --git a/src/api/routes/connections/#connection_name/authorize.ts b/src/api/routes/connections/#connection_name/authorize.ts index 8e640a69..5ce420cf 100644 --- a/src/api/routes/connections/#connection_name/authorize.ts +++ b/src/api/routes/connections/#connection_name/authorize.ts @@ -6,7 +6,7 @@ import { route } from "../../../util"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - const { connection_id: connection_name } = req.params; + const { connection_name } = req.params; const connection = ConnectionStore.connections.get(connection_name); if (!connection) throw FieldErrors({ diff --git a/src/api/routes/connections/#connection_name/callback.ts b/src/api/routes/connections/#connection_name/callback.ts index f158a037..80a5b784 100644 --- a/src/api/routes/connections/#connection_name/callback.ts +++ b/src/api/routes/connections/#connection_name/callback.ts @@ -13,7 +13,7 @@ router.post( "/", route({ body: "ConnectionCallbackSchema" }), async (req: Request, res: Response) => { - const { connection_id: connection_name } = req.params; + const { connection_name } = req.params; const connection = ConnectionStore.connections.get(connection_name); if (!connection) throw FieldErrors({ diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 0fd0aa18..1b725afd 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -118,15 +118,10 @@ export default class BattleNetConnection extends Connection { if (exists) return false; await this.createConnection({ user_id: userId, - external_id: userInfo.id, + external_id: userInfo.id.toString(), friend_sync: params.friend_sync, name: userInfo.battletag, - revoked: false, - show_activity: false, type: this.id, - verified: true, - visibility: 0, - integrations: [], }); return true; } diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index a96ac68e..41806a67 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -99,15 +99,10 @@ export default class GitHubConnection extends Connection { if (exists) return false; await this.createConnection({ user_id: userId, - external_id: userInfo.id, + external_id: userInfo.id.toString(), friend_sync: params.friend_sync, name: userInfo.name, - revoked: false, - show_activity: false, type: this.id, - verified: true, - visibility: 0, - integrations: [], }); return true; } diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index 02104d39..e8d41c36 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -1,7 +1,7 @@ import crypto from "crypto"; import { ConnectedAccount } from "../entities"; import { OrmUtils } from "../imports"; -import { ConnectionCallbackSchema } from "../schemas"; +import { ConnectedAccountSchema, ConnectionCallbackSchema } from "../schemas"; import { DiscordApiErrors } from "../util"; export default abstract class Connection { @@ -54,7 +54,7 @@ export default abstract class Connection { this.states.delete(state); } - async createConnection(data: any): Promise { + async createConnection(data: ConnectedAccountSchema): Promise { const ca = OrmUtils.mergeDeep(new ConnectedAccount(), data); await ca.save(); } diff --git a/src/util/connections/ConnectionConfig.ts b/src/util/connections/ConnectionConfig.ts index 9b120c93..dd372ff7 100644 --- a/src/util/connections/ConnectionConfig.ts +++ b/src/util/connections/ConnectionConfig.ts @@ -21,7 +21,6 @@ export const ConnectionConfig = { set: function set(val: Partial) { if (!config || !val) return; config = val.merge(config); - console.debug("config", config); // TODO: if no more issues with sql, remove this or find the reason why it's happening return applyConfig(config); }, diff --git a/src/util/connections/ConnectionLoader.ts b/src/util/connections/ConnectionLoader.ts index d06a6446..7467739c 100644 --- a/src/util/connections/ConnectionLoader.ts +++ b/src/util/connections/ConnectionLoader.ts @@ -23,7 +23,6 @@ export class ConnectionLoader { dirs.forEach(async (x) => { let modPath = path.resolve(path.join(root, x)); - console.log(`Loading connection: ${modPath}`); const mod = new (require(modPath).default)() as Connection; ConnectionStore.connections.set(mod.id, mod); @@ -55,11 +54,13 @@ export class ConnectionLoader { console.log( `[ConnectionConfig/WARN] ${id} tried to set config=null!`, ); - await ConnectionConfig.set({ + + const a = { [id]: OrmUtils.mergeDeep( ConnectionLoader.getConnectionConfig(id) || {}, config, ), - }); + }; + await ConnectionConfig.set(a); } } diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts index a0287086..36a4e6b3 100644 --- a/src/util/dtos/ConnectedAccountDTO.ts +++ b/src/util/dtos/ConnectedAccountDTO.ts @@ -4,17 +4,17 @@ export class ConnectedAccountDTO { id: string; user_id: string; access_token?: string; - friend_sync: boolean; + friend_sync?: boolean; name: string; - revoked: boolean; - show_activity: boolean; + revoked?: boolean; + show_activity?: boolean; type: string; - verified: boolean; - visibility: boolean; - integrations: string[]; - metadata_: any; - metadata_visibility: boolean; - two_way_link: boolean; + verified?: boolean; + visibility?: number; + integrations?: string[]; + metadata_?: any; + metadata_visibility?: number; + two_way_link?: boolean; constructor( connectedAccount: ConnectedAccount, diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts index 70923d2c..25d5a0c7 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts @@ -40,39 +40,39 @@ export class ConnectedAccount extends BaseClass { }) user: User; - @Column({ select: false }) - access_token: string; + @Column({ select: false, nullable: true }) + access_token?: string; @Column({ select: false }) - friend_sync: boolean = false; + friend_sync?: boolean = false; @Column() name: string; @Column({ select: false }) - revoked: boolean = false; + revoked?: boolean = false; @Column({ select: false }) - show_activity: boolean = true; + show_activity?: boolean = true; @Column() type: string; @Column() - verified: boolean; + verified?: boolean = true; @Column({ select: false }) - visibility: boolean = true; + visibility?: number = 0; @Column({ type: "simple-array" }) - integrations: string[]; + integrations?: string[] = []; - @Column({ type: "simple-json", name: "metadata" }) - metadata_: any; + @Column({ type: "simple-json", name: "metadata", nullable: true }) + metadata_?: any; @Column() - metadata_visibility: boolean = true; + metadata_visibility?: number = 0; @Column() - two_way_link: boolean = false; + two_way_link?: boolean = false; } diff --git a/src/util/index.ts b/src/util/index.ts index a3495a0c..cb180ee8 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -25,3 +25,4 @@ export * from "./dtos/index"; export * from "./schemas"; export * from "./imports"; export * from "./config"; +export * from "./connections" \ No newline at end of file diff --git a/src/util/schemas/ConnectedAccountSchema.ts b/src/util/schemas/ConnectedAccountSchema.ts new file mode 100644 index 00000000..e00e4fa1 --- /dev/null +++ b/src/util/schemas/ConnectedAccountSchema.ts @@ -0,0 +1,16 @@ +export interface ConnectedAccountSchema { + external_id: string; + user_id: string; + access_token?: string; + friend_sync?: boolean; + name: string; + revoked?: boolean; + show_activity?: boolean; + type: string; + verified?: boolean; + visibility?: number; + integrations?: string[]; + metadata_?: any; + metadata_visibility?: number; + two_way_link?: boolean; +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index 2831d42a..71b4c2ce 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -30,6 +30,7 @@ export * from "./ChannelModifySchema"; export * from "./ChannelPermissionOverwriteSchema"; export * from "./ChannelReorderSchema"; export * from "./CodesVerificationSchema"; +export * from "./ConnectedAccountSchema"; export * from "./ConnectionCallbackSchema"; export * from "./DmChannelCreateSchema"; export * from "./EmojiCreateSchema"; -- cgit 1.5.1 From a4961800d7b6b37864b7b7c44893c734ef1b05ae Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Fri, 23 Dec 2022 12:44:04 +1100 Subject: `handleCallback` returns connection if created for `USER_CONNECTIONS_UPDATE` --- package-lock.json | 1 - .../routes/connections/#connection_name/callback.ts | 15 ++++++++------- src/connections/BattleNet/index.ts | 18 ++++++++---------- src/connections/GitHub/index.ts | 10 +++++----- src/util/connections/Connection.ts | 7 ++++--- 5 files changed, 25 insertions(+), 26 deletions(-) (limited to 'src/util/connections/Connection.ts') diff --git a/package-lock.json b/package-lock.json index 8927001f..cea4dbb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,6 @@ "probe-image-size": "^7.2.3", "proxy-agent": "^5.0.0", "reflect-metadata": "^0.1.13", - "sqlite3": "^5.1.5", "ts-node": "^10.9.1", "tslib": "^2.4.1", "typeorm": "^0.3.10", diff --git a/src/api/routes/connections/#connection_name/callback.ts b/src/api/routes/connections/#connection_name/callback.ts index 80a5b784..250d4710 100644 --- a/src/api/routes/connections/#connection_name/callback.ts +++ b/src/api/routes/connections/#connection_name/callback.ts @@ -1,11 +1,11 @@ -import { Request, Response, Router } from "express"; +import { route } from "@fosscord/api"; import { ConnectionCallbackSchema, + ConnectionStore, emitEvent, FieldErrors, -} from "../../../../util"; -import { ConnectionStore } from "../../../../util/connections"; -import { route } from "../../../util"; +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); @@ -36,15 +36,16 @@ router.post( const body = req.body as ConnectionCallbackSchema; const userId = connection.getUserId(body.state); - const emit = await connection.handleCallback(body); + const connectedAccnt = await connection.handleCallback(body); // whether we should emit a connections update event, only used when a connection doesnt already exist - if (emit) + if (connectedAccnt) emitEvent({ event: "USER_CONNECTIONS_UPDATE", - data: {}, + data: connectedAccnt, user_id: userId, }); + res.sendStatus(204); }, ); diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 1b725afd..5bbfefe9 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -1,5 +1,5 @@ import fetch from "node-fetch"; -import { Config, ConnectionCallbackSchema, DiscordApiErrors } from "../../util"; +import { Config, ConnectedAccount, ConnectionCallbackSchema, DiscordApiErrors } from "../../util"; import Connection from "../../util/connections/Connection"; import { ConnectionLoader } from "../../util/connections/ConnectionLoader"; import { BattleNetSettings } from "./BattleNetSettings"; @@ -46,8 +46,7 @@ export default class BattleNetConnection extends Connection { // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. url.searchParams.append( "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" + `${Config.get().cdn.endpointPrivate || "http://localhost:3001" }/connections/${this.id}/callback`, ); url.searchParams.append("scope", this.scopes.join(" ")); @@ -75,9 +74,8 @@ export default class BattleNetConnection extends Connection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: `${Config.get().cdn.endpointPrivate || "http://localhost:3001" + }/connections/${this.id}/callback`, }), }) .then((res) => res.json()) @@ -108,21 +106,21 @@ export default class BattleNetConnection extends Connection { }); } - async handleCallback(params: ConnectionCallbackSchema): Promise { + async handleCallback(params: ConnectionCallbackSchema): Promise { const userId = this.getUserId(params.state); const token = await this.exchangeCode(params.state, params.code!); const userInfo = await this.getUser(token); const exists = await this.hasConnection(userId, userInfo.id.toString()); - if (exists) return false; - await this.createConnection({ + if (exists) return null; + + return await this.createConnection({ user_id: userId, external_id: userInfo.id.toString(), friend_sync: params.friend_sync, name: userInfo.battletag, type: this.id, }); - return true; } } diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index 41806a67..fc44ff9d 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -1,5 +1,5 @@ import fetch from "node-fetch"; -import { Config, ConnectionCallbackSchema, DiscordApiErrors } from "../../util"; +import { Config, ConnectedAccount, ConnectionCallbackSchema, DiscordApiErrors } from "../../util"; import Connection from "../../util/connections/Connection"; import { ConnectionLoader } from "../../util/connections/ConnectionLoader"; import { GitHubSettings } from "./GitHubSettings"; @@ -89,21 +89,21 @@ export default class GitHubConnection extends Connection { }).then((res) => res.json()); } - async handleCallback(params: ConnectionCallbackSchema): Promise { + async handleCallback(params: ConnectionCallbackSchema): Promise { const userId = this.getUserId(params.state); const token = await this.exchangeCode(params.state, params.code!); const userInfo = await this.getUser(token); const exists = await this.hasConnection(userId, userInfo.id.toString()); - if (exists) return false; - await this.createConnection({ + if (exists) return null; + + return await this.createConnection({ user_id: userId, external_id: userInfo.id.toString(), friend_sync: params.friend_sync, name: userInfo.name, type: this.id, }); - return true; } } diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index e8d41c36..164cfac7 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -21,7 +21,7 @@ export default abstract class Connection { * Processes the callback * @param args Callback arguments */ - abstract handleCallback(params: ConnectionCallbackSchema): Promise; + abstract handleCallback(params: ConnectionCallbackSchema): Promise; /** * Gets a user id from state @@ -54,9 +54,10 @@ export default abstract class Connection { this.states.delete(state); } - async createConnection(data: ConnectedAccountSchema): Promise { - const ca = OrmUtils.mergeDeep(new ConnectedAccount(), data); + async createConnection(data: ConnectedAccountSchema): Promise { + const ca = OrmUtils.mergeDeep(new ConnectedAccount(), data) as ConnectedAccount; await ca.save(); + return ca; } async hasConnection(userId: string, externalId: string): Promise { -- cgit 1.5.1 From 0db1fa5f0b2b9b357c1f96178c0e5df7858a99ab Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Fri, 23 Dec 2022 18:34:36 -0500 Subject: Refreshable connections, refactoring, access-token endpoint - Aded /users/@me/connections/:connection_name/:connection_id/access-token - Replaced `access_token` property on ConnectedAccount with `token_data` object for refreshing tokens - Made a common interface for connection things like ComonOAuthTokenResponse - Added `RefreshableConnection` class - Added token refresh to Spotify connection (disabled) --- .../#connection_id/access-token.ts | 84 ++++++++++++++++++++++ src/api/routes/users/@me/connections/index.ts | 2 +- src/connections/BattleNet/index.ts | 31 ++++---- src/connections/Discord/index.ts | 19 ++--- src/connections/EpicGames/index.ts | 22 +++--- src/connections/Facebook/index.ts | 31 ++++---- src/connections/GitHub/index.ts | 19 ++--- src/connections/Reddit/index.ts | 20 ++---- src/connections/Spotify/index.ts | 79 +++++++++++++++----- src/util/connections/Connection.ts | 25 +++++-- src/util/connections/ConnectionStore.ts | 4 +- src/util/connections/RefreshableConnection.ts | 30 ++++++++ src/util/connections/index.ts | 1 + src/util/dtos/ConnectedAccountDTO.ts | 4 +- src/util/entities/ConnectedAccount.ts | 7 +- src/util/interfaces/ConnectedAccount.ts | 16 +++++ src/util/interfaces/index.ts | 5 +- src/util/schemas/ConnectedAccountSchema.ts | 4 +- 18 files changed, 292 insertions(+), 111 deletions(-) create mode 100644 src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts create mode 100644 src/util/connections/RefreshableConnection.ts create mode 100644 src/util/interfaces/ConnectedAccount.ts (limited to 'src/util/connections/Connection.ts') 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 new file mode 100644 index 00000000..8d51a770 --- /dev/null +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts @@ -0,0 +1,84 @@ +import { route } from "@fosscord/api"; +import { + ApiError, + ConnectedAccount, + ConnectionStore, + DiscordApiErrors, + FieldErrors, +} from "@fosscord/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) + +// spotify is disabled here because it cant be used +const ALLOWED_CONNECTIONS = ["twitch", "youtube"]; + +router.get("/", route({}), async (req: Request, res: Response) => { + // TODO: get the current access token or refresh it if it's expired + const { connection_name, connection_id } = req.params; + + const connection = ConnectionStore.connections.get(connection_id); + + if (!ALLOWED_CONNECTIONS.includes(connection_name) || !connection) + throw FieldErrors({ + provider_id: { + code: "BASE_TYPE_CHOICES", + message: req.t("common:field.BASE_TYPE_CHOICES", { + types: ALLOWED_CONNECTIONS.join(", "), + }), + }, + }); + + if (!connection.settings.enabled) + throw FieldErrors({ + provider_id: { + message: "This connection has been disabled server-side.", + }, + }); + + const connectedAccount = await ConnectedAccount.findOne({ + where: { + type: connection_name, + id: connection_id, + user_id: req.user_id, + }, + select: [ + "external_id", + "type", + "name", + "verified", + "visibility", + "show_activity", + "revoked", + "token_data", + "friend_sync", + "integrations", + ], + }); + if (!connectedAccount) throw DiscordApiErrors.UNKNOWN_CONNECTION; + if (connectedAccount.revoked) + throw new ApiError("Connection revoked", 0, 400); + if (!connectedAccount.token_data) + throw new ApiError("No token data", 0, 400); + + let access_token = connectedAccount.token_data.access_token; + const { expires_at, expires_in } = connectedAccount.token_data; + + if (expires_at && expires_at < Date.now()) { + if (!(connection instanceof RefreshableConnection)) + throw new ApiError("Access token expired", 0, 400); + const tokenData = await connection.refresh(connectedAccount); + access_token = tokenData.access_token; + } else if (expires_in && expires_in < Date.now()) { + if (!(connection instanceof RefreshableConnection)) + throw new ApiError("Access token expired", 0, 400); + const tokenData = await connection.refresh(connectedAccount); + access_token = tokenData.access_token; + } + + res.json({ access_token }); +}); + +export default router; diff --git a/src/api/routes/users/@me/connections/index.ts b/src/api/routes/users/@me/connections/index.ts index a5041be1..8e762f19 100644 --- a/src/api/routes/users/@me/connections/index.ts +++ b/src/api/routes/users/@me/connections/index.ts @@ -35,7 +35,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { "visibility", "show_activity", "revoked", - "access_token", + "token_data", "friend_sync", "integrations", ], diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index f4b317cc..ecba0fa9 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -1,6 +1,7 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, @@ -9,14 +10,6 @@ import fetch from "node-fetch"; import Connection from "../../util/connections/Connection"; import { BattleNetSettings } from "./BattleNetSettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - interface BattleNetConnectionUser { sub: string; id: number; @@ -65,7 +58,10 @@ export default class BattleNetConnection extends Connection { return this.tokenUrl; } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(); @@ -86,10 +82,15 @@ export default class BattleNetConnection extends Connection { }), }) .then((res) => res.json()) - .then((res: OAuthTokenResponse & BattleNetErrorResponse) => { - if (res.error) throw new Error(res.error_description); - return res.access_token; - }) + .then( + ( + res: ConnectedAccountCommonOAuthTokenResponse & + BattleNetErrorResponse, + ) => { + if (res.error) throw new Error(res.error_description); + return res; + }, + ) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -117,8 +118,8 @@ export default class BattleNetConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.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 05074c26..61efcfc5 100644 --- a/src/connections/Discord/index.ts +++ b/src/connections/Discord/index.ts @@ -1,6 +1,7 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, @@ -9,14 +10,6 @@ import fetch from "node-fetch"; import Connection from "../../util/connections/Connection"; import { DiscordSettings } from "./DiscordSettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - interface UserResponse { id: string; username: string; @@ -65,7 +58,10 @@ export default class DiscordConnection extends Connection { return this.tokenUrl; } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(); @@ -86,7 +82,6 @@ export default class DiscordConnection extends Connection { }), }) .then((res) => res.json()) - .then((res: OAuthTokenResponse) => res.access_token) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -109,8 +104,8 @@ export default class DiscordConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.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 cffafc60..f1f3f24c 100644 --- a/src/connections/EpicGames/index.ts +++ b/src/connections/EpicGames/index.ts @@ -1,6 +1,7 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, @@ -9,21 +10,14 @@ import fetch from "node-fetch"; import Connection from "../../util/connections/Connection"; import { EpicGamesSettings } from "./EpicGamesSettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - export interface UserResponse { accountId: string; displayName: string; preferredLanguage: string; } -export interface EpicTokenResponse extends OAuthTokenResponse { +export interface EpicTokenResponse + extends ConnectedAccountCommonOAuthTokenResponse { expires_at: string; refresh_expires_in: number; refresh_expires_at: string; @@ -70,7 +64,10 @@ export default class EpicGamesConnection extends Connection { return this.tokenUrl; } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(); @@ -90,7 +87,6 @@ export default class EpicGamesConnection extends Connection { }), }) .then((res) => res.json()) - .then((res: EpicTokenResponse) => res.access_token) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -117,8 +113,8 @@ export default class EpicGamesConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.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 80950a8e..2d490c63 100644 --- a/src/connections/Facebook/index.ts +++ b/src/connections/Facebook/index.ts @@ -1,6 +1,7 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, @@ -9,14 +10,6 @@ import fetch from "node-fetch"; import Connection from "../../util/connections/Connection"; import { FacebookSettings } from "./FacebookSettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - export interface FacebookErrorResponse { error: { message: string; @@ -81,7 +74,10 @@ export default class FacebookConnection extends Connection { return url.toString(); } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(code); @@ -93,10 +89,15 @@ export default class FacebookConnection extends Connection { }, }) .then((res) => res.json()) - .then((res: OAuthTokenResponse & FacebookErrorResponse) => { - if (res.error) throw new Error(res.error.message); - return res.access_token; - }) + .then( + ( + res: ConnectedAccountCommonOAuthTokenResponse & + FacebookErrorResponse, + ) => { + if (res.error) throw new Error(res.error.message); + return res; + }, + ) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -124,8 +125,8 @@ export default class FacebookConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.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 d79fd51e..ab3f8e65 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -1,6 +1,7 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, @@ -9,14 +10,6 @@ import fetch from "node-fetch"; import Connection from "../../util/connections/Connection"; import { GitHubSettings } from "./GitHubSettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - interface UserResponse { login: string; id: number; @@ -63,7 +56,10 @@ export default class GitHubConnection extends Connection { return url.toString(); } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(code); @@ -75,7 +71,6 @@ export default class GitHubConnection extends Connection { }, }) .then((res) => res.json()) - .then((res: OAuthTokenResponse) => res.access_token) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -98,8 +93,8 @@ export default class GitHubConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.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 c529cfa3..182cd5a5 100644 --- a/src/connections/Reddit/index.ts +++ b/src/connections/Reddit/index.ts @@ -1,6 +1,7 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, @@ -9,14 +10,6 @@ import fetch from "node-fetch"; import Connection from "../../util/connections/Connection"; import { RedditSettings } from "./RedditSettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - export interface UserResponse { verified: boolean; coins: number; @@ -72,7 +65,10 @@ export default class RedditConnection extends Connection { return this.tokenUrl; } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(); @@ -95,7 +91,6 @@ export default class RedditConnection extends Connection { }), }) .then((res) => res.json()) - .then((res: OAuthTokenResponse) => res.access_token) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -118,8 +113,8 @@ export default class RedditConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.code!); + const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id.toString()); @@ -128,7 +123,6 @@ export default class RedditConnection extends Connection { // TODO: connection metadata return await this.createConnection({ - access_token: token, user_id: userId, external_id: userInfo.id.toString(), friend_sync: params.friend_sync, diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts index eb662141..b40d6189 100644 --- a/src/connections/Spotify/index.ts +++ b/src/connections/Spotify/index.ts @@ -1,22 +1,15 @@ import { Config, ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, ConnectionLoader, DiscordApiErrors, } from "@fosscord/util"; import fetch from "node-fetch"; -import Connection from "../../util/connections/Connection"; +import RefreshableConnection from "../../util/connections/RefreshableConnection"; import { SpotifySettings } from "./SpotifySettings"; -interface OAuthTokenResponse { - access_token: string; - token_type: string; - scope: string; - refresh_token?: string; - expires_in?: number; -} - export interface UserResponse { display_name: string; id: string; @@ -34,7 +27,7 @@ export interface ErrorResponse { }; } -export default class SpotifyConnection extends Connection { +export default class SpotifyConnection extends RefreshableConnection { public readonly id = "spotify"; public readonly authorizeUrl = "https://accounts.spotify.com/authorize"; public readonly tokenUrl = "https://accounts.spotify.com/api/token"; @@ -48,6 +41,11 @@ export default class SpotifyConnection extends Connection { settings: SpotifySettings = new SpotifySettings(); init(): void { + /** + * The way Discord shows the currently playing song is by using Spotifys partner API. This is obviously not possible for us. + * So to prevent spamming the spotify api we disable the ability to refresh. + */ + this.refreshEnabled = false; this.settings = ConnectionLoader.getConnectionConfig( this.id, this.settings, @@ -76,7 +74,10 @@ export default class SpotifyConnection extends Connection { return this.tokenUrl; } - async exchangeCode(state: string, code: string): Promise { + async exchangeCode( + state: string, + code: string, + ): Promise { this.validateState(state); const url = this.getTokenUrl(); @@ -99,10 +100,15 @@ export default class SpotifyConnection extends Connection { }), }) .then((res) => res.json()) - .then((res: OAuthTokenResponse & TokenErrorResponse) => { - if (res.error) throw new Error(res.error_description); - return res.access_token; - }) + .then( + ( + res: ConnectedAccountCommonOAuthTokenResponse & + TokenErrorResponse, + ) => { + if (res.error) throw new Error(res.error_description); + return res; + }, + ) .catch((e) => { console.error( `Error exchanging token for ${this.id} connection: ${e}`, @@ -111,6 +117,44 @@ export default class SpotifyConnection extends Connection { }); } + async refreshToken(connectedAccount: ConnectedAccount) { + if (!connectedAccount.token_data?.refresh_token) + throw new Error("No refresh token available."); + const refresh_token = connectedAccount.token_data.refresh_token; + const url = this.getTokenUrl(); + + return fetch(url.toString(), { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from( + `${this.settings.clientId!}:${this.settings.clientSecret!}`, + ).toString("base64")}`, + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token, + }), + }) + .then((res) => res.json()) + .then( + ( + res: ConnectedAccountCommonOAuthTokenResponse & + TokenErrorResponse, + ) => { + if (res.error) throw new Error(res.error_description); + return res; + }, + ) + .catch((e) => { + console.error( + `Error refreshing token for ${this.id} connection: ${e}`, + ); + throw DiscordApiErrors.INVALID_OAUTH_TOKEN; + }); + } + async getUser(token: string): Promise { const url = new URL(this.userInfoUrl); return fetch(url.toString(), { @@ -130,14 +174,15 @@ export default class SpotifyConnection extends Connection { params: ConnectionCallbackSchema, ): Promise { const userId = this.getUserId(params.state); - const token = await this.exchangeCode(params.state, params.code!); - const userInfo = await this.getUser(token); + const tokenData = await this.exchangeCode(params.state, params.code!); + const userInfo = await this.getUser(tokenData.access_token); const exists = await this.hasConnection(userId, userInfo.id); if (exists) return null; return await this.createConnection({ + token_data: tokenData, user_id: userId, external_id: userInfo.id, friend_sync: params.friend_sync, diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index 164cfac7..8b60b0d2 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -1,9 +1,11 @@ import crypto from "crypto"; import { ConnectedAccount } from "../entities"; -import { OrmUtils } from "../imports"; import { ConnectedAccountSchema, ConnectionCallbackSchema } from "../schemas"; import { DiscordApiErrors } from "../util"; +/** + * A connection that can be used to connect to an external service. + */ export default abstract class Connection { id: string; settings: { enabled: boolean }; @@ -21,7 +23,9 @@ export default abstract class Connection { * Processes the callback * @param args Callback arguments */ - abstract handleCallback(params: ConnectionCallbackSchema): Promise; + abstract handleCallback( + params: ConnectionCallbackSchema, + ): Promise; /** * Gets a user id from state @@ -54,12 +58,25 @@ export default abstract class Connection { this.states.delete(state); } - async createConnection(data: ConnectedAccountSchema): Promise { - const ca = OrmUtils.mergeDeep(new ConnectedAccount(), data) as ConnectedAccount; + /** + * Creates a Connected Account in the database. + * @param data connected account data + * @returns the new connected account + */ + async createConnection( + data: ConnectedAccountSchema, + ): Promise { + const ca = ConnectedAccount.create({ ...data }); await ca.save(); return ca; } + /** + * Checks if a user has an exist connected account for the given extenal id. + * @param userId the user id + * @param externalId the connection id to find + * @returns + */ async hasConnection(userId: string, externalId: string): Promise { const existing = await ConnectedAccount.findOne({ where: { diff --git a/src/util/connections/ConnectionStore.ts b/src/util/connections/ConnectionStore.ts index 406e8232..759b6de7 100644 --- a/src/util/connections/ConnectionStore.ts +++ b/src/util/connections/ConnectionStore.ts @@ -1,5 +1,7 @@ import Connection from "./Connection"; +import RefreshableConnection from "./RefreshableConnection"; export class ConnectionStore { - public static connections: Map = new Map(); + public static connections: Map = + new Map(); } diff --git a/src/util/connections/RefreshableConnection.ts b/src/util/connections/RefreshableConnection.ts new file mode 100644 index 00000000..0008cbc0 --- /dev/null +++ b/src/util/connections/RefreshableConnection.ts @@ -0,0 +1,30 @@ +import { ConnectedAccount } from "../entities"; +import { ConnectedAccountCommonOAuthTokenResponse } from "../interfaces"; +import Connection from "./Connection"; + +/** + * A connection that can refresh its token. + */ +export default abstract class RefreshableConnection extends Connection { + refreshEnabled = true; + /** + * Refreshes the token for a connected account. + * @param connectedAccount The connected account to refresh + */ + abstract refreshToken( + connectedAccount: ConnectedAccount, + ): Promise; + + /** + * Refreshes the token for a connected account and saves it to the database. + * @param connectedAccount The connected account to refresh + */ + async refresh( + connectedAccount: ConnectedAccount, + ): Promise { + const tokenData = await this.refreshToken(connectedAccount); + connectedAccount.token_data = tokenData; + await connectedAccount.save(); + return tokenData; + } +} diff --git a/src/util/connections/index.ts b/src/util/connections/index.ts index e15d0c8c..8d20bf27 100644 --- a/src/util/connections/index.ts +++ b/src/util/connections/index.ts @@ -2,3 +2,4 @@ export * from "./Connection"; export * from "./ConnectionConfig"; export * from "./ConnectionLoader"; export * from "./ConnectionStore"; +export * from "./RefreshableConnection"; diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts index debc5535..ca15ff41 100644 --- a/src/util/dtos/ConnectedAccountDTO.ts +++ b/src/util/dtos/ConnectedAccountDTO.ts @@ -23,8 +23,8 @@ export class ConnectedAccountDTO { this.id = connectedAccount.external_id; this.user_id = connectedAccount.user_id; this.access_token = - connectedAccount.access_token && with_token - ? connectedAccount.access_token + connectedAccount.token_data && with_token + ? connectedAccount.token_data.access_token : undefined; this.friend_sync = connectedAccount.friend_sync; this.name = connectedAccount.name; diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts index 25d5a0c7..beb53e41 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts @@ -17,6 +17,7 @@ */ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { ConnectedAccountTokenData } from "../interfaces"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; @@ -40,9 +41,6 @@ export class ConnectedAccount extends BaseClass { }) user: User; - @Column({ select: false, nullable: true }) - access_token?: string; - @Column({ select: false }) friend_sync?: boolean = false; @@ -75,4 +73,7 @@ export class ConnectedAccount extends BaseClass { @Column() two_way_link?: boolean = false; + + @Column({ select: false, nullable: true, type: "simple-json" }) + token_data?: ConnectedAccountTokenData; } diff --git a/src/util/interfaces/ConnectedAccount.ts b/src/util/interfaces/ConnectedAccount.ts new file mode 100644 index 00000000..c96e5f79 --- /dev/null +++ b/src/util/interfaces/ConnectedAccount.ts @@ -0,0 +1,16 @@ +export interface ConnectedAccountCommonOAuthTokenResponse { + access_token: string; + token_type: string; + scope: string; + refresh_token?: string; + expires_in?: number; +} + +export interface ConnectedAccountTokenData { + access_token: string; + token_type?: string; + scope?: string; + refresh_token?: string; + expires_in?: number; + expires_at?: number; +} diff --git a/src/util/interfaces/index.ts b/src/util/interfaces/index.ts index fa259ce1..e194d174 100644 --- a/src/util/interfaces/index.ts +++ b/src/util/interfaces/index.ts @@ -17,7 +17,8 @@ */ export * from "./Activity"; -export * from "./Presence"; -export * from "./Interaction"; +export * from "./ConnectedAccount"; export * from "./Event"; +export * from "./Interaction"; +export * from "./Presence"; export * from "./Status"; diff --git a/src/util/schemas/ConnectedAccountSchema.ts b/src/util/schemas/ConnectedAccountSchema.ts index e00e4fa1..e5f838d0 100644 --- a/src/util/schemas/ConnectedAccountSchema.ts +++ b/src/util/schemas/ConnectedAccountSchema.ts @@ -1,7 +1,9 @@ +import { ConnectedAccountTokenData } from "../interfaces"; + export interface ConnectedAccountSchema { external_id: string; user_id: string; - access_token?: string; + token_data?: ConnectedAccountTokenData; friend_sync?: boolean; name: string; revoked?: boolean; -- cgit 1.5.1 From c7277efbad5d3979222518ae543366ba8a08ca77 Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:15:26 +1100 Subject: Move redirect uri generation to getRedirectUri function of Connection class. Use api_endpointPublic instead of cdn_endpointPublic --- package-lock.json | 5 +++++ .../#connection_name/#connection_id/index.ts | 12 +++++++----- src/connections/BattleNet/index.ts | 14 ++------------ src/connections/Discord/index.ts | 15 ++------------- src/connections/EpicGames/index.ts | 9 +-------- src/connections/Facebook/index.ts | 16 ++-------------- src/connections/GitHub/index.ts | 9 +-------- src/connections/Reddit/index.ts | 14 ++------------ src/connections/Spotify/index.ts | 14 ++------------ src/connections/Twitch/index.ts | 14 ++------------ src/connections/Twitter/index.ts | 19 +++---------------- src/connections/Xbox/index.ts | 14 ++------------ src/connections/Youtube/index.ts | 14 ++------------ src/util/config/types/ApiConfiguration.ts | 2 +- src/util/connections/Connection.ts | 12 +++++++++++- 15 files changed, 45 insertions(+), 138 deletions(-) (limited to 'src/util/connections/Connection.ts') diff --git a/package-lock.json b/package-lock.json index 8947d9e4..db3cbd8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14178,6 +14178,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "wretch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/wretch/-/wretch-2.3.2.tgz", + "integrity": "sha512-brN97Z2Mwed+w5z+keYI1u5OwWhPIaW0sJi9CxtKBVxLc3aqP6j1+2FCoIskM7WJq6SUHdxTFx20ox0iDLa0mQ==" + }, "ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts index 07440eac..5b8936f0 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts @@ -3,7 +3,7 @@ import { ConnectedAccount, ConnectionUpdateSchema, DiscordApiErrors, - emitEvent + emitEvent, } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); @@ -38,10 +38,12 @@ router.patch( if (!connection) return DiscordApiErrors.UNKNOWN_CONNECTION; // TODO: do we need to do anything if the connection is revoked? - //@ts-ignore For some reason the client sends this as a boolean, even tho docs say its a number? - if (typeof body.visibility === "boolean") body.visibility = body.visibility ? 1 : 0; - //@ts-ignore For some reason the client sends this as a boolean, even tho docs say its a number? - if (typeof body.show_activity === "boolean") body.show_activity = body.show_activity ? 1 : 0; + if (typeof body.visibility === "boolean") + //@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number? + body.visibility = body.visibility ? 1 : 0; + if (typeof body.show_activity === "boolean") + //@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number? + body.show_activity = body.show_activity ? 1 : 0; connection.assign(req.body); diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 96c3993c..a88633ab 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -41,13 +40,7 @@ export default class BattleNetConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); url.searchParams.append("response_type", "code"); @@ -76,10 +69,7 @@ export default class BattleNetConnection extends Connection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Discord/index.ts b/src/connections/Discord/index.ts index 52fc9ffd..1f812e4d 100644 --- a/src/connections/Discord/index.ts +++ b/src/connections/Discord/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -42,14 +41,7 @@ export default class DiscordConnection extends Connection { url.searchParams.append("response_type", "code"); // controls whether, on repeated authorizations, the consent screen is shown url.searchParams.append("consent", "none"); - - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); return url.toString(); } @@ -76,10 +68,7 @@ export default class DiscordConnection extends Connection { client_secret: this.settings.clientSecret!, grant_type: "authorization_code", code: code, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/EpicGames/index.ts b/src/connections/EpicGames/index.ts index 247d2435..db09c74f 100644 --- a/src/connections/EpicGames/index.ts +++ b/src/connections/EpicGames/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -47,13 +46,7 @@ export default class EpicGamesConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); diff --git a/src/connections/Facebook/index.ts b/src/connections/Facebook/index.ts index 5413f867..cc298ed7 100644 --- a/src/connections/Facebook/index.ts +++ b/src/connections/Facebook/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -46,13 +45,7 @@ export default class FacebookConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("state", state); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -65,12 +58,7 @@ export default class FacebookConnection extends Connection { url.searchParams.append("client_id", this.settings.clientId!); url.searchParams.append("client_secret", this.settings.clientSecret!); url.searchParams.append("code", code); - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); return url.toString(); } diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index 8380e765..ea5e5493 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -36,13 +35,7 @@ export default class GitHubConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); return url.toString(); diff --git a/src/connections/Reddit/index.ts b/src/connections/Reddit/index.ts index 70b4a8af..7e5a1318 100644 --- a/src/connections/Reddit/index.ts +++ b/src/connections/Reddit/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -48,13 +47,7 @@ export default class RedditConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -85,10 +78,7 @@ export default class RedditConnection extends Connection { new URLSearchParams({ grant_type: "authorization_code", code: code, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts index 54ec2696..ff06d341 100644 --- a/src/connections/Spotify/index.ts +++ b/src/connections/Spotify/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -57,13 +56,7 @@ export default class SpotifyConnection extends RefreshableConnection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -94,10 +87,7 @@ export default class SpotifyConnection extends RefreshableConnection { new URLSearchParams({ grant_type: "authorization_code", code: code, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Twitch/index.ts b/src/connections/Twitch/index.ts index 264db3cc..7cc88caa 100644 --- a/src/connections/Twitch/index.ts +++ b/src/connections/Twitch/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -49,13 +48,7 @@ export default class TwitchConnection extends RefreshableConnection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -85,10 +78,7 @@ export default class TwitchConnection extends RefreshableConnection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Twitter/index.ts b/src/connections/Twitter/index.ts index ad9d55d4..8292b2c5 100644 --- a/src/connections/Twitter/index.ts +++ b/src/connections/Twitter/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -49,13 +48,7 @@ export default class TwitterConnection extends RefreshableConnection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -89,10 +82,7 @@ export default class TwitterConnection extends RefreshableConnection { grant_type: "authorization_code", code: code, client_id: this.settings.clientId!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), ) @@ -126,10 +116,7 @@ export default class TwitterConnection extends RefreshableConnection { grant_type: "refresh_token", refresh_token, client_id: this.settings.clientId!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), ) diff --git a/src/connections/Xbox/index.ts b/src/connections/Xbox/index.ts index 80a04dea..1f736373 100644 --- a/src/connections/Xbox/index.ts +++ b/src/connections/Xbox/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -56,13 +55,7 @@ export default class XboxConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -121,10 +114,7 @@ export default class XboxConnection extends Connection { grant_type: "authorization_code", code: code, client_id: this.settings.clientId!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), scope: this.scopes.join(" "), }), ) diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts index afc9356b..9fa8eb38 100644 --- a/src/connections/Youtube/index.ts +++ b/src/connections/Youtube/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -56,13 +55,7 @@ export default class YoutubeConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -92,10 +85,7 @@ export default class YoutubeConnection extends Connection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/util/config/types/ApiConfiguration.ts b/src/util/config/types/ApiConfiguration.ts index 0389ed3e..579b1f2d 100644 --- a/src/util/config/types/ApiConfiguration.ts +++ b/src/util/config/types/ApiConfiguration.ts @@ -20,5 +20,5 @@ export class ApiConfiguration { defaultVersion: string = "9"; activeVersions: string[] = ["6", "7", "8", "9"]; useFosscordEnhancements: boolean = true; - endpointPublic: string = "/api"; + endpointPublic: string | null = null; } diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index 8b60b0d2..26279299 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -1,7 +1,7 @@ import crypto from "crypto"; import { ConnectedAccount } from "../entities"; import { ConnectedAccountSchema, ConnectionCallbackSchema } from "../schemas"; -import { DiscordApiErrors } from "../util"; +import { Config, DiscordApiErrors } from "../util"; /** * A connection that can be used to connect to an external service. @@ -19,6 +19,16 @@ export default abstract class Connection { */ abstract getAuthorizationUrl(userId: string): string; + /** + * Returns the redirect_uri for a connection type + * @returns redirect_uri for this connection + */ + getRedirectUri() { + const endpointPublic = + Config.get().api.endpointPublic ?? "http://localhost:3001"; + return `${endpointPublic}/connections/${this.id}/callback`; + } + /** * Processes the callback * @param args Callback arguments -- cgit 1.5.1