From 3b452d66aa4871da9d0aa50d5058d2ca2f70a509 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 26 Sep 2023 12:33:43 -1000 Subject: [PATCH] Small refactors --- database/entities/RawActivity.ts | 58 ++++++++++++++- database/entities/User.ts | 19 +++++ server/api/[username]/inbox/index.ts | 6 +- server/api/api/v1/accounts/[id]/block.ts | 3 +- server/api/api/v1/accounts/[id]/follow.ts | 3 +- server/api/api/v1/accounts/[id]/index.ts | 10 ++- server/api/api/v1/accounts/[id]/mute.ts | 3 +- server/api/api/v1/accounts/[id]/note.ts | 3 +- server/api/api/v1/accounts/[id]/pin.ts | 3 +- .../v1/accounts/[id]/remove_from_followers.ts | 3 +- server/api/api/v1/accounts/[id]/unblock.ts | 3 +- server/api/api/v1/accounts/[id]/unfollow.ts | 3 +- server/api/api/v1/accounts/[id]/unmute.ts | 3 +- server/api/api/v1/accounts/[id]/unpin.ts | 3 +- .../v1/accounts/familiar_followers/index.ts | 3 +- .../api/v1/accounts/relationships/index.ts | 3 +- .../v1/accounts/update_credentials/index.ts | 72 ++++++++++++++----- .../v1/accounts/verify_credentials/index.ts | 4 +- .../api/v1/apps/verify_credentials/index.ts | 4 +- server/api/api/v1/statuses/[id]/index.ts | 8 ++- server/api/api/v1/statuses/index.ts | 4 +- utils/auth.ts | 20 ------ 22 files changed, 165 insertions(+), 76 deletions(-) delete mode 100644 utils/auth.ts diff --git a/database/entities/RawActivity.ts b/database/entities/RawActivity.ts index 3510e709..5ea4a2ab 100644 --- a/database/entities/RawActivity.ts +++ b/database/entities/RawActivity.ts @@ -12,6 +12,9 @@ import { RawActor } from "./RawActor"; import { getConfig } from "@config"; import { errorResponse } from "@response"; +/** + * Represents a raw activity entity in the database. + */ @Entity({ name: "activities", }) @@ -30,6 +33,11 @@ export class RawActivity extends BaseEntity { @JoinTable() actors!: RawActor[]; + /** + * Retrieves all activities that contain an object with the given ID. + * @param id The ID of the object to search for. + * @returns A promise that resolves to an array of matching activities. + */ static async getByObjectId(id: string) { return await RawActivity.createQueryBuilder("activity") .leftJoinAndSelect("activity.objects", "objects") @@ -38,6 +46,11 @@ export class RawActivity extends BaseEntity { .getMany(); } + /** + * Retrieves the activity with the given ID. + * @param id The ID of the activity to retrieve. + * @returns A promise that resolves to the matching activity, or undefined if not found. + */ static async getById(id: string) { return await RawActivity.createQueryBuilder("activity") .leftJoinAndSelect("activity.objects", "objects") @@ -46,6 +59,11 @@ export class RawActivity extends BaseEntity { .getOne(); } + /** + * Retrieves the latest activity with the given ID. + * @param id The ID of the activity to retrieve. + * @returns A promise that resolves to the latest matching activity, or undefined if not found. + */ static async getLatestById(id: string) { return await RawActivity.createQueryBuilder("activity") .where("activity.data->>'id' = :id", { id }) @@ -55,10 +73,20 @@ export class RawActivity extends BaseEntity { .getOne(); } + /** + * Checks if an activity with the given ID exists. + * @param id The ID of the activity to check for. + * @returns A promise that resolves to true if the activity exists, false otherwise. + */ static async exists(id: string) { return !!(await RawActivity.getById(id)); } + /** + * Updates an object in the database if it exists. + * @param object The object to update. + * @returns A promise that resolves to the updated object, or an error response if the object does not exist or is filtered. + */ static async updateObjectIfExists(object: APObject) { const rawObject = await RawObject.getById(object.id ?? ""); @@ -76,6 +104,11 @@ export class RawActivity extends BaseEntity { return rawObject; } + /** + * Deletes an object from the database if it exists. + * @param object The object to delete. + * @returns A promise that resolves to the deleted object, or an error response if the object does not exist. + */ static async deleteObjectIfExists(object: APObject) { const dbObject = await RawObject.getById(object.id ?? ""); @@ -110,7 +143,16 @@ export class RawActivity extends BaseEntity { return dbObject; } - static async addIfNotExists(activity: APActivity, addObject?: RawObject) { + /** + * Adds an activity to the database if it does not already exist. + * @param activity The activity to add. + * @param addObject An optional object to add to the activity. + * @returns A promise that resolves to the added activity, or an error response if the activity already exists or is filtered. + */ + static async createIfNotExists( + activity: APActivity, + addObject?: RawObject + ) { if (await RawActivity.exists(activity.id ?? "")) { return errorResponse("Activity already exists", 409); } @@ -144,6 +186,10 @@ export class RawActivity extends BaseEntity { return rawActivity; } + /** + * Returns the ActivityPub representation of the activity. + * @returns The ActivityPub representation of the activity. + */ makeActivityPubRepresentation() { return { ...this.data, @@ -152,6 +198,11 @@ export class RawActivity extends BaseEntity { }; } + /** + * Adds an object to the activity if it does not already exist. + * @param object The object to add. + * @returns A promise that resolves to the added object, or an error response if the object already exists or is filtered. + */ async addObjectIfNotExists(object: APObject) { if (this.objects.some(o => o.data.id === object.id)) { return errorResponse("Object already exists", 409); @@ -169,6 +220,11 @@ export class RawActivity extends BaseEntity { return rawObject; } + /** + * Adds an actor to the activity if it does not already exist. + * @param actor The actor to add. + * @returns A promise that resolves to the added actor, or an error response if the actor already exists or is filtered. + */ async addActorIfNotExists(actor: APActor) { const dbActor = await RawActor.getByActorId(actor.id ?? ""); diff --git a/database/entities/User.ts b/database/entities/User.ts index ae88d6c2..4088e482 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -139,6 +139,25 @@ export class User extends BaseEntity { return user; } + static async retrieveFromToken(access_token: string) { + if (!access_token) return null; + + const token = await Token.findOne({ + where: { + access_token, + }, + relations: { + user: { + relationships: true, + }, + }, + }); + + if (!token) return null; + + return token.user; + } + async getRelationshipToOtherUser(other: User) { const relationship = await Relationship.findOne({ where: { diff --git a/server/api/[username]/inbox/index.ts b/server/api/[username]/inbox/index.ts index 977a0570..6105d01b 100644 --- a/server/api/[username]/inbox/index.ts +++ b/server/api/[username]/inbox/index.ts @@ -45,7 +45,7 @@ export default async ( // TODO: Add authentication // Check is Activity already exists - const activity = await RawActivity.addIfNotExists(body); + const activity = await RawActivity.createIfNotExists(body); if (activity instanceof Response) { return activity; @@ -65,7 +65,7 @@ export default async ( return object; } - const activity = await RawActivity.addIfNotExists(body, object); + const activity = await RawActivity.createIfNotExists(body, object); if (activity instanceof Response) { return activity; @@ -87,7 +87,7 @@ export default async ( } // Store the Delete event in the database - const activity = await RawActivity.addIfNotExists(body); + const activity = await RawActivity.createIfNotExists(body); if (activity instanceof Response) { return activity; diff --git a/server/api/api/v1/accounts/[id]/block.ts b/server/api/api/v1/accounts/[id]/block.ts index bf09cb46..4af533aa 100644 --- a/server/api/api/v1/accounts/[id]/block.ts +++ b/server/api/api/v1/accounts/[id]/block.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/follow.ts b/server/api/api/v1/accounts/[id]/follow.ts index 2cceeb2e..eed3a27f 100644 --- a/server/api/api/v1/accounts/[id]/follow.ts +++ b/server/api/api/v1/accounts/[id]/follow.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; @@ -20,7 +19,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/index.ts b/server/api/api/v1/accounts/[id]/index.ts index ac9c486a..b41e2ce2 100644 --- a/server/api/api/v1/accounts/[id]/index.ts +++ b/server/api/api/v1/accounts/[id]/index.ts @@ -1,7 +1,7 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { RawActor } from "~database/entities/RawActor"; +import { User } from "~database/entities/User"; /** * Fetch a user @@ -13,8 +13,12 @@ export default async ( const id = matchedRoute.params.id; // Check auth token - const token = req.headers.get("Authorization")?.split(" ")[1] || null; - const user = await getUserByToken(token); + const token = req.headers.get("Authorization")?.split(" ")[1]; + + if (!token) + return errorResponse("This method requires an authenticated user", 422); + + const user = await User.retrieveFromToken(token); let foundUser: RawActor | null; try { diff --git a/server/api/api/v1/accounts/[id]/mute.ts b/server/api/api/v1/accounts/[id]/mute.ts index 4396db3f..4669ef34 100644 --- a/server/api/api/v1/accounts/[id]/mute.ts +++ b/server/api/api/v1/accounts/[id]/mute.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; @@ -20,7 +19,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/note.ts b/server/api/api/v1/accounts/[id]/note.ts index 04a321e9..66ba3b4c 100644 --- a/server/api/api/v1/accounts/[id]/note.ts +++ b/server/api/api/v1/accounts/[id]/note.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; @@ -20,7 +19,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/pin.ts b/server/api/api/v1/accounts/[id]/pin.ts index 4f495d5e..db85fe3d 100644 --- a/server/api/api/v1/accounts/[id]/pin.ts +++ b/server/api/api/v1/accounts/[id]/pin.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/remove_from_followers.ts b/server/api/api/v1/accounts/[id]/remove_from_followers.ts index f636e5da..382c0930 100644 --- a/server/api/api/v1/accounts/[id]/remove_from_followers.ts +++ b/server/api/api/v1/accounts/[id]/remove_from_followers.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/unblock.ts b/server/api/api/v1/accounts/[id]/unblock.ts index 7f9fa22d..64734d08 100644 --- a/server/api/api/v1/accounts/[id]/unblock.ts +++ b/server/api/api/v1/accounts/[id]/unblock.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/unfollow.ts b/server/api/api/v1/accounts/[id]/unfollow.ts index a7f02ce2..4912e4fd 100644 --- a/server/api/api/v1/accounts/[id]/unfollow.ts +++ b/server/api/api/v1/accounts/[id]/unfollow.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/unmute.ts b/server/api/api/v1/accounts/[id]/unmute.ts index 21822776..07b6d5b6 100644 --- a/server/api/api/v1/accounts/[id]/unmute.ts +++ b/server/api/api/v1/accounts/[id]/unmute.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/[id]/unpin.ts b/server/api/api/v1/accounts/[id]/unpin.ts index 44a7ecbd..44686e92 100644 --- a/server/api/api/v1/accounts/[id]/unpin.ts +++ b/server/api/api/v1/accounts/[id]/unpin.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { Relationship } from "~database/entities/Relationship"; @@ -19,7 +18,7 @@ export default async ( if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/familiar_followers/index.ts b/server/api/api/v1/accounts/familiar_followers/index.ts index 651aa9d3..ac4f42ab 100644 --- a/server/api/api/v1/accounts/familiar_followers/index.ts +++ b/server/api/api/v1/accounts/familiar_followers/index.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { User } from "~database/entities/User"; @@ -14,7 +13,7 @@ export default async (req: Request): Promise => { if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/relationships/index.ts b/server/api/api/v1/accounts/relationships/index.ts index 94d88b72..8c847510 100644 --- a/server/api/api/v1/accounts/relationships/index.ts +++ b/server/api/api/v1/accounts/relationships/index.ts @@ -1,4 +1,3 @@ -import { getUserByToken } from "@auth"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { Relationship } from "~database/entities/Relationship"; @@ -14,7 +13,7 @@ export default async (req: Request): Promise => { if (!token) return errorResponse("This method requires an authenticated user", 422); - const self = await getUserByToken(token); + const self = await User.retrieveFromToken(token); if (!self) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index 5c9a133c..81c50ec7 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -1,7 +1,7 @@ -import { getUserByToken } from "@auth"; import { getConfig } from "@config"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; +import { User } from "~database/entities/User"; /** * Patches a user @@ -17,27 +17,35 @@ export default async (req: Request): Promise => { if (!token) return errorResponse("This method requires an authenticated user", 422); - const user = await getUserByToken(token); + const user = await User.retrieveFromToken(token); if (!user) return errorResponse("Unauthorized", 401); const config = getConfig(); - const { display_name, note, avatar, header, locked, bot, discoverable } = - await parseRequest<{ - display_name: string; - note: string; - avatar: File; - header: File; - locked: string; - bot: string; - discoverable: string; - }>(req); - - // TODO: Implement other options like field or source - // const source_privacy = body.get("source[privacy]")?.toString() || null; - // const source_sensitive = body.get("source[sensitive]")?.toString() || null; - // const source_language = body.get("source[language]")?.toString() || null; + const { + display_name, + note, + avatar, + header, + locked, + bot, + discoverable, + "source[privacy]": source_privacy, + "source[sensitive]": source_sensitive, + "source[language]": source_language, + } = await parseRequest<{ + display_name: string; + note: string; + avatar: File; + header: File; + locked: string; + bot: string; + discoverable: string; + "source[privacy]": string; + "source[sensitive]": string; + "source[language]": string; + }>(req); if (display_name) { // Check if within allowed display name lengths @@ -66,6 +74,36 @@ export default async (req: Request): Promise => { user.note = note; } + if (source_privacy) { + // Check if within allowed privacy values + if ( + !["public", "unlisted", "private", "direct"].includes( + source_privacy + ) + ) { + return errorResponse( + "Privacy must be one of public, unlisted, private, or direct", + 422 + ); + } + + user.source.privacy = source_privacy; + } + + if (source_sensitive) { + // Check if within allowed sensitive values + if (source_sensitive !== "true" && source_sensitive !== "false") { + return errorResponse("Sensitive must be a boolean", 422); + } + + user.source.sensitive = source_sensitive === "true"; + } + + if (source_language) { + // TODO: Check if proper ISO code + user.source.language = source_language; + } + if (avatar) { // Check if within allowed avatar length (avatar is an image) if (avatar.size > config.validation.max_avatar_size) { diff --git a/server/api/api/v1/accounts/verify_credentials/index.ts b/server/api/api/v1/accounts/verify_credentials/index.ts index 86c7bc61..4c509626 100644 --- a/server/api/api/v1/accounts/verify_credentials/index.ts +++ b/server/api/api/v1/accounts/verify_credentials/index.ts @@ -1,5 +1,5 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; +import { User } from "~database/entities/User"; /** * Patches a user @@ -16,7 +16,7 @@ export default async (req: Request): Promise => { if (!token) return errorResponse("This method requires an authenticated user", 422); - const user = await getUserByToken(token); + const user = await User.retrieveFromToken(token); if (!user) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/apps/verify_credentials/index.ts b/server/api/api/v1/apps/verify_credentials/index.ts index bfdd6766..be239ef9 100644 --- a/server/api/api/v1/apps/verify_credentials/index.ts +++ b/server/api/api/v1/apps/verify_credentials/index.ts @@ -1,6 +1,6 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { Application } from "~database/entities/Application"; +import { User } from "~database/entities/User"; /** * Returns OAuth2 credentials @@ -12,7 +12,7 @@ export default async (req: Request): Promise => { if (!token) return errorResponse("This method requires an authenticated user", 422); - const user = await getUserByToken(token); + const user = await User.retrieveFromToken(token); const application = await Application.getFromToken(token); if (!user) return errorResponse("Unauthorized", 401); diff --git a/server/api/api/v1/statuses/[id]/index.ts b/server/api/api/v1/statuses/[id]/index.ts index f2031d9d..4b34d7fb 100644 --- a/server/api/api/v1/statuses/[id]/index.ts +++ b/server/api/api/v1/statuses/[id]/index.ts @@ -1,8 +1,8 @@ -import { getUserByToken } from "@auth"; import { errorResponse, jsonResponse } from "@response"; import { MatchedRoute } from "bun"; import { RawObject } from "~database/entities/RawObject"; import { Status } from "~database/entities/Status"; +import { User } from "~database/entities/User"; /** * Fetch a user @@ -15,7 +15,11 @@ export default async ( // Check auth token const token = req.headers.get("Authorization")?.split(" ")[1] || null; - const user = await getUserByToken(token); + + if (!token) + return errorResponse("This method requires an authenticated user", 422); + + const user = await User.retrieveFromToken(token); // TODO: Add checks for user's permissions to view this status diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 25a48862..df997e09 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { getUserByToken } from "@auth"; import { getConfig } from "@config"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { Application } from "~database/entities/Application"; import { Status } from "~database/entities/Status"; +import { User } from "~database/entities/User"; /** * Post new status @@ -20,7 +20,7 @@ export default async (req: Request): Promise => { if (!token) return errorResponse("This method requires an authenticated user", 422); - const user = await getUserByToken(token); + const user = await User.retrieveFromToken(token); const application = await Application.getFromToken(token); if (!user) return errorResponse("Unauthorized", 401); diff --git a/utils/auth.ts b/utils/auth.ts deleted file mode 100644 index 9afc53da..00000000 --- a/utils/auth.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Token } from "~database/entities/Token"; - -export const getUserByToken = async (access_token: string | null) => { - if (!access_token) return null; - - const token = await Token.findOne({ - where: { - access_token, - }, - relations: { - user: { - relationships: true, - }, - }, - }); - - if (!token) return null; - - return token.user; -};