diff --git a/backend/matching-service/.env.example b/backend/matching-service/.env.example index 7012733a6d..9b0351f960 100644 --- a/backend/matching-service/.env.example +++ b/backend/matching-service/.env.example @@ -18,7 +18,7 @@ RELAXATION_INTERVAL=3000 MATCH_TIMEOUT=30000 CLEANUP_INTERVAL=75000 -JWT_ACCESS_TOKEN_SECRET= +JWT_ACCESS_TOKEN_SECRET=d2636f0c0ce9119c4aca178220a3a5a7bba0e5f6dffa982f8095f5b566162029 # Copy root .env # If using mongoDB containerization, set to true. Else set to false (i.e local testing) diff --git a/backend/question-service/.env.example b/backend/question-service/.env.example index 348868f742..8b05c702bb 100644 --- a/backend/question-service/.env.example +++ b/backend/question-service/.env.example @@ -7,7 +7,7 @@ PORT=3002 # Initialize data from questions.json POPULATE_DB=true -JWT_ACCESS_TOKEN_SECRET= +JWT_ACCESS_TOKEN_SECRET=d2636f0c0ce9119c4aca178220a3a5a7bba0e5f6dffa982f8095f5b566162029 # Kafka configuration KAFKA_HOST=localhost diff --git a/backend/question-service/controllers/historyController.ts b/backend/question-service/controllers/historyController.ts index 0548ed66c2..adc529af47 100644 --- a/backend/question-service/controllers/historyController.ts +++ b/backend/question-service/controllers/historyController.ts @@ -23,11 +23,11 @@ export const getUserHistoryEntries = async (req: any, res: Response) => { key: entry._id, roomId: entry.roomId, attemptStartedAt: entry.attemptStartedAt.getTime(), - attemptCompletedAt: entry.attemptCompletedAt.getTime(), + lastAttemptSubmittedAt: entry.lastAttemptSubmittedAt.getTime(), title: entry.question.title, difficulty: entry.question.difficulty, topics: entry.question.categories.map((cat: any) => cat.name), - attemptCodes: entry.attemptCodes, + attemptCodes: entry.attemptCodes.filter((attemptCode) => attemptCode && attemptCode !== ""), }; }); res.status(200).json(historyViewModels); @@ -40,7 +40,7 @@ export const createOrUpdateUserHistoryEntry = async (req: any, res: Response) => try { const userId = req.userId; - const { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode } = req.body; + const { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, isInitial } = req.body; if (!roomId) { return res.status(400).json({ error: "roomId is required" }); @@ -48,30 +48,37 @@ export const createOrUpdateUserHistoryEntry = async (req: any, res: Response) => const existingEntry = await historyEntryModel.findOne({ userId, roomId }); - if (existingEntry) { + if (!isInitial && existingEntry && attemptCode && attemptCode !== "") { existingEntry.question = questionId; existingEntry.attemptStartedAt = attemptStartedAt; - existingEntry.attemptCompletedAt = attemptCompletedAt; + existingEntry.lastAttemptSubmittedAt = attemptCompletedAt; existingEntry.collaboratorId = collaboratorId; existingEntry.attemptCodes.push(attemptCode); const updatedEntry = await existingEntry.save(); return res.status(200).json(updatedEntry); + } else if (!existingEntry) { + try { + const newHistoryEntry = new historyEntryModel({ + userId, + question: questionId, + roomId, + attemptStartedAt, + attemptCompletedAt, + collaboratorId, + attemptCodes: isInitial ? [attemptCode] : [], + }); + + const savedEntry = await newHistoryEntry.save(); + + return res.status(201).json(savedEntry); + } catch { + return res.status(200).json("Attempt already exists."); + } } else { - const newHistoryEntry = new historyEntryModel({ - userId, - question: questionId, - roomId, - attemptStartedAt, - attemptCompletedAt, - collaboratorId, - attemptCodes: [attemptCode], - }); - - const savedEntry = await newHistoryEntry.save(); - - return res.status(201).json(savedEntry); + return res.status(200).json("Attempt already exists."); + // To reach here, this must be initial creation and there's an existing entry. No need to create new attempt. } } catch (error) { return res.status(500).json({ error: getErrorMessage(error) }); diff --git a/backend/question-service/models/HistoryEntry.ts b/backend/question-service/models/HistoryEntry.ts index 9de99440cd..65070dc7b8 100644 --- a/backend/question-service/models/HistoryEntry.ts +++ b/backend/question-service/models/HistoryEntry.ts @@ -7,7 +7,7 @@ export interface HistoryEntry extends mongoose.Document { question: Question; roomId: string; // Note: This should not be used to retrieve the room from matching service! This is only here to serve as a uniqueness check for updating attempt information! attemptStartedAt: Date; - attemptCompletedAt: Date; + lastAttemptSubmittedAt: Date; collaboratorId: string; attemptCodes: string[]; } @@ -17,10 +17,12 @@ const historyEntrySchema: Schema = new Schema({ question: { type: Schema.Types.ObjectId, ref: 'question', required: true }, roomId: { type: String, required: true, unique: false }, attemptStartedAt: { type: Date, required: true, default: Date.now() }, - attemptCompletedAt: { type: Date, required: true, default: Date.now() }, + lastAttemptSubmittedAt: { type: Date, required: true, default: Date.now() }, collaboratorId: { type: String, required: true }, - attemptCodes: [{ type: String, required: true }], + attemptCodes: [{ type: String }], }); +historyEntrySchema.index({ userId: 1, roomId: 1 }, { unique: true }); + const historyEntryModel = mongoose.model('historyEntry', historyEntrySchema); export default historyEntryModel; diff --git a/backend/user-service/.env.example b/backend/user-service/.env.example index 37751c6918..e9f3cebcc0 100644 --- a/backend/user-service/.env.example +++ b/backend/user-service/.env.example @@ -11,9 +11,9 @@ DB_CLOUD_URI= ENV=production # Secrets for creating JWT signatures -JWT_ACCESS_TOKEN_SECRET= -JWT_REFRESH_TOKEN_SECRET= -JWT_RESET_TOKEN_SECRET= +JWT_ACCESS_TOKEN_SECRET=d2636f0c0ce9119c4aca178220a3a5a7bba0e5f6dffa982f8095f5b566162029 +JWT_REFRESH_TOKEN_SECRET=65863c16dc76d23f06668c12b9223f93cb25f4f2c9f0919ba3330000abaa9253 +JWT_RESET_TOKEN_SECRET=70468fdf0966eaf0769cbd17f3348cf3debd9a47ac8b85dbee9aff3ebd5d074c # If using mongoDB containerization, set to true. Else set to false (i.e local testing) DB_REQUIRE_AUTH=true diff --git a/frontend/src/data/repositories/HistoryRemoteDataSource.ts b/frontend/src/data/repositories/HistoryRemoteDataSource.ts index 40d15a82b5..3cc17889ff 100644 --- a/frontend/src/data/repositories/HistoryRemoteDataSource.ts +++ b/frontend/src/data/repositories/HistoryRemoteDataSource.ts @@ -13,8 +13,8 @@ export class HistoryRemoteDataSource extends BaseApi { return await this.protectedGet("/"); } - async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise { - return await this.protectedPost("/", { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode }); + async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string, isInitial: boolean): Promise { + return await this.protectedPost("/", { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, isInitial }); } async deleteSelectedHistories(selectedHistoryIds: string[]): Promise { diff --git a/frontend/src/data/repositories/HistoryRepositoryImpl.ts b/frontend/src/data/repositories/HistoryRepositoryImpl.ts index 743cf52142..732f580760 100644 --- a/frontend/src/data/repositories/HistoryRepositoryImpl.ts +++ b/frontend/src/data/repositories/HistoryRepositoryImpl.ts @@ -8,8 +8,8 @@ export class HistoryRepositoryImpl { return this.dataSource.getAllUserHistories(); } - async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise { - this.dataSource.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode) + async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string, isInitial: boolean): Promise { + this.dataSource.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, isInitial) } async deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise { diff --git a/frontend/src/domain/entities/HistoryEntry.ts b/frontend/src/domain/entities/HistoryEntry.ts index c9d488b374..83320b2a41 100644 --- a/frontend/src/domain/entities/HistoryEntry.ts +++ b/frontend/src/domain/entities/HistoryEntry.ts @@ -3,7 +3,7 @@ export interface HistoryEntry { key: string; roomId: string; attemptStartedAt: string; - attemptCompletedAt: string; + lastAttemptSubmittedAt: string; title: string; difficulty: 'Easy' | 'Medium' | 'Hard'; topics: string[]; diff --git a/frontend/src/domain/repositories/IHistoryRepository.ts b/frontend/src/domain/repositories/IHistoryRepository.ts index dc5c5d0d77..a4697dc803 100644 --- a/frontend/src/domain/repositories/IHistoryRepository.ts +++ b/frontend/src/domain/repositories/IHistoryRepository.ts @@ -2,7 +2,7 @@ import { HistoryEntry } from "domain/entities/HistoryEntry"; export interface IHistoryRepository { getAllUserHistories(): Promise; - createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise; + createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string, isInitial: boolean): Promise; deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise; deleteAllUserHistories(): Promise; } \ No newline at end of file diff --git a/frontend/src/domain/usecases/HistoryUseCases.ts b/frontend/src/domain/usecases/HistoryUseCases.ts index 31d2a673bf..80e2d6d16a 100644 --- a/frontend/src/domain/usecases/HistoryUseCases.ts +++ b/frontend/src/domain/usecases/HistoryUseCases.ts @@ -14,13 +14,29 @@ export class HistoryUseCases { return allHistories; } + /** + * Creates a new User History Entry. + * (Note that just roomId isn't enough because the collaborator will also have a very similar History Entry) + * @returns Promise resolving when the history has been successfully created. + */ + async createUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise { + if (!questionId || questionId.trim() === "" + || !roomId || roomId.trim() === "" + || !attemptStartedAt || attemptStartedAt.trim() === "" + || !attemptCompletedAt || attemptCompletedAt.trim() === "" + || !collaboratorId || collaboratorId.trim() === "") { + throw new Error("Missing Attempt Details"); + } + await this.historyRepository.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, true); + } + /** * Creates a new User History Entry. If there already exists a History Entry with the same userId (obtained in * backend via JWT token and not passed here) and roomId, update the existing one instead. * (Note that just roomId isn't enough because the collaborator will also have a very similar History Entry) * @returns Promise resolving when the history has been successfully created. */ - async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise { + async updateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise { if (!questionId || questionId.trim() === "" || !roomId || roomId.trim() === "" || !attemptStartedAt || attemptStartedAt.trim() === "" @@ -28,7 +44,7 @@ export class HistoryUseCases { || !collaboratorId || collaboratorId.trim() === "") { throw new Error("Missing Attempt Details"); } - await this.historyRepository.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode); + await this.historyRepository.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, false); } /** diff --git a/frontend/src/presentation/components/CodeEditor/LeaveButton.tsx b/frontend/src/presentation/components/CodeEditor/LeaveButton.tsx index bc5c1bba2e..c2364a01a4 100644 --- a/frontend/src/presentation/components/CodeEditor/LeaveButton.tsx +++ b/frontend/src/presentation/components/CodeEditor/LeaveButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Modal, message } from "antd"; import { StopOutlined } from "@ant-design/icons"; import styles from "./LeaveButton.module.css"; @@ -23,6 +23,7 @@ const LeaveButton: React.FC = ({ const navigate = useNavigate(); const [isModalVisible, setIsModalVisible] = useState(false); const [editorContent, setEditorContent] = useState(""); + const [created, setCreated] = useState(false); const showModal = () => { const content = getEditorText(); @@ -41,7 +42,7 @@ const LeaveButton: React.FC = ({ const handleSaveAndLeave = async () => { try { - await historyUseCases.createOrUpdateUserHistory( + await historyUseCases.updateUserHistory( questionId, roomId, attemptStartedAt.getTime().toString(), @@ -64,6 +65,24 @@ const LeaveButton: React.FC = ({ } }; + useEffect(() => { //On init, send request to create attempt if it is absent. + const createEntry = async () => { + await historyUseCases.createUserHistory( + questionId, + roomId, + attemptStartedAt.getTime().toString(), + Date.now().toString(), + collaboratorId, + getEditorText(), + ); + } + if (!roomId || !questionId || !attemptStartedAt || !collaboratorId || !getEditorText) return; + if (!created) { + setCreated(true); + createEntry(); + }; + }, [roomId, questionId, attemptStartedAt, collaboratorId, getEditorText, created]) + return ( <> , ]} width={900} - bodyStyle={{ padding: 0 }} > {currentCodes.length > 0 ? (