From 05140f0d6f87019cbbf86f1e410bd0e243912b02 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 10 Mar 2024 12:48:14 -1000 Subject: [PATCH] Begin work on refactoring every single route to use new subsystems --- .vscode/launch.json | 18 +++++++++++ .vscode/settings.json | 4 ++- classes/configmanager.ts | 27 ++++++++-------- server/api/.well-known/host-meta/index.ts | 23 +++++--------- server/api/.well-known/lysand.ts | 17 +++------- server/api/.well-known/nodeinfo/index.ts | 16 +++------- server/api/.well-known/webfinger/index.ts | 33 ++++++++------------ server/api/auth/login/index.ts | 21 +++++-------- server/api/media/[id]/index.ts | 10 ++---- server/api/nodeinfo/2.0/index.ts | 7 ++--- server/api/oauth/authorize-external/index.ts | 14 +++------ server/api/oauth/callback/[issuer]/index.ts | 13 +++----- server/api/oauth/providers/index.ts | 10 +++--- server/api/oauth/token/index.ts | 23 +++++++------- server/api/object/[uuid]/index.ts | 15 ++------- server/api/users/[uuid]/inbox/index.ts | 18 ++++------- server/api/users/[uuid]/index.ts | 15 ++------- server/api/users/[uuid]/outbox/index.ts | 22 ++++++------- utils/api.ts | 5 +++ 19 files changed, 128 insertions(+), 183 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..980184cd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests.v2.lysand", + "request": "launch", + "args": [ + "test", + "${jest.testFile}" + ], + "cwd": "/home/jessew/Dev/lysand", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "/home/jessew/.bun/bin/bun" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d360cbc..6286bcf3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "jest.jestCommandLine": "/home/jessew/.bun/bin/bun test", + "jest.rootPath": "." } diff --git a/classes/configmanager.ts b/classes/configmanager.ts index 45e9d4f0..2007bbed 100644 --- a/classes/configmanager.ts +++ b/classes/configmanager.ts @@ -6,7 +6,7 @@ * Fuses both and provides a way to retrieve individual values */ -import { parse, stringify } from "@iarna/toml"; +/* import { parse, stringify } from "@iarna/toml"; import chalk from "chalk"; import merge from "merge-deep-ts"; @@ -23,10 +23,10 @@ const scanConfig = async () => { } return parse(await config.text()) as ConfigType; -}; +}; */ // Creates the internal config with nothing in it if it doesnt exist -const scanInternalConfig = async () => { +/* const scanInternalConfig = async () => { const config = Bun.file(process.cwd() + "/config/config.internal.toml"); if (!(await config.exists())) { @@ -397,18 +397,18 @@ export const configDefaults: ConfigType = { max_coeff: 1, }, custom_ratelimits: {}, -}; +}; */ -export const getConfig = () => { +/* export const getConfig = () => { // Deeply merge configDefaults, config and internalConfig return merge([configDefaults, config, internalConfig]) as any as ConfigType; }; - + */ /** * Sets the internal config * @param newConfig Any part of ConfigType */ -export const setConfig = async (newConfig: Partial) => { +/* export const setConfig = async (newConfig: Partial) => { const newInternalConfig = merge([ internalConfig, newConfig, @@ -421,16 +421,16 @@ export const setConfig = async (newConfig: Partial) => { newInternalConfig as any )}` ); -}; +}; */ -export const getHost = () => { +/* export const getHost = () => { const url = new URL(getConfig().http.base_url); return url.host; }; - + */ // Refresh config every 5 seconds -setInterval(() => { +/* setInterval(() => { scanConfig() .then(newConfig => { if (newConfig !== config) { @@ -440,6 +440,7 @@ setInterval(() => { .catch(e => { console.error(e); }); -}, 5000); +}, 5000); */ -export { config }; +/* export { config }; + */ diff --git a/server/api/.well-known/host-meta/index.ts b/server/api/.well-known/host-meta/index.ts index ca416ba3..c473fe6d 100644 --- a/server/api/.well-known/host-meta/index.ts +++ b/server/api/.well-known/host-meta/index.ts @@ -1,7 +1,5 @@ -import { MatchedRoute } from "bun"; -import { getConfig, getHost } from "~classes/configmanager"; import { xmlResponse } from "@response"; -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -16,18 +14,13 @@ export const meta = applyConfig({ }); -/** - * Host meta endpoint - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - const config = getConfig(); +export default apiRoute(async (req, matchedRoute, extraData) => { + const config = await extraData.configManager.getConfig(); + return xmlResponse(` - - + + - + `); -}; +}); diff --git a/server/api/.well-known/lysand.ts b/server/api/.well-known/lysand.ts index 6a80d16e..5f7de0b6 100644 --- a/server/api/.well-known/lysand.ts +++ b/server/api/.well-known/lysand.ts @@ -1,7 +1,5 @@ import { jsonResponse } from "@response"; -import { MatchedRoute } from "bun"; -import { getConfig } from "~classes/configmanager"; -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -15,14 +13,9 @@ export const meta = applyConfig({ route: "/.well-known/lysand", }); -/** - * Lysand instance metadata endpoint - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - const config = getConfig(); +export default apiRoute(async (req, matchedRoute, extraData) => { + const config = await extraData.configManager.getConfig(); + // In the format acct:name@example.com return jsonResponse({ type: "ServerMetadata", @@ -47,4 +40,4 @@ export default async ( website: "https://lysand.org", // TODO: Add admins, moderators field }) -}; +}) diff --git a/server/api/.well-known/nodeinfo/index.ts b/server/api/.well-known/nodeinfo/index.ts index 81bd149b..a348c4da 100644 --- a/server/api/.well-known/nodeinfo/index.ts +++ b/server/api/.well-known/nodeinfo/index.ts @@ -1,6 +1,4 @@ -import { MatchedRoute } from "bun"; -import { getConfig, getHost } from "~classes/configmanager"; -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -15,14 +13,8 @@ export const meta = applyConfig({ }); -/** - * Redirect to /nodeinfo/2.0 - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - const config = getConfig(); +export default apiRoute(async (req, matchedRoute, extraData) => { + const config = await extraData.configManager.getConfig(); return new Response("", { status: 301, @@ -30,4 +22,4 @@ export default async ( Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`, }, }); -}; +}); diff --git a/server/api/.well-known/webfinger/index.ts b/server/api/.well-known/webfinger/index.ts index 9e9e7e1e..f9b35a84 100644 --- a/server/api/.well-known/webfinger/index.ts +++ b/server/api/.well-known/webfinger/index.ts @@ -1,7 +1,5 @@ import { errorResponse, jsonResponse } from "@response"; -import { MatchedRoute } from "bun"; -import { getConfig, getHost } from "~classes/configmanager"; -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; import { client } from "~database/datasource"; export const meta = applyConfig({ @@ -16,35 +14,30 @@ export const meta = applyConfig({ route: "/.well-known/webfinger", }); -/** - * ActivityPub WebFinger endpoint - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(async (req, matchedRoute, extraData) => { // In the format acct:name@example.com const resource = matchedRoute.query.resource; const requestedUser = resource.split("acct:")[1]; - - const config = getConfig(); - + + const config = await extraData.configManager.getConfig(); + const host = new URL(config.http.base_url).hostname; + // Check if user is a local user - if (requestedUser.split("@")[1] !== getHost()) { + if (requestedUser.split("@")[1] !== host) { return errorResponse("User is a remote user", 404); } - + const user = await client.user.findUnique({ where: { username: requestedUser.split("@")[0] }, }); - + if (!user) { return errorResponse("User not found", 404); } - + return jsonResponse({ - subject: `acct:${user.username}@${getHost()}`, - + subject: `acct:${user.username}@${host}`, + links: [ { rel: "self", @@ -63,4 +56,4 @@ export default async ( } ] }) -}; +}); \ No newline at end of file diff --git a/server/api/auth/login/index.ts b/server/api/auth/login/index.ts index c8d6bc9a..7b9094b6 100644 --- a/server/api/auth/login/index.ts +++ b/server/api/auth/login/index.ts @@ -1,12 +1,10 @@ -import { applyConfig } from "@api"; -import type { MatchedRoute } from "bun"; +import { apiRoute, applyConfig } from "@api"; import { randomBytes } from "crypto"; import { client } from "~database/datasource"; import { TokenType } from "~database/entities/Token"; import { userRelations } from "~database/entities/User"; -import type { APIRouteMeta } from "~types/api"; -export const meta: APIRouteMeta = applyConfig({ +export const meta = applyConfig({ allowedMethods: ["POST"], ratelimits: { max: 4, @@ -21,10 +19,10 @@ export const meta: APIRouteMeta = applyConfig({ /** * OAuth Code flow */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute<{ + email: string; + password: string; +}>(async (req, matchedRoute, extraData) => { const scopes = (matchedRoute.query.scope || "") .replaceAll("+", " ") .split(" "); @@ -32,10 +30,7 @@ export default async ( const response_type = matchedRoute.query.response_type; const client_id = matchedRoute.query.client_id; - const formData = await req.formData(); - - const email = formData.get("email")?.toString() || null; - const password = formData.get("password")?.toString() || null; + const { email, password } = extraData.parsedRequest; const redirectToLogin = (error: string) => Response.redirect( @@ -96,4 +91,4 @@ export default async ( // Redirect back to application return Response.redirect(`${redirect_uri}?code=${code}`, 302); -}; +}); diff --git a/server/api/media/[id]/index.ts b/server/api/media/[id]/index.ts index cd8ebfae..4ee0589a 100644 --- a/server/api/media/[id]/index.ts +++ b/server/api/media/[id]/index.ts @@ -1,6 +1,5 @@ import { errorResponse } from "@response"; -import { applyConfig } from "@api"; -import type { MatchedRoute } from "bun"; +import { apiRoute, applyConfig } from "@api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -14,10 +13,7 @@ export const meta = applyConfig({ }, }); -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(async (req, matchedRoute) => { // TODO: Add checks for disabled or not email verified accounts const id = matchedRoute.params.id; @@ -46,4 +42,4 @@ export default async ( "Content-Range": `bytes ${start}-${end}/${file.size}`, }, }); -}; +}); diff --git a/server/api/nodeinfo/2.0/index.ts b/server/api/nodeinfo/2.0/index.ts index 8d3c0cef..99288f60 100644 --- a/server/api/nodeinfo/2.0/index.ts +++ b/server/api/nodeinfo/2.0/index.ts @@ -1,4 +1,4 @@ -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; import { jsonResponse } from "@response"; export const meta = applyConfig({ @@ -16,8 +16,7 @@ export const meta = applyConfig({ /** * ActivityPub nodeinfo 2.0 endpoint */ -// eslint-disable-next-line @typescript-eslint/require-await -export default async (): Promise => { +export default apiRoute(() => { // TODO: Implement this return jsonResponse({ version: "2.0", @@ -31,4 +30,4 @@ export default async (): Promise => { openRegistrations: false, metadata: {}, }); -}; +}); diff --git a/server/api/oauth/authorize-external/index.ts b/server/api/oauth/authorize-external/index.ts index db24bbcb..688f7a6a 100644 --- a/server/api/oauth/authorize-external/index.ts +++ b/server/api/oauth/authorize-external/index.ts @@ -1,7 +1,5 @@ -import { applyConfig } from "@api"; -import { getConfig } from "~classes/configmanager"; +import { apiRoute, applyConfig } from "@api"; import { oauthRedirectUri } from "@constants"; -import type { MatchedRoute } from "bun"; import { calculatePKCECodeChallenge, discoveryRequest, @@ -25,11 +23,7 @@ export const meta = applyConfig({ /** * Redirects the user to the external OAuth provider */ -export default async ( - req: Request, - matchedRoute: MatchedRoute - // eslint-disable-next-line @typescript-eslint/require-await -): Promise => { +export default apiRoute(async (req, matchedRoute, extraData) => { const redirectToLogin = (error: string) => Response.redirect( `/oauth/authorize?` + @@ -49,7 +43,7 @@ export default async ( return redirectToLogin("Missing client_id"); } - const config = getConfig(); + const config = await extraData.configManager.getConfig(); const issuer = config.oidc.providers.find( provider => provider.id === issuerId @@ -98,4 +92,4 @@ export default async ( }).toString(), 302 ); -}; +}); diff --git a/server/api/oauth/callback/[issuer]/index.ts b/server/api/oauth/callback/[issuer]/index.ts index dd5290a2..15d905eb 100644 --- a/server/api/oauth/callback/[issuer]/index.ts +++ b/server/api/oauth/callback/[issuer]/index.ts @@ -1,7 +1,5 @@ -import { applyConfig } from "@api"; -import { getConfig } from "~classes/configmanager"; +import { apiRoute, applyConfig } from "@api"; import { oauthRedirectUri } from "@constants"; -import type { MatchedRoute } from "bun"; import { randomBytes } from "crypto"; import { authorizationCodeGrantRequest, @@ -33,10 +31,7 @@ export const meta = applyConfig({ /** * Redirects the user to the external OAuth provider */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(async (req, matchedRoute, extraData) => { const redirectToLogin = (error: string) => Response.redirect( `/oauth/authorize?` + @@ -65,7 +60,7 @@ export default async ( return redirectToLogin("Invalid flow"); } - const config = getConfig(); + const config = await extraData.configManager.getConfig(); const issuer = config.oidc.providers.find( provider => provider.id === issuerParam @@ -192,4 +187,4 @@ export default async ( `${flow.application.redirect_uris}?code=${code}`, 302 ); -}; +}); diff --git a/server/api/oauth/providers/index.ts b/server/api/oauth/providers/index.ts index c3cc1ac8..ad0d9769 100644 --- a/server/api/oauth/providers/index.ts +++ b/server/api/oauth/providers/index.ts @@ -1,5 +1,4 @@ -import { applyConfig } from "@api"; -import { getConfig } from "~classes/configmanager"; +import { apiRoute, applyConfig } from "@api"; import { jsonResponse } from "@response"; export const meta = applyConfig({ @@ -17,9 +16,8 @@ export const meta = applyConfig({ /** * Lists available OAuth providers */ -// eslint-disable-next-line @typescript-eslint/require-await -export default async (): Promise => { - const config = getConfig(); +export default apiRoute(async (req, matchedRoute, extraData) => { + const config = await extraData.configManager.getConfig(); return jsonResponse( config.oidc.providers.map(p => ({ @@ -28,4 +26,4 @@ export default async (): Promise => { id: p.id, })) ); -}; +}); diff --git a/server/api/oauth/token/index.ts b/server/api/oauth/token/index.ts index 70a68db8..f75f2209 100644 --- a/server/api/oauth/token/index.ts +++ b/server/api/oauth/token/index.ts @@ -1,5 +1,4 @@ -import { applyConfig } from "@api"; -import { parseRequest } from "@request"; +import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; @@ -18,16 +17,16 @@ export const meta = applyConfig({ /** * Allows getting token from OAuth code */ -export default async (req: Request): Promise => { +export default apiRoute<{ + grant_type: string; + code: string; + redirect_uri: string; + client_id: string; + client_secret: string; + scope: string; +}>(async (req, matchedRoute, extraData) => { const { grant_type, code, redirect_uri, client_id, client_secret, scope } = - await parseRequest<{ - grant_type: string; - code: string; - redirect_uri: string; - client_id: string; - client_secret: string; - scope: string; - }>(req); + extraData.parsedRequest; if (grant_type !== "authorization_code") return errorResponse( @@ -61,4 +60,4 @@ export default async (req: Request): Promise => { scope: token.scope, created_at: token.created_at, }); -}; +}); diff --git a/server/api/object/[uuid]/index.ts b/server/api/object/[uuid]/index.ts index 4d91f5b9..3803234b 100644 --- a/server/api/object/[uuid]/index.ts +++ b/server/api/object/[uuid]/index.ts @@ -1,8 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; import { jsonResponse } from "@response"; -import type { MatchedRoute } from "bun"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -16,12 +13,6 @@ export const meta = applyConfig({ route: "/object/:id", }); -/** - * Fetch a user - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(() => { return jsonResponse({}); -}; +}); diff --git a/server/api/users/[uuid]/inbox/index.ts b/server/api/users/[uuid]/inbox/index.ts index bc325870..f1547cc0 100644 --- a/server/api/users/[uuid]/inbox/index.ts +++ b/server/api/users/[uuid]/inbox/index.ts @@ -1,10 +1,7 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { applyConfig } from "@api"; -import { getConfig } from "~classes/configmanager"; +// TODO: Refactor into smaller packages +import { apiRoute, applyConfig } from "@api"; import { getBestContentType } from "@content_types"; import { errorResponse, jsonResponse } from "@response"; -import type { MatchedRoute } from "bun"; import { client } from "~database/datasource"; import { parseEmojis } from "~database/entities/Emoji"; import { createLike, deleteLike } from "~database/entities/Like"; @@ -39,13 +36,10 @@ export const meta = applyConfig({ /** * ActivityPub user inbox endpoint */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(async (req, matchedRoute, extraData) => { const username = matchedRoute.params.username; - const config = getConfig(); + const config = await extraData.configManager.getConfig(); try { if ( @@ -313,7 +307,7 @@ export default async ( } // Create new reblog - const newReblog = await client.status.create({ + await client.status.create({ data: { authorId: author.id, reblogId: rebloggedStatus.id, @@ -405,4 +399,4 @@ export default async ( } return jsonResponse({}); -}; +}); diff --git a/server/api/users/[uuid]/index.ts b/server/api/users/[uuid]/index.ts index 3c392ee6..763caf08 100644 --- a/server/api/users/[uuid]/index.ts +++ b/server/api/users/[uuid]/index.ts @@ -1,9 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { applyConfig } from "@api"; -import { getConfig } from "~classes/configmanager"; +import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import type { MatchedRoute } from "bun"; import { client } from "~database/datasource"; import { userRelations, userToLysand } from "~database/entities/User"; @@ -22,14 +18,9 @@ export const meta = applyConfig({ /** * ActivityPub user inbox endpoint */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(async (req, matchedRoute) => { const uuid = matchedRoute.params.uuid; - const config = getConfig(); - const user = await client.user.findUnique({ where: { id: uuid, @@ -42,4 +33,4 @@ export default async ( } return jsonResponse(userToLysand(user)); -}; +}); diff --git a/server/api/users/[uuid]/outbox/index.ts b/server/api/users/[uuid]/outbox/index.ts index d130668a..90add2c7 100644 --- a/server/api/users/[uuid]/outbox/index.ts +++ b/server/api/users/[uuid]/outbox/index.ts @@ -1,7 +1,5 @@ import { jsonResponse } from "@response"; -import type { MatchedRoute } from "bun"; -import { getConfig, getHost } from "~classes/configmanager"; -import { applyConfig } from "@api"; +import { apiRoute, applyConfig } from "@api"; import { statusAndUserRelations, statusToLysand, @@ -23,13 +21,11 @@ export const meta = applyConfig({ /** * ActivityPub user outbox endpoint */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { +export default apiRoute(async (req, matchedRoute, extraData) => { const uuid = matchedRoute.params.uuid; const pageNumber = Number(matchedRoute.query.page) || 1; - const config = getConfig(); + const config = await extraData.configManager.getConfig(); + const host = new URL(config.http.base_url).hostname; const statuses = await client.status.findMany({ where: { @@ -53,19 +49,19 @@ export default async ( }); return jsonResponse({ - first: `${getHost()}/users/${uuid}/outbox?page=1`, - last: `${getHost()}/users/${uuid}/outbox?page=1`, + first: `${host}/users/${uuid}/outbox?page=1`, + last: `${host}/users/${uuid}/outbox?page=1`, total_items: totalStatuses, // Server actor author: `${config.http.base_url}/users/actor`, next: statuses.length === 20 - ? `${getHost()}/users/${uuid}/outbox?page=${pageNumber + 1}` + ? `${host}/users/${uuid}/outbox?page=${pageNumber + 1}` : undefined, prev: pageNumber > 1 - ? `${getHost()}/users/${uuid}/outbox?page=${pageNumber - 1}` + ? `${host}/users/${uuid}/outbox?page=${pageNumber - 1}` : undefined, items: statuses.map(s => statusToLysand(s)), }); -}; +}); diff --git a/utils/api.ts b/utils/api.ts index 01a479f7..a30ea4ed 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -1,4 +1,5 @@ import { getConfig } from "~classes/configmanager"; +import type { RouteHandler } from "~server/api/routes.type"; import type { APIRouteMeta } from "~types/api"; export const applyConfig = (routeMeta: APIRouteMeta) => { @@ -16,3 +17,7 @@ export const applyConfig = (routeMeta: APIRouteMeta) => { return newMeta; }; + +export const apiRoute = (routeFunction: RouteHandler) => { + return routeFunction; +};