From d73bd1dd049547c56f5fe0641108580bf240c4ed Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Sun, 11 Feb 2024 13:37:51 +0100 Subject: [PATCH] :sparkles: User profile view Signed-off-by: Pau Costa --- DevSpaceOApi.json | 1394 +++++++++++------------ client/src/api/api.ts | 172 +-- client/src/app/loginSlice.ts | 8 + client/src/app/store.ts | 1 + client/src/app/usersSlice.ts | 14 +- client/src/components/postListItem.tsx | 30 +- client/src/index.tsx | 2 +- client/src/routes/postList.tsx | 91 +- client/src/routes/profile.tsx | 149 ++- client/src/routes/search.tsx | 8 + server/src/controller/UserController.ts | 41 +- server/src/controller/postController.ts | 898 ++++++++------- server/src/entity/User.ts | 12 +- 13 files changed, 1523 insertions(+), 1297 deletions(-) diff --git a/DevSpaceOApi.json b/DevSpaceOApi.json index 7f33f13..b822b7f 100644 --- a/DevSpaceOApi.json +++ b/DevSpaceOApi.json @@ -179,751 +179,719 @@ } }, "paths": { - "/auth/signup": { - "post": { - "tags": [ - "Authentication" - ], - "summary": "Sign up a new user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "description": "The email of the user" - }, - "password": { - "type": "string", - "description": "The password of the user" - }, - "passwordValidation": { - "type": "string", - "description": "Password validation field" - }, - "firstName": { - "type": "string", - "description": "The first name of the user" - }, - "lastName": { - "type": "string", - "description": "The last name of the user" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successfully signed up the user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } + "/auth/signup": { + "post": { + "tags": ["Authentication"], + "summary": "Sign up a new user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "The email of the user" }, - "400": { - "description": "Invalid input or user already exists" - } - } - } - }, - "/auth/login": { - "post": { - "tags": [ - "Authentication" - ], - "summary": "Log in a user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "description": "The email of the user" - }, - "password": { - "type": "string", - "description": "The password of the user" - }, - "longExpiration": { - "type": "boolean", - "description": "Whether to keep the user logged in for a long time" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successfully logged in the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success" - }, - "token": { - "type": "string", - "description": "The JWT token for the user" - } - } - } - } - } + "password": { + "type": "string", + "description": "The password of the user" }, - "400": { - "description": "Missing email or password" + "passwordValidation": { + "type": "string", + "description": "Password validation field" }, - "401": { - "description": "Incorrect email or password" - } - } - } - }, - "/auth/logout": { - "get": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Authentication" - ], - "summary": "Log out the user", - "responses": { - "200": { - "description": "Successfully logged out the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success" - } - } - } - } - } - } - } - } - }, - "/posts": { - "get": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "getAllPosts", - "summary": "Get all posts", - "responses": { - "200": { - "description": "A list of all posts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Post" - } - } - } - } - } - } - }, - "post": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "createPost", - "summary": "Create a post", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Post title" - }, - "content": { - "type": "string", - "description": "Post content" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "A post", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Post" - } - } - } + "firstName": { + "type": "string", + "description": "The first name of the user" }, - "400": { - "description": "Title and content are required" - } - } - } - }, - "/posts/{id}": { - "get": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "getPost", - "summary": "Get a post by ID", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the post", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A post", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Post" - } - } - } - }, - "400": { - "description": "Invalid ID" - }, - "404": { - "description": "No post found with that ID" - } - } - }, - "patch": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "updatePost", - "summary": "Update a post by ID", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the post", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Post title" - }, - "content": { - "type": "string", - "description": "Post content" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "A post", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Post" - } - } - } - }, - "400": { - "description": "Invalid ID or Title and content are required" - }, - "404": { - "description": "No post found with that ID" - } - } - }, - "delete": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "deletePost", - "summary": "Delete a post by ID", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the post", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "204": { - "description": "No content" - }, - "400": { - "description": "Invalid ID" - }, - "404": { - "description": "No post found with that ID" - } - } - } - }, - "/posts/followed": { - "get": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "getFollowedPosts", - "summary": "Get posts of followed users", - "responses": { - "200": { - "description": "A list of posts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Post" - } - } - } - } - } - } - } - }, - "/posts/{id}/like": { - "post": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "likePost", - "summary": "Like a post by ID", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the post", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A post", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Post" - } - } - } - }, - "400": { - "description": "Invalid ID or You have already liked this post" - }, - "404": { - "description": "No post found with that ID" - } - } - }, - "delete": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "unlikePost", - "summary": "Unlike a post by ID", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the post", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A post", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Post" - } - } - } - }, - "400": { - "description": "Invalid ID or You have not liked this post" - }, - "404": { - "description": "No post found with that ID" - } - } - } - }, - "/posts/{id}/comment": { - "post": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Posts" - ], - "operationId": "commentPost", - "summary": "Comment on a post by ID", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the post", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Comment content" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "A comment", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Comment" - } - } - } - }, - "400": { - "description": "Invalid Request" - }, - "404": { - "description": "No post found with that ID" - } - } - } - }, - "/users": { - "get": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Users" - ], - "summary": "Get all users", - "responses": { - "200": { - "description": "A list of all users", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - } - } - }, - "/users/{id}": { - "get": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Users" - ], - "summary": "Get a single user", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "description": "ID of the user", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A single user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserWithRelations" - } - } - } - } - } - } - }, - "/users/me": { - "get": { - "tags": ["Users"], - "summary": "Get the currently logged in user", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "The currently logged in user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserWithRelationsAndNotifications" + "lastName": { + "type": "string", + "description": "The last name of the user" } } } } } }, - "patch": { - "security": [ - { - "bearerAuth": [] + "responses": { + "200": { + "description": "Successfully signed up the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } } - ], - "tags": ["Users"], - "summary": "Update the currently logged in user", - "requestBody": { - "required": true, + }, + "400": { + "description": "Invalid input or user already exists" + } + } + } + }, + "/auth/login": { + "post": { + "tags": ["Authentication"], + "summary": "Log in a user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "The email of the user" + }, + "password": { + "type": "string", + "description": "The password of the user" + }, + "longExpiration": { + "type": "boolean", + "description": "Whether to keep the user logged in for a long time" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully logged in the user", "content": { "application/json": { "schema": { "type": "object", "properties": { - "isPrivate": { - "type": "boolean", - "description": "Whether the user's account is private" - }, - "profilePictureId": { + "status": { "type": "string", - "description": "The ID of the user's profile picture" + "example": "success" + }, + "token": { + "type": "string", + "description": "The JWT token for the user" } } } } } }, - "responses": { - "200": { - "description": "Successfully updated the user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserWithRelationsAndNotifications" + "400": { + "description": "Missing email or password" + }, + "401": { + "description": "Incorrect email or password" + } + } + } + }, + "/auth/logout": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Authentication"], + "summary": "Log out the user", + "responses": { + "200": { + "description": "Successfully logged out the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + } + } + } + } + } + } + } + } + }, + "/posts": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "getAllPosts", + "summary": "Get all posts", + "responses": { + "200": { + "description": "A list of all posts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" } } } - }, - "400": { - "description": "Invalid request body" } } } }, - "/users/{id}/follow": { - "post": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Users" - ], - "summary": "Follow a user", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "integer", - "description": "The ID of the user to follow" - } - } - ], - "responses": { - "200": { - "description": "Successfully followed the user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserWithRelations" - } - } - } - }, - "400": { - "description": "Invalid ID" - }, - "404": { - "description": "No user found with that ID" - } - } - }, - "delete": { - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - "Users" - ], - "summary": "Unfollow a user", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "integer", - "description": "The ID of the user to unfollow" - } - } - ], - "responses": { - "200": { - "description": "Successfully unfollowed the user", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserWithRelations" - } - } - } - }, - "400": { - "description": "Invalid ID" - }, - "404": { - "description": "No user found with that ID" - } - } + "post": { + "security": [ + { + "bearerAuth": [] } + ], + "tags": ["Posts"], + "operationId": "createPost", + "summary": "Create a post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Post title" + }, + "content": { + "type": "string", + "description": "Post content" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Title and content are required" + } + } } + }, + "/posts/{id}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "getPost", + "summary": "Get a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No post found with that ID" + } + } + }, + "patch": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "updatePost", + "summary": "Update a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Post title" + }, + "content": { + "type": "string", + "description": "Post content" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID or Title and content are required" + }, + "404": { + "description": "No post found with that ID" + } + } + }, + "delete": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "deletePost", + "summary": "Delete a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No post found with that ID" + } + } + } + }, + "/posts/followed": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "getFollowedPosts", + "summary": "Get posts of followed users", + "responses": { + "200": { + "description": "A list of posts", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + } + } + } + } + } + } + }, + "/posts/{id}/like": { + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "likePost", + "summary": "Like a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID or You have already liked this post" + }, + "404": { + "description": "No post found with that ID" + } + } + }, + "delete": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "unlikePost", + "summary": "Unlike a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A post", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Post" + } + } + } + }, + "400": { + "description": "Invalid ID or You have not liked this post" + }, + "404": { + "description": "No post found with that ID" + } + } + } + }, + "/posts/{id}/comment": { + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Posts"], + "operationId": "commentPost", + "summary": "Comment on a post by ID", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the post", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Comment content" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "A comment", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Comment" + } + } + } + }, + "400": { + "description": "Invalid Request" + }, + "404": { + "description": "No post found with that ID" + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Users"], + "summary": "Get all users", + "responses": { + "200": { + "description": "A list of all users", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Users"], + "summary": "Get a single user", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "description": "ID of the user", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A single user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelations" + } + } + } + } + } + } + }, + "/users/me": { + "get": { + "tags": ["Users"], + "summary": "Get the currently logged in user", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "The currently logged in user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelationsAndNotifications" + } + } + } + } + } + }, + "patch": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Users"], + "summary": "Update the currently logged in user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isPrivate": { + "type": "boolean", + "description": "Whether the user's account is private" + }, + "profilePictureId": { + "type": "string", + "description": "The ID of the user's profile picture" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully updated the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelationsAndNotifications" + } + } + } + }, + "400": { + "description": "Invalid request body" + } + } + } + }, + "/users/follow/{id}": { + "post": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Users"], + "summary": "Follow a user", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer", + "description": "The ID of the user to follow" + } + } + ], + "responses": { + "200": { + "description": "Successfully followed the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelations" + } + } + } + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No user found with that ID" + } + } + }, + "delete": { + "security": [ + { + "bearerAuth": [] + } + ], + "tags": ["Users"], + "summary": "Unfollow a user", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer", + "description": "The ID of the user to unfollow" + } + } + ], + "responses": { + "200": { + "description": "Successfully unfollowed the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithRelations" + } + } + } + }, + "400": { + "description": "Invalid ID" + }, + "404": { + "description": "No user found with that ID" + } + } + } + } }, "tags": [] } \ No newline at end of file diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 1291ec4..adffc4b 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -1406,40 +1406,6 @@ export class PostsApi extends BaseAPI { */ export const UsersApiAxiosParamCreator = function (configuration?: Configuration) { return { - /** - * - * @summary Get all users - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - usersGet: async (options: RawAxiosRequestConfig = {}): Promise => { - const localVarPath = `/users`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearerAuth required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @summary Unfollow a user @@ -1447,10 +1413,10 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - usersIdFollowDelete: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + usersFollowIdDelete: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined - assertParamExists('usersIdFollowDelete', 'id', id) - const localVarPath = `/users/{id}/follow` + assertParamExists('usersFollowIdDelete', 'id', id) + const localVarPath = `/users/follow/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -1485,10 +1451,10 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - usersIdFollowPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { + usersFollowIdPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined - assertParamExists('usersIdFollowPost', 'id', id) - const localVarPath = `/users/{id}/follow` + assertParamExists('usersFollowIdPost', 'id', id) + const localVarPath = `/users/follow/{id}` .replace(`{${"id"}}`, encodeURIComponent(String(id))); // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -1507,6 +1473,40 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersGet: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/users`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -1638,18 +1638,6 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration export const UsersApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration) return { - /** - * - * @summary Get all users - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, /** * * @summary Unfollow a user @@ -1657,10 +1645,10 @@ export const UsersApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowDelete(id, options); + async usersFollowIdDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersFollowIdDelete(id, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowDelete']?.[localVarOperationServerIndex]?.url; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersFollowIdDelete']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** @@ -1670,10 +1658,22 @@ export const UsersApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async usersIdFollowPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowPost(id, options); + async usersFollowIdPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersFollowIdPost(id, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowPost']?.[localVarOperationServerIndex]?.url; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersFollowIdPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** @@ -1724,15 +1724,6 @@ export const UsersApiFp = function(configuration?: Configuration) { export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = UsersApiFp(configuration) return { - /** - * - * @summary Get all users - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - usersGet(options?: any): AxiosPromise> { - return localVarFp.usersGet(options).then((request) => request(axios, basePath)); - }, /** * * @summary Unfollow a user @@ -1740,8 +1731,8 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - usersIdFollowDelete(id: number, options?: any): AxiosPromise { - return localVarFp.usersIdFollowDelete(id, options).then((request) => request(axios, basePath)); + usersFollowIdDelete(id: number, options?: any): AxiosPromise { + return localVarFp.usersFollowIdDelete(id, options).then((request) => request(axios, basePath)); }, /** * @@ -1750,8 +1741,17 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - usersIdFollowPost(id: number, options?: any): AxiosPromise { - return localVarFp.usersIdFollowPost(id, options).then((request) => request(axios, basePath)); + usersFollowIdPost(id: number, options?: any): AxiosPromise { + return localVarFp.usersFollowIdPost(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + usersGet(options?: any): AxiosPromise> { + return localVarFp.usersGet(options).then((request) => request(axios, basePath)); }, /** * @@ -1792,17 +1792,6 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath * @extends {BaseAPI} */ export class UsersApi extends BaseAPI { - /** - * - * @summary Get all users - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof UsersApi - */ - public usersGet(options?: RawAxiosRequestConfig) { - return UsersApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath)); - } - /** * * @summary Unfollow a user @@ -1811,8 +1800,8 @@ export class UsersApi extends BaseAPI { * @throws {RequiredError} * @memberof UsersApi */ - public usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig) { - return UsersApiFp(this.configuration).usersIdFollowDelete(id, options).then((request) => request(this.axios, this.basePath)); + public usersFollowIdDelete(id: number, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersFollowIdDelete(id, options).then((request) => request(this.axios, this.basePath)); } /** @@ -1823,8 +1812,19 @@ export class UsersApi extends BaseAPI { * @throws {RequiredError} * @memberof UsersApi */ - public usersIdFollowPost(id: number, options?: RawAxiosRequestConfig) { - return UsersApiFp(this.configuration).usersIdFollowPost(id, options).then((request) => request(this.axios, this.basePath)); + public usersFollowIdPost(id: number, options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersFollowIdPost(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get all users + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public usersGet(options?: RawAxiosRequestConfig) { + return UsersApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/client/src/app/loginSlice.ts b/client/src/app/loginSlice.ts index 0d2285e..a788094 100644 --- a/client/src/app/loginSlice.ts +++ b/client/src/app/loginSlice.ts @@ -17,6 +17,8 @@ interface loginState { firstName: string; lastName: string; jwt: string; + id: number; + profilePictureId: string; }; } @@ -28,6 +30,8 @@ const initialState: loginState = { firstName: "", lastName: "", jwt: "", + id: -1, + profilePictureId: "", }, }; @@ -37,9 +41,11 @@ export const loginSlice = createSlice({ reducers: { login: (state, action) => { state.loggedIn = true; + state.userInfo.id = action.payload.id; state.userInfo.jwt = action.payload.jwt; state.userInfo.firstName = action.payload.firstName; state.userInfo.lastName = action.payload.lastName; + state.userInfo.profilePictureId = action.payload.profilePictureId; }, logoff: (state) => { state.loggedIn = false; @@ -85,6 +91,8 @@ export const postLogin = jwt: response.data.token, firstName: userResponse.data.firstName, lastName: userResponse.data.lastName, + id: userResponse.data.id, + profilePictureId: userResponse.data.profilePictureId, }) ); dispatch(setStatus(Status.succeeded)); diff --git a/client/src/app/store.ts b/client/src/app/store.ts index fb13f32..f30473a 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -10,6 +10,7 @@ export const store = configureStore({ post: postReducer, users: usersReducer, }, + devTools: true, }); export type AppDispatch = typeof store.dispatch; diff --git a/client/src/app/usersSlice.ts b/client/src/app/usersSlice.ts index 71a1494..d72aeda 100644 --- a/client/src/app/usersSlice.ts +++ b/client/src/app/usersSlice.ts @@ -58,7 +58,7 @@ export const getUserWithRelations = dispatch(setStatus(Status.loading)); try { const response = await api.usersIdGet(userId); - dispatch(setUserWithRelations(response)); + dispatch(setUserWithRelations(response.data)); dispatch(setStatus(Status.idle)); } catch (error) { dispatch(setError((error as Error).message)); @@ -73,10 +73,12 @@ export const followUser = dispatch(setStatus(Status.loading)); try { - await api.usersIdFollowPost(userId); - dispatch(getUsers()); + console.log("Trying to follow: ", userId); + await api.usersFollowIdPost(userId); } catch (error) { dispatch(setError((error as Error).message)); + } finally { + dispatch(getUserWithRelations(userId)); dispatch(setStatus(Status.idle)); } }; @@ -88,10 +90,12 @@ export const unFollowUser = dispatch(setStatus(Status.loading)); try { - await api.usersIdFollowDelete(userId); - dispatch(getUsers()); + console.log("Trying to unfollow: ", userId); + await api.usersFollowIdDelete(userId); } catch (error) { dispatch(setError((error as Error).message)); + } finally { + dispatch(getUserWithRelations(userId)); dispatch(setStatus(Status.idle)); } }; diff --git a/client/src/components/postListItem.tsx b/client/src/components/postListItem.tsx index b825d79..eaa92d7 100644 --- a/client/src/components/postListItem.tsx +++ b/client/src/components/postListItem.tsx @@ -28,31 +28,31 @@ export default function PostListItem(props: PostListItemProps) { const dispatch = useAppDispatch(); const navigate = useNavigate(); const userInfo = useSelector(selectUserInfo); - const [liked, setLiked] = useState(false); - const [numberOfLikes, setNumberOfLikes] = useState(0); + const [liked, setLiked] = useState( + props.post.likedBy?.some((user) => user.id === userInfo.id) || false + ); + const [numberOfLikes, setNumberOfLikes] = useState( + props.post.likedBy?.length || 0 + ); - // On component mount, set the number of likes - // and whether the user has liked the post useEffect(() => { + setLiked( + props.post.likedBy?.some((user) => user.id === userInfo.id) || false + ); setNumberOfLikes(props.post.likedBy?.length || 0); - setLiked(props.post.likedBy?.includes(userInfo) || false); - }, []); + }, [props.post.likedBy, userInfo.id]); const handleLike = () => { if (!liked) { - // Instant feedback to the user - setLiked(true); setNumberOfLikes(numberOfLikes + 1); - // Dispatch the call to the API dispatch(likePost(props.post.id as number)); } // If the user has already liked the post, unlike it else { - setLiked(false); setNumberOfLikes(numberOfLikes - 1); - dispatch(unLikePost(props.post.id as number)); } + setLiked((prevState) => !prevState); }; const handlePostClick = () => { @@ -72,7 +72,13 @@ export default function PostListItem(props: PostListItemProps) { alignItems: "start", }} > - + + + + + + {userToDisplay!.posts?.map((post: Post) => ( + + )) || ( + + This user has no posts yet! + + )} + + + )} + + + ); } diff --git a/client/src/routes/search.tsx b/client/src/routes/search.tsx index 226ec84..3fa088d 100644 --- a/client/src/routes/search.tsx +++ b/client/src/routes/search.tsx @@ -22,10 +22,17 @@ export default function Search() { const dispatch = useAppDispatch(); const navigate = useNavigate(); + // Dispatch the getUsers actions, and set it as the displayUserList + // once on the first render + useEffect(() => { dispatch(getUsers()); }, []); + useEffect(() => { + setDisplayUserList(userList); + }, [userList]); + const handleSearchChange = (event: React.ChangeEvent) => { const searchValue = event.currentTarget.value; if (searchValue === "") { @@ -39,6 +46,7 @@ export default function Search() { const handleClick: MouseEventHandler = (event) => { const userId = event.currentTarget.id; + console.log("User Id from the search component: ", userId); navigate(`/user/${userId}`); }; diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts index 5415762..3d73af4 100644 --- a/server/src/controller/UserController.ts +++ b/server/src/controller/UserController.ts @@ -81,7 +81,7 @@ export class UserController { relations: { followed: true, followers: true, - posts: true, + posts: { likedBy: true }, comments: true, }, }); @@ -89,17 +89,36 @@ export class UserController { if (!user) return next(new AppError("No user found with that ID", 404)); // remove sensitive fields - user.deleteSensitiveFields(); - user.followed.forEach((followedUser) => - followedUser.deleteSensitiveFields() - ); - user.followers.forEach((follower) => follower.deleteSensitiveFields()); + user.deletePrivateFields(); + if (user.posts) { + user.posts.forEach((post) => { + if (post.likedBy) { + post.likedBy.forEach((user) => user.deleteSensitiveFields()); + } + if (post.comments) { + post.comments.forEach((comment) => + comment.createdBy.deleteSensitiveFields() + ); + } + }); + } + if (user.followed) { + user.followed.forEach((followedUser) => + followedUser.deleteSensitiveFields() + ); + } + let followStatus = false; + if (user.followers) { + user.followers.forEach((follower) => follower.deleteSensitiveFields()); + // Set the followed status of the user + followStatus = user.followers.some( + (follower) => follower.id === req.user.id + ); + } // If the user is private, only return the user // if the requesting user is following - const followStatus = user.followers.some( - (follower) => follower.id === req.user.id - ); + if (user.isPrivate && !followStatus) { user.followed = []; user.followers = []; @@ -206,7 +225,7 @@ export class UserController { /** * @swagger - * /users/{id}/follow: + * /users/follow/{id}: * post: * security: * - bearerAuth: [] @@ -281,7 +300,7 @@ export class UserController { /** * @swagger - * /users/{id}/follow: + * /users/follow/{id}: * delete: * security: * - bearerAuth: [] diff --git a/server/src/controller/postController.ts b/server/src/controller/postController.ts index b5293ed..b10b222 100644 --- a/server/src/controller/postController.ts +++ b/server/src/controller/postController.ts @@ -8,13 +8,13 @@ import {AppError} from "../util/AppError"; import {AppRequest} from "../util/AppRequest"; export class PostController { - private postRepository = AppDataSource.getRepository(Post) + private postRepository = AppDataSource.getRepository(Post); - private commentRepository = AppDataSource.getRepository(Comment) + private commentRepository = AppDataSource.getRepository(Comment); - private notificationRepository = AppDataSource.getRepository(Notification) + private notificationRepository = AppDataSource.getRepository(Notification); - private userRepository = AppDataSource.getRepository(User) + private userRepository = AppDataSource.getRepository(User); /** * @swagger @@ -36,477 +36,523 @@ export class PostController { * items: * $ref: '#/components/schemas/Post' */ - public getAllPosts = catchAsync(async (_req, res, _next) => { + public getAllPosts = catchAsync(async (req: AppRequest, res, _next) => { const posts = await this.postRepository.find({ - relations: { createdBy: true, likedBy: true, comments: {createdBy: true} }, + relations: { + createdBy: true, + likedBy: true, + comments: { createdBy: true }, + }, }); - // Remove sensitive fields - posts.forEach(post => { - post.deleteSensitiveFields() - }) + // Remove sensitive fields + posts.forEach((post) => { + post.deleteSensitiveFields(); + }); + // Remove private, non followed users + const filteredPosts = posts.filter((post) => { + let followStatus = false; + + if(post.createdBy.followers){ + followStatus = post.createdBy.followers.some( + (follower) => follower.id === req.user.id + ); + } + + if (post.createdBy.isPrivate && !followStatus) { + return post.createdBy.followers.some( + (follower) => follower.id === req.user.id + ); + } + return true; + }); - res.status(200).send(posts); + res.status(200).send(filteredPosts); }); - - /** - * @swagger - * /posts/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: getPost - * summary: Get a post by ID - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the post - * schema: - * type: integer - * responses: - * 200: - * description: A post - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Post' - * 400: - * description: Invalid ID - * 404: - * description: No post found with that ID - */ + + /** + * @swagger + * /posts/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getPost + * summary: Get a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID + * 404: + * description: No post found with that ID + */ public getPost = catchAsync(async (req, res, next) => { const { post, errorMessage } = await this.validateRequestAndGetEntities( req ); - if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) - if(errorMessage == 'No post found with that ID'){ - return next(new AppError('No post found with that ID', 404)) - } + if (errorMessage == "Invalid ID") + return next(new AppError("Invalid ID", 400)); + if (errorMessage == "No post found with that ID") { + return next(new AppError("No post found with that ID", 404)); + } - // Remove sensitive fields - post.deleteSensitiveFields() + // Remove sensitive fields + post.deleteSensitiveFields(); res.send(post); }); - - /** - * @swagger - * /posts/followed: - * get: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: getFollowedPosts - * summary: Get posts of followed users - * responses: - * 200: - * description: A list of posts - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Post' - */ - public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => { - const user = await this.userRepository.findOne({ - where: { id: req.user.id }, - relations: { - followed: true, - posts: { likedBy: true, comments: true }, - }, - }); + /** + * @swagger + * /posts/followed: + * get: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: getFollowedPosts + * summary: Get posts of followed users + * responses: + * 200: + * description: A list of posts + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/Post' + */ - const followedPosts = user.followed.map(followedUser => followedUser.posts).flat() + public getFollowedPosts = catchAsync(async (req: AppRequest, res, _next) => { + const user = await this.userRepository.findOne({ + where: { id: req.user.id }, + relations: { + followed: true, + posts: { likedBy: true, comments: true }, + }, + }); - // Remove sensitive fields - followedPosts.forEach(post => { - post.deleteSensitiveFields() - }) + const followedPosts = user.followed + .map((followedUser) => followedUser.posts) + .flat(); + + // Remove sensitive fields + followedPosts.forEach((post) => { + post.deleteSensitiveFields(); + }); res.send(followedPosts); }); - - /** - * @swagger - * /posts: - * post: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: createPost - * summary: Create a post - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * title: - * type: string - * description: Post title - * content: - * type: string - * description: Post content - * responses: - * 201: - * description: A post - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Post' - * 400: - * description: Title and content are required - */ - public createPost = catchAsync(async (req : AppRequest, res, next) => { - const user = await this.userRepository.findOne({where: {id: req.user.id}}) - const {title, content} = req.body + /** + * @swagger + * /posts: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: createPost + * summary: Create a post + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: Post title + * content: + * type: string + * description: Post content + * responses: + * 201: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Title and content are required + */ - if(!title || !content) return next(new AppError('Title and content are required', 400)) + public createPost = catchAsync(async (req: AppRequest, res, next) => { + const user = await this.userRepository.findOne({ + where: { id: req.user.id }, + }); + const { title, content } = req.body; + if (!title || !content) + return next(new AppError("Title and content are required", 400)); - const newPost = Object.assign(new Post(), { - title, - content, - createdBy: user, - createdAt: new Date()}) + const newPost = Object.assign(new Post(), { + title, + content, + createdBy: user, + createdAt: new Date(), + }); - const post = await this.postRepository.save(newPost) + const post = await this.postRepository.save(newPost); - // Remove sensitive fields - post.deleteSensitiveFields() + // Remove sensitive fields + post.deleteSensitiveFields(); res.status(201).send(post); }); - - /** - * @swagger - * /posts/{id}: - * patch: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: updatePost - * summary: Update a post by ID - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the post - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * title: - * type: string - * description: Post title - * content: - * type: string - * description: Post content - * responses: - * 200: - * description: A post - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Post' - * 400: - * description: Invalid ID or Title and content are required - * 404: - * description: No post found with that ID - */ - public updatePost = catchAsync(async (req : AppRequest, res, next) => { - const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) - const {title, content} = req.body + /** + * @swagger + * /posts/{id}: + * patch: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: updatePost + * summary: Update a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: Post title + * content: + * type: string + * description: Post content + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or Title and content are required + * 404: + * description: No post found with that ID + */ - if(!title || !content) return next(new AppError('Title and content are required', 400)) + public updatePost = catchAsync(async (req: AppRequest, res, next) => { + const { user, post, errorMessage } = + await this.validateRequestAndGetEntities(req); + const { title, content } = req.body; - if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) - if(errorMessage == 'No post found with that ID'){ - return next(new AppError('No post found with that ID', 404)) - } + if (!title || !content) + return next(new AppError("Title and content are required", 400)); - if(post.createdBy.id !== user.id){ - return next(new AppError('You are not authorized to update this post', 403)) - } + if (errorMessage == "Invalid ID") + return next(new AppError("Invalid ID", 400)); + if (errorMessage == "No post found with that ID") { + return next(new AppError("No post found with that ID", 404)); + } - post.title = title - post.content = content - await this.postRepository.save(post) + if (post.createdBy.id !== user.id) { + return next( + new AppError("You are not authorized to update this post", 403) + ); + } - // Remove sensitive fields - post.deleteSensitiveFields() + post.title = title; + post.content = content; + await this.postRepository.save(post); + + // Remove sensitive fields + post.deleteSensitiveFields(); res.status(200).send(post); }); - - /** - * @swagger - * /posts/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: deletePost - * summary: Delete a post by ID - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the post - * schema: - * type: integer - * responses: - * 204: - * description: No content - * 400: - * description: Invalid ID - * 404: - * description: No post found with that ID - */ - public deletePost = catchAsync(async (req : AppRequest, res, next) => { - const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + /** + * @swagger + * /posts/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: deletePost + * summary: Delete a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 204: + * description: No content + * 400: + * description: Invalid ID + * 404: + * description: No post found with that ID + */ - if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) - if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) + public deletePost = catchAsync(async (req: AppRequest, res, next) => { + const { user, post, errorMessage } = + await this.validateRequestAndGetEntities(req); - if(post.createdBy.id !== user.id){ - return next(new AppError('You are not authorized to delete this post', 403)) - } + if (errorMessage == "Invalid ID") + return next(new AppError("Invalid ID", 400)); + if (errorMessage == "No post found with that ID") + return next(new AppError("No post found with that ID", 404)); + + if (post.createdBy.id !== user.id) { + return next( + new AppError("You are not authorized to delete this post", 403) + ); + } await this.postRepository.remove(post); res.status(204).send(); }); - - /** - * @swagger - * /posts/{id}/like: - * post: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: likePost - * summary: Like a post by ID - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the post - * schema: - * type: integer - * responses: - * 200: - * description: A post - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Post' - * 400: - * description: Invalid ID or You have already liked this post - * 404: - * description: No post found with that ID - */ - public likePost = catchAsync(async (req : AppRequest, res, next) => { - const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + /** + * @swagger + * /posts/{id}/like: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: likePost + * summary: Like a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or You have already liked this post + * 404: + * description: No post found with that ID + */ - if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) - if(errorMessage == 'No post found with that ID'){ - return next(new AppError('No post found with that ID', 404)) - } - - // Check if user has already liked the post - if(post.likedBy.some(likedUser => likedUser.id === user.id)){ - return next(new AppError('You have already liked this post', 400)) - } - - post.likedBy.push(user) - await this.postRepository.save(post) - - // Send notification to post creator - const newNotification = Object.assign(new Notification(),{ - message: `${user.firstName} ${user.lastName} liked your post`, - belongsTo: post.createdBy, - timeStamp: new Date() - }) - const notification = await this.notificationRepository.save(newNotification) - post.createdBy.notifications.push(notification) - await this.userRepository.save(post.createdBy) - - // Remove sensitive fields - post.deleteSensitiveFields() - - res.status(200).send(post); - }); - - /** - * @swagger - * /posts/{id}/like: - * delete: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: unlikePost - * summary: Unlike a post by ID - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the post - * schema: - * type: integer - * responses: - * 200: - * description: A post - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Post' - * 400: - * description: Invalid ID or You have not liked this post - * 404: - * description: No post found with that ID - */ - - public unlikePost = catchAsync(async (req : AppRequest, res, next) => { - const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) - - if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) - if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) - - // Check if user likes the post - if(!post.likedBy.some(likedUser => likedUser.id === user.id)){ - return next(new AppError('You have not liked this post', 400)) - } - - post.likedBy = post.likedBy.filter(likedUser => likedUser.id !== user.id) - await this.postRepository.save(post) - - // Remove sensitive fields - post.deleteSensitiveFields() - - res.status(200).send(post); - }); - - /** - * @swagger - * /posts/{id}/comment: - * post: - * security: - * - bearerAuth: [] - * tags: - * - Posts - * operationId: commentPost - * summary: Comment on a post by ID - * parameters: - * - in: path - * name: id - * required: true - * description: ID of the post - * schema: - * type: integer - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * content: - * type: string - * description: Comment content - * responses: - * 201: - * description: A comment - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Comment' - * 400: - * description: Invalid Request - * 404: - * description: No post found with that ID - */ - - public commentPost = catchAsync(async (req : AppRequest, res, next) => { - const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) - - if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) - if(errorMessage == 'No post found with that ID') { - return next(new AppError('No post found with that ID', 404)) - } - - const {content} = req.body - const newComment = Object.assign(new Comment(), { - content, - createdBy: user, - post, - createdAt: new Date() - }) - - const comment = await this.commentRepository.save(newComment) - - // Send notification to post creator - const newNotification = Object.assign(new Notification(),{ - message: `${user.firstName} ${user.lastName} commented on your post`, - belongsTo: post.createdBy, - timeStamp: new Date() - }) - await this.notificationRepository.save(newNotification) - - // Remove sensitive fields - comment.createdBy.deleteSensitiveFields() - post.deleteSensitiveFields() - - res.status(201).send(comment) - }) - - - private validateRequestAndGetEntities = async (req : AppRequest,) : Promise => { - const postId = req.params.id - const parsedId = parseInt(postId) - let errorMessage: 'Invalid ID' | 'No post found with that ID' - - // Check if ID is a number - if(isNaN(parsedId)) errorMessage = 'Invalid ID' - - const user = req.user - const post = await this.postRepository.findOne({ - where: {id: parsedId}, - relations: { - likedBy: true, - comments: { createdBy: true }, - createdBy: {notifications: true} - } - }) - - // Check if post exists - if(!post) errorMessage = 'No post found with that ID' - - return {user, post, errorMessage} + public likePost = catchAsync(async (req: AppRequest, res, next) => { + const { user, post, errorMessage } = + await this.validateRequestAndGetEntities(req); + if (errorMessage == "Invalid ID") + return next(new AppError("Invalid ID", 400)); + if (errorMessage == "No post found with that ID") { + return next(new AppError("No post found with that ID", 404)); } + + // Check if user has already liked the post + if (post.likedBy.some((likedUser) => likedUser.id === user.id)) { + return next(new AppError("You have already liked this post", 400)); + } + + post.likedBy.push(user); + await this.postRepository.save(post); + + // Send notification to post creator + const newNotification = Object.assign(new Notification(), { + message: `${user.firstName} ${user.lastName} liked your post`, + belongsTo: post.createdBy, + timeStamp: new Date(), + }); + const notification = await this.notificationRepository.save( + newNotification + ); + post.createdBy.notifications.push(notification); + await this.userRepository.save(post.createdBy); + + // Remove sensitive fields + post.deleteSensitiveFields(); + + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}/like: + * delete: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: unlikePost + * summary: Unlike a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * responses: + * 200: + * description: A post + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Post' + * 400: + * description: Invalid ID or You have not liked this post + * 404: + * description: No post found with that ID + */ + + public unlikePost = catchAsync(async (req: AppRequest, res, next) => { + const { user, post, errorMessage } = + await this.validateRequestAndGetEntities(req); + + if (errorMessage == "Invalid ID") + return next(new AppError("Invalid ID", 400)); + if (errorMessage == "No post found with that ID") + return next(new AppError("No post found with that ID", 404)); + + // Check if user likes the post + if (!post.likedBy.some((likedUser) => likedUser.id === user.id)) { + return next(new AppError("You have not liked this post", 400)); + } + + post.likedBy = post.likedBy.filter((likedUser) => likedUser.id !== user.id); + await this.postRepository.save(post); + + // Remove sensitive fields + post.deleteSensitiveFields(); + + res.status(200).send(post); + }); + + /** + * @swagger + * /posts/{id}/comment: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Posts + * operationId: commentPost + * summary: Comment on a post by ID + * parameters: + * - in: path + * name: id + * required: true + * description: ID of the post + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * content: + * type: string + * description: Comment content + * responses: + * 201: + * description: A comment + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Comment' + * 400: + * description: Invalid Request + * 404: + * description: No post found with that ID + */ + + public commentPost = catchAsync(async (req: AppRequest, res, next) => { + const { user, post, errorMessage } = + await this.validateRequestAndGetEntities(req); + + if (errorMessage == "Invalid ID") + return next(new AppError("Invalid ID", 400)); + if (errorMessage == "No post found with that ID") { + return next(new AppError("No post found with that ID", 404)); + } + + const { content } = req.body; + const newComment = Object.assign(new Comment(), { + content, + createdBy: user, + post, + createdAt: new Date(), + }); + + const comment = await this.commentRepository.save(newComment); + + // Send notification to post creator + const newNotification = Object.assign(new Notification(), { + message: `${user.firstName} ${user.lastName} commented on your post`, + belongsTo: post.createdBy, + timeStamp: new Date(), + }); + await this.notificationRepository.save(newNotification); + + // Remove sensitive fields + comment.createdBy.deleteSensitiveFields(); + post.deleteSensitiveFields(); + + res.status(201).send(comment); + }); + + private validateRequestAndGetEntities = async ( + req: AppRequest + ): Promise => { + const postId = req.params.id; + const parsedId = parseInt(postId); + let errorMessage: "Invalid ID" | "No post found with that ID"; + + // Check if ID is a number + if (isNaN(parsedId)) errorMessage = "Invalid ID"; + + const user = req.user; + const post = await this.postRepository.findOne({ + where: { id: parsedId }, + relations: { + likedBy: true, + comments: { createdBy: true }, + createdBy: { notifications: true }, + }, + }); + + // Check if post exists + if (!post) errorMessage = "No post found with that ID"; + + return { user, post, errorMessage }; + }; } interface validatedEntities { user: User diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts index 2c06bf2..fb740a2 100644 --- a/server/src/entity/User.ts +++ b/server/src/entity/User.ts @@ -53,10 +53,16 @@ export class User { } public deleteSensitiveFields() { - delete this.password; - delete this.email; - delete this.notifications; + this.deleteExtraFields(); + this.deletePrivateFields(); + } + public deleteExtraFields() { delete this.followed; delete this.followers; } + public deletePrivateFields(){ + delete this.email; + delete this.password; + delete this.notifications; + } }