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)
This commit is contained in:
Puyodead1
2023-03-18 19:27:39 -04:00
parent 50f068400d
commit 0db1fa5f0b
18 changed files with 292 additions and 111 deletions
@@ -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;
@@ -35,7 +35,7 @@ router.get("/", route({}), async (req: Request, res: Response) => {
"visibility",
"show_activity",
"revoked",
"access_token",
"token_data",
"friend_sync",
"integrations",
],