diff --git a/packages/backend-internal-server/README.md b/packages/backend-internal-server/README.md new file mode 100644 index 00000000..f54d9926 --- /dev/null +++ b/packages/backend-internal-server/README.md @@ -0,0 +1,17 @@ +# @unifed/backend-internal-server + +This package implements the GraphQL server, which +communicates with the frontend of the application. + +This is where user authentication is handled. + +## Resolvers + +This directory contains the API calls provided to allow the frontend and backend +to communicate. + +## Services + +This directory contains more atomic functions that can be used from within the +resolvers directory. The functions in Services mostly deal with +manipulating the database. diff --git a/packages/backend-internal-server/src/resolvers/communities.ts b/packages/backend-internal-server/src/resolvers/communities.ts index c850e5ac..1c44545c 100644 --- a/packages/backend-internal-server/src/resolvers/communities.ts +++ b/packages/backend-internal-server/src/resolvers/communities.ts @@ -79,6 +79,21 @@ export class CommunitiesResolver implements ResolverInterface { return true; } + /** + * Allows a user to create a community to moderate. + * + * @param id ID of the new community to make. + * + * @param title Title of the new community. + * + * @param description Description of the new community. + * + * @param user Currently logged in user. + * + * @returns True on success. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async createCommunity( @@ -90,11 +105,29 @@ export class CommunitiesResolver implements ResolverInterface { return await this.communitiesService.create(user.username, id, title, description); } + /** + * Fetches the communities available at a host. + * + * @param host The host server to search + * + * @returns An array of communities. + * + * @internal + */ @Query(() => [Community]) async getCommunities(@Arg("host") host: string): Promise { return await this.communitiesService.getAll(await translateHost(host)); } + /** + * Fetches a community on the federated network. + * + * @param community Reference to the community. + * + * @returns The community if it exists. + * + * @internal + */ @Query(() => Community, { nullable: true }) async getCommunity(@Arg("community") community: RemoteReferenceInput): Promise { return await this.communitiesService.getOne(await translateHost(community.host), community.id); diff --git a/packages/backend-internal-server/src/resolvers/inputs/create-post.ts b/packages/backend-internal-server/src/resolvers/inputs/create-post.ts index 435a992f..3670a048 100644 --- a/packages/backend-internal-server/src/resolvers/inputs/create-post.ts +++ b/packages/backend-internal-server/src/resolvers/inputs/create-post.ts @@ -6,11 +6,23 @@ import { InputType, Field } from "type-graphql"; import { RemoteReferenceInput } from "./remote-reference"; import { UpdatePostInput } from "./update-post"; +/** + * Input used when creating a post. + * + * @internal + */ @InputType() export class CreatePostInput extends UpdatePostInput { + /** + * Reference to the community on which the post is made. + */ @Field(() => RemoteReferenceInput) community!: RemoteReferenceInput; + /** + * The post a comment is attach to. + * Null if the post is not a comment. + */ @Field({ nullable: true }) parentPost!: string; } diff --git a/packages/backend-internal-server/src/resolvers/inputs/remote-reference.ts b/packages/backend-internal-server/src/resolvers/inputs/remote-reference.ts index fba8a471..57e9b11f 100644 --- a/packages/backend-internal-server/src/resolvers/inputs/remote-reference.ts +++ b/packages/backend-internal-server/src/resolvers/inputs/remote-reference.ts @@ -4,11 +4,22 @@ import { InputType, Field } from "type-graphql"; +/** + * Input type for a reference in the federated network. + * + * @internal + */ @InputType() export class RemoteReferenceInput { + /** + * ID + */ @Field() id!: string; + /** + * Host server + */ @Field() host!: string; } diff --git a/packages/backend-internal-server/src/resolvers/inputs/update-post.ts b/packages/backend-internal-server/src/resolvers/inputs/update-post.ts index 39196c56..5872f9fd 100644 --- a/packages/backend-internal-server/src/resolvers/inputs/update-post.ts +++ b/packages/backend-internal-server/src/resolvers/inputs/update-post.ts @@ -4,11 +4,23 @@ import { InputType, Field } from "type-graphql"; +/** + * Input used when updating a post. + * + * @internal + */ @InputType() export class UpdatePostInput { + /** + * Title of the post. + * Null if post is a comment. + */ @Field({ nullable: true }) title!: string; + /** + * Main content of the post. + */ @Field() body!: string; } diff --git a/packages/backend-internal-server/src/resolvers/inputs/user-profile.ts b/packages/backend-internal-server/src/resolvers/inputs/user-profile.ts index f6ebb053..6a07c8c3 100644 --- a/packages/backend-internal-server/src/resolvers/inputs/user-profile.ts +++ b/packages/backend-internal-server/src/resolvers/inputs/user-profile.ts @@ -4,8 +4,16 @@ import { InputType, Field } from "type-graphql"; +/** + * User profile input used in API. + * + * @internal + */ @InputType() export class UserProfileInput { + /** + * Name of the user. + */ @Field() name!: string; } diff --git a/packages/backend-internal-server/src/resolvers/posts.ts b/packages/backend-internal-server/src/resolvers/posts.ts index 870cce56..252b83a3 100644 --- a/packages/backend-internal-server/src/resolvers/posts.ts +++ b/packages/backend-internal-server/src/resolvers/posts.ts @@ -28,6 +28,17 @@ export class PostsResolver implements ResolverInterface { private readonly usersService: UsersService, ) {} + /** + * Allows a user to create a new post. + * + * @param post The new post to make. + * + * @param user Currently logged in user. + * + * @returns The newly created post. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Post, { nullable: true }) async createPost( @@ -42,6 +53,18 @@ export class PostsResolver implements ResolverInterface { }); } + /** + * Allows the author of a post or an admin of the community the post was made on to delete + * the post. + * + * @param post Reference to the post on the federated network. + * + * @param user Currently logged in user. + * + * @returns True once the call is made. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async deletePost( @@ -52,6 +75,22 @@ export class PostsResolver implements ResolverInterface { return true; } + /** + * Allows the author of a post or an admin of the community the post was made on to update + * the content of the post. + * + * @param post Reference to the post in the federated network. + * + * @param body The new body of the post. + * + * @param user Currently logged in user. + * + * @param title The new title of the post. + * + * @returns The updated post. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Post) async updatePost( @@ -67,6 +106,15 @@ export class PostsResolver implements ResolverInterface { }); } + /** + * Allows a user to report a post and set its approved flag to false. + * + * @param post The post to report + * + * @returns True on report success. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async reportPost(@Arg("post") post: RemoteReferenceInput): Promise { @@ -76,6 +124,15 @@ export class PostsResolver implements ResolverInterface { return false; } + /** + * Fetches the list of posts from communities the user is subscribed to. + * + * @param user The currently logged in user. + * + * @returns Array of posts. + * + * @internal + */ @AuthoriseUser() @Query(() => [Post]) async getSubscribedPosts(@CurrentUser() user: User): Promise { @@ -88,6 +145,17 @@ export class PostsResolver implements ResolverInterface { return posts.flat(); } + /** + * Allows administrators to set the approved flag on the provided posts to true. + * + * @param user The admin making the approval. + * + * @param posts Array of post references to approve. + * + * @returns Returns true once the provided posts are approved. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async approvePosts( @@ -103,6 +171,17 @@ export class PostsResolver implements ResolverInterface { return true; } + /** + * Allows administrators to delete an array of posts. + * + * @param user The admin deleting the posts. + * + * @param posts Array of post references to delete. + * + * @returns Returns true once the provided posts are deleted. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async deletePosts( @@ -115,6 +194,15 @@ export class PostsResolver implements ResolverInterface { return true; } + /** + * Fetches an array of unapproved posts from communities the user moderates (is an admin in). + * + * @param user A community administrator. + * + * @returns Array of of unapproved posts. + * + * @internal + */ @AuthoriseUser() @Query(() => [Post]) async getUnapprovedPosts(@CurrentUser() user: User): Promise { @@ -132,6 +220,17 @@ export class PostsResolver implements ResolverInterface { return unapprovedPosts; } + /** + * Fetches all the posts from a community. + * + * @param community Community to fetch posts from. + * + * @param user The currently logged in user. + * + * @returns Array of posts. + * + * @internal + */ @Query(() => [Post]) async getPosts( @Arg("community") community: RemoteReferenceInput, @@ -144,6 +243,17 @@ export class PostsResolver implements ResolverInterface { ); } + /** + * Fetches a post from the federated network. + * + * @param post Post to fetch. + * + * @param user The currently logged in user. + * + * @returns Object representing the post. + * + * @internal + */ @Query(() => Post) async getPost(@Arg("post") post: RemoteReferenceInput, @CurrentUser() user: User): Promise { return await this.postsService.getById(user.username, await translateHost(post.host), post.id); diff --git a/packages/backend-internal-server/src/resolvers/users.ts b/packages/backend-internal-server/src/resolvers/users.ts index a51004d1..f42a8e1e 100644 --- a/packages/backend-internal-server/src/resolvers/users.ts +++ b/packages/backend-internal-server/src/resolvers/users.ts @@ -28,6 +28,17 @@ export class UsersResolver implements ResolverInterface { private readonly postsService: PostsService, ) {} + /** + * Allows user to update their profile. + * + * @param profile The new profile to update to. + * + * @param user The currently logged in user. + * + * @returns True once profile has been updated. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async updateUserProfile( @@ -37,6 +48,17 @@ export class UsersResolver implements ResolverInterface { return this.usersService.updateProfile(user.id, profile.name); } + /** + * Allows users to subscribe to a community. + * + * @param community Reference to the community. + * + * @param user Currently logged in user. + * + * @returns True on success. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async subscribe( @@ -46,6 +68,17 @@ export class UsersResolver implements ResolverInterface { return this.usersService.subscribe(user.id, await translateHost(community.host), community.id); } + /** + * Allows users to unsubscribe from a community. + * + * @param community Reference to the community. + * + * @param user Currently logged in user. + * + * @returns True on success. + * + * @internal + */ @AuthoriseUser() @Mutation(() => Boolean) async unsubscribe( @@ -59,6 +92,15 @@ export class UsersResolver implements ResolverInterface { ); } + /** + * Fetches all the communities the user is subscribed to. + * + * @param user Currently logged in user. + * + * @returns Array of references to the communities on the federated network. + * + * @internal + */ @AuthoriseUser() @Query(() => [RemoteReference]) async getSubscriptions(@CurrentUser() user: User): Promise { @@ -70,6 +112,17 @@ export class UsersResolver implements ResolverInterface { return user.profile; } + /** + * Fetches all the posts a user has posted. + * + * @param user Currently logged in user. + * + * @param username Username of the user to get posts of. + * + * @returns The posts made by the specified user. + * + * @internal + */ @AuthoriseUser() @Query(() => [Post]) async getAllPosts(@CurrentUser() user: User, @Arg("username") username: string): Promise { diff --git a/packages/backend-internal-server/src/services/posts.ts b/packages/backend-internal-server/src/services/posts.ts index 929f1512..17dd58f5 100644 --- a/packages/backend-internal-server/src/services/posts.ts +++ b/packages/backend-internal-server/src/services/posts.ts @@ -17,6 +17,19 @@ import { plainToClass } from "class-transformer"; @Service() export class PostsService extends PostsFederationService { + /** + * Creates a post at a specified host. + * + * @param username Local user making the post. + * + * @param host The host server the post is made on. + * + * @param post The post being made. + * + * @returns The created post. + * + * @internal + */ async create(username: string, host: string, post: CreatePostProps): Promise { const result = await super.create(username, host, post); @@ -29,6 +42,17 @@ export class PostsService extends PostsFederationService { return result; } + /** + * Deletes a post at a specified host. + * + * @param username User deleting the post. + * + * @param host The host server the post is on. + * + * @param id The id of the post on the federated network. + * + * @internal + */ async delete(username: string, host: string, id: string): Promise { await super.delete(username, host, id); @@ -39,11 +63,31 @@ export class PostsService extends PostsFederationService { await UserModel.update({ username: username }, { $pull: { posts: postReference } }); } + /** + * Sets the approved flag to false on a post. + * + * @param id The ID of the post. + * + * @returns True on success. + * + * @internal + */ async report(id: string): Promise { await PostModel.update({ _id: id }, { $set: { approved: false } }); return true; } + /** + * Allows an admin to approve a post. + * + * @param username Username of user approving. + * + * @param postId The ID of the post. + * + * @returns True on success. + * + * @internal + */ async approve(username: string, postId: string): Promise { const post = await PostModel.findOne({ _id: postId }).exec(); if (!this.isAdmin(username, post)) return false; @@ -51,6 +95,18 @@ export class PostsService extends PostsFederationService { return true; } + /** + * Allows an admin to delete a post. Also removes the reference to the post. + * for the author if they are a local user. + * + * @param username Username of user deleting post. + * + * @param postId The ID of the post. + * + * @returns True on success. + * + * @internal + */ async adminDelete(username: string, postId: string): Promise { const post = await PostModel.findOne({ _id: postId }).exec(); if (!post || !this.isAdmin(username, post)) return false; @@ -66,6 +122,18 @@ export class PostsService extends PostsFederationService { return true; } + /** + * Checks if the username belongs to an admin of the community the + * post was created on. + * + * @param username User to check. + * + * @param post Post to check. + * + * @returns True if the user is an admin of the post's community. + * + * @internal + */ async isAdmin(username: string, post: Post | null): Promise { // finds the admins of the community of the post const communityId = getIdFromRef(post?.community); @@ -84,6 +152,16 @@ export class PostsService extends PostsFederationService { return false; } + /** + * Fetches the unfiltered version of the posts for community admins. + * Unfiltered posts won't hide the contents of their title and body. + * + * @param community The community to fetch posts from. + * + * @returns Unfiltered posts. + * + * @internal + */ async getUnfilteredPosts(community: string): Promise { const posts = await PostModel.find({ community: community }).lean(); const ptcPosts = plainToClass(Post, posts).map((post) => { diff --git a/packages/backend-internal-server/src/services/users.ts b/packages/backend-internal-server/src/services/users.ts index b6625dfa..ed00a168 100644 --- a/packages/backend-internal-server/src/services/users.ts +++ b/packages/backend-internal-server/src/services/users.ts @@ -16,6 +16,20 @@ export class UsersService { return true; } + /** + * Allows a user to subscribe to a community. Adds a reference to the subscribed + * community for the user. + * + * @param userId The user subscribing. + * + * @param host The host of the community. + * + * @param communityId The id of the community on the federated network. + * + * @returns True on success. + * + * @internal + */ async subscribe(userId: string, host: string, communityId: string): Promise { const community: RemoteReference = new RemoteReference(); community.id = communityId; @@ -33,6 +47,20 @@ export class UsersService { ); } + /** + * Allows a user to unsubscribe from a community. If there is an existing + * reference to the community, it is removed. + * + * @param userId ID of the user. + * + * @param host The server hosting the community. + * + * @param communityId The id of the community on the federated network. + * + * @returns True on success. + * + * @internal + */ async unsubscribe(userId: string, host: string, communityId: string): Promise { const community: RemoteReference = new RemoteReference(); community.id = communityId; @@ -50,6 +78,15 @@ export class UsersService { return true; } + /** + * Fetches an array of references to the communities a user is subscribed to. + * + * @param id ID of the user. + * + * @returns Array of subscribed communities. Empty array if the user does not exist. + * + * @internal + */ async getSubscriptions(id: string): Promise { const user = await UserModel.findOne({ _id: id }, "subscriptions").lean(); @@ -58,6 +95,15 @@ export class UsersService { return plainToClass(RemoteReference, user.subscriptions); } + /** + * Fetches all the posts made by a user. + * + * @param username Username of the user. + * + * @returns Array of posts. Empty array if the user does not exist. + * + * @internal + */ async getAllPosts(username: string): Promise { const user = await UserModel.findOne({ username: username }, "posts").lean();