diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2023-04-02 11:30:31 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-02 11:30:31 +1000 |
commit | 86ac90b1e4e83cb1f55c45055d9ab3a488fe67bd (patch) | |
tree | 83c97c8d7464bbfb0b1924597f1dde8c69528ba9 /src/api | |
parent | Remove ALL fosscord mentions (diff) | |
parent | Less spammy user connection logs (diff) | |
download | server-86ac90b1e4e83cb1f55c45055d9ab3a488fe67bd.tar.xz |
Merge pull request #1009 from Puyodead1/refactor/dev/connections
Connections Part 1
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/Server.ts | 5 | ||||
-rw-r--r-- | src/api/middlewares/Authentication.ts | 2 | ||||
-rw-r--r-- | src/api/routes/connections/#connection_name/#connection_id/refresh.ts | 11 | ||||
-rw-r--r-- | src/api/routes/connections/#connection_name/authorize.ts | 34 | ||||
-rw-r--r-- | src/api/routes/connections/#connection_name/callback.ts | 53 | ||||
-rw-r--r-- | src/api/routes/policies/instance/domains.ts | 2 | ||||
-rw-r--r-- | src/api/routes/users/#id/profile.ts | 4 | ||||
-rw-r--r-- | src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts | 81 | ||||
-rw-r--r-- | src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts | 88 | ||||
-rw-r--r-- | src/api/routes/users/@me/connections/index.ts (renamed from src/api/routes/users/@me/connections.ts) | 22 |
10 files changed, 298 insertions, 4 deletions
diff --git a/src/api/Server.ts b/src/api/Server.ts index 447a4802..472ab1d6 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -25,6 +25,8 @@ import { registerRoutes, Sentry, WebAuthn, + ConnectionConfig, + ConnectionLoader, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { Server, ServerOptions } from "lambert-server"; @@ -72,6 +74,7 @@ export class SpacebarServer extends Server { await Config.init(); await initEvent(); await Email.init(); + await ConnectionConfig.init(); await initInstance(); await Sentry.init(this.app); WebAuthn.init(); @@ -142,6 +145,8 @@ export class SpacebarServer extends Server { Sentry.errorHandler(this.app); + ConnectionLoader.loadConnections(); + if (logRequests) console.log( red( diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 09644eee..d0e4d8a0 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -52,6 +52,8 @@ export const NO_AUTHORIZATION_ROUTES = [ "/oauth2/callback", // Asset delivery /\/guilds\/\d+\/widget\.(json|png)/, + // Connections + /\/connections\/\w+\/callback/, ]; export const API_PREFIX = /^\/api(\/v\d+)?/; diff --git a/src/api/routes/connections/#connection_name/#connection_id/refresh.ts b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts new file mode 100644 index 00000000..4fa64978 --- /dev/null +++ b/src/api/routes/connections/#connection_name/#connection_id/refresh.ts @@ -0,0 +1,11 @@ +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.post("/", route({}), async (req: Request, res: Response) => { + // TODO: + const { connection_name, connection_id } = req.params; + res.sendStatus(204); +}); + +export default router; diff --git a/src/api/routes/connections/#connection_name/authorize.ts b/src/api/routes/connections/#connection_name/authorize.ts new file mode 100644 index 00000000..63738e7c --- /dev/null +++ b/src/api/routes/connections/#connection_name/authorize.ts @@ -0,0 +1,34 @@ +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; +import { ConnectionStore, FieldErrors } from "../../../../util"; + +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { connection_name } = req.params; + const connection = ConnectionStore.connections.get(connection_name); + if (!connection) + throw FieldErrors({ + provider_id: { + code: "BASE_TYPE_CHOICES", + message: req.t("common:field.BASE_TYPE_CHOICES", { + types: Array.from(ConnectionStore.connections.keys()).join( + ", ", + ), + }), + }, + }); + + if (!connection.settings.enabled) + throw FieldErrors({ + provider_id: { + message: "This connection has been disabled server-side.", + }, + }); + + res.json({ + url: await connection.getAuthorizationUrl(req.user_id), + }); +}); + +export default router; diff --git a/src/api/routes/connections/#connection_name/callback.ts b/src/api/routes/connections/#connection_name/callback.ts new file mode 100644 index 00000000..74021170 --- /dev/null +++ b/src/api/routes/connections/#connection_name/callback.ts @@ -0,0 +1,53 @@ +import { route } from "@spacebar/api"; +import { + ConnectionCallbackSchema, + ConnectionStore, + emitEvent, + FieldErrors, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; + +const router = Router(); + +router.post( + "/", + route({ body: "ConnectionCallbackSchema" }), + async (req: Request, res: Response) => { + const { connection_name } = req.params; + const connection = ConnectionStore.connections.get(connection_name); + if (!connection) + throw FieldErrors({ + provider_id: { + code: "BASE_TYPE_CHOICES", + message: req.t("common:field.BASE_TYPE_CHOICES", { + types: Array.from( + ConnectionStore.connections.keys(), + ).join(", "), + }), + }, + }); + + if (!connection.settings.enabled) + throw FieldErrors({ + provider_id: { + message: "This connection has been disabled server-side.", + }, + }); + + const body = req.body as ConnectionCallbackSchema; + const userId = connection.getUserId(body.state); + const connectedAccnt = await connection.handleCallback(body); + + // whether we should emit a connections update event, only used when a connection doesnt already exist + if (connectedAccnt) + emitEvent({ + event: "USER_CONNECTIONS_UPDATE", + data: { ...connectedAccnt, token_data: undefined }, + user_id: userId, + }); + + res.sendStatus(204); + }, +); + +export default router; diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts index fe032b50..696a8510 100644 --- a/src/api/routes/policies/instance/domains.ts +++ b/src/api/routes/policies/instance/domains.ts @@ -31,7 +31,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { process.env.GATEWAY || "ws://localhost:3001", defaultApiVersion: api.defaultVersion ?? 9, - apiEndpoint: api.endpointPublic ?? "/api", + apiEndpoint: api.endpointPublic ?? "http://localhost:3001/api/", }; res.json(IdentityForm); diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts index 4727e215..2836c563 100644 --- a/src/api/routes/users/#id/profile.ts +++ b/src/api/routes/users/#id/profile.ts @@ -133,7 +133,9 @@ router.get( guild_id, }; res.json({ - connected_accounts: user.connected_accounts, + connected_accounts: user.connected_accounts.filter( + (x) => x.visibility != 0, + ), premium_guild_since: premium_guild_since, // TODO premium_since: user.premium_since, // TODO mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true 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..a290d88a --- /dev/null +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/access-token.ts @@ -0,0 +1,81 @@ +import { route } from "@spacebar/api"; +import { + ApiError, + ConnectedAccount, + ConnectionStore, + DiscordApiErrors, + FieldErrors, +} 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) + +// spotify is disabled here because it cant be used +const ALLOWED_CONNECTIONS = ["twitch", "youtube"]; + +// NOTE: this route has not been extensively tested, as the required connections are not implemented as of writing +router.get("/", route({}), async (req: Request, res: Response) => { + const { connection_name, connection_id } = req.params; + + const connection = ConnectionStore.connections.get(connection_name); + + 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, + external_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 DiscordApiErrors.CONNECTION_REVOKED; + 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, fetched_at } = connectedAccount.token_data; + + if ( + (expires_at && expires_at < Date.now()) || + (expires_in && fetched_at + expires_in * 1000 < 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/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts new file mode 100644 index 00000000..afb0df06 --- /dev/null +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts @@ -0,0 +1,88 @@ +import { route } from "@spacebar/api"; +import { + ConnectedAccount, + ConnectionUpdateSchema, + DiscordApiErrors, + emitEvent, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; +const router = Router(); + +// TODO: connection update schema +router.patch( + "/", + route({ body: "ConnectionUpdateSchema" }), + async (req: Request, res: Response) => { + const { connection_name, connection_id } = req.params; + const body = req.body as ConnectionUpdateSchema; + + const connection = await ConnectedAccount.findOne({ + where: { + user_id: req.user_id, + external_id: connection_id, + type: connection_name, + }, + select: [ + "external_id", + "type", + "name", + "verified", + "visibility", + "show_activity", + "revoked", + "friend_sync", + "integrations", + ], + }); + + if (!connection) return DiscordApiErrors.UNKNOWN_CONNECTION; + // TODO: do we need to do anything if the connection is revoked? + + 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; + if (typeof body.metadata_visibility === "boolean") + //@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number? + body.metadata_visibility = body.metadata_visibility ? 1 : 0; + + connection.assign(req.body); + + await ConnectedAccount.update( + { + user_id: req.user_id, + external_id: connection_id, + type: connection_name, + }, + connection, + ); + res.json(connection.toJSON()); + }, +); + +router.delete("/", route({}), async (req: Request, res: Response) => { + const { connection_name, connection_id } = req.params; + + const account = await ConnectedAccount.findOneOrFail({ + where: { + user_id: req.user_id, + external_id: connection_id, + type: connection_name, + }, + }); + + await Promise.all([ + ConnectedAccount.remove(account), + emitEvent({ + event: "USER_CONNECTIONS_UPDATE", + data: account, + user_id: req.user_id, + }), + ]); + + return res.sendStatus(200); +}); + +export default router; diff --git a/src/api/routes/users/@me/connections.ts b/src/api/routes/users/@me/connections/index.ts index 1d4564da..620ce3b5 100644 --- a/src/api/routes/users/@me/connections.ts +++ b/src/api/routes/users/@me/connections/index.ts @@ -18,12 +18,30 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; +import { ConnectedAccount, ConnectedAccountDTO } from "@spacebar/util"; const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - //TODO - res.json([]).status(200); + const connections = await ConnectedAccount.find({ + where: { + user_id: req.user_id, + }, + select: [ + "external_id", + "type", + "name", + "verified", + "visibility", + "show_activity", + "revoked", + "token_data", + "friend_sync", + "integrations", + ], + }); + + res.json(connections.map((x) => new ConnectedAccountDTO(x, true))); }); export default router; |