diff --git a/.env.sample b/.env.sample index b572969a2b..fbd0149134 100644 --- a/.env.sample +++ b/.env.sample @@ -8,11 +8,10 @@ QUESTION_DB_USERNAME=user QUESTION_DB_PASSWORD=password # User Service -USER_SERVICE_CLOUD_URI=mongodb+srv://admin:@cluster0.uo0vu.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 -USER_SERVICE_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB - -# Will use cloud MongoDB Atlas database -ENV=PROD +USER_DB_CLOUD_URI= +USER_DB_LOCAL_URI=mongodb://user-db:27017/user +USER_DB_USERNAME=user +USER_DB_PASSWORD=password # Secret for creating JWT signature JWT_SECRET=you-can-replace-this-with-your-own-secret diff --git a/compose.dev.yml b/compose.dev.yml index c450c933a4..26be2028b3 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -18,4 +18,8 @@ services: command: npm run dev volumes: - /app/node_modules - - ./services/user:/app \ No newline at end of file + - ./services/user:/app + + user-db: + ports: + - 27018:27017 \ No newline at end of file diff --git a/compose.yml b/compose.yml index 96db768d65..0be9fb2a85 100644 --- a/compose.yml +++ b/compose.yml @@ -49,15 +49,34 @@ services: ports: - 8082:8082 environment: - USER_SERVICE_CLOUD_URI: ${USER_SERVICE_CLOUD_URI} - USER_SERVICE_LOCAL_URI: ${USER_SERVICE_LOCAL_URI} - ENV: ${ENV} + DB_CLOUD_URI: ${USER_DB_CLOUD_URI} + DB_LOCAL_URI: ${USER_DB_LOCAL_URI} + DB_USERNAME: ${USER_DB_USERNAME} + DB_PASSWORD: ${USER_DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} + networks: + - user-db-network + restart: always + + user-db: + container_name: user-db + image: mongo:7.0.14 + environment: + MONGO_INITDB_ROOT_USERNAME: ${USER_DB_USERNAME} + MONGO_INITDB_ROOT_PASSWORD: ${USER_DB_PASSWORD} + volumes: + - user-db:/data/db + networks: + - user-db-network + command: --quiet restart: always volumes: question-db: + user-db: networks: question-db-network: + driver: bridge + user-db-network: driver: bridge \ No newline at end of file diff --git a/frontend/src/_services/question.service.ts b/frontend/src/_services/question.service.ts index 1b35097ad4..6c7debe601 100644 --- a/frontend/src/_services/question.service.ts +++ b/frontend/src/_services/question.service.ts @@ -7,6 +7,7 @@ import { QuestionResponse, QuestionBody, UploadQuestionsResponse, + MessageOnlyResponse, } from '../app/questions/question.model'; import { TopicResponse } from '../app/questions/topic.model'; @@ -14,7 +15,7 @@ import { TopicResponse } from '../app/questions/topic.model'; providedIn: 'root', }) export class QuestionService { - private baseUrl = API_CONFIG.baseUrl; + private baseUrl = API_CONFIG.baseUrl + '/questions'; private httpOptions = { headers: new HttpHeaders({ @@ -46,11 +47,11 @@ export class QuestionService { } // send request - return this.http.get(this.baseUrl + '/questions', { params }); + return this.http.get(this.baseUrl, { params }); } - getQuestionByID(id: number): Observable { - return this.http.get(this.baseUrl + '/questions/' + id); + getQuestionByID(id: number): Observable { + return this.http.get(this.baseUrl + '/' + id); } getQuestionByParam(topics: string[], difficulty: string, limit?: number): Observable { @@ -61,16 +62,16 @@ export class QuestionService { } params = params.append('topics', topics.join(',')).append('difficulty', difficulty); - return this.http.get(this.baseUrl + '/questions/search', { params }); + return this.http.get(this.baseUrl + '/search', { params }); } getTopics(): Observable { - return this.http.get(this.baseUrl + '/questions/topics'); + return this.http.get(this.baseUrl + '/topics'); } addQuestion(question: QuestionBody): Observable { return this.http - .post(this.baseUrl + '/questions', question, this.httpOptions) + .post(this.baseUrl, question, this.httpOptions) .pipe(catchError(this.handleError)); } @@ -82,13 +83,17 @@ export class QuestionService { updateQuestion(id: number, question: QuestionBody): Observable { return this.http - .put(this.baseUrl + '/questions/' + id, question, this.httpOptions) + .put(this.baseUrl + '/' + id, question, this.httpOptions) .pipe(catchError(this.handleError)); } deleteQuestion(id: number): Observable { + return this.http.delete(this.baseUrl + '/' + id).pipe(catchError(this.handleError)); + } + + deleteQuestions(ids: number[]): Observable { return this.http - .delete(this.baseUrl + '/questions/' + id) + .post(this.baseUrl + '/delete', { ids }, this.httpOptions) .pipe(catchError(this.handleError)); } diff --git a/frontend/src/app/questions/question.model.ts b/frontend/src/app/questions/question.model.ts index 395edcc2a5..375ca5b767 100644 --- a/frontend/src/app/questions/question.model.ts +++ b/frontend/src/app/questions/question.model.ts @@ -1,12 +1,17 @@ -export interface QuestionResponse { +export interface BaseResponse { status: string; message: string; +} + +export interface MessageOnlyResponse extends BaseResponse { + data: null; +} + +export interface QuestionResponse extends BaseResponse { data?: Question[] | null; } -export interface SingleQuestionResponse { - status: string; - message: string; +export interface SingleQuestionResponse extends BaseResponse { data: Question; } diff --git a/frontend/src/app/questions/questions.component.html b/frontend/src/app/questions/questions.component.html index 49dfb56295..6a4d408477 100644 --- a/frontend/src/app/questions/questions.component.html +++ b/frontend/src/app/questions/questions.component.html @@ -37,7 +37,7 @@ [value]="questions" [(selection)]="selectedQuestions" datakey="id" - [tableStyle]="{ 'table-layout': 'fixed' }" + [tableStyle]="{ 'min-width': '50rem' }" [paginator]="true" [rows]="5" [rowsPerPageOptions]="[5, 10, 20]" diff --git a/frontend/src/app/questions/questions.component.ts b/frontend/src/app/questions/questions.component.ts index 70bf2a2af7..3321015c16 100644 --- a/frontend/src/app/questions/questions.component.ts +++ b/frontend/src/app/questions/questions.component.ts @@ -14,7 +14,6 @@ import { DropdownModule } from 'primeng/dropdown'; import { ProgressSpinnerModule } from 'primeng/progressspinner'; import { Question } from './question.model'; import { QuestionService } from '../../_services/question.service'; -import { forkJoin } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { QuestionDialogComponent } from './question-dialog.component'; import { Column } from './column.model'; @@ -87,9 +86,7 @@ export class QuestionsComponent implements OnInit { message: 'Are you sure you want to delete the selected questions?', header: 'Delete Confirmation', icon: 'pi pi-exclamation-triangle', - accept: () => { - this.handleDeleteQuestionResponse(); - }, + accept: () => this.handleDeleteQuestionsResponse(), }); } @@ -103,21 +100,15 @@ export class QuestionsComponent implements OnInit { }; } - handleDeleteQuestionResponse() { - const deleteRequests = this.selectedQuestions?.map(q => this.questionService.deleteQuestion(q.id)); - - forkJoin(deleteRequests!).subscribe({ + handleDeleteQuestionsResponse() { + const ids = this.selectedQuestions?.map(q => q.id) || []; + this.questionService.deleteQuestions(ids).subscribe({ next: () => { - // delete locally - this.questions = this.questions?.filter(val => !this.selectedQuestions?.includes(val)); + this.questions = this.questions?.filter(q => !ids.includes(q.id)); this.selectedQuestions = null; }, - error: (error: HttpErrorResponse) => { - this.onErrorReceive('Some questions could not be deleted. ' + error.error.message); - }, - complete: () => { - this.onSuccessfulRequest('Question(s) Deleted'); - }, + error: (error: HttpErrorResponse) => this.onErrorReceive(error.error.message), + complete: () => this.onSuccessfulRequest('Question(s) Deleted'), }); } diff --git a/services/question/README.md b/services/question/README.md index 7ec11ee526..64859e5ec2 100644 --- a/services/question/README.md +++ b/services/question/README.md @@ -461,3 +461,41 @@ curl -X DELETE http://localhost:8081/questions/21 ``` --- + +## Delete Questions + +This endpoint allows the deletion of multiple questions by their question IDs. + +- **HTTP Method**: `POST` +- **Endpoint**: `/questions/delete` + +### Parameters: + +- `ids` (Required) - An array of integers representing the IDs of the questions to delete, e.g. `[1, 2, 3]`. + +### Responses: + +| Response Code | Explanation | +|-----------------------------|------------------------------------------------------| +| 200 (OK) | Success, the question is deleted successfully. | +| 400 (Bad Request) | The `ids` parameter was not specified or is invalid. | +| 404 (Not Found) | A question with the specified id not found. | +| 500 (Internal Server Error) | Unexpected error in the database or server. | + +### Command Line Example: + +``` +curl -X POST http://localhost:8081/questions/delete -H "Content-Type: application/json" -d '{"ids": [21, 22]}' +``` + +### Example of Response Body for Success: + +```json +{ + "status": "Success", + "message": "Questions deleted successfully", + "data": null +} +``` + +--- diff --git a/services/question/src/controllers/questionController.ts b/services/question/src/controllers/questionController.ts index a6d49d6496..e83722cab9 100644 --- a/services/question/src/controllers/questionController.ts +++ b/services/question/src/controllers/questionController.ts @@ -303,3 +303,32 @@ export const uploadQuestions = async (req: Request, res: Response) => { } } }; + +/* + * This endpoint allows deletion of multiple questions by the question ID. + * @param req + * @param res + */ +export const deleteQuestions = async (req: Request, res: Response) => { + const { ids } = req.body; + + if (!ids || !Array.isArray(ids)) { + return handleBadRequest(res, 'IDs are missing or not specified as an array'); + } + + const deletedIDs = ids.map((id: any) => parseInt(id, 10)); + if (deletedIDs.some((id: any) => isNaN(id))) { + return handleBadRequest(res, 'Invalid question ID'); + } + + try { + const count = await Question.countDocuments({ id: { $in: deletedIDs } }); + + if (count !== ids.length) return handleNotFound(res, 'Question not found'); + await Question.deleteMany({ id: { $in: deletedIDs } }); + handleSuccess(res, 200, 'Questions deleted successfully', null); + } catch (error) { + console.log('Error in deleteQuestions:', error); + handleError(res, 'Failed to delete questions'); + } +}; diff --git a/services/question/src/routes/questionRoutes.ts b/services/question/src/routes/questionRoutes.ts index 9960a9534c..48f7d4e999 100644 --- a/services/question/src/routes/questionRoutes.ts +++ b/services/question/src/routes/questionRoutes.ts @@ -8,6 +8,7 @@ import { deleteQuestion, updateQuestion, uploadQuestions, + deleteQuestions, } from '../controllers/questionController'; import { upload } from '../utils/multer'; @@ -45,4 +46,9 @@ questionRouter.put('/:id', updateQuestion); */ questionRouter.delete('/:id', deleteQuestion); +/** + * Delete questions from the database. + */ +questionRouter.post('/delete', deleteQuestions); + export default questionRouter; diff --git a/services/user/.env.sample b/services/user/.env.sample index f07ad5efa6..f78e16586f 100644 --- a/services/user/.env.sample +++ b/services/user/.env.sample @@ -1,10 +1,8 @@ -# User Service -USER_SERVICE_CLOUD_URI= -USER_SERVICE_LOCAL_URI=mongodb://127.0.0.1:27017/peerprepUserServiceDB -PORT=8082 - -# Will use cloud MongoDB Atlas database -ENV=PROD +# This is a sample environment configuration file. +# Copy this file to .env and replace the placeholder values with your own. +DB_CLOUD_URI= +DB_LOCAL_URI=mongodb://user-db:27017/user +PORT=8083 # Secret for creating JWT signature JWT_SECRET=you-can-replace-this-with-your-own-secret diff --git a/services/user/src/model/repository.ts b/services/user/src/model/repository.ts index 4a1a8c5b6a..8559d1c227 100644 --- a/services/user/src/model/repository.ts +++ b/services/user/src/model/repository.ts @@ -3,14 +3,17 @@ import 'dotenv/config'; import { connect } from 'mongoose'; export async function connectToDB() { - const mongoDBUri = - process.env.ENV === 'PROD' ? process.env.USER_SERVICE_CLOUD_URI : process.env.USER_SERVICE_LOCAL_URI; + const mongoUri = process.env.NODE_ENV === 'production' ? process.env.DB_CLOUD_URI : process.env.DB_LOCAL_URI; - if (!mongoDBUri) { + if (!mongoUri) { throw new Error('MongoDB URI not specified'); } - await connect(mongoDBUri); + await connect(mongoUri, { + authSource: 'admin', + user: process.env.DB_USERNAME, + pass: process.env.DB_PASSWORD, + }); } export async function createUser(username: string, email: string, password: string) {