Skip to content
2 changes: 1 addition & 1 deletion backend/matching-service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RELAXATION_INTERVAL=3000
MATCH_TIMEOUT=30000
CLEANUP_INTERVAL=75000

JWT_ACCESS_TOKEN_SECRET=<your-jwt-accecss-token>
JWT_ACCESS_TOKEN_SECRET=d2636f0c0ce9119c4aca178220a3a5a7bba0e5f6dffa982f8095f5b566162029

# Copy root .env
# If using mongoDB containerization, set to true. Else set to false (i.e local testing)
Expand Down
2 changes: 1 addition & 1 deletion backend/question-service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ PORT=3002
# Initialize data from questions.json
POPULATE_DB=true

JWT_ACCESS_TOKEN_SECRET=<your-jwt-accecss-token>
JWT_ACCESS_TOKEN_SECRET=d2636f0c0ce9119c4aca178220a3a5a7bba0e5f6dffa982f8095f5b566162029

# Kafka configuration
KAFKA_HOST=localhost
Expand Down
43 changes: 25 additions & 18 deletions backend/question-service/controllers/historyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -40,38 +40,45 @@ 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" });
}

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) });
Expand Down
8 changes: 5 additions & 3 deletions backend/question-service/models/HistoryEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand All @@ -17,10 +17,12 @@ const historyEntrySchema: Schema = new Schema<HistoryEntry>({
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>('historyEntry', historyEntrySchema);
export default historyEntryModel;
6 changes: 3 additions & 3 deletions backend/user-service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ DB_CLOUD_URI=<your-cloud-mongodb-uri>
ENV=production

# Secrets for creating JWT signatures
JWT_ACCESS_TOKEN_SECRET=<your-jwt-access-token-secret>
JWT_REFRESH_TOKEN_SECRET=<your-jwt-refresh-token-secret>
JWT_RESET_TOKEN_SECRET=<your-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
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/data/repositories/HistoryRemoteDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export class HistoryRemoteDataSource extends BaseApi {
return await this.protectedGet<HistoryEntry[]>("/");
}

async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void> {
return await this.protectedPost<void>("/", { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode });
async createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string, isInitial: boolean): Promise<void> {
return await this.protectedPost<void>("/", { questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, isInitial });
}

async deleteSelectedHistories(selectedHistoryIds: string[]): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/data/repositories/HistoryRepositoryImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
this.dataSource.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, isInitial)
}

async deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/domain/entities/HistoryEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/domain/repositories/IHistoryRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HistoryEntry } from "domain/entities/HistoryEntry";

export interface IHistoryRepository {
getAllUserHistories(): Promise<HistoryEntry[]>;
createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void>;
createOrUpdateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string, isInitial: boolean): Promise<void>;
deleteSelectedUserHistories(selectedHistoryIds: string[]): Promise<void>;
deleteAllUserHistories(): Promise<void>;
}
20 changes: 18 additions & 2 deletions frontend/src/domain/usecases/HistoryUseCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,37 @@ 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<void> {
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<void> {
async updateUserHistory(questionId: string, roomId: string, attemptStartedAt: string, attemptCompletedAt: string, collaboratorId: string, attemptCode: string): Promise<void> {
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);
await this.historyRepository.createOrUpdateUserHistory(questionId, roomId, attemptStartedAt, attemptCompletedAt, collaboratorId, attemptCode, false);
}

/**
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/presentation/components/CodeEditor/LeaveButton.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,6 +23,7 @@ const LeaveButton: React.FC<LeaveButtonProps> = ({
const navigate = useNavigate();
const [isModalVisible, setIsModalVisible] = useState(false);
const [editorContent, setEditorContent] = useState("");
const [created, setCreated] = useState(false);

const showModal = () => {
const content = getEditorText();
Expand All @@ -41,7 +42,7 @@ const LeaveButton: React.FC<LeaveButtonProps> = ({

const handleSaveAndLeave = async () => {
try {
await historyUseCases.createOrUpdateUserHistory(
await historyUseCases.updateUserHistory(
questionId,
roomId,
attemptStartedAt.getTime().toString(),
Expand All @@ -64,6 +65,24 @@ const LeaveButton: React.FC<LeaveButtonProps> = ({
}
};

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 (
<>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const SaveButton: React.FC<SaveButtonProps> = ({

const handleSave = async () => {
try {
await historyUseCases.createOrUpdateUserHistory(
await historyUseCases.updateUserHistory(
questionId,
roomId,
attemptStartedAt.getTime().toString(),
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/presentation/components/RecentAttemptsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ export const RecentAttemptsTable: React.FC = () => {
title: 'Date',
dataIndex: 'date',
key: 'date',
sorter: (a, b) => new Date(a.attemptCompletedAt).getTime() - new Date(b.attemptCompletedAt).getTime(),
sorter: (a, b) => new Date(a.lastAttemptSubmittedAt).getTime() - new Date(b.lastAttemptSubmittedAt).getTime(),
render: (_text, record) => {
const formattedStartDate = formatDate(record.attemptStartedAt);
const formattedEndDate = formatDate(record.attemptCompletedAt);
const duration = calculateDuration(record.attemptStartedAt, record.attemptCompletedAt);
const formattedEndDate = formatDate(record.lastAttemptSubmittedAt);
const duration = calculateDuration(record.attemptStartedAt, record.lastAttemptSubmittedAt);
return (
<div className={styles.dateContainer}>
<div>
Expand Down Expand Up @@ -279,7 +279,6 @@ export const RecentAttemptsTable: React.FC = () => {
</Button>,
]}
width={900}
bodyStyle={{ padding: 0 }}
>
{currentCodes.length > 0 ? (
<Tabs
Expand Down
Loading