From 100834e456e0ae2425410874827d20c6d30db742 Mon Sep 17 00:00:00 2001 From: Tino <132175332+Tinogwanz@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:40:48 +0200 Subject: [PATCH 1/2] fixed comments bugs --- frontend/src/components/Comments/comments.tsx | 33 +++----- frontend/src/services/tickets.service.ts | 18 ++-- .../src/controllers/giveaway.controller.ts | 4 +- .../src/controllers/tickets.controller.ts | 8 +- nodejs_api/src/services/giveaway.service.ts | 4 +- nodejs_api/src/services/tenders.service.ts | 1 + nodejs_api/src/services/tickets.service.ts | 83 +++++++++++++++---- 7 files changed, 99 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/Comments/comments.tsx b/frontend/src/components/Comments/comments.tsx index 8fad258a..5266af2a 100644 --- a/frontend/src/components/Comments/comments.tsx +++ b/frontend/src/components/Comments/comments.tsx @@ -9,6 +9,7 @@ import { getTicketComments, getUserFirstLastName, } from '@/services/tickets.service'; +import { UserRole } from '@/types/custom.types'; interface CommentsProps { onBack: () => void; @@ -28,24 +29,7 @@ const Comments: React.FC = ({ onBack, isCitizen, ticketId }) => { const userSession = String(user_data.current?.session_token); const commentsData = await getTicketComments(ticketId, userSession); - const userPoolId = process.env.USER_POOL_ID; - if (!userPoolId) { - throw new Error("USER_POOL_ID is not defined"); - } - - const enrichedComments = await Promise.allSettled(commentsData.map(async (comment: any) => { - const userAttributes = await getUserFirstLastName(comment.user_id, userPoolId); - return { - ...comment, - userName: `${userAttributes?.given_name} ${userAttributes?.family_name}`, - userImage: userAttributes?.picture || 'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png?20150327203541', - time: new Date(comment.date) - }; - })); - - const fulfilledComments = enrichedComments.filter((comment: any) => comment.status === "fulfilled").map((comment: any) => comment.value); - - setComments(fulfilledComments); + setComments(commentsData); setLoading(false); } catch (error) { console.error("Error fetching comments:", error); @@ -65,15 +49,18 @@ const Comments: React.FC = ({ onBack, isCitizen, ticketId }) => { const user_picture = String(user_data.current?.picture); const userSession = String(user_data.current?.session_token); const user_email = String(user_data.current?.email); + const userRole = user_data.current?.user_role || UserRole.CITIZEN; // default to citizen if user_role is not set + + const dateCommentCreated = new Date(); const newCommentData = { userName, - userImage: user_picture || 'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png?20150327203541', - time: new Date(), + userImage: user_picture || "https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png?20150327203541", + time: dateCommentCreated, commentText: newComment, }; - await addCommentWithoutImage(newCommentData.commentText, ticketId, user_email, userSession); + await addCommentWithoutImage(newCommentData.commentText, ticketId, user_email, dateCommentCreated, userRole, userSession); setComments([...comments, newCommentData]); setNewComment(''); @@ -100,8 +87,8 @@ const Comments: React.FC = ({ onBack, isCitizen, ticketId }) => { key={index} userName={comment.userName} userImage={comment.userImage} - time={new Date(comment.time)} - commentText={comment.commentText} // Use commentText to ensure the actual comment is displayed + time={new Date(comment.date)} + commentText={comment.comment} // Use commentText to ensure the actual comment is displayed /> )) ) : ( diff --git a/frontend/src/services/tickets.service.ts b/frontend/src/services/tickets.service.ts index 6000ad50..49c4c1d9 100644 --- a/frontend/src/services/tickets.service.ts +++ b/frontend/src/services/tickets.service.ts @@ -1,4 +1,4 @@ -import { DashboardTicket, FaultGeoData, FaultType, PaginatedResults, UnprocessedFaultGeoData } from "@/types/custom.types"; +import { DashboardTicket, FaultGeoData, FaultType, PaginatedResults, UnprocessedFaultGeoData, UserRole } from "@/types/custom.types"; import { CognitoIdentityProviderClient, AdminGetUserCommand } from "@aws-sdk/client-cognito-identity-provider"; interface UserAttributes { @@ -467,14 +467,17 @@ export async function addCommentWithImage(comment: string, ticket_id: string, im } } -export async function addCommentWithoutImage(comment: string, ticket_id: string, user_id: string, user_session: string,) { +export async function addCommentWithoutImage(comment: string, ticket_id: string, user_id: string, dateCreated: Date, user_role: UserRole, user_session: string,) { try { const apiUrl = "/api/tickets/add-comment-without-image"; const data = { - comment, - ticket_id, - user_id + comment: comment, + ticket_id: ticket_id, + user_id: user_id, + date_created: dateCreated.toISOString(), + user_role: String(user_role) }; + const response = await fetch(apiUrl, { method: "POST", headers: { @@ -498,13 +501,12 @@ export async function addCommentWithoutImage(comment: string, ticket_id: string, export async function getTicketComments(ticket_id: string, user_session: string) { try { - const apiUrl = `/api/tickets/comments`; + const apiUrl = `/api/tickets/comments?ticket_id=${encodeURIComponent(ticket_id)}`; const response = await fetch(apiUrl, { method: "GET", headers: { "Authorization": user_session, - "Content-Type": "application/json", - "X-Ticket-ID": ticket_id, // Add ticket_id in the headers + "Content-Type": "application/json" }, }); diff --git a/nodejs_api/src/controllers/giveaway.controller.ts b/nodejs_api/src/controllers/giveaway.controller.ts index dc24a38d..60f419e0 100644 --- a/nodejs_api/src/controllers/giveaway.controller.ts +++ b/nodejs_api/src/controllers/giveaway.controller.ts @@ -1,11 +1,11 @@ import { NextFunction, Request, Response } from "express"; import * as giveawayService from "../services/giveaway.service"; -import { cacheResponse } from "../config/redis.config"; +import { cacheResponse, DEFAULT_CACHE_DURATION } from "../config/redis.config"; export const getParticipantCount = async (req: Request, res: Response, next:NextFunction) => { try { const response = await giveawayService.getParticipantCount(); - cacheResponse(req.originalUrl, 30, response); // cache for 30 seconds + cacheResponse(req.originalUrl, DEFAULT_CACHE_DURATION, response); return res.status(200).json(response); } catch (error: any) { next(error); diff --git a/nodejs_api/src/controllers/tickets.controller.ts b/nodejs_api/src/controllers/tickets.controller.ts index b2c60de3..8bb2b5f4 100644 --- a/nodejs_api/src/controllers/tickets.controller.ts +++ b/nodejs_api/src/controllers/tickets.controller.ts @@ -229,7 +229,7 @@ export const addCommentWithImage = async (req: Request, res: Response) => { }; export const addCommentWithoutImage = async (req: Request, res: Response) => { - const requiredFields = ["comment", "ticket_id", "user_id"]; + const requiredFields = ["comment", "ticket_id", "user_id", "date_created", "user_role"]; const missingFields = requiredFields.filter(field => !req.body[field]); if (missingFields.length > 0) { @@ -238,7 +238,7 @@ export const addCommentWithoutImage = async (req: Request, res: Response) => { try { const { comment, ticket_id, user_id } = req.body; - const response = await ticketsService.addTicketCommentWithoutImage(comment, ticket_id, user_id); + const response = await ticketsService.addTicketCommentWithoutImage(req.body); return res.status(200).json(response); } catch (error: any) { return res.status(500).json({ Error: error.message }); @@ -246,9 +246,9 @@ export const addCommentWithoutImage = async (req: Request, res: Response) => { }; export const getTicketComments = async (req: Request, res: Response) => { - const ticketId = req.headers["X-Ticket-ID"] as string; + const ticketId = req.query["ticket_id"] as string; if (!ticketId) { - return res.status(400).json({ Error: "Missing request header: X-Ticket-ID" }); + return res.status(400).json({ Error: "Missing parameter: ticket_id" }); } try { const response = await ticketsService.getTicketComments(ticketId); diff --git a/nodejs_api/src/services/giveaway.service.ts b/nodejs_api/src/services/giveaway.service.ts index acab6271..6f74a341 100644 --- a/nodejs_api/src/services/giveaway.service.ts +++ b/nodejs_api/src/services/giveaway.service.ts @@ -2,7 +2,7 @@ import { PutCommandInput, PutCommandOutput, QueryCommandInput, QueryCommandOutpu import { GIVEAWAY_TABLE, TICKETS_TABLE } from "../config/dynamodb.config"; import { addJobToReadQueue, addJobToWriteQueue } from "./jobs.service"; import { JobData } from "../types/job.types"; -import { DB_PUT, DB_QUERY, DB_SCAN } from "../config/redis.config"; +import { DB_PUT, DB_QUERY, DB_SCAN, deleteCacheKey } from "../config/redis.config"; import { CustomError } from "../errors/CustomError"; export const getParticipantCount = async () => { @@ -25,6 +25,8 @@ export const getParticipantCount = async () => { }; export const addParticipant = async (formData: any) => { + await deleteCacheKey("/giveaway/participant/count"); + // verify that provided ticketNumber exists const params: QueryCommandInput = { TableName: TICKETS_TABLE, diff --git a/nodejs_api/src/services/tenders.service.ts b/nodejs_api/src/services/tenders.service.ts index e53dcf9a..b1c164af 100644 --- a/nodejs_api/src/services/tenders.service.ts +++ b/nodejs_api/src/services/tenders.service.ts @@ -27,6 +27,7 @@ interface AcceptOrRejectTenderData { export const createTender = async (senderData: TenderData) => { await deleteAllCache(); + const companyPid = await getCompanyIDFromName(senderData.company_name); if (!companyPid) { throw new BadRequestError("Company Does not Exist"); diff --git a/nodejs_api/src/services/tickets.service.ts b/nodejs_api/src/services/tickets.service.ts index 758276d9..a7f0604b 100644 --- a/nodejs_api/src/services/tickets.service.ts +++ b/nodejs_api/src/services/tickets.service.ts @@ -1,13 +1,14 @@ import { QueryCommandInput, GetCommandInput, GetCommandOutput, PutCommandInput, QueryCommandOutput, ScanCommandInput, ScanCommandOutput, PutCommandOutput, UpdateCommandInput } from "@aws-sdk/lib-dynamodb"; import { BadRequestError, ClientError } from "../types/error.types"; -import { ASSETS_TABLE, TENDERS_TABLE, TICKET_UPDATE_TABLE, TICKETS_TABLE, WATCHLIST_TABLE } from "../config/dynamodb.config"; +import { ASSETS_TABLE, cognitoClient, TENDERS_TABLE, TICKET_UPDATE_TABLE, TICKETS_TABLE, WATCHLIST_TABLE } from "../config/dynamodb.config"; import { generateId, generateTicketNumber, getCompanyIDFromName, getMunicipality, getTicketDateOpened, getUserProfile, updateCommentCounts, updateTicketTable, validateTicketId } from "../utils/tickets.utils"; import { uploadFile } from "../config/s3bucket.config"; import WebSocket from "ws"; import { addJobToReadQueue, addJobToWriteQueue } from "./jobs.service"; import { JobData } from "../types/job.types"; -import { deleteAllCache, DB_GET, DB_PUT, DB_QUERY, DB_SCAN, DB_UPDATE } from "../config/redis.config"; +import { deleteAllCache, DB_GET, DB_PUT, DB_QUERY, DB_SCAN, DB_UPDATE, deleteCacheKey } from "../config/redis.config"; import { sendWebSocketMessage } from "../utils/tenders.utils"; +import { AdminGetUserCommand, AdminGetUserCommandOutput } from "@aws-sdk/client-cognito-identity-provider"; interface Ticket { dateClosed: string; @@ -960,25 +961,23 @@ export const addTicketCommentWithImage = async (comment: string, ticket_id: stri return response; }; -export const addTicketCommentWithoutImage = async (comment: string, ticket_id: string, user_id: string) => { +export const addTicketCommentWithoutImage = async (commentData: any) => { + await deleteAllCache(); + // Validate ticket_id - validateTicketId(ticket_id); + validateTicketId(commentData.ticket_id); // Generate unique ticket update ID const ticketupdate_id = generateId(); - // Get current date and time - const currentDatetime = new Date(); - const formattedDatetime = currentDatetime.toISOString(); - // Prepare comment item const commentItem = { ticketupdate_id: ticketupdate_id, - comment: comment, - date: formattedDatetime, + comment: commentData.comment, + date: commentData.date_created, imageURL: "", // Set to if no image is provided - ticket_id: ticket_id, - user_id: user_id + ticket_id: commentData.ticket_id, + user_id: commentData.user_id }; // Insert comment into ticket_updates table @@ -997,7 +996,7 @@ export const addTicketCommentWithoutImage = async (comment: string, ticket_id: s const response = { message: "Comment added successfully", - ticketupdate_id: ticketupdate_id, + ticketupdate_id: ticketupdate_id }; return response; @@ -1010,11 +1009,16 @@ export const getTicketComments = async (currTicketId: string) => { TableName: TICKET_UPDATE_TABLE, IndexName: "ticket_id-index", KeyConditionExpression: "ticket_id = :ticket_id", + ProjectionExpression: "ticketupdate_id, #comment, user_id", + ExpressionAttributeNames: { + "#comment": "comment" // alias the reserved keyword "comment" + }, ExpressionAttributeValues: { ":ticket_id": currTicketId } }; + const jobData: JobData = { type: DB_QUERY, params: params @@ -1023,7 +1027,58 @@ export const getTicketComments = async (currTicketId: string) => { const job = await addJobToReadQueue(jobData); const response = await job.finished() as QueryCommandOutput; const items = response.Items || []; - return items; + + if (items.length > 0) { + let cognitoUsername: string = ""; + try { + const USER_POOL_ID = process.env.USER_POOL_ID; + for (let commentItem of items) { + cognitoUsername = (commentItem["user_id"] as string).toLowerCase(); + const userResponse: AdminGetUserCommandOutput = await cognitoClient.send( + new AdminGetUserCommand({ + UserPoolId: USER_POOL_ID, + Username: cognitoUsername + }) + ); + + let userImage: string | null = null; + let userGivenName: string | null = null; + let userFamilyName: string | null = null; + + if (userResponse.UserAttributes) { + for (let attr of userResponse.UserAttributes) { + if (attr.Name === "picture") { + userImage = attr.Value || "https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png?20150327203541"; + } + + if (attr.Name === "given_name") { + userGivenName = attr.Value!; + } + + if (attr.Name === "family_name") { + userFamilyName = attr.Value!; + } + + if (userImage && userGivenName && userFamilyName) { + break; + } + } + } + + commentItem["userImage"] = userImage; + commentItem["userName"] = userGivenName + " " + userFamilyName; + } + + return items; + } catch (error: any) { + if (error.name === "UserNotFoundException") { + console.error(`${error.message}: ${cognitoUsername}`); + } else { + console.error("An error occurred:", error); + } + } + } + } catch (e: any) { if (e instanceof ClientError) { throw new BadRequestError(`Failed to search for the ticket comments: ${e.response.Error.Message}`); From 29a4c51774009fbc669dcdac08a897b117b8b0c7 Mon Sep 17 00:00:00 2001 From: Tino <132175332+Tinogwanz@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:51:35 +0200 Subject: [PATCH 2/2] updated tests for comments --- .../__tests__/integration/tickets.route.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nodejs_api/__tests__/integration/tickets.route.test.ts b/nodejs_api/__tests__/integration/tickets.route.test.ts index 2b0c75c8..65297f1a 100644 --- a/nodejs_api/__tests__/integration/tickets.route.test.ts +++ b/nodejs_api/__tests__/integration/tickets.route.test.ts @@ -666,7 +666,9 @@ describe("Integration Tests - /tickets", () => { const mockRequestBody = { comment: "This is a test comment without an image.", ticket_id: "ticket123", - user_id: "user456" + user_id: "user456", + user_role: "CITIZEN", + date_created: "2022-01-01T12:00:00Z" }; const mockServiceResponse = { @@ -684,11 +686,7 @@ describe("Integration Tests - /tickets", () => { expect(response.body).toEqual(mockServiceResponse); - expect(ticketsService.addTicketCommentWithoutImage).toHaveBeenCalledWith( - mockRequestBody.comment, - mockRequestBody.ticket_id, - mockRequestBody.user_id - ); + expect(ticketsService.addTicketCommentWithoutImage).toHaveBeenCalledWith(mockRequestBody); }); test("should return 400 if required fields are missing", async () => { @@ -703,14 +701,16 @@ describe("Integration Tests - /tickets", () => { expect(response.statusCode).toBe(400); - expect(response.body.error).toBe("Missing parameter(s): comment"); + expect(response.body.error).toBe("Missing parameter(s): comment, date_created, user_role"); }); test("should return 500 if there is an internal server error", async () => { const mockRequestBody = { comment: "This is a test comment without an image.", ticket_id: "ticket123", - user_id: "user456" + user_id: "user456", + user_role: "CITIZEN", + date_created: "2022-01-01T12:00:00Z" }; jest.spyOn(ticketsService, "addTicketCommentWithoutImage").mockRejectedValue(new Error("Internal Server Error"));