diff --git a/README.md b/README.md index c270833..bf9b5af 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Backend service for **SmartNotes**, a personal project that enables smart note-taking with AI integration to help individuals have a better experience in note-taking, organizing, and revising knowledge. -**SmartNotes Frontend Repository:** [https://github.com/pvdev1805/SmartNotes](https://github.com/pvdev1805/SmartNotes) +**SmartNotes Frontend Repository:** [https://github.com/TUT888/SmartNotes-FE](https://github.com/TUT888/SmartNotes-FE) ## Table of Contents @@ -20,6 +20,7 @@ help individuals have a better experience in note-taking, organizing, and revisi - [Prerequisites](#prerequisites) - [HuggingFace API](#huggingface-api) - [Environment Variables](#environment-variables) +- [How to Run](#how-to-run) - [How to Test](#how-to-test) - [Test Commands](#test-commands-windows) - [Test Structure](#test-structure) @@ -123,6 +124,16 @@ REDIS_PORT= [Back to top](#smartnotes-backend) +## How to run +1. Start Redis server with Docker Desktop: + ```bash + docker run --name smart-notes-redis -p 6379:6379 redis:latest + ``` +2. Run the application (Windows) + ```bash + ./mvnw.cmd spring-boot:run + ``` + ## How to test ### Test commands (Windows) Run all tests @@ -177,11 +188,11 @@ src/test/java/ ## Contributors **Project Maintainers:** This project (both frontend and backend) is developed and maintained by: -- **Alice Tat** ([@TUT888](https://github.com/TUT888)) -- **Phu Vo** ([@pvdev1805](https://github.com/pvdev1805)) +- Owner: **Alice Tat** ([@TUT888](https://github.com/TUT888)) +- Contributor: **Phu Vo** ([@pvdev1805](https://github.com/pvdev1805)) **Project Repositories:** - Backend: [SmartNotes Backend](https://github.com/TUT888/SmartNotes) -- Frontend: [SmartNotes Frontend](https://github.com/pvdev1805/SmartNotes) +- Frontend: [SmartNotes Frontend](https://github.com/TUT888/SmartNotes-FE) [Back to top](#smartnotes-backend) \ No newline at end of file diff --git a/pom.xml b/pom.xml index bbf615a..20115cc 100644 --- a/pom.xml +++ b/pom.xml @@ -209,6 +209,13 @@ jsonschema-module-jakarta-validation 4.31.1 + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + diff --git a/src/main/java/com/be08/smart_notes/common/SecurityConstants.java b/src/main/java/com/be08/smart_notes/common/SecurityConstants.java index 6784ece..0713673 100644 --- a/src/main/java/com/be08/smart_notes/common/SecurityConstants.java +++ b/src/main/java/com/be08/smart_notes/common/SecurityConstants.java @@ -4,6 +4,10 @@ public class SecurityConstants { public static final String[] PUBLIC_ENDPOINTS = { "/api/auth/register", "/api/auth/login", - "/api/auth/refresh" + "/api/auth/refresh", + "/api-docs", + "/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html" }; } diff --git a/src/main/java/com/be08/smart_notes/config/OpenAPIAuthConfiguration.java b/src/main/java/com/be08/smart_notes/config/OpenAPIAuthConfiguration.java new file mode 100644 index 0000000..a842a56 --- /dev/null +++ b/src/main/java/com/be08/smart_notes/config/OpenAPIAuthConfiguration.java @@ -0,0 +1,15 @@ +package com.be08.smart_notes.config; + +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Configuration; + +@Configuration +@SecurityScheme( + name = "Bearer Authentication", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) +public class OpenAPIAuthConfiguration { +} diff --git a/src/main/java/com/be08/smart_notes/controller/AIGenerationController.java b/src/main/java/com/be08/smart_notes/controller/AIGenerationController.java index c6c70ba..eebe31b 100644 --- a/src/main/java/com/be08/smart_notes/controller/AIGenerationController.java +++ b/src/main/java/com/be08/smart_notes/controller/AIGenerationController.java @@ -6,11 +6,13 @@ import com.be08.smart_notes.dto.response.QuizSetResponse; import com.be08.smart_notes.validation.group.MultipleDocument; import com.be08.smart_notes.validation.group.SingleDocument; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -20,36 +22,40 @@ @RequestMapping("/api/ai/generation") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "AI", description = "Operation for AI-related features") +@SecurityRequirement(name = "Bearer Authentication") public class AIGenerationController { QuizGenerationService quizGenerationService; @GetMapping("/quiz-sets/sample") - public ResponseEntity generateSampleQuizSet() { - QuizResponse quizResponse = quizGenerationService.generateSampleQuiz(); - ApiResponse apiResponse = ApiResponse.builder() - .message("Sample quiz set successfully generated") + @Operation(summary = "Get sample generated quiz") + public ApiResponse getSampleQuiz() { + QuizResponse quizResponse = quizGenerationService.getSampleQuiz(); + return ApiResponse.builder() + .message("Sample quiz successfully generated") .data(quizResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @PostMapping("/quiz-sets/default") - public ResponseEntity generateQuiz(@Validated(SingleDocument.class) @RequestBody QuizGenerationRequest quizGenerationRequest) { + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Generate single quiz based given request, stored in default quiz set") + public ApiResponse generateQuiz(@Validated(SingleDocument.class) @RequestBody QuizGenerationRequest quizGenerationRequest) { QuizResponse quizSetResponse = quizGenerationService.generateQuiz(quizGenerationRequest); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz successfully generated and added to default set") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse); } @PostMapping("/quiz-sets") - public ResponseEntity generateQuizSet(@Validated(MultipleDocument.class) @RequestBody QuizGenerationRequest quizGenerationRequest) { + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Generate multiple quizzes based given request, grouped in new quiz set") + public ApiResponse generateQuizSet(@Validated(MultipleDocument.class) @RequestBody QuizGenerationRequest quizGenerationRequest) { QuizSetResponse quizSetResponse = quizGenerationService.generateQuizSet(quizGenerationRequest); - ApiResponse apiResponse = ApiResponse.builder() - .message("Quiz set successfully generated") + return ApiResponse.builder() + .message("Quizzes successfully generated and added to new quiz set") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse); } } diff --git a/src/main/java/com/be08/smart_notes/controller/AttemptController.java b/src/main/java/com/be08/smart_notes/controller/AttemptController.java index 3b578cc..71a0aa2 100644 --- a/src/main/java/com/be08/smart_notes/controller/AttemptController.java +++ b/src/main/java/com/be08/smart_notes/controller/AttemptController.java @@ -8,12 +8,14 @@ import com.be08.smart_notes.dto.view.AttemptView; import com.be08.smart_notes.service.AttemptService; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -21,83 +23,86 @@ @RequestMapping("/api/quizzes") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Quiz Attempts", description = "Operation for quiz attempts") +@SecurityRequirement(name = "Bearer Authentication") public class AttemptController { AttemptService attemptService; @PostMapping("/{quizId}/attempts") + @ResponseStatus(HttpStatus.CREATED) @JsonView(AttemptView.Detail.class) - public ResponseEntity createAttempt(@PathVariable int quizId) { + @Operation(summary = "Create new attempt for target quiz") + public ApiResponse createAttempt(@PathVariable int quizId) { AttemptResponse attemptResponse = attemptService.createNewAttempt(quizId); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Attempt created successfully") .data(attemptResponse) .build(); - return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse); } @GetMapping("/{quizId}/attempts") @JsonView(AttemptView.Basic.class) - public ResponseEntity getAllAttemptsForQuiz(@PathVariable int quizId, + @Operation(summary = "Get all attempts by page") + public ApiResponse> getAllAttemptsForQuiz(@PathVariable int quizId, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { PageResponse attemptResponseList = attemptService.getAllAttemptsByQuizId(quizId, page, size); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.>builder() .message("All attempts for quiz fetched successfully") .data(attemptResponseList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping("/{quizId}/attempts/{attemptId}") @JsonView(AttemptView.Detail.class) - public ResponseEntity getAttempt(@PathVariable int quizId, @PathVariable int attemptId) { + @Operation(summary = "Get attempt and its recorded answers") + public ApiResponse getAttempt(@PathVariable int quizId, @PathVariable int attemptId) { AttemptResponse attemptResponseList = attemptService.getAttemptByIdAndQuizId(quizId, attemptId); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Attempt fetched successfully") .data(attemptResponseList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping("/{quizId}/attempts/{attemptId}/answer") @JsonView(AttemptView.Answer.class) - public ResponseEntity getAttemptResult(@PathVariable int quizId, @PathVariable int attemptId) { + @Operation(summary = "Get attempt with its recorded answers and correct answers") + public ApiResponse getAttemptResult(@PathVariable int quizId, @PathVariable int attemptId) { AttemptResponse attemptResponseList = attemptService.getAttemptByIdAndQuizId(quizId, attemptId); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Attempt result fetched successfully") .data(attemptResponseList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @PostMapping("/{quizId}/attempts/{attemptId}/answer") @JsonView(AttemptView.Answer.class) - public ResponseEntity finishAttempt(@PathVariable int quizId, @PathVariable int attemptId) { + @Operation(summary = "Finish attempt and calculate result") + public ApiResponse finishAttempt(@PathVariable int quizId, @PathVariable int attemptId) { AttemptResponse attemptResponseList = attemptService.calculateAttemptResult(quizId, attemptId); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Attempt result calculated successfully") .data(attemptResponseList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @PatchMapping("/{quizId}/attempts/{attemptId}") @JsonView(AttemptView.Answer.class) - public ResponseEntity updateAttemptDetail(@PathVariable int quizId, @PathVariable int attemptId, @Valid @RequestBody AttemptDetailUpdateRequest request) { + @Operation(summary = "Update attempt detail and/or record answer") + public ApiResponse updateAttemptDetail(@PathVariable int quizId, @PathVariable int attemptId, @Valid @RequestBody AttemptDetailUpdateRequest request) { AttemptResponse.Detail attemptDetailResponse = attemptService.updateAttemptDetail(quizId, attemptId, request); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Attempt detail updated successfully") .data(attemptDetailResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @DeleteMapping("/{quizId}/attempts/{attemptId}") - public ResponseEntity deleteAttempt(@PathVariable int quizId, @PathVariable int attemptId) { + @Operation(summary = "Delete attempt") + public ApiResponse deleteAttempt(@PathVariable int quizId, @PathVariable int attemptId) { attemptService.deleteAttemptByIdAndQuizId(quizId, attemptId); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Attempt deleted successfully") .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } } diff --git a/src/main/java/com/be08/smart_notes/controller/AuthenticationController.java b/src/main/java/com/be08/smart_notes/controller/AuthenticationController.java index 4aabdb0..a6d5c46 100644 --- a/src/main/java/com/be08/smart_notes/controller/AuthenticationController.java +++ b/src/main/java/com/be08/smart_notes/controller/AuthenticationController.java @@ -8,20 +8,22 @@ import com.be08.smart_notes.dto.response.UserResponse; import com.be08.smart_notes.service.AuthenticationService; import com.be08.smart_notes.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; +import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Authentication", description = "Operation for authentication features") public class AuthenticationController { AuthenticationService authenticationService; UserService userService; @@ -32,6 +34,8 @@ public class AuthenticationController { * @return ApiResponse containing UserResponse */ @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Register new user") ApiResponse register(@RequestBody @Valid UserCreationRequest request){ UserResponse response = userService.createUser(request); return ApiResponse.builder() @@ -46,6 +50,7 @@ ApiResponse register(@RequestBody @Valid UserCreationRequest reque * @return ApiResponse containing AuthenticationResponse */ @PostMapping("/login") + @Operation(summary = "Login with email and password") ApiResponse login(@RequestBody @Valid LoginRequest request){ AuthenticationResponse response = authenticationService.login(request); return ApiResponse.builder() @@ -60,6 +65,7 @@ ApiResponse login(@RequestBody @Valid LoginRequest reque * @return ApiResponse containing new AuthenticationResponse */ @PostMapping("/refresh") + @Operation(summary = "Refresh JWT Token") ApiResponse refreshToken(@RequestBody @Valid RefreshTokenRequest request){ AuthenticationResponse response = authenticationService.refreshToken(request); return ApiResponse.builder() @@ -75,6 +81,8 @@ ApiResponse refreshToken(@RequestBody @Valid RefreshToke * @return ApiResponse with logout confirmation */ @PostMapping("/logout") + @Operation(summary = "Logout and blacklist current token") + @SecurityRequirement(name = "Bearer Authentication") ApiResponse logout(Authentication authentication){ authenticationService.logout(authentication); diff --git a/src/main/java/com/be08/smart_notes/controller/DocumentController.java b/src/main/java/com/be08/smart_notes/controller/DocumentController.java index 1dc6096..84073ce 100644 --- a/src/main/java/com/be08/smart_notes/controller/DocumentController.java +++ b/src/main/java/com/be08/smart_notes/controller/DocumentController.java @@ -2,11 +2,12 @@ import com.be08.smart_notes.common.DefaultConstants; import com.be08.smart_notes.dto.response.PageResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.be08.smart_notes.dto.response.ApiResponse; @@ -17,27 +18,29 @@ @RequestMapping("/api/documents") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Documents", description = "Operation for documents (notes and PDFs)") +@SecurityRequirement(name = "Bearer Authentication") public class DocumentController { DocumentService documentService; @GetMapping - public ResponseEntity getAllDocuments( + @Operation(summary = "Get all documents by page") + public ApiResponse> getAllDocuments( @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { PageResponse documentList = documentService.getAllDocuments(page, size); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.>builder() .message("All document fetched successfully") .data(documentList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @DeleteMapping("/{id}") - public ResponseEntity deleteDocument(@PathVariable int id) { + @Operation(summary = "Delete document") + public ApiResponse deleteDocument(@PathVariable int id) { documentService.deleteDocument(id); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Document deleted successfully") .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } } diff --git a/src/main/java/com/be08/smart_notes/controller/FlashcardController.java b/src/main/java/com/be08/smart_notes/controller/FlashcardController.java index 95c36a3..9b8dff5 100644 --- a/src/main/java/com/be08/smart_notes/controller/FlashcardController.java +++ b/src/main/java/com/be08/smart_notes/controller/FlashcardController.java @@ -4,20 +4,27 @@ import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.FlashcardResponse; import com.be08.smart_notes.service.FlashcardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/flashcards") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Flashcards", description = "Operation for flashcards") +@SecurityRequirement(name = "Bearer Authentication") public class FlashcardController { FlashcardService flashcardService; @PostMapping + @Operation(summary = "Create new flashcard") public ApiResponse createFlashcard(@RequestBody FlashcardCreationRequest request){ FlashcardResponse response = flashcardService.createFlashcard(request); @@ -28,6 +35,8 @@ public ApiResponse createFlashcard(@RequestBody FlashcardCrea } @GetMapping("/{flashcardId}") + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Create flashcard") public ApiResponse getFlashcard(@PathVariable int flashcardId){ FlashcardResponse response = flashcardService.getFlashcardById(flashcardId); @@ -38,6 +47,7 @@ public ApiResponse getFlashcard(@PathVariable int flashcardId } @PutMapping("/{flashcardId}") + @Operation(summary = "Update flashcard") public ApiResponse updateFlashcard(@PathVariable int flashcardId, @RequestBody @Valid FlashcardCreationRequest request){ FlashcardResponse response = flashcardService.updateFlashcard(flashcardId, request); @@ -48,6 +58,7 @@ public ApiResponse updateFlashcard(@PathVariable int flashcar } @DeleteMapping("/{flashcardId}") + @Operation(summary = "Delete flashcard") public ApiResponse deleteFlashcard(@PathVariable int flashcardId){ flashcardService.deleteFlashcard(flashcardId); diff --git a/src/main/java/com/be08/smart_notes/controller/FlashcardSetController.java b/src/main/java/com/be08/smart_notes/controller/FlashcardSetController.java index b6fc92c..3850d0a 100644 --- a/src/main/java/com/be08/smart_notes/controller/FlashcardSetController.java +++ b/src/main/java/com/be08/smart_notes/controller/FlashcardSetController.java @@ -6,10 +6,14 @@ import com.be08.smart_notes.dto.response.FlashcardSetResponse; import com.be08.smart_notes.service.FlashcardService; import com.be08.smart_notes.service.FlashcardSetService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -18,6 +22,8 @@ @RequestMapping("/api/flashcard-sets") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Flashcard Sets", description = "Operation for flashcard sets") +@SecurityRequirement(name = "Bearer Authentication") public class FlashcardSetController { FlashcardSetService flashcardSetService; FlashcardService flashcardService; @@ -28,6 +34,8 @@ public class FlashcardSetController { * @return ApiResponse containing FlashcardSetResponse */ @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Create flashcard set") public ApiResponse createFlashcardSet(@RequestBody @Valid FlashcardSetCreationRequest request){ FlashcardSetResponse response = flashcardSetService.createFlashcardSet(request); @@ -42,6 +50,7 @@ public ApiResponse createFlashcardSet(@RequestBody @Valid * @return ApiResponse containing list of FlashcardSetResponse */ @GetMapping + @Operation(summary = "Get all flashcard sets") public ApiResponse> getAllFlashcardSets(){ List response = flashcardSetService.getAllFlashcardSets(); @@ -57,6 +66,7 @@ public ApiResponse> getAllFlashcardSets(){ * @return ApiResponse containing FlashcardSetResponse */ @GetMapping("/{flashcardSetId}") + @Operation(summary = "Get flashcard set") public ApiResponse getFlashcardSet(@PathVariable int flashcardSetId){ FlashcardSetResponse response = flashcardSetService.getFlashcardSet(flashcardSetId); @@ -72,6 +82,7 @@ public ApiResponse getFlashcardSet(@PathVariable int flash * @return ApiResponse containing list of FlashcardResponse */ @GetMapping("/{flashcardSetId}/flashcards") + @Operation(summary = "Get all flashcards from current set") public ApiResponse> getFlashcardsBySet(@PathVariable int flashcardSetId){ List response = flashcardService.getFlashcardsBySetId(flashcardSetId); @@ -88,6 +99,7 @@ public ApiResponse> getFlashcardsBySet(@PathVariable int * @return ApiResponse containing updated FlashcardSetResponse */ @PutMapping("/{flashcardSetId}") + @Operation(summary = "Update flashcard set") public ApiResponse updateFlashcardSet(@PathVariable int flashcardSetId, @RequestBody @Valid FlashcardSetCreationRequest request){ FlashcardSetResponse response = flashcardSetService.updateFlashcardSet(flashcardSetId, request); @@ -103,6 +115,7 @@ public ApiResponse updateFlashcardSet(@PathVariable int fl * @return ApiResponse with deletion confirmation */ @DeleteMapping("/{flashcardSetId}") + @Operation(summary = "Delete flashcard set") public ApiResponse deleteFlashcardSet(@PathVariable int flashcardSetId){ flashcardSetService.deleteFlashcardSet(flashcardSetId); diff --git a/src/main/java/com/be08/smart_notes/controller/NoteController.java b/src/main/java/com/be08/smart_notes/controller/NoteController.java index 7771240..5fadd4a 100644 --- a/src/main/java/com/be08/smart_notes/controller/NoteController.java +++ b/src/main/java/com/be08/smart_notes/controller/NoteController.java @@ -4,12 +4,14 @@ import com.be08.smart_notes.dto.filter.BasicFilterDTO; import com.be08.smart_notes.dto.response.NoteResponse; import com.be08.smart_notes.dto.response.PageResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.be08.smart_notes.dto.request.NoteUpsertRequest; @@ -20,60 +22,63 @@ @RequestMapping("/api/documents/notes") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Notes", description = "All operations for notes") +@SecurityRequirement(name = "Bearer Authentication") public class NoteController { NoteService noteService; @PostMapping - public ResponseEntity createNote(@RequestBody @Valid NoteUpsertRequest noteCreationRequest) { + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Create new note") + public ApiResponse createNote(@RequestBody @Valid NoteUpsertRequest noteCreationRequest) { NoteResponse createdNote = noteService.createNote(noteCreationRequest); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Note created successfully") .data(createdNote) .build(); - return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse); } @GetMapping("/{id}") - public ResponseEntity getNote(@PathVariable int id) { + @Operation(summary = "Get note") + public ApiResponse getNote(@PathVariable int id) { NoteResponse note = noteService.getNote(id); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Note fetched successfully") .data(note) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping - public ResponseEntity getAllNotes( + @Operation(summary = "Get all notes by page") + public ApiResponse> getAllNotes( @ModelAttribute BasicFilterDTO filterDTO, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { PageResponse pageResponse = noteService.getAllNotes(filterDTO, sortBy, sortOrder, page, size); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.>builder() .message("Note fetched successfully") .data(pageResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @PatchMapping("/{id}") - public ResponseEntity updateNote(@PathVariable int id, @RequestBody @Valid NoteUpsertRequest noteUpdateRequest) { + @Operation(summary = "Update note") + public ApiResponse updateNote(@PathVariable int id, @RequestBody @Valid NoteUpsertRequest noteUpdateRequest) { NoteResponse note = noteService.updateNote(id, noteUpdateRequest); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Note updated successfully") .data(note) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @DeleteMapping("/{id}") - public ResponseEntity deleteNote(@PathVariable int id) { + @Operation(summary = "Delete note") + public ApiResponse deleteNote(@PathVariable int id) { noteService.deleteNote(id); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Note deleted successfully") .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } } diff --git a/src/main/java/com/be08/smart_notes/controller/QuizController.java b/src/main/java/com/be08/smart_notes/controller/QuizController.java index 483c6a1..bdfd8ef 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizController.java @@ -11,37 +11,41 @@ import com.be08.smart_notes.validation.group.OnCreate; import com.be08.smart_notes.validation.group.OnUpdate; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/api/quizzes") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Quizzes", description = "All operations for quizzes") +@SecurityRequirement(name = "Bearer Authentication") public class QuizController { QuizService quizService; @PostMapping + @ResponseStatus(HttpStatus.CREATED) @JsonView(QuizView.Detail.class) - public ResponseEntity createQuiz(@RequestBody @Validated(OnCreate.class) QuizUpsertDTO request) { + @Operation(summary = "Create new quiz with associated questions") + public ApiResponse createQuiz(@RequestBody @Validated(OnCreate.class) QuizUpsertDTO request) { QuizResponse quizResponse = quizService.createQuiz(request); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz created successfully in default set") .data(quizResponse) .build(); - return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse); } @GetMapping @JsonView(QuizView.Basic.class) - public ResponseEntity getAllQuizzes( + @Operation(summary = "Get all quizzes by page") + public ApiResponse> getAllQuizzes( @ModelAttribute QuizFilterDTO filterDTO, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, @@ -49,41 +53,40 @@ public ResponseEntity getAllQuizzes( @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size ) { PageResponse quizResponseList = quizService.getAllQuizzes(filterDTO, sortBy, sortOrder, page, size); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.>builder() .message("Quizzes fetched successfully") .data(quizResponseList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping("/{id}") @JsonView(QuizView.Detail.class) - public ResponseEntity getQuiz(@PathVariable int id) { + @Operation(summary = "Get quiz and all of its questions") + public ApiResponse getQuiz(@PathVariable int id) { QuizResponse quizResponse = quizService.getQuizById(id); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz fetched successfully") .data(quizResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @PatchMapping("/{id}") - @JsonView(QuizView.Detail.class) - public ResponseEntity updateQuiz(@PathVariable int id, @Validated(OnUpdate.class) @RequestBody QuizUpsertDTO request) { + @JsonView(QuizView.Basic.class) + @Operation(summary = "Update quiz information") + public ApiResponse updateQuiz(@PathVariable int id, @Validated(OnUpdate.class) @RequestBody QuizUpsertDTO request) { QuizResponse quizResponse = quizService.updateQuiz(id, request); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz updated successfully") .data(quizResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @DeleteMapping("/{id}") - public ResponseEntity deleteQuiz(@PathVariable int id) { + @Operation(summary = "Delete quiz") + public ApiResponse deleteQuiz(@PathVariable int id) { quizService.deleteQuizById(id); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set deleted successfully") .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } } diff --git a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java index 0b13ff0..f62433e 100644 --- a/src/main/java/com/be08/smart_notes/controller/QuizSetController.java +++ b/src/main/java/com/be08/smart_notes/controller/QuizSetController.java @@ -10,107 +10,112 @@ import com.be08.smart_notes.enums.OriginType; import com.be08.smart_notes.service.QuizSetService; import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/quiz-sets") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "Quiz Sets", description = "All operations for quiz sets") +@SecurityRequirement(name = "Bearer Authentication") public class QuizSetController { QuizSetService quizSetService; @PostMapping + @ResponseStatus(HttpStatus.CREATED) @JsonView(QuizView.Detail.class) - public ResponseEntity createQuizSet(@RequestBody @Valid QuizSetUpsertRequest request) { + @Operation(summary = "Create new quiz set") + public ApiResponse createQuizSet(@RequestBody @Valid QuizSetUpsertRequest request) { QuizSetResponse quizSetResponse = quizSetService.createQuizSet(request, OriginType.USER); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set created successfully") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse); } @GetMapping @JsonView(QuizView.Basic.class) - public ResponseEntity getAllQuizSets( + @Operation(summary = "Get all quiz sets") + public ApiResponse> getAllQuizSets( @ModelAttribute BasicFilterDTO filterDTO, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_BY) String sortBy, @RequestParam(required = false, defaultValue = DefaultConstants.SORT_ORDER) String sortOrder, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_NUMBER) int page, @RequestParam(required = false, defaultValue = DefaultConstants.PAGE_SIZE) int size) { PageResponse quizSetResponseList = quizSetService.getAllQuizSets(filterDTO, sortBy, sortOrder, page, size); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.>builder() .message("Quiz set fetched successfully") .data(quizSetResponseList) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping("/default") @JsonView(QuizView.Detail.class) - public ResponseEntity getDefaultQuizSet() { + @Operation(summary = "Get default quiz set and its quizzes") + public ApiResponse getDefaultQuizSet() { QuizSetResponse quizSetResponse = quizSetService.getDefaultSet(); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set fetched successfully") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping("/{id}") - @JsonView(QuizView.Detail.class) - public ResponseEntity getQuizSet(@PathVariable int id) { + @JsonView(QuizView.Basic.class) + @Operation(summary = "Get quiz set") + public ApiResponse getQuizSet(@PathVariable int id) { QuizSetResponse quizSetResponse = quizSetService.getQuizSetById(id, false); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set fetched successfully") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @GetMapping("/{id}/quizzes") @JsonView(QuizView.Detail.class) - public ResponseEntity getQuizSetWithQuizzes(@PathVariable int id) { + @Operation(summary = "Get quiz set and its quizzes") + public ApiResponse getQuizSetWithQuizzes(@PathVariable int id) { QuizSetResponse quizSetResponse = quizSetService.getQuizSetById(id, true); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set fetched successfully") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @PatchMapping("/{id}") @JsonView(QuizView.Detail.class) - public ResponseEntity updateQuizSet(@PathVariable int id, @RequestBody QuizSetUpsertRequest request) { + @Operation(summary = "Update quiz set information") + public ApiResponse updateQuizSet(@PathVariable int id, @RequestBody QuizSetUpsertRequest request) { QuizSetResponse quizSetResponse = quizSetService.updateQuizSet(id, request); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set updated successfully") .data(quizSetResponse) .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @DeleteMapping - public ResponseEntity deleteAllQuizSet() { + @Operation(summary = "Delete all quiz sets") + public ApiResponse deleteAllQuizSet() { quizSetService.deleteAllQuizSet(); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("All quiz set deleted successfully") .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } @DeleteMapping("/{id}") - public ResponseEntity deleteQuizSet(@PathVariable int id) { + @Operation(summary = "Delete quiz set") + public ApiResponse deleteQuizSet(@PathVariable int id) { quizSetService.deleteQuizSetById(id); - ApiResponse apiResponse = ApiResponse.builder() + return ApiResponse.builder() .message("Quiz set deleted successfully") .build(); - return ResponseEntity.status(HttpStatus.OK).body(apiResponse); } } diff --git a/src/main/java/com/be08/smart_notes/controller/UserController.java b/src/main/java/com/be08/smart_notes/controller/UserController.java index b77239a..de83a51 100644 --- a/src/main/java/com/be08/smart_notes/controller/UserController.java +++ b/src/main/java/com/be08/smart_notes/controller/UserController.java @@ -3,6 +3,9 @@ import com.be08.smart_notes.dto.response.ApiResponse; import com.be08.smart_notes.dto.response.UserResponse; import com.be08.smart_notes.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; @@ -14,6 +17,8 @@ @RequestMapping("/api/users") @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +@Tag(name = "User", description = "All operations for user-related features") +@SecurityRequirement(name = "Bearer Authentication") public class UserController { UserService userService; @@ -22,6 +27,7 @@ public class UserController { * @return ApiResponse containing UserResponse */ @GetMapping("/me") + @Operation(summary = "Get user detail") public ApiResponse getMe(){ UserResponse response = userService.getMe(); diff --git a/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java b/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java index f98dc72..301269e 100644 --- a/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java +++ b/src/main/java/com/be08/smart_notes/dto/filter/QuizFilterDTO.java @@ -10,6 +10,6 @@ @NoArgsConstructor @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) -public class QuizFilterDTO extends BasicFilterDTO{ +public class QuizFilterDTO extends BasicFilterDTO { Integer quizSetId; } diff --git a/src/main/java/com/be08/smart_notes/service/ai/QuizGenerationService.java b/src/main/java/com/be08/smart_notes/service/ai/QuizGenerationService.java index 2f12001..7be655c 100644 --- a/src/main/java/com/be08/smart_notes/service/ai/QuizGenerationService.java +++ b/src/main/java/com/be08/smart_notes/service/ai/QuizGenerationService.java @@ -75,7 +75,7 @@ public QuizGenerationService(AIService aiService, NoteService noteService, QuizS * A sample function to retrieve a quiz for testing purposes, it will neither interact with the AI provider nor save data to the database. * @return quiz response dto */ - public QuizResponse generateSampleQuiz() { + public QuizResponse getSampleQuiz() { // Below is sample generated content String generatedContent = "{\"topic\":\"Object-Oriented Programming Concepts\",\"questions\":[{\"question\":\"What is the main purpose of Object-Oriented Programming (OOP)?\",\"options\":[\"A. To simplify data structures\",\"B. To organize code around objects\",\"C. To create complex algorithms\",\"D. To optimize code execution speed\"], \"correct_index\": 1}, {\"question\":\"Which of the following best describes encapsulation in OOP?\",\"options\":[\"A. Hiding internal data and exposing only necessary information\",\"B. Creating multiple objects from a single class\",\"C. Passing data between different classes\",\"D. Defining the structure of a class\",\"\"], \"correct_index\": 1}, {\"question\":\"What is the primary function of a constructor in OOP?\",\"options\":[\"A. To delete an object from memory\",\"B. To store data for an object\",\"C. To initialize an object when it is created\",\"D. To define the behavior of an object\",\"\"], \"correct_index\": 3}, {\"question\":\"How does inheritance work in OOP?\",\"options\":[\"A. It allows objects to inherit properties and methods from other objects\",\"B. It creates a new class based on an existing one and adds new features\",\"C. It allows objects to access private members of other objects\",\"D. It enables objects to communicate with each other through messages\",\"\"], \"correct_index\": 1}, {\"question\":\"What does polymorphism refer to in OOP?\",\"options\":[\"A. The ability of an object to be accessed from multiple classes\",\"B. The ability of an object to behave differently based on its context\",\"C. The ability of an object to be used in different programming languages\",\"D. The ability of an object to be inherited from other objects\",\"\"], \"correct_index\": 1}, {\"question\":\"Which of the following is NOT a benefit of OOP?\",\"options\":[\"A. Improved code reusability\",\"B. Easier code maintenance\",\"C. Increased program complexity\",\"D. Enhanced code readability\",\"\"], \"correct_index\": 3}, {\"question\":\"What is the primary difference between a class and an object?\",\"options\":[\"A. A class is a blueprint for creating objects, while an object is an instance of that blueprint\",\"B. A class is a data structure, while an object is a programming language\",\"C. A class is a variable, while an object is a function\",\"D. A class is a method, while an object is a program\",\"\"], \"correct_index\": 1}, {\"question\":\"What is the main purpose of a static method?\",\"options\":[\"A. To define a method that is specific to a particular object\",\"B. To define a method that belongs to a class and not to individual objects\",\"C. To define a method that is called when an object is created\",\"D. To define a method that is called when an object is destroyed\",\"\"], \"correct_index\": 1}, {\"question\":\"Which of the following is an example of a common mistake to avoid in OOP?\",\"options\":[\"A. Using inheritance when it is not needed\",\"B. Using public access modifiers for every method\",\"C. Using static methods for every method\",\"D. Creating complex objects that are not needed\",\"\"], \"correct_index\": 1}, {\"question\":\"What is the purpose of an interface in OOP?\",\"options\":[\"A. To define the behavior of a class\",\"B. To create a contract that classes must follow\",\"C. To define the structure of a class\",\"D. To create a blueprint for creating objects\",\"\"], \"correct_index\": 1}, {\"question\":\"What is the purpose of a method overriding?\",\"options\":[\"A. To create a new class that is based on an existing one\",\"B. To define a new method with a different implementation in a child class\",\"C. To create a new method that overrides the behavior of a parent class\",\"D. To create a new class that inherits from a different class\",\"\"], \"correct_index\": 3}]}\n"; diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 2826672..a6b6299 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,3 +1,7 @@ +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.url=/api-docs +springdoc.swagger-ui.tagsSorter=alpha + # Database Configuration for Development (Local MySQL) spring.datasource.url=jdbc:mysql://localhost:${DB_PORT}/${DB_NAME} spring.datasource.username=${DB_USERNAME}